From 62a67844eb505b2a6c6019c24e17140ab4daa57f Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 3 May 2025 19:06:37 -0400 Subject: [PATCH 1/7] v2: add linter --- Cargo.toml | 2 + crates/squawk_linter/Cargo.toml | 22 + crates/squawk_linter/README.md | 31 + crates/squawk_linter/src/ignore.rs | 215 ++ crates/squawk_linter/src/ignore_index.rs | 57 + crates/squawk_linter/src/lib.rs | 636 ++++ .../src/rules/adding_field_with_default.rs | 301 ++ .../rules/adding_foreign_key_constraint.rs | 130 + .../src/rules/adding_not_null_field.rs | 98 + .../rules/adding_primary_key_constraint.rs | 92 + .../src/rules/adding_required_field.rs | 120 + .../ban_alter_domain_with_add_constraint.rs | 61 + .../squawk_linter/src/rules/ban_char_field.rs | 161 + ...oncurrent_index_creation_in_transaction.rs | 123 + .../ban_create_domain_with_constraint.rs | 82 + .../src/rules/ban_drop_column.rs | 43 + .../src/rules/ban_drop_database.rs | 42 + .../src/rules/ban_drop_not_null.rs | 47 + .../squawk_linter/src/rules/ban_drop_table.rs | 41 + .../src/rules/changing_column_type.rs | 68 + .../src/rules/constraint_missing_not_valid.rs | 307 ++ .../src/rules/disallow_unique_constraint.rs | 172 + crates/squawk_linter/src/rules/mod.rs | 57 + .../rules/non_volatile_built_in_functions.txt | 2963 +++++++++++++++++ .../squawk_linter/src/rules/prefer_big_int.rs | 231 ++ .../src/rules/prefer_bigint_over_int.rs | 107 + .../src/rules/prefer_bigint_over_smallint.rs | 106 + .../src/rules/prefer_identity.rs | 101 + .../src/rules/prefer_robust_stmts.rs | 571 ++++ .../src/rules/prefer_text_field.rs | 175 + .../src/rules/prefer_timestamptz.rs | 139 + .../src/rules/renaming_column.rs | 43 + .../squawk_linter/src/rules/renaming_table.rs | 43 + .../require_concurrent_index_creation.rs | 106 + .../require_concurrent_index_deletion.rs | 87 + ...ld_with_default__test__add_numbers_ok.snap | 5 + ...ith_default__test__arbitrary_func_err.snap | 14 + ...d_with_default__test__default_bool_ok.snap | 5 + ...d_with_default__test__default_enum_ok.snap | 5 + ...th_default__test__default_func_now_ok.snap | 5 + ..._with_default__test__default_jsonb_ok.snap | 5 + ...t__test__default_random_with_args_err.snap | 14 + ...ld_with_default__test__default_str_ok.snap | 5 + ...ith_default__test__default_uuid_error.snap | 14 + ...__test__default_uuid_error_multi_stmt.snap | 14 + ...ault__test__default_volatile_func_err.snap | 14 + ...d_with_default__test__docs_example_ok.snap | 5 + ...ult__test__docs_example_ok_post_pg_11.snap | 5 + ...h_default__test__generated_stored_err.snap | 14 + ...ng_not_null_field__test__set_not_null.snap | 14 + ...y_constraint__test__plain_primary_key.snap | 12 + ..._constraint__test__serial_primary_key.snap | 12 + ...field__test__not_null_without_default.snap | 12 + ...domain_with_add_constraint__test__err.snap | 12 + ...__ban_char_field__test__all_the_types.snap | 42 + ...ban_char_field__test__alter_table_err.snap | 12 + ...char_field__test__array_char_type_err.snap | 12 + ...test__creating_table_with_char_errors.snap | 30 + ...on__test__assuming_in_transaction_err.snap | 12 + ...ent_index_creation_in_transaction_err.snap | 12 + ...ate_domain_with_constraint__test__err.snap | 12 + ...__test__err_with_multiple_constraints.snap | 12 + ...er__rules__ban_drop_column__test__err.snap | 12 + ...rop_database__test__ban_drop_database.snap | 24 + ...__rules__ban_drop_not_null__test__err.snap | 12 + ...ter__rules__ban_drop_table__test__err.snap | 24 + ...anging_column_type__test__another_err.snap | 18 + ...ules__changing_column_type__test__err.snap | 12 + ...id__test__adding_check_constraint_err.snap | 12 + ...issing_not_valid__test__adding_fk_err.snap | 12 + ...valid_validate_assume_transaction_err.snap | 14 + ...t__not_valid_validate_transaction_err.snap | 14 + ..._transaction_with_explicit_commit_err.snap | 14 + ...t__test__adding_unique_constraint_err.snap | 12 + ...ique_constraint_inline_add_column_err.snap | 12 + ...nstraint_inline_add_column_unique_err.snap | 12 + ...int__test__alter_table_add_column_err.snap | 12 + ...st__alter_table_alter_column_type_err.snap | 12 + ...ble_alter_column_type_with_quotes_err.snap | 12 + ..._big_int__test__create_table_many_err.snap | 18 + ...ter__rules__prefer_big_int__test__err.snap | 54 + ...es__prefer_bigint_over_int__test__err.snap | 30 + ...refer_bigint_over_smallint__test__err.snap | 30 + ...er__rules__prefer_identity__test__err.snap | 42 + ...ts__test__alter_table_drop_column_err.snap | 12 + ...test__alter_table_drop_constraint_err.snap | 12 + ...r_robust_stmts__test__alter_table_err.snap | 12 + ...__test__create_index_concurrently_err.snap | 14 + ...ate_index_concurrently_muli_stmts_err.snap | 22 + ...create_index_concurrently_unnamed_err.snap | 14 + ..._robust_stmts__test__create_table_err.snap | 12 + ..._test__disable_row_level_security_err.snap | 12 + ...tmts__test__double_add_after_drop_err.snap | 12 + ...er_robust_stmts__test__drop_index_err.snap | 12 + ...__test__enable_row_level_security_err.snap | 12 + ...vel_security_without_exists_check_err.snap | 12 + ...eld__test__adding_column_non_text_err.snap | 12 + ...eate_table_with_pgcatalog_varchar_err.snap | 12 + ...__test__create_table_with_varchar_err.snap | 12 + ...ield__test__increase_varchar_size_err.snap | 12 + ..._test__alter_table_with_timestamp_err.snap | 18 + ...test__create_table_with_timestamp_err.snap | 18 + ...er__rules__renaming_column__test__err.snap | 12 + ...ter__rules__renaming_table__test__err.snap | 12 + ...st__adding_index_non_concurrently_err.snap | 12 + ...__drop_index_missing_concurrently_err.snap | 12 + crates/squawk_linter/src/text.rs | 8 + 107 files changed, 8622 insertions(+) create mode 100644 crates/squawk_linter/Cargo.toml create mode 100644 crates/squawk_linter/README.md create mode 100644 crates/squawk_linter/src/ignore.rs create mode 100644 crates/squawk_linter/src/ignore_index.rs create mode 100644 crates/squawk_linter/src/lib.rs create mode 100644 crates/squawk_linter/src/rules/adding_field_with_default.rs create mode 100644 crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs create mode 100644 crates/squawk_linter/src/rules/adding_not_null_field.rs create mode 100644 crates/squawk_linter/src/rules/adding_primary_key_constraint.rs create mode 100644 crates/squawk_linter/src/rules/adding_required_field.rs create mode 100644 crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs create mode 100644 crates/squawk_linter/src/rules/ban_char_field.rs create mode 100644 crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs create mode 100644 crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs create mode 100644 crates/squawk_linter/src/rules/ban_drop_column.rs create mode 100644 crates/squawk_linter/src/rules/ban_drop_database.rs create mode 100644 crates/squawk_linter/src/rules/ban_drop_not_null.rs create mode 100644 crates/squawk_linter/src/rules/ban_drop_table.rs create mode 100644 crates/squawk_linter/src/rules/changing_column_type.rs create mode 100644 crates/squawk_linter/src/rules/constraint_missing_not_valid.rs create mode 100644 crates/squawk_linter/src/rules/disallow_unique_constraint.rs create mode 100644 crates/squawk_linter/src/rules/mod.rs create mode 100644 crates/squawk_linter/src/rules/non_volatile_built_in_functions.txt create mode 100644 crates/squawk_linter/src/rules/prefer_big_int.rs create mode 100644 crates/squawk_linter/src/rules/prefer_bigint_over_int.rs create mode 100644 crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs create mode 100644 crates/squawk_linter/src/rules/prefer_identity.rs create mode 100644 crates/squawk_linter/src/rules/prefer_robust_stmts.rs create mode 100644 crates/squawk_linter/src/rules/prefer_text_field.rs create mode 100644 crates/squawk_linter/src/rules/prefer_timestamptz.rs create mode 100644 crates/squawk_linter/src/rules/renaming_column.rs create mode 100644 crates/squawk_linter/src/rules/renaming_table.rs create mode 100644 crates/squawk_linter/src/rules/require_concurrent_index_creation.rs create mode 100644 crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__add_numbers_ok.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__arbitrary_func_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_bool_ok.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_enum_ok.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_func_now_ok.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_jsonb_ok.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_random_with_args_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_str_ok.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_uuid_error.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_uuid_error_multi_stmt.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_volatile_func_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__docs_example_ok.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__docs_example_ok_post_pg_11.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__generated_stored_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_not_null_field__test__set_not_null.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_primary_key_constraint__test__plain_primary_key.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_primary_key_constraint__test__serial_primary_key.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_required_field__test__not_null_without_default.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_alter_domain_with_add_constraint__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__all_the_types.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__alter_table_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__array_char_type_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__creating_table_with_char_errors.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_concurrent_index_creation_in_transaction__test__assuming_in_transaction_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_concurrent_index_creation_in_transaction__test__ban_concurrent_index_creation_in_transaction_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_create_domain_with_constraint__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_create_domain_with_constraint__test__err_with_multiple_constraints.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_column__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_database__test__ban_drop_database.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_not_null__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_table__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__changing_column_type__test__another_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__changing_column_type__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__adding_check_constraint_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__adding_fk_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_assume_transaction_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_transaction_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_with_assume_in_transaction_with_explicit_commit_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__adding_unique_constraint_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__unique_constraint_inline_add_column_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__unique_constraint_inline_add_column_unique_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_add_column_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_alter_column_type_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_alter_column_type_with_quotes_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__create_table_many_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_bigint_over_int__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_bigint_over_smallint__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_identity__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_drop_column_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_drop_constraint_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_muli_stmts_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_unnamed_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_table_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__disable_row_level_security_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__double_add_after_drop_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__drop_index_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__enable_row_level_security_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__enable_row_level_security_without_exists_check_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__adding_column_non_text_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__create_table_with_pgcatalog_varchar_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__create_table_with_varchar_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__increase_varchar_size_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__alter_table_with_timestamp_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__create_table_with_timestamp_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__renaming_column__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__renaming_table__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__require_concurrent_index_creation__test__adding_index_non_concurrently_err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__require_concurrent_index_deletion__test__drop_index_missing_concurrently_err.snap create mode 100644 crates/squawk_linter/src/text.rs diff --git a/Cargo.toml b/Cargo.toml index cfd644b0..ed31bbb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ camino = "1.1.9" pg_query = "6.1.0" rowan = "0.15.15" smol_str = "0.3.2" +enum-iterator = "2.1.0" +line-index = "0.1.2" # local squawk-parser = { version = "0.0.0", path = "./crates/parser" } diff --git a/crates/squawk_linter/Cargo.toml b/crates/squawk_linter/Cargo.toml new file mode 100644 index 00000000..bb60c196 --- /dev/null +++ b/crates/squawk_linter/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "squawk_linter" +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +syntax.workspace = true +lexer.workspace = true +rowan.workspace = true + +serde.workspace = true +lazy_static.workspace = true +insta.workspace = true +enum-iterator.workspace = true +line-index.workspace = true + + +[lints] +workspace = true diff --git a/crates/squawk_linter/README.md b/crates/squawk_linter/README.md new file mode 100644 index 00000000..da13a12f --- /dev/null +++ b/crates/squawk_linter/README.md @@ -0,0 +1,31 @@ +# linter + +## rules + +| name | done? | +| -------------------------------------------- | ----- | +| adding_field_with_default | y | +| adding_foreign_key_constraint | y | +| adding_not_null_field | y | +| adding_primary_key_constraint | y | +| adding_required_field | y | +| ban_char_field | y | +| ban_concurrent_index_creation_in_transaction | y | +| ban_drop_column | y | +| ban_drop_database | y | +| ban_drop_not_null | y | +| ban_drop_table | y | +| changing_column_type | y | +| constraint_missing_not_valid | y | +| disallow_unique_constraint | y | +| prefer_big_int | y | +| prefer_bigint_over_int | y | +| prefer_bigint_over_smallint | y | +| prefer_identity | y | +| prefer_robust_stmts | y | +| prefer_text_field | y | +| prefer_timestamptz | y | +| renaming_column | y | +| renaming_table | y | +| require_concurrent_index_creation | y | +| require_concurrent_index_deletion | y | diff --git a/crates/squawk_linter/src/ignore.rs b/crates/squawk_linter/src/ignore.rs new file mode 100644 index 00000000..902115f2 --- /dev/null +++ b/crates/squawk_linter/src/ignore.rs @@ -0,0 +1,215 @@ +use std::collections::HashSet; + +use rowan::{NodeOrToken, TextRange, TextSize}; +use syntax::{SyntaxKind, SyntaxNode, SyntaxToken}; + +use crate::{ErrorCode, Linter, Violation}; + +#[derive(Debug)] +pub struct Ignore { + pub range: TextRange, + pub violation_names: HashSet, +} + +fn comment_body(token: &SyntaxToken) -> Option<(&str, TextRange)> { + let range = token.text_range(); + if token.kind() == SyntaxKind::COMMENT { + let text = token.text(); + if let Some(trimmed) = text.strip_prefix("--") { + if let Some(start) = range.start().checked_add(2.into()) { + let end = range.end(); + let updated_range = TextRange::new(start, end); + return Some((trimmed, updated_range)); + } + } + if let Some(trimmed) = text.strip_prefix("/*").and_then(|x| x.strip_suffix("*/")) { + if let Some(start) = range.start().checked_add(2.into()) { + if let Some(end) = range.end().checked_sub(2.into()) { + let updated_range = TextRange::new(start, end); + return Some((trimmed, updated_range)); + } + } + } + } + None +} + +const IGNORE_TEXT: &str = "squawk-ignore"; + +fn ignore_rule_names(token: &SyntaxToken) -> Option<(&str, TextRange)> { + if let Some((comment_body, range)) = comment_body(token) { + let without_start = comment_body.trim_start(); + let trim_start_size = comment_body.len() - without_start.len(); + let trimmed_comment = without_start.trim_end(); + let trim_end_size = without_start.len() - trimmed_comment.len(); + + if let Some(without_prefix) = trimmed_comment.strip_prefix(IGNORE_TEXT) { + let range = TextRange::new( + range.start() + TextSize::new((trim_start_size + IGNORE_TEXT.len()) as u32), + range.end() - TextSize::new(trim_end_size as u32), + ); + return Some((without_prefix, range)); + } + } + None +} + +pub(crate) fn find_ignores(ctx: &mut Linter, file: &SyntaxNode) { + for event in file.preorder_with_tokens() { + match event { + rowan::WalkEvent::Enter(NodeOrToken::Token(token)) + if token.kind() == SyntaxKind::COMMENT => + { + if let Some((rule_names, range)) = ignore_rule_names(&token) { + let mut set = HashSet::new(); + let mut offset = 0usize; + + // we need to keep track of our offset and report specific + // ranges for any unknown names we encounter, which makes + // this more complicated + for x in rule_names.split(",") { + if x.is_empty() { + continue; + } + if let Ok(violation_name) = ErrorCode::try_from(x.trim()) { + set.insert(violation_name); + } else { + let without_start = x.trim_start(); + let trim_start_size = x.len() - without_start.len(); + let trimmed = without_start.trim_end(); + + let range = range.checked_add(TextSize::new(offset as u32)).unwrap(); + + let start = range.start() + TextSize::new(trim_start_size as u32); + let end = start + TextSize::new(trimmed.len() as u32); + let range = TextRange::new(start, end); + + ctx.report(Violation::new( + ErrorCode::UnusedIgnore, + format!("unknown name {trimmed}"), + range, + vec![], + )); + } + + offset += x.len() + 1; + } + ctx.ignore(Ignore { + range, + violation_names: set, + }); + } + } + _ => (), + } + } +} + +#[cfg(test)] +mod test { + + use crate::{find_ignores, ErrorCode, Linter, Violation}; + + #[test] + fn single_ignore() { + let sql = r#" +-- squawk-ignore ban-drop-column +alter table t drop column c cascade; + "#; + let parse = syntax::SourceFile::parse(sql); + + let mut linter = Linter::from([]); + find_ignores(&mut linter, &parse.syntax_node()); + + assert_eq!(linter.ignores.len(), 1); + let ignore = &linter.ignores[0]; + assert!(ignore.violation_names.contains(&ErrorCode::BanDropColumn)); + } + + #[test] + fn single_ignore_c_style_comment() { + let sql = r#" +/* squawk-ignore ban-drop-column */ +alter table t drop column c cascade; + "#; + let parse = syntax::SourceFile::parse(sql); + + let mut linter = Linter::from([]); + + find_ignores(&mut linter, &parse.syntax_node()); + + assert_eq!(linter.ignores.len(), 1); + let ignore = &linter.ignores[0]; + assert!(ignore.violation_names.contains(&ErrorCode::BanDropColumn)); + } + + #[test] + fn multi_ignore() { + let sql = r#" +-- squawk-ignore ban-drop-column, renaming-column,ban-drop-database +alter table t drop column c cascade; + "#; + let parse = syntax::SourceFile::parse(sql); + + let mut linter = Linter::from([]); + + find_ignores(&mut linter, &parse.syntax_node()); + + assert_eq!(linter.ignores.len(), 1); + let ignore = &linter.ignores[0]; + assert!(ignore.violation_names.contains(&ErrorCode::BanDropColumn)); + assert!(ignore.violation_names.contains(&ErrorCode::RenamingColumn)); + assert!(ignore.violation_names.contains(&ErrorCode::BanDropDatabase)); + } + + #[test] + fn multi_ignore_c_style_comment() { + let sql = r#" +/* squawk-ignore ban-drop-column, renaming-column,ban-drop-database */ +alter table t drop column c cascade; + "#; + let parse = syntax::SourceFile::parse(sql); + + let mut linter = Linter::from([]); + + find_ignores(&mut linter, &parse.syntax_node()); + + assert_eq!(linter.ignores.len(), 1); + let ignore = &linter.ignores[0]; + assert!(ignore.violation_names.contains(&ErrorCode::BanDropColumn)); + assert!(ignore.violation_names.contains(&ErrorCode::RenamingColumn)); + assert!(ignore.violation_names.contains(&ErrorCode::BanDropDatabase)); + } + + #[test] + fn ignore_multiple_stmts() { + let mut linter = Linter::with_all_rules(); + let sql = r#" +-- squawk-ignore ban-char-field,prefer-robust-stmts +alter table t add column c char; + +ALTER TABLE foo +-- squawk-ignore adding-field-with-default,prefer-robust-stmts +ADD COLUMN bar numeric GENERATED + ALWAYS AS (bar + baz) STORED; + +-- squawk-ignore prefer-robust-stmts +create table users ( +); +"#; + + let parse = syntax::SourceFile::parse(sql); + let errors: Vec<&Violation> = linter.lint(parse, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn starting_line_aka_zero() { + let mut linter = Linter::with_all_rules(); + let sql = r#"alter table t add column c char;"#; + + let parse = syntax::SourceFile::parse(sql); + let errors: Vec<&Violation> = linter.lint(parse, sql); + assert_eq!(errors.len(), 1); + } +} diff --git a/crates/squawk_linter/src/ignore_index.rs b/crates/squawk_linter/src/ignore_index.rs new file mode 100644 index 00000000..b4b0c447 --- /dev/null +++ b/crates/squawk_linter/src/ignore_index.rs @@ -0,0 +1,57 @@ +use std::{ + collections::{HashMap, HashSet}, + fmt, +}; + +use line_index::LineIndex; +use rowan::TextRange; + +use crate::{ErrorCode, Ignore}; + +pub(crate) struct IgnoreIndex { + line_to_ignored_names: HashMap>, + line_index: LineIndex, +} + +impl fmt::Debug for IgnoreIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "IgnoreIndex:")?; + let mut keys = self.line_to_ignored_names.keys().collect::>(); + keys.sort(); + for line in keys { + if let Some(set) = &self.line_to_ignored_names.get(line) { + writeln!(f, " {line}: {set:?}")?; + } + } + Ok(()) + } +} + +impl IgnoreIndex { + pub(crate) fn new(text: &str, ignores: &[Ignore]) -> Self { + let line_index = LineIndex::new(text); + let mut line_to_ignored_names: HashMap> = HashMap::new(); + for ignore in ignores { + let line = line_index.line_col(ignore.range.start()).line; + line_to_ignored_names.insert(line, ignore.violation_names.clone()); + } + // TODO: we want to report unused ignores + Self { + line_to_ignored_names, + line_index, + } + } + + pub(crate) fn contains(&self, range: TextRange, error_name: ErrorCode) -> bool { + // TODO: hmmm basically we want to ensure that either it's on the line before or it's inside the start of the node. we parse stuff so that the comment ends up inside the node :/ + let line = self.line_index.line_col(range.start()).line; + for line in [line, if line == 0 { 0 } else { line - 1 }] { + if let Some(set) = self.line_to_ignored_names.get(&line) { + if set.contains(&error_name) { + return true; + } + } + } + false + } +} diff --git a/crates/squawk_linter/src/lib.rs b/crates/squawk_linter/src/lib.rs new file mode 100644 index 00000000..d8f2d80e --- /dev/null +++ b/crates/squawk_linter/src/lib.rs @@ -0,0 +1,636 @@ +use std::collections::HashSet; +use std::fmt; + +use enum_iterator::all; +use enum_iterator::Sequence; +use ignore::find_ignores; +pub use ignore::Ignore; +use ignore_index::IgnoreIndex; +use lazy_static::lazy_static; +use rowan::TextRange; +use serde::{Deserialize, Serialize}; + +use syntax::{Parse, SourceFile}; + +mod ignore; +mod ignore_index; + +mod rules; +mod text; +use rules::adding_field_with_default; +use rules::adding_foreign_key_constraint; +use rules::adding_not_null_field; +use rules::adding_primary_key_constraint; +use rules::adding_required_field; +use rules::ban_alter_domain_with_add_constraint; +use rules::ban_char_field; +use rules::ban_concurrent_index_creation_in_transaction; +use rules::ban_create_domain_with_constraint; +use rules::ban_drop_column; +use rules::ban_drop_not_null; +use rules::ban_drop_table; +use rules::changing_column_type; +use rules::constraint_missing_not_valid; +use rules::disallow_unique_constraint; +use rules::prefer_big_int; +use rules::prefer_bigint_over_int; +use rules::prefer_bigint_over_smallint; +use rules::prefer_identity; +use rules::prefer_robust_stmts; +use rules::prefer_text_field; +use rules::prefer_timestamptz; +use rules::renaming_column; +use rules::renaming_table; +use rules::require_concurrent_index_creation; +use rules::require_concurrent_index_deletion; +// xtask:new-lint:rule-import + +use rules::ban_drop_database; + +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Hash, Eq, Deserialize)] +pub enum ErrorCode { + #[serde(rename = "require-concurrent-index-creation")] + RequireConcurrentIndexCreation, + #[serde(rename = "require-concurrent-index-deletion")] + RequireConcurrentIndexDeletion, + #[serde(rename = "constraint-missing-not-valid")] + ConstraintMissingNotValid, + #[serde(rename = "adding-field-with-default")] + AddingFieldWithDefault, + #[serde(rename = "adding-foreign-key-constraint")] + AddingForeignKeyConstraint, + #[serde(rename = "changing-column-type")] + ChangingColumnType, + #[serde(rename = "adding-not-nullable-field")] + AddingNotNullableField, + #[serde(rename = "adding-serial-primary-key-field")] + AddingSerialPrimaryKeyField, + #[serde(rename = "renaming-column")] + RenamingColumn, + #[serde(rename = "renaming-table")] + RenamingTable, + #[serde(rename = "disallowed-unique-constraint")] + DisallowedUniqueConstraint, + #[serde(rename = "ban-drop-database")] + BanDropDatabase, + #[serde(rename = "prefer-big-int")] + PreferBigInt, + #[serde(rename = "prefer-bigint-over-int")] + PreferBigintOverInt, + #[serde(rename = "prefer-bigint-over-smallint")] + PreferBigintOverSmallint, + #[serde(rename = "prefer-identity")] + PreferIdentity, + #[serde(rename = "prefer-robust-stmts")] + PreferRobustStmts, + #[serde(rename = "prefer-text-field")] + PreferTextField, + #[serde(rename = "prefer-timestamptz")] + PreferTimestampTz, + #[serde(rename = "ban-char-field")] + BanCharField, + #[serde(rename = "ban-drop-column")] + BanDropColumn, + #[serde(rename = "ban-drop-table")] + BanDropTable, + #[serde(rename = "ban-drop-not-null")] + BanDropNotNull, + #[serde(rename = "transaction-nesting")] + TransactionNesting, + #[serde(rename = "adding-required-field")] + AddingRequiredField, + #[serde(rename = "ban-concurrent-index-creation-in-transaction")] + BanConcurrentIndexCreationInTransaction, + #[serde(rename = "unused-ignore")] + UnusedIgnore, + #[serde(rename = "ban-create-domain-with-constraint")] + BanCreateDomainWithConstraint, + #[serde(rename = "ban-alter-domain-with-add-constraint")] + BanAlterDomainWithAddConstraint, + // xtask:new-lint:error-name +} + +impl TryFrom<&str> for ErrorCode { + type Error = String; + + fn try_from(s: &str) -> Result { + match s { + "require-concurrent-index-creation" => Ok(ErrorCode::RequireConcurrentIndexCreation), + "require-concurrent-index-deletion" => Ok(ErrorCode::RequireConcurrentIndexDeletion), + "constraint-missing-not-valid" => Ok(ErrorCode::ConstraintMissingNotValid), + "adding-field-with-default" => Ok(ErrorCode::AddingFieldWithDefault), + "adding-foreign-key-constraint" => Ok(ErrorCode::AddingForeignKeyConstraint), + "changing-column-type" => Ok(ErrorCode::ChangingColumnType), + "adding-not-nullable-field" => Ok(ErrorCode::AddingNotNullableField), + "adding-serial-primary-key-field" => Ok(ErrorCode::AddingSerialPrimaryKeyField), + "renaming-column" => Ok(ErrorCode::RenamingColumn), + "renaming-table" => Ok(ErrorCode::RenamingTable), + "disallowed-unique-constraint" => Ok(ErrorCode::DisallowedUniqueConstraint), + "ban-drop-database" => Ok(ErrorCode::BanDropDatabase), + "prefer-big-int" => Ok(ErrorCode::PreferBigInt), + "prefer-bigint-over-int" => Ok(ErrorCode::PreferBigintOverInt), + "prefer-bigint-over-smallint" => Ok(ErrorCode::PreferBigintOverSmallint), + "prefer-identity" => Ok(ErrorCode::PreferIdentity), + "prefer-robust-stmts" => Ok(ErrorCode::PreferRobustStmts), + "prefer-text-field" => Ok(ErrorCode::PreferTextField), + "prefer-timestamptz" => Ok(ErrorCode::PreferTimestampTz), + "ban-char-field" => Ok(ErrorCode::BanCharField), + "ban-drop-column" => Ok(ErrorCode::BanDropColumn), + "ban-drop-table" => Ok(ErrorCode::BanDropTable), + "ban-drop-not-null" => Ok(ErrorCode::BanDropNotNull), + "transaction-nesting" => Ok(ErrorCode::TransactionNesting), + "adding-required-field" => Ok(ErrorCode::AddingRequiredField), + "ban-concurrent-index-creation-in-transaction" => { + Ok(ErrorCode::BanConcurrentIndexCreationInTransaction) + } + "ban-create-domain-with-constraint" => Ok(ErrorCode::BanCreateDomainWithConstraint), + "ban-alter-domain-with-add-constraint" => { + Ok(ErrorCode::BanAlterDomainWithAddConstraint) + } + // xtask:new-lint:str-name + _ => Err(format!("Unknown violation name: {}", s)), + } + } +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match &self { + ErrorCode::RequireConcurrentIndexCreation => "require-concurrent-index-creation", + ErrorCode::RequireConcurrentIndexDeletion => "require-concurrent-index-deletion", + ErrorCode::ConstraintMissingNotValid => "constraint-missing-not-valid", + ErrorCode::AddingFieldWithDefault => "adding-field-with-default", + ErrorCode::AddingForeignKeyConstraint => "adding-foreign-key-constraint", + ErrorCode::ChangingColumnType => "changing-column-type", + ErrorCode::AddingNotNullableField => "adding-not-nullable-field", + ErrorCode::AddingSerialPrimaryKeyField => "adding-serial-primary-key-field", + ErrorCode::RenamingColumn => "renaming-column", + ErrorCode::RenamingTable => "renaming-table", + ErrorCode::DisallowedUniqueConstraint => "disallowed-unique-constraint", + ErrorCode::BanDropDatabase => "ban-drop-database", + ErrorCode::PreferBigInt => "prefer-big-int", + ErrorCode::PreferBigintOverInt => "prefer-bigint-over-int", + ErrorCode::PreferBigintOverSmallint => "prefer-bigint-over-smallint", + ErrorCode::PreferIdentity => "prefer-identity", + ErrorCode::PreferRobustStmts => "prefer-robust-stmts", + ErrorCode::PreferTextField => "prefer-text-field", + ErrorCode::PreferTimestampTz => "prefer-timestamp-tz", + ErrorCode::BanCharField => "ban-char-field", + ErrorCode::BanDropColumn => "ban-drop-column", + ErrorCode::BanDropTable => "ban-drop-table", + ErrorCode::BanDropNotNull => "ban-drop-not-null", + ErrorCode::TransactionNesting => "transaction-nesting", + ErrorCode::AddingRequiredField => "adding-required-field", + ErrorCode::BanConcurrentIndexCreationInTransaction => { + "ban-concurrent-index-creation-in-transaction" + } + ErrorCode::BanCreateDomainWithConstraint => "ban-create-domain-with-constraint", + ErrorCode::UnusedIgnore => "unused-ignore", + ErrorCode::BanAlterDomainWithAddConstraint => "ban-alter-domain-with-add-constraint", + }; + write!(f, "{}", val) + } +} + +impl ErrorCode { + pub fn meta(&self) -> ViolationMeta { + match self { + ErrorCode::RequireConcurrentIndexCreation => ViolationMeta::new( + "Require Concurrent Index Creation", + [ + ViolationMessage::Note("Creating an index blocks writes."), + ViolationMessage::Help("Create the index CONCURRENTLY."), + ] + ), + ErrorCode::RequireConcurrentIndexDeletion => ViolationMeta::new( + "Require Concurrent Index Deletion", + [ + ViolationMessage::Note("Deleting an index blocks selects, inserts, updates, and deletes on the index's table."), + ViolationMessage::Help("Delete the index CONCURRENTLY."), + ] + ), + ErrorCode::ConstraintMissingNotValid => ViolationMeta::new( + "Constraint Missing Not Valid", + [ + ViolationMessage::Note("Requires a table scan to verify constraint and an ACCESS EXCLUSIVE lock which blocks reads."), + ViolationMessage::Help("Add NOT VALID to the constraint in one transaction and then VALIDATE the constraint in a separate transaction."), + ] + ), + ErrorCode::AddingFieldWithDefault => ViolationMeta::new( + "Adding Field With Default", + [ + ViolationMessage::Note("Adding a field with a VOLATILE DEFAULT requires a table rewrite with an ACCESS EXCLUSIVE lock. In Postgres versions 11+, non-VOLATILE DEFAULTs can be added without a rewrite."), + ViolationMessage::Help("Add the field as nullable, then set a default, backfill, and remove nullabilty."), + ] + ), + ErrorCode::AddingForeignKeyConstraint => ViolationMeta::new( + "Adding Foreign Key Constraint", + [ + ViolationMessage::Note("Requires a table scan of the table you're altering and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes to both tables while your table is scanned."), + ViolationMessage::Help("Add NOT VALID to the constraint in one transaction and then VALIDATE the constraint in a separate transaction."), + ] + ), + ErrorCode::ChangingColumnType => ViolationMeta::new( + "Changing Column Type", + [ + ViolationMessage::Note("Requires an ACCESS EXCLUSIVE lock on the table which blocks reads."), + ViolationMessage::Note("Changing the type may break existing clients."), + ] + ), + ErrorCode::AddingNotNullableField => ViolationMeta::new( + "Adding Not Nullable Field", + [ + ViolationMessage::Note("Adding a NOT NULL field requires exclusive locks and table rewrites."), + ViolationMessage::Help("Make the field nullable."), + ] + ), + ErrorCode::AddingSerialPrimaryKeyField => ViolationMeta::new( + "Adding Serial Primary Key Field", + [ + ViolationMessage::Note("Adding a PRIMARY KEY constraint results in locks and table rewrites"), + ViolationMessage::Help("Add the PRIMARY KEY constraint USING an index."), + ] + ), + ErrorCode::RenamingColumn => ViolationMeta::new( + "Renaming Column", + [ViolationMessage::Note("Renaming a column may break existing clients.")] + ), + ErrorCode::RenamingTable => ViolationMeta::new( + "Renaming Table", + [ViolationMessage::Note("Renaming a table may break existing clients.")] + ), + ErrorCode::DisallowedUniqueConstraint => ViolationMeta::new( + "Disallowed Unique Constraint", + [ + ViolationMessage::Note("Adding a UNIQUE constraint requires an ACCESS EXCLUSIVE lock which blocks reads."), + ViolationMessage::Help("Create an index CONCURRENTLY and create the constraint using the index."), + ] + ), + ErrorCode::BanDropDatabase => ViolationMeta::new( + "Ban Drop Database", + [ViolationMessage::Note("Dropping a database may break existing clients.")] + ), + ErrorCode::PreferBigInt => ViolationMeta::new( + "Prefer Big Int", + [ + ViolationMessage::Note("Hitting the max 32 bit integer is possible and may break your application."), + ViolationMessage::Help("Use 64bit integer values instead to prevent hitting this limit."), + ] + ), + ErrorCode::PreferBigintOverSmallint => ViolationMeta::new( + "Prefer Bigint Over Smallint", + [ + ViolationMessage::Note("Hitting the max 16 bit integer is possible and may break your application."), + ViolationMessage::Help("Use 64bit integer values instead to prevent hitting this limit."), + ] + ), + ErrorCode::PreferIdentity => ViolationMeta::new( + "Prefer Identity", + [ + ViolationMessage::Note("Serial types have confusing behaviors that make schema management difficult."), + ViolationMessage::Help("Use identity columns instead for more features and better usability."), + ] + ), + ErrorCode::PreferRobustStmts => ViolationMeta::new( + "Prefer Robust Statements", + [ViolationMessage::Help("Consider wrapping in a transaction or adding a IF NOT EXISTS clause if the statement supports it.")] + ), + ErrorCode::PreferTextField => ViolationMeta::new( + "Prefer Text Field", + [ + ViolationMessage::Note("Changing the size of a varchar field requires an ACCESS EXCLUSIVE lock."), + ViolationMessage::Help("Use a text field with a check constraint."), + ] + ), + ErrorCode::PreferTimestampTz => ViolationMeta::new( + "Prefer Timestamp with Timezone", + [ + ViolationMessage::Note("A timestamp field without a timezone can lead to data loss, depending on your database session timezone."), + ViolationMessage::Help("Use timestamptz instead of timestamp for your column type."), + ] + ), + ErrorCode::BanCharField => ViolationMeta::new( + "Ban Char Field", + [ViolationMessage::Help("Use text or varchar instead.")] + ), + ErrorCode::BanDropColumn => ViolationMeta::new( + "Dropping columns not allowed", + [ViolationMessage::Note("Dropping a column may break existing clients.")] + ), + ErrorCode::BanDropTable => ViolationMeta::new( + "Ban Drop Table", + [ViolationMessage::Note("Dropping a table may break existing clients.")] + ), + ErrorCode::BanDropNotNull => ViolationMeta::new( + "Ban Drop Not Null", + [ViolationMessage::Note("Dropping a NOT NULL constraint may break existing clients.")] + ), + ErrorCode::TransactionNesting => ViolationMeta::new( + "Transaction Nesting", + [ + ViolationMessage::Note("There is an existing transaction already in progress."), + ViolationMessage::Help("COMMIT the previous transaction before issuing a BEGIN or START TRANSACTION statement."), + ] + ), + ErrorCode::AddingRequiredField => ViolationMeta::new( + "Adding Required Field", + [ + ViolationMessage::Note("Adding a NOT NULL field without a DEFAULT will fail for a populated table."), + ViolationMessage::Help("Make the field nullable or add a non-VOLATILE DEFAULT (Postgres 11+)."), + ] + ), + ErrorCode::BanConcurrentIndexCreationInTransaction => ViolationMeta::new( + "Ban Concurrent Index Creation in Transaction", + [ + ViolationMessage::Note("Concurrent index creation is not allowed inside a transaction."), + ViolationMessage::Help("Build the index outside any transactions."), + ] + ), + ErrorCode::PreferBigintOverInt => ViolationMeta::new( + "Prefer Big Int Over Int", + [ + ViolationMessage::Note( + "Hitting the max 32 bit integer is possible and may break your application." + ), + ViolationMessage::Help( + "Use 64bit integer values instead to prevent hitting this limit." + ), + ] + ), + ErrorCode::BanCreateDomainWithConstraint => ViolationMeta::new( + "Ban Create Domains with Constraints", + [ + ViolationMessage::Note( + "Domains with constraints have poor support for online migrations", + ), + ] + ), + ErrorCode::BanAlterDomainWithAddConstraint => ViolationMeta::new( + "Ban Alter Domain With Add Constraints", + [ + ViolationMessage::Note( + "Domains with constraints have poor support for online migrations", + ) + ] + ), + ErrorCode::UnusedIgnore => ViolationMeta::new("Unused linter ignore", []) + } + } +} + +#[derive(Debug)] +pub enum ViolationMessage<'a> { + Note(&'a str), + Help(&'a str), +} + +#[derive(Debug)] +pub struct ViolationMeta<'a> { + /// A description of the rule that's used when rendering the error message + /// in on the CLI. It should be a slightly expanded version of the [ViolationName] + pub title: String, + /// Messages rendered for each error to provide context and offer advice on how to fix. + pub messages: Vec>, +} + +impl<'a> ViolationMeta<'a> { + pub fn new( + title: impl Into, + messages: impl Into>>, + ) -> ViolationMeta<'a> { + ViolationMeta { + title: title.into(), + messages: messages.into(), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Violation { + pub code: ErrorCode, + pub message: String, + pub text_range: TextRange, + pub messages: Vec, +} + +impl Violation { + #[must_use] + pub(crate) fn new( + code: ErrorCode, + message: String, + text_range: TextRange, + messages: impl Into>>, + ) -> Self { + Self { + code, + text_range, + message, + messages: messages.into().unwrap_or_default(), + } + } +} + +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] +pub struct Version { + major: i32, + minor: Option, + patch: Option, +} + +impl Version { + #[must_use] + pub(crate) fn new( + major: i32, + minor: impl Into>, + patch: impl Into>, + ) -> Self { + Self { + major, + minor: minor.into(), + patch: patch.into(), + } + } +} + +#[derive(PartialEq, Eq, Hash, Copy, Clone, Sequence)] +pub enum Rule { + AddingFieldWithDefault, + AddingForeignKeyConstraint, + AddingNotNullField, + AddingPrimaryKeyConstraint, + AddingRequiredField, + BanDropDatabase, + BanCharField, + BanConcurrentIndexCreationInTransaction, + BanDropColumn, + BanDropNotNull, + BanDropTable, + ChangingColumnType, + ConstraintMissingNotValid, + DisallowUniqueConstraint, + PreferBigInt, + PreferBigintOverInt, + PreferBigintOverSmallint, + PreferIdentity, + PreferRobustStmts, + PreferTextField, + PreferTimestamptz, + RenamingColumn, + RenamingTable, + RequireConcurrentIndexCreation, + RequireConcurrentIndexDeletion, + BanCreateDomainWithConstraint, + BanAlterDomainWithAddConstraint, + // xtask:new-lint:name +} + +pub struct LinterSettings { + pub pg_version: Version, + pub assume_in_transaction: bool, +} + +lazy_static! { + static ref DEFAULT_PG_VERSION: Version = Version::new(15, 0, 0); +} + +pub struct Linter { + errors: Vec, + ignores: Vec, + pub rules: HashSet, + pub settings: LinterSettings, +} + +impl Linter { + fn report(&mut self, error: Violation) { + self.errors.push(error); + } + + fn ignore(&mut self, ignore: Ignore) { + self.ignores.push(ignore); + } + + #[must_use] + pub fn lint(&mut self, file: Parse, text: &str) -> Vec<&Violation> { + if self.rules.contains(&Rule::AddingFieldWithDefault) { + adding_field_with_default(self, &file); + } + if self.rules.contains(&Rule::AddingForeignKeyConstraint) { + adding_foreign_key_constraint(self, &file); + } + if self.rules.contains(&Rule::AddingNotNullField) { + adding_not_null_field(self, &file); + } + if self.rules.contains(&Rule::AddingPrimaryKeyConstraint) { + adding_primary_key_constraint(self, &file); + } + if self.rules.contains(&Rule::AddingRequiredField) { + adding_required_field(self, &file); + } + if self.rules.contains(&Rule::BanDropDatabase) { + ban_drop_database(self, &file); + } + if self.rules.contains(&Rule::BanCharField) { + ban_char_field(self, &file); + } + if self + .rules + .contains(&Rule::BanConcurrentIndexCreationInTransaction) + { + ban_concurrent_index_creation_in_transaction(self, &file); + } + if self.rules.contains(&Rule::BanDropColumn) { + ban_drop_column(self, &file); + } + if self.rules.contains(&Rule::BanDropNotNull) { + ban_drop_not_null(self, &file); + } + if self.rules.contains(&Rule::BanDropTable) { + ban_drop_table(self, &file); + } + if self.rules.contains(&Rule::ChangingColumnType) { + changing_column_type(self, &file); + } + if self.rules.contains(&Rule::ConstraintMissingNotValid) { + constraint_missing_not_valid(self, &file); + } + if self.rules.contains(&Rule::DisallowUniqueConstraint) { + disallow_unique_constraint(self, &file); + } + if self.rules.contains(&Rule::PreferBigInt) { + prefer_big_int(self, &file); + } + if self.rules.contains(&Rule::PreferBigintOverInt) { + prefer_bigint_over_int(self, &file); + } + if self.rules.contains(&Rule::PreferBigintOverSmallint) { + prefer_bigint_over_smallint(self, &file); + } + if self.rules.contains(&Rule::PreferIdentity) { + prefer_identity(self, &file); + } + if self.rules.contains(&Rule::PreferRobustStmts) { + prefer_robust_stmts(self, &file); + } + if self.rules.contains(&Rule::PreferTextField) { + prefer_text_field(self, &file); + } + if self.rules.contains(&Rule::PreferTimestamptz) { + prefer_timestamptz(self, &file); + } + if self.rules.contains(&Rule::RenamingColumn) { + renaming_column(self, &file); + } + if self.rules.contains(&Rule::RenamingTable) { + renaming_table(self, &file); + } + if self.rules.contains(&Rule::RequireConcurrentIndexCreation) { + require_concurrent_index_creation(self, &file); + } + if self.rules.contains(&Rule::RequireConcurrentIndexDeletion) { + require_concurrent_index_deletion(self, &file); + } + if self.rules.contains(&Rule::BanCreateDomainWithConstraint) { + ban_create_domain_with_constraint(self, &file); + } + if self.rules.contains(&Rule::BanAlterDomainWithAddConstraint) { + ban_alter_domain_with_add_constraint(self, &file); + } + // xtask:new-lint:rule-call + + // locate any ignores in the file + find_ignores(self, &file.syntax_node()); + + self.errors(text) + } + + fn errors(&mut self, text: &str) -> Vec<&Violation> { + // ensure we order them by where they appear in the file + self.errors.sort_by_key(|x| x.text_range.start()); + + let ignore_index = IgnoreIndex::new(text, &self.ignores); + // TODO: we should have errors for when there was an ignore but that + // ignore didn't actually ignore anything + + self.errors + .iter() + .filter(|err| !ignore_index.contains(err.text_range, err.code)) + .collect::>() + } + + pub fn with_all_rules() -> Self { + let rules = all::().collect::>(); + Linter::from(rules) + } + + pub fn from(rules: impl Into>) -> Self { + Self { + errors: vec![], + ignores: vec![], + rules: rules.into(), + settings: LinterSettings { + pg_version: *DEFAULT_PG_VERSION, + assume_in_transaction: false, + }, + } + } +} diff --git a/crates/squawk_linter/src/rules/adding_field_with_default.rs b/crates/squawk_linter/src/rules/adding_field_with_default.rs new file mode 100644 index 00000000..a7bd25ac --- /dev/null +++ b/crates/squawk_linter/src/rules/adding_field_with_default.rs @@ -0,0 +1,301 @@ +use lazy_static::lazy_static; +use std::collections::HashSet; + +use syntax::ast; +use syntax::ast::{AstNode, HasArgList}; +use syntax::{ast::HasModuleItem, Parse, SourceFile}; + +use crate::{ErrorCode, Linter, Violation}; + +fn is_const_expr(expr: &ast::Expr) -> bool { + match expr { + ast::Expr::Literal(_) => true, + ast::Expr::CastExpr(cast) => matches!(cast.expr(), Some(ast::Expr::Literal(_))), + _ => false, + } +} + +lazy_static! { + static ref NON_VOLATILE_FUNCS: HashSet = { + NON_VOLATILE_BUILT_IN_FUNCTIONS + .split('\n') + .map(|x| x.trim().to_lowercase()) + .filter(|x| !x.is_empty()) + .collect() + }; +} + +fn is_non_volatile(expr: &ast::Expr) -> bool { + match expr { + ast::Expr::CallExpr(call_expr) => { + if let Some(arglist) = call_expr.arg_list() { + let no_args = arglist.args().count() == 0; + + // TODO: what about FieldExpr? like, pg_catalog.uuid() + let Some(ast::Expr::NameRef(name_ref)) = call_expr.expr() else { + return false; + }; + + let non_volatile_name = NON_VOLATILE_FUNCS.contains(name_ref.text().as_str()); + + no_args && non_volatile_name + } else { + false + } + } + _ => false, + } +} + +// Generated via the following Postgres query: +// select proname from pg_proc where provolatile <> 'v'; +const NON_VOLATILE_BUILT_IN_FUNCTIONS: &str = include_str!("non_volatile_built_in_functions.txt"); + +pub(crate) fn adding_field_with_default(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + // TODO: use match_ast! like in #api_walkthrough + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + if let ast::AlterTableAction::AddColumn(add_column) = action { + for constraint in add_column.constraints() { + match constraint { + ast::Constraint::DefaultConstraint(default) => { + let Some(expr) = default.expr() else { + continue; + }; + if is_const_expr(&expr) || is_non_volatile(&expr) { + continue; + } + ctx.report(Violation::new( + ErrorCode::AddingFieldWithDefault, + "Adding a generated column requires a table rewrite with an `ACCESS EXCLUSIVE` lock.".into(), + expr.syntax().text_range(), + vec![ + "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.".into(), + ], + )) + } + ast::Constraint::GeneratedConstraint(generated) => { + ctx.report(Violation::new( + ErrorCode::AddingFieldWithDefault, + "Adding a generated column requires a table rewrite with an `ACCESS EXCLUSIVE` lock.".into(), + generated.syntax().text_range(), + vec![ + "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.".into(), + ], + )); + } + _ => (), + } + } + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn docs_example_ok_post_pg_11() { + // TODO: differing from squawk because we aren't checking the postgres + // version, maybe we should be default to a more recent version like 15 + // instead of 11? + let sql = r#" +-- instead of +ALTER TABLE "core_recipe" ADD COLUMN "foo" integer DEFAULT 10; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn docs_example_ok() { + let sql = r#" +-- use +ALTER TABLE "core_recipe" ADD COLUMN "foo" integer; +ALTER TABLE "core_recipe" ALTER COLUMN "foo" SET DEFAULT 10; +-- backfill +-- remove nullability + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn default_uuid_error_multi_stmt() { + let sql = r#" +alter table t set logged, add column c integer default uuid(); + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn default_uuid_error() { + let sql = r#" +ALTER TABLE "core_recipe" ADD COLUMN "foo" integer DEFAULT uuid(); + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn default_volatile_func_err() { + let sql = r#" + -- VOLATILE +ALTER TABLE "core_recipe" ADD COLUMN "foo" boolean DEFAULT random(); + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn default_bool_ok() { + let sql = r#" + -- NON-VOLATILE +ALTER TABLE "core_recipe" ADD COLUMN "foo" boolean DEFAULT true; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn default_str_ok() { + let sql = r#" +-- NON-VOLATILE +ALTER TABLE "core_recipe" ADD COLUMN "foo" text DEFAULT 'some-str'; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn default_enum_ok() { + let sql = r#" +-- NON-VOLATILE +ALTER TABLE "core_recipe" ADD COLUMN "foo" some_enum_type DEFAULT 'my-enum-variant'; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn default_jsonb_ok() { + let sql = r#" +-- NON-VOLATILE +ALTER TABLE "core_recipe" ADD COLUMN "foo" jsonb DEFAULT '{}'::jsonb; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn arbitrary_func_err() { + let sql = r#" +-- NON-VOLATILE +ALTER TABLE "core_recipe" ADD COLUMN "foo" jsonb DEFAULT myjsonb(); + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn default_random_with_args_err() { + let sql = r#" +-- NON-VOLATILE +ALTER TABLE "core_recipe" ADD COLUMN "foo" timestamptz DEFAULT now(123); + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + #[test] + fn default_func_now_ok() { + let sql = r#" +-- NON-VOLATILE +ALTER TABLE "core_recipe" ADD COLUMN "foo" timestamptz DEFAULT now(); + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn add_numbers_ok() { + let sql = r#" +alter table account_metadata add column blah integer default 2 + 2; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn generated_stored_err() { + let sql = r#" +ALTER TABLE foo +ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingFieldWithDefault]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs b/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs new file mode 100644 index 00000000..f46cb18d --- /dev/null +++ b/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs @@ -0,0 +1,130 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn adding_foreign_key_constraint(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + // TODO: use match_ast! like in #api_walkthrough + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + match action { + ast::AlterTableAction::AddConstraint(add_constraint) => { + if add_constraint.not_valid().is_some() { + // Adding foreign key is okay when NOT VALID is specified. + continue; + } + if let Some(constraint) = add_constraint.constraint() { + if matches!( + constraint, + ast::Constraint::ForeignKeyConstraint(_) + | ast::Constraint::ReferencesConstraint(_) + ) { + ctx.report(Violation::new( + ErrorCode::AddingForeignKeyConstraint, + "Adding a foreign key constraint requires a table scan and a `SHARE ROW EXCLUSIVE` lock on both tables, which blocks writes to each table.".into(), + constraint.syntax().text_range(), + None, + )) + } + } + } + ast::AlterTableAction::AddColumn(add_column) => { + for constraint in add_column.constraints() { + if matches!( + constraint, + ast::Constraint::ForeignKeyConstraint(_) + | ast::Constraint::ReferencesConstraint(_) + ) { + ctx.report(Violation::new( + ErrorCode::AddingForeignKeyConstraint, + "Adding a foreign key constraint requires a table scan and a `SHARE ROW EXCLUSIVE` lock on both tables, which blocks writes to each table.".into(), + constraint.syntax().text_range(), + None, + )) + } + } + } + _ => continue, + } + } + } + } +} + +#[cfg(test)] +mod test { + use crate::{ErrorCode, Linter, Rule}; + + #[test] + fn create_table_with_foreign_key_constraint() { + let sql = r#" + BEGIN; + CREATE TABLE email ( + id BIGINT GENERATED ALWAYS AS IDENTITY, + user_id BIGINT, + email TEXT, + PRIMARY KEY(id), + CONSTRAINT fk_user + FOREIGN KEY ("user_id") + REFERENCES "user" ("id") + ); + COMMIT; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingForeignKeyConstraint]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } + + #[test] + fn add_foreign_key_constraint_not_valid_validate() { + let sql = r#" +BEGIN; +ALTER TABLE "email" ADD COLUMN "user_id" INT; +ALTER TABLE "email" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "user" ("id") NOT VALID; +ALTER TABLE "email" VALIDATE CONSTRAINT "fk_user"; +COMMIT; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingForeignKeyConstraint]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } + + #[test] + fn add_foreign_key_constraint_lock() { + let sql = r#" +BEGIN; +ALTER TABLE "email" ADD COLUMN "user_id" INT; +ALTER TABLE "email" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "user" ("id"); +COMMIT; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingForeignKeyConstraint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].code, ErrorCode::AddingForeignKeyConstraint); + } + + #[test] + fn add_column_references_lock() { + let sql = r#" +BEGIN; +ALTER TABLE "emails" ADD COLUMN "user_id" INT REFERENCES "user" ("id"); +COMMIT; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingForeignKeyConstraint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].code, ErrorCode::AddingForeignKeyConstraint); + } +} diff --git a/crates/squawk_linter/src/rules/adding_not_null_field.rs b/crates/squawk_linter/src/rules/adding_not_null_field.rs new file mode 100644 index 00000000..e7490d82 --- /dev/null +++ b/crates/squawk_linter/src/rules/adding_not_null_field.rs @@ -0,0 +1,98 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Version, Violation}; + +pub(crate) fn adding_not_null_field(ctx: &mut Linter, parse: &Parse) { + if ctx.settings.pg_version >= Version::new(11, 0, 0) { + return; + } + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + if let ast::AlterTableAction::AlterColumn(alter_column) = action { + let Some(option) = alter_column.option() else { + continue; + }; + + if matches!(option, ast::AlterColumnOption::SetNotNull(_)) { + ctx.report(Violation::new( + ErrorCode::AddingNotNullableField, + "Setting a column NOT NULL blocks reads while the table is scanned." + .into(), + option.syntax().text_range(), + vec!["Use a check constraint instead.".into()], + )); + } + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule, Version}; + + #[test] + fn set_not_null() { + let sql = r#" +ALTER TABLE "core_recipe" ALTER COLUMN "foo" SET NOT NULL; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingNotNullField]); + linter.settings.pg_version = Version::new(10, 0, 0); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn adding_field_that_is_not_nullable() { + let sql = r#" +BEGIN; +-- This will cause a table rewrite for Postgres versions before 11, but that is handled by +-- adding-field-with-default. +ALTER TABLE "core_recipe" ADD COLUMN "foo" integer DEFAULT 10 NOT NULL; +ALTER TABLE "core_recipe" ALTER COLUMN "foo" DROP DEFAULT; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingNotNullField]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } + + #[test] + fn adding_field_that_is_not_nullable_without_default() { + let sql = r#" +-- This won't work if the table is populated, but that error is caught by adding-required-field. +ALTER TABLE "core_recipe" ADD COLUMN "foo" integer NOT NULL; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingNotNullField]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } + + #[test] + fn adding_field_that_is_not_nullable_in_version_11() { + let sql = r#" +BEGIN; +-- +-- Add field foo to recipe +-- +ALTER TABLE "core_recipe" ADD COLUMN "foo" integer NOT NULL DEFAULT 10; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingNotNullField]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } +} diff --git a/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs b/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs new file mode 100644 index 00000000..b3e8a7b0 --- /dev/null +++ b/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs @@ -0,0 +1,92 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn adding_primary_key_constraint(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + match action { + ast::AlterTableAction::AddConstraint(add_constraint) => { + if let Some(ast::Constraint::PrimaryKeyConstraint(primary_key_constraint)) = + add_constraint.constraint() + { + if primary_key_constraint.using_index().is_none() { + ctx.report(Violation::new( + ErrorCode::AddingSerialPrimaryKeyField, + "Adding a primary key constraint requires an `ACCESS EXCLUSIVE` lock that will block all reads and writes to the table while the primary key index is built.".into(), + primary_key_constraint.syntax().text_range(), + None, + )); + } + } + } + ast::AlterTableAction::AddColumn(add_column) => { + for constraint in add_column.constraints() { + if let ast::Constraint::PrimaryKeyConstraint(primary_key_constraint) = + constraint + { + if primary_key_constraint.using_index().is_none() { + ctx.report(Violation::new( + ErrorCode::AddingSerialPrimaryKeyField, + "Adding a primary key constraint requires an `ACCESS EXCLUSIVE` lock that will block all reads and writes to the table while the primary key index is built.".into(), + primary_key_constraint.syntax().text_range(), + None, + )); + } + } + } + } + _ => (), + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn serial_primary_key() { + let sql = r#" + ALTER TABLE a ADD COLUMN b SERIAL PRIMARY KEY; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingPrimaryKeyConstraint]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn plain_primary_key() { + let sql = r#" +ALTER TABLE items ADD PRIMARY KEY (id); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingPrimaryKeyConstraint]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn okay_add_constraint() { + let sql = r#" +ALTER TABLE items ADD CONSTRAINT items_pk PRIMARY KEY USING INDEX items_pk; + "#; + + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingPrimaryKeyConstraint]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } +} diff --git a/crates/squawk_linter/src/rules/adding_required_field.rs b/crates/squawk_linter/src/rules/adding_required_field.rs new file mode 100644 index 00000000..7c3836ee --- /dev/null +++ b/crates/squawk_linter/src/rules/adding_required_field.rs @@ -0,0 +1,120 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn adding_required_field(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + if let ast::AlterTableAction::AddColumn(add_column) = action { + if has_generated_constrait(add_column.constraints()) { + continue; + } + if has_not_null_and_no_default_constraint(add_column.constraints()) { + ctx.report(Violation::new( + ErrorCode::AddingRequiredField, + "Adding a new column that is `NOT NULL` and has no default value to an existing table effectively makes it required.".into(), + add_column.syntax().text_range(), + None, + )); + } + } + } + } + } +} + +fn has_generated_constrait(constraints: ast::AstChildren) -> bool { + for c in constraints { + if let ast::Constraint::GeneratedConstraint(_) = c { + return true; + } + } + false +} + +fn has_not_null_and_no_default_constraint(constraints: ast::AstChildren) -> bool { + let mut has_not_null = false; + let mut has_default = false; + for c in constraints { + match c { + ast::Constraint::NotNullConstraint(_) => { + has_not_null = true; + } + ast::Constraint::DefaultConstraint(_) => { + has_default = true; + } + _ => (), + } + } + has_not_null && !has_default +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn nullable_ok() { + let sql = r#" +ALTER TABLE "recipe" ADD COLUMN "public" boolean; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingRequiredField]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } + + #[test] + fn not_null_with_default() { + let sql = r#" +ALTER TABLE "recipe" ADD COLUMN "public" boolean NOT NULL DEFAULT true; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingRequiredField]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } + + #[test] + fn not_null_without_default() { + let sql = r#" +ALTER TABLE "recipe" ADD COLUMN "public" boolean NOT NULL; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingRequiredField]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } + + #[test] + fn generated_stored_not_null_ok() { + let sql = r#" +ALTER TABLE foo + ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED NOT NULL; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingRequiredField]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } + + #[test] + fn generated_stored_ok() { + let sql = r#" + ALTER TABLE foo + ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED ; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::AddingRequiredField]); + let errors = linter.lint(file, sql); + assert!(errors.is_empty()); + } +} diff --git a/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs b/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs new file mode 100644 index 00000000..0aae03aa --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs @@ -0,0 +1,61 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn ban_alter_domain_with_add_constraint(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterDomain(alter_domain) = item { + let actions = alter_domain.actions(); + for action in actions { + if let ast::AlterDomainAction::AddConstraint(add_constraint) = action { + ctx.report(Violation::new( + ErrorCode::BanAlterDomainWithAddConstraint, + "Domains with constraints have poor support for online migrations. Use table and column constraints instead.".into(), + add_constraint.syntax().text_range(), + None, + )) + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn err() { + let sql = r#" + ALTER DOMAIN domain_name ADD CONSTRAINT constraint_name CHECK (value > 0); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanAlterDomainWithAddConstraint]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn ok() { + let sql = r#" + ALTER DOMAIN domain_name_2 SET NOT NULL; + ALTER DOMAIN domain_name_3 DROP CONSTRAINT other_domain_name; + ALTER DOMAIN domain_name_4 RENAME CONSTRAINT constraint_name TO other_constraint_name; + ALTER DOMAIN domain_name_5 RENAME TO other_domain_name; + ALTER DOMAIN domain_name_6 VALIDATE CONSTRAINT constraint_name; + ALTER DOMAIN domain_name_7 OWNER TO you; + ALTER DOMAIN domain_name_8 SET SCHEMA foo; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanAlterDomainWithAddConstraint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/ban_char_field.rs b/crates/squawk_linter/src/rules/ban_char_field.rs new file mode 100644 index 00000000..e2c58106 --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_char_field.rs @@ -0,0 +1,161 @@ +use syntax::{ + ast::{self, AstNode}, + Parse, SourceFile, TokenText, +}; + +use crate::prefer_big_int::check_not_allowed_types; +use crate::{ErrorCode, Linter, Violation}; + +fn is_char_type(x: TokenText<'_>) -> bool { + if x == "char" || x == "character" || x == "bpchar" { + return true; + } + false +} + +fn check_path_type(ctx: &mut Linter, path_type: ast::PathType) { + if let Some(name_ref) = path_type + .path() + .and_then(|x| x.segment()) + .and_then(|x| x.name_ref()) + { + if is_char_type(name_ref.text()) { + ctx.report(Violation::new( + ErrorCode::BanCharField, + "Using `character` is likely a mistake and should almost always be replaced by `text` or `varchar`.".into(), + path_type.syntax().text_range(), + None, + )); + } + } +} + +fn check_char_type(ctx: &mut Linter, char_type: ast::CharType) { + if is_char_type(char_type.text()) { + ctx.report(Violation::new( + ErrorCode::BanCharField, + "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.".into(), + char_type.syntax().text_range(), + None, + )); + } +} + +fn check_ty(ctx: &mut Linter, ty: Option) { + match ty { + Some(ast::Type::ArrayType(array_type)) => match array_type.ty() { + Some(ast::Type::CharType(char_type)) => { + check_char_type(ctx, char_type); + } + Some(ast::Type::PathType(path_type)) => { + check_path_type(ctx, path_type); + } + _ => (), + }, + Some(ast::Type::PathType(path_type)) => { + check_path_type(ctx, path_type); + } + Some(ast::Type::CharType(char_type)) => { + check_char_type(ctx, char_type); + } + _ => (), + } +} + +pub(crate) fn ban_char_field(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + check_not_allowed_types(ctx, &file, check_ty); +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn creating_table_with_char_errors() { + let sql = r#" +CREATE TABLE "core_bar" ( + "id" serial NOT NULL PRIMARY KEY, + "alpha" char(100) NOT NULL, + "beta" character(100) NOT NULL, + "charlie" char NOT NULL, + "delta" character NOT NULL +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanCharField]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn creating_table_with_var_char_and_text_okay() { + let sql = r#" +CREATE TABLE "core_bar" ( + "id" serial NOT NULL PRIMARY KEY, + "alpha" varchar(100) NOT NULL, + "beta" text NOT NULL +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanCharField]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn all_the_types() { + let sql = r#" +create table t ( + a serial not null primary key, + b char(100), + c character(100), + d char, + e character, + f double precision, + g time with time zone, + h interval, + j int[5][10], + k bar(10), + l bit varying, + m int array[], + o pg_catalog.char, + p char[] +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanCharField]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn array_char_type_err() { + let sql = r#" +create table t ( + a char[] +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanCharField]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_err() { + let sql = r#" +alter table t add column c char; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanCharField]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs b/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs new file mode 100644 index 00000000..2319c2bb --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs @@ -0,0 +1,123 @@ +use syntax::{ + ast::{self, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn ban_concurrent_index_creation_in_transaction( + ctx: &mut Linter, + parse: &Parse, +) { + let mut in_transaction = ctx.settings.assume_in_transaction; + let file = parse.tree(); + let mut errors = vec![]; + let mut stmt_count = 0; + for item in file.items() { + stmt_count += 1; + match item { + ast::Item::Begin(_) => { + in_transaction = true; + } + ast::Item::Commit(_) => { + in_transaction = false; + } + ast::Item::CreateIndex(create_index) => { + if in_transaction { + if let Some(concurrently) = create_index.concurrently_token() { + errors.push(Violation::new( + ErrorCode::BanConcurrentIndexCreationInTransaction, + "While regular index creation can happen inside a transaction, this is not allowed when the CONCURRENTLY option is used.".into(), + concurrently.text_range(), + None, + )); + } + } + } + _ => (), + } + } + if stmt_count > 1 { + for error in errors { + ctx.report(error); + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn ban_concurrent_index_creation_in_transaction_err() { + let sql = r#" + -- instead of + BEGIN; + CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); + COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn ban_concurrent_index_creation_in_transaction_ok() { + let sql = r#" + -- run outside a transaction + CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn assuming_in_transaction_err() { + let sql = r#" + -- instead of + CREATE UNIQUE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); + ALTER TABLE "table_name" ADD CONSTRAINT "field_name_id" UNIQUE USING INDEX "field_name_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn assuming_in_transaction_ok() { + let sql = r#" + -- run index creation in a standalone migration + CREATE UNIQUE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn assuming_in_transaction_ok_also() { + let sql = r#" + -- the following will work too + COMMIT; + CREATE UNIQUE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); + BEGIN; + ALTER TABLE "table_name" ADD CONSTRAINT "field_name_id" UNIQUE USING INDEX "field_name_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs b/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs new file mode 100644 index 00000000..198694bd --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs @@ -0,0 +1,82 @@ +use rowan::TextRange; +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn ban_create_domain_with_constraint(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + match item { + ast::Item::CreateDomain(domain) => { + let range = domain.constraints().map(|c| c.syntax().text_range()).fold( + None, + |prev, cur| match prev { + None => Some(cur), + Some(prev) => { + let new_start = prev.start().min(cur.start()); + let new_end = prev.end().max(cur.end()); + Some(TextRange::new(new_start, new_end)) + } + }, + ); + if let Some(range) = range { + ctx.report(Violation::new( + ErrorCode::BanCreateDomainWithConstraint, + "Domains with constraints have poor support for online migrations. Use table and column constraints instead.".into(), + range, + None, + )) + } + } + _ => (), + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +CREATE DOMAIN domain_name_3 AS NUMERIC(15,5) CHECK (value > 0); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanCreateDomainWithConstraint]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn err_with_multiple_constraints() { + // checking that we highlight all the constraints in our range + let sql = r#" +create domain d as t check (value > 0) not null; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanCreateDomainWithConstraint]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn ok() { + // creating without a constraint is okay + let sql = r#" +CREATE DOMAIN domain_name_1 AS TEXT; +CREATE DOMAIN domain_name_2 AS CHARACTER VARYING; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanCreateDomainWithConstraint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/ban_drop_column.rs b/crates/squawk_linter/src/rules/ban_drop_column.rs new file mode 100644 index 00000000..cfbcee9a --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_column.rs @@ -0,0 +1,43 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn ban_drop_column(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + if let ast::AlterTableAction::DropColumn(drop_column) = action { + ctx.report(Violation::new( + ErrorCode::BanDropColumn, + "Dropping a column may break existing clients.".into(), + drop_column.syntax().text_range(), + None, + )); + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +ALTER TABLE "bar_tbl" DROP COLUMN "foo_col" CASCADE; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanDropColumn]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/ban_drop_database.rs b/crates/squawk_linter/src/rules/ban_drop_database.rs new file mode 100644 index 00000000..6d2a91d5 --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_database.rs @@ -0,0 +1,42 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +/// Brad's Rule aka ban dropping database statements. +pub(crate) fn ban_drop_database(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::DropDatabase(drop_database) = item { + ctx.report(Violation::new( + ErrorCode::BanDropDatabase, + "Dropping a database may break existing clients.".into(), + drop_database.syntax().text_range(), + None, + )); + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn ban_drop_database() { + let sql = r#" + DROP DATABASE "table_name"; + DROP DATABASE IF EXISTS "table_name"; + DROP DATABASE IF EXISTS "table_name" + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanDropDatabase]); + let errors = linter.lint(file, sql); + assert!(!errors.is_empty()); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/ban_drop_not_null.rs b/crates/squawk_linter/src/rules/ban_drop_not_null.rs new file mode 100644 index 00000000..a0091b33 --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_not_null.rs @@ -0,0 +1,47 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn ban_drop_not_null(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + if let ast::AlterTableAction::AlterColumn(alter_column) = action { + if let Some(ast::AlterColumnOption::DropNotNull(drop_not_null)) = + alter_column.option() + { + ctx.report(Violation::new( + ErrorCode::BanDropNotNull, + "Dropping a NOT NULL constraint may break existing clients.".into(), + drop_not_null.syntax().text_range(), + None, + )); + } + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +ALTER TABLE "bar_tbl" ALTER COLUMN "foo_col" DROP NOT NULL; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanDropNotNull]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/ban_drop_table.rs b/crates/squawk_linter/src/rules/ban_drop_table.rs new file mode 100644 index 00000000..418eb790 --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_table.rs @@ -0,0 +1,41 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn ban_drop_table(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::DropTable(drop_table) = item { + ctx.report(Violation::new( + ErrorCode::BanDropTable, + "Dropping a table may break existing clients.".into(), + drop_table.syntax().text_range(), + None, + )); + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +DROP TABLE "table_name"; +DROP TABLE IF EXISTS "table_name"; +DROP TABLE IF EXISTS "table_name" + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::BanDropTable]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/changing_column_type.rs b/crates/squawk_linter/src/rules/changing_column_type.rs new file mode 100644 index 00000000..d6095d12 --- /dev/null +++ b/crates/squawk_linter/src/rules/changing_column_type.rs @@ -0,0 +1,68 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn changing_column_type(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + if let ast::AlterTableAction::AlterColumn(alter_column) = action { + if let Some(ast::AlterColumnOption::SetType(set_type)) = alter_column.option() { + ctx.report(Violation::new( + ErrorCode::ChangingColumnType, + "Changing a column type requires an `ACCESS EXCLUSIVE` lock on the table which blocks reads and writes while the table is rewritten. Changing the type of the column may also break other clients reading from the table.".into(), + set_type.syntax().text_range(), + None, + )); + } + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +BEGIN; +-- +-- Alter field edits on recipe +-- +ALTER TABLE "core_recipe" ALTER COLUMN "edits" TYPE text USING "edits"::text; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ChangingColumnType]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn another_err() { + let sql = r#" +BEGIN; +-- +-- Alter field foo on recipe +-- +ALTER TABLE "core_recipe" ALTER COLUMN "foo" TYPE varchar(255) USING "foo"::varchar(255); +ALTER TABLE "core_recipe" ALTER COLUMN "foo" TYPE text USING "foo"::text; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ChangingColumnType]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs b/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs new file mode 100644 index 00000000..52a5889c --- /dev/null +++ b/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs @@ -0,0 +1,307 @@ +use std::collections::HashSet; + +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{text::trim_quotes, ErrorCode, Linter, Violation}; + +pub fn tables_created_in_transaction( + assume_in_transaction: bool, + file: &ast::SourceFile, +) -> HashSet { + let mut created_table_names = HashSet::new(); + let mut inside_transaction = assume_in_transaction; + for item in file.items() { + match item { + ast::Item::Begin(_) => { + inside_transaction = true; + } + ast::Item::Commit(_) => { + inside_transaction = false; + } + ast::Item::CreateTable(create_table) if inside_transaction => { + let Some(table_name) = create_table + .path() + .and_then(|x| x.segment()) + .and_then(|x| x.name()) + else { + continue; + }; + created_table_names.insert(trim_quotes(&table_name.text()).to_string()); + } + _ => (), + } + } + created_table_names +} + +fn not_valid_validate_in_transaction( + ctx: &mut Linter, + assume_in_transaction: bool, + file: &ast::SourceFile, +) { + let mut inside_transaction = assume_in_transaction; + let mut not_valid_names: HashSet = HashSet::new(); + for item in file.items() { + match item { + ast::Item::AlterTable(alter_table) => { + for action in alter_table.actions() { + match action { + ast::AlterTableAction::ValidateConstraint(validate_constraint) => { + if let Some(constraint_name) = + validate_constraint.name_ref().map(|x| x.text().to_string()) + { + if inside_transaction + && not_valid_names.contains(trim_quotes(&constraint_name)) + { + ctx.report( + Violation::new( + ErrorCode::ConstraintMissingNotValid, + "Using NOT VALID and VALIDATE CONSTRAINT in the same transaction will block all reads while the constraint is validated.".into(), + validate_constraint.syntax().text_range(), + vec![ + "Add constraint as NOT VALID in one transaction and VALIDATE CONSTRAINT in a separate transaction.".into(), + ] + )) + } + } + } + ast::AlterTableAction::AddConstraint(add_constraint) => { + if add_constraint.not_valid().is_some() { + if let Some(constraint) = add_constraint.constraint() { + if let Some(constraint_name) = constraint.name() { + not_valid_names.insert( + trim_quotes(&constraint_name.text()).to_string(), + ); + } + } + } + } + _ => (), + } + } + } + ast::Item::Begin(_) => { + if !inside_transaction { + not_valid_names.clear(); + } + inside_transaction = true; + } + ast::Item::Commit(_) => { + inside_transaction = false; + } + _ => (), + } + } +} + +pub(crate) fn constraint_missing_not_valid(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + + let assume_in_transaction = ctx.settings.assume_in_transaction; + + not_valid_validate_in_transaction(ctx, assume_in_transaction, &file); + + let tables_created = tables_created_in_transaction(assume_in_transaction, &file); + + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + let Some(table_name) = alter_table + .path() + .and_then(|x| x.segment()) + .and_then(|x| x.name_ref()) + .map(|x| x.text().to_string()) + else { + continue; + }; + for action in alter_table.actions() { + if let ast::AlterTableAction::AddConstraint(add_constraint) = action { + if !tables_created.contains(trim_quotes(&table_name)) + && add_constraint.not_valid().is_none() + { + if let Some(ast::Constraint::UniqueConstraint(uc)) = + add_constraint.constraint() + { + if uc.using_index().is_some() { + continue; + } + } + + ctx.report(Violation::new( + ErrorCode::ConstraintMissingNotValid, + "By default new constraints require a table scan and block writes to the table while that scan occurs.".into(), + add_constraint.syntax().text_range(), + None, + )); + } + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn not_valid_validate_transaction_err() { + let sql = r#" +BEGIN; +ALTER TABLE "app_email" ADD CONSTRAINT "fk_user" FOREIGN KEY (user_id) REFERENCES "app_user" (id) NOT VALID; +ALTER TABLE "app_email" VALIDATE CONSTRAINT "fk_user"; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn not_valid_validate_assume_transaction_err() { + let sql = r#" +ALTER TABLE "app_email" ADD CONSTRAINT "fk_user" FOREIGN KEY (user_id) REFERENCES "app_user" (id) NOT VALID; +ALTER TABLE "app_email" VALIDATE CONSTRAINT "fk_user"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn not_valid_validate_with_assume_in_transaction_with_explicit_commit_err() { + let sql = r#" +ALTER TABLE "app_email" ADD CONSTRAINT "fk_user" FOREIGN KEY (user_id) REFERENCES "app_user" (id) NOT VALID; +ALTER TABLE "app_email" VALIDATE CONSTRAINT "fk_user"; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn adding_fk_err() { + let sql = r#" +-- instead of +ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn adding_fk_not_valid_ok() { + let sql = r#" +-- use `NOT VALID` +ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address) NOT VALID; +ALTER TABLE distributors VALIDATE CONSTRAINT distfk; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn adding_check_constraint_err() { + let sql = r#" +-- instead of +ALTER TABLE "accounts" ADD CONSTRAINT "positive_balance" CHECK ("balance" >= 0); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn adding_check_constraint_ok() { + let sql = r#" +-- use `NOT VALID` +ALTER TABLE "accounts" ADD CONSTRAINT "positive_balance" CHECK ("balance" >= 0) NOT VALID; +ALTER TABLE accounts VALIDATE CONSTRAINT positive_balance; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn new_table_with_transaction_ok() { + let sql = r#" +BEGIN; +CREATE TABLE "core_foo" ( +"id" serial NOT NULL PRIMARY KEY, +"age" integer NOT NULL +); +ALTER TABLE "core_foo" ADD CONSTRAINT "age_restriction" CHECK ("age" >= 25); +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn new_table_assume_transaction_ok() { + let sql = r#" +CREATE TABLE "core_foo" ( +"id" serial NOT NULL PRIMARY KEY, +"age" integer NOT NULL +); +ALTER TABLE "core_foo" ADD CONSTRAINT "age_restriction" CHECK ("age" >= 25); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn regression_with_indexing_ok() { + let sql = r#" +CREATE TABLE "core_foo" ( +"id" serial NOT NULL PRIMARY KEY, +"age" integer NOT NULL +); +ALTER TABLE "core_foo" ADD CONSTRAINT "age_restriction" CHECK ("age" >= 25); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn using_unique_index_ok() { + let sql = r#" +ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/disallow_unique_constraint.rs b/crates/squawk_linter/src/rules/disallow_unique_constraint.rs new file mode 100644 index 00000000..b56d23f0 --- /dev/null +++ b/crates/squawk_linter/src/rules/disallow_unique_constraint.rs @@ -0,0 +1,172 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{text::trim_quotes, ErrorCode, Linter, Violation}; + +use super::constraint_missing_not_valid::tables_created_in_transaction; + +pub(crate) fn disallow_unique_constraint(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + let tables_created = tables_created_in_transaction(ctx.settings.assume_in_transaction, &file); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + let Some(table_name) = alter_table + .path() + .and_then(|x| x.segment()) + .and_then(|x| x.name_ref()) + .map(|x| x.text().to_string()) + else { + continue; + }; + for action in alter_table.actions() { + match action { + ast::AlterTableAction::AddConstraint(add_constraint) => { + if let Some(ast::Constraint::UniqueConstraint(unique_constraint)) = + add_constraint.constraint() + { + if unique_constraint.using_index().is_none() + && !tables_created.contains(trim_quotes(&table_name)) + { + ctx.report(Violation::new( + ErrorCode::DisallowedUniqueConstraint, + "Adding a `UNIQUE` constraint requires an `ACCESS EXCLUSIVE` lock which blocks reads and writes to the table while the index is built.".into(), + unique_constraint.syntax().text_range(), + None, + )); + } + } + } + ast::AlterTableAction::AddColumn(add_column) => { + for constraint in add_column.constraints() { + if let ast::Constraint::UniqueConstraint(unique_constraint) = constraint + { + ctx.report(Violation::new( + ErrorCode::DisallowedUniqueConstraint, + "Adding a `UNIQUE` constraint requires an `ACCESS EXCLUSIVE` lock which blocks reads and writes to the table while the index is built.".into(), + unique_constraint.syntax().text_range(), + None, + )); + } + } + } + _ => (), + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn adding_unique_constraint_err() { + let sql = r#" +ALTER TABLE table_name ADD CONSTRAINT field_name_constraint UNIQUE (field_name); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn ok() { + let sql = r#" +ALTER TABLE table_name DROP CONSTRAINT field_name_constraint; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn also_ok() { + let sql = r#" +CREATE UNIQUE INDEX CONCURRENTLY dist_id_temp_idx ON distributors (dist_id); +ALTER TABLE distributors DROP CONSTRAINT distributors_pkey, +ADD CONSTRAINT distributors_pkey PRIMARY KEY USING INDEX dist_id_temp_idx; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn unique_constraint_ok() { + let sql = r#" +CREATE UNIQUE INDEX CONCURRENTLY "legacy_questiongrouppg_mongo_id_1f8f47d9_uniq_idx" + ON "legacy_questiongrouppg" ("mongo_id"); +ALTER TABLE "legacy_questiongrouppg" ADD CONSTRAINT "legacy_questiongrouppg_mongo_id_1f8f47d9_uniq" UNIQUE USING INDEX "legacy_questiongrouppg_mongo_id_1f8f47d9_uniq_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn unique_constraint_after_create_table() { + let sql = r#" +BEGIN; +CREATE TABLE products ( + id bigint generated by default as identity primary key, + sku text not null +); +ALTER TABLE products ADD CONSTRAINT sku_constraint UNIQUE (sku); +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn unique_constraint_after_create_table_with_assume_in_transaction() { + let sql = r#" +CREATE TABLE products ( + id bigint generated by default as identity primary key, + sku text not null +); +ALTER TABLE products ADD CONSTRAINT sku_constraint UNIQUE (sku); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn unique_constraint_inline_add_column_err() { + let sql = r#" +ALTER TABLE foo ADD COLUMN bar text CONSTRAINT foo_bar_unique UNIQUE; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn unique_constraint_inline_add_column_unique_err() { + let sql = r#" +ALTER TABLE foo ADD COLUMN bar text UNIQUE; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/mod.rs b/crates/squawk_linter/src/rules/mod.rs new file mode 100644 index 00000000..934e70c1 --- /dev/null +++ b/crates/squawk_linter/src/rules/mod.rs @@ -0,0 +1,57 @@ +pub(crate) mod adding_field_with_default; +pub(crate) mod adding_foreign_key_constraint; +pub(crate) mod adding_not_null_field; +pub(crate) mod adding_primary_key_constraint; +pub(crate) mod adding_required_field; +pub(crate) mod ban_char_field; +pub(crate) mod ban_concurrent_index_creation_in_transaction; +pub(crate) mod ban_create_domain_with_constraint; +pub(crate) mod ban_drop_column; +pub(crate) mod ban_drop_database; +pub(crate) mod ban_drop_not_null; +pub(crate) mod ban_drop_table; +pub(crate) mod changing_column_type; +pub(crate) mod constraint_missing_not_valid; +pub(crate) mod disallow_unique_constraint; +pub(crate) mod prefer_big_int; +pub(crate) mod prefer_bigint_over_int; +pub(crate) mod prefer_bigint_over_smallint; +pub(crate) mod prefer_identity; +pub(crate) mod prefer_robust_stmts; +pub(crate) mod prefer_text_field; +pub(crate) mod prefer_timestamptz; +pub(crate) mod renaming_column; +pub(crate) mod renaming_table; +pub(crate) mod require_concurrent_index_creation; +pub(crate) mod require_concurrent_index_deletion; +pub(crate) mod ban_alter_domain_with_add_constraint; +// xtask:new-lint:mod-decl + +pub(crate) use adding_field_with_default::adding_field_with_default; +pub(crate) use adding_foreign_key_constraint::adding_foreign_key_constraint; +pub(crate) use adding_not_null_field::adding_not_null_field; +pub(crate) use adding_primary_key_constraint::adding_primary_key_constraint; +pub(crate) use adding_required_field::adding_required_field; +pub(crate) use ban_char_field::ban_char_field; +pub(crate) use ban_concurrent_index_creation_in_transaction::ban_concurrent_index_creation_in_transaction; +pub(crate) use ban_create_domain_with_constraint::ban_create_domain_with_constraint; +pub(crate) use ban_drop_column::ban_drop_column; +pub(crate) use ban_drop_database::ban_drop_database; +pub(crate) use ban_drop_not_null::ban_drop_not_null; +pub(crate) use ban_drop_table::ban_drop_table; +pub(crate) use changing_column_type::changing_column_type; +pub(crate) use constraint_missing_not_valid::constraint_missing_not_valid; +pub(crate) use disallow_unique_constraint::disallow_unique_constraint; +pub(crate) use prefer_big_int::prefer_big_int; +pub(crate) use prefer_bigint_over_int::prefer_bigint_over_int; +pub(crate) use prefer_bigint_over_smallint::prefer_bigint_over_smallint; +pub(crate) use prefer_identity::prefer_identity; +pub(crate) use prefer_robust_stmts::prefer_robust_stmts; +pub(crate) use prefer_text_field::prefer_text_field; +pub(crate) use prefer_timestamptz::prefer_timestamptz; +pub(crate) use renaming_column::renaming_column; +pub(crate) use renaming_table::renaming_table; +pub(crate) use require_concurrent_index_creation::require_concurrent_index_creation; +pub(crate) use require_concurrent_index_deletion::require_concurrent_index_deletion; +pub(crate) use ban_alter_domain_with_add_constraint::ban_alter_domain_with_add_constraint; +// xtask:new-lint:export diff --git a/crates/squawk_linter/src/rules/non_volatile_built_in_functions.txt b/crates/squawk_linter/src/rules/non_volatile_built_in_functions.txt new file mode 100644 index 00000000..cc13e6d7 --- /dev/null +++ b/crates/squawk_linter/src/rules/non_volatile_built_in_functions.txt @@ -0,0 +1,2963 @@ +boolin +boolout +byteain +byteaout +charin +charout +namein +nameout +int2in +int2out +int2vectorin +int2vectorout +int4in +int4out +regprocin +regprocout +to_regproc +to_regprocedure +textin +textout +tidin +tidout +xidin +xidout +xid8in +xid8out +xid8recv +xid8send +cidin +cidout +oidvectorin +oidvectorout +boollt +boolgt +booleq +chareq +nameeq +int2eq +int2lt +int4eq +int4lt +texteq +starts_with +xideq +xidneq +xid8eq +xid8ne +xid8lt +xid8gt +xid8le +xid8ge +xid8cmp +xid +cideq +charne +charlt +charle +chargt +charge +int4 +char +nameregexeq +nameregexne +textregexeq +textregexne +textregexeq_support +textlen +textcat +boolne +version +pg_ddl_command_in +pg_ddl_command_out +pg_ddl_command_recv +pg_ddl_command_send +eqsel +neqsel +scalarltsel +scalargtsel +eqjoinsel +neqjoinsel +scalarltjoinsel +scalargtjoinsel +scalarlesel +scalargesel +scalarlejoinsel +scalargejoinsel +unknownin +unknownout +box_above_eq +box_below_eq +point_in +point_out +lseg_in +lseg_out +path_in +path_out +box_in +box_out +box_overlap +box_ge +box_gt +box_eq +box_lt +box_le +point_above +point_left +point_right +point_below +point_eq +on_pb +on_ppath +box_center +areasel +areajoinsel +int4mul +int4ne +int2ne +int2gt +int4gt +int2le +int4le +int4ge +int2ge +int2mul +int2div +int4div +int2mod +int4mod +textne +int24eq +int42eq +int24lt +int42lt +int24gt +int42gt +int24ne +int42ne +int24le +int42le +int24ge +int42ge +int24mul +int42mul +int24div +int42div +int2pl +int4pl +int24pl +int42pl +int2mi +int4mi +int24mi +int42mi +oideq +oidne +box_same +box_contain +box_left +box_overleft +box_overright +box_right +box_contained +box_contain_pt +pg_node_tree_in +pg_node_tree_out +pg_node_tree_recv +pg_node_tree_send +float4in +float4out +float4mul +float4div +float4pl +float4mi +float4um +float4abs +float4_accum +float4larger +float4smaller +int4um +int2um +float8in +float8out +float8mul +float8div +float8pl +float8mi +float8um +float8abs +float8_accum +float8_combine +float8larger +float8smaller +lseg_center +path_center +poly_center +dround +dtrunc +ceil +ceiling +floor +sign +dsqrt +dcbrt +dpow +dexp +dlog1 +float8 +float4 +int2 +int2 +line_distance +nameeqtext +namelttext +nameletext +namegetext +namegttext +namenetext +btnametextcmp +texteqname +textltname +textlename +textgename +textgtname +textnename +bttextnamecmp +nameconcatoid +inter_sl +inter_lb +float48mul +float48div +float48pl +float48mi +float84mul +float84div +float84pl +float84mi +float4eq +float4ne +float4lt +float4le +float4gt +float4ge +float8eq +float8ne +float8lt +float8le +float8gt +float8ge +float48eq +float48ne +float48lt +float48le +float48gt +float48ge +float84eq +float84ne +float84lt +float84le +float84gt +float84ge +width_bucket +float8 +float4 +int4 +int2 +float8 +int4 +float4 +int4 +pg_indexam_has_property +pg_index_has_property +pg_index_column_has_property +pg_indexam_progress_phasename +poly_same +poly_contain +poly_left +poly_overleft +poly_overright +poly_right +poly_contained +poly_overlap +poly_in +poly_out +btint2cmp +btint2sortsupport +btint4cmp +btint4sortsupport +btint8cmp +btint8sortsupport +btfloat4cmp +btfloat4sortsupport +btfloat8cmp +btfloat8sortsupport +btoidcmp +btoidsortsupport +btoidvectorcmp +btcharcmp +btnamecmp +btnamesortsupport +bttextcmp +bttextsortsupport +btvarstrequalimage +cash_cmp +btarraycmp +in_range +in_range +in_range +in_range +in_range +in_range +in_range +in_range +in_range +in_range +lseg_distance +lseg_interpt +dist_ps +dist_sp +dist_pb +dist_bp +dist_sb +dist_bs +close_ps +close_pb +close_sb +on_ps +path_distance +dist_ppath +dist_pathp +on_sb +inter_sb +text +text +name +bpchar +name +hashint2 +hashint2extended +hashint4 +hashint4extended +hashint8 +hashint8extended +hashfloat4 +hashfloat4extended +hashfloat8 +hashfloat8extended +hashoid +hashoidextended +hashchar +hashcharextended +hashname +hashnameextended +hashtext +hashtextextended +hashvarlena +hashvarlenaextended +hashoidvector +hashoidvectorextended +hash_aclitem +hash_aclitem_extended +hashmacaddr +hashmacaddrextended +hashinet +hashinetextended +hash_numeric +hash_numeric_extended +hashmacaddr8 +hashmacaddr8extended +num_nulls +num_nonnulls +text_larger +text_smaller +int8in +int8out +int8um +int8pl +int8mi +int8mul +int8div +int8eq +int8ne +int8lt +int8gt +int8le +int8ge +int84eq +int84ne +int84lt +int84gt +int84le +int84ge +int4 +int8 +float8 +int8 +hash_array +hash_array_extended +float4 +int8 +int2 +int8 +namelt +namele +namegt +namege +namene +bpchar +varchar_support +varchar +oidvectorne +oidvectorlt +oidvectorle +oidvectoreq +oidvectorge +oidvectorgt +getpgusername +oidlt +oidle +octet_length +get_byte +set_byte +get_bit +set_bit +overlay +overlay +bit_count +dist_pl +dist_lp +dist_lb +dist_bl +dist_sl +dist_ls +dist_cpoly +dist_polyc +poly_distance +dist_ppoly +dist_polyp +dist_cpoint +text_lt +text_le +text_gt +text_ge +current_user +session_user +array_eq +array_ne +array_lt +array_gt +array_le +array_ge +array_dims +array_ndims +array_in +array_out +array_lower +array_upper +array_length +cardinality +array_append +array_prepend +array_cat +string_to_array +string_to_array +string_to_table +string_to_table +array_to_string +array_to_string +array_larger +array_smaller +array_position +array_position +array_positions +generate_subscripts +generate_subscripts +array_fill +array_fill +unnest +array_unnest_support +array_remove +array_replace +array_agg_transfn +array_agg_finalfn +array_agg +array_agg_array_transfn +array_agg_array_finalfn +array_agg +width_bucket +trim_array +array_typanalyze +arraycontsel +arraycontjoinsel +int4inc +int4larger +int4smaller +int2larger +int2smaller +cash_mul_flt4 +cash_div_flt4 +flt4_mul_cash +position +textlike +textlike_support +textnlike +int48eq +int48ne +int48lt +int48gt +int48le +int48ge +namelike +namenlike +bpchar +current_database +int8_mul_cash +int4_mul_cash +int2_mul_cash +cash_mul_int8 +cash_div_int8 +cash_mul_int4 +cash_div_int4 +cash_mul_int2 +cash_div_int2 +cash_in +cash_out +cash_eq +cash_ne +cash_lt +cash_le +cash_gt +cash_ge +cash_pl +cash_mi +cash_mul_flt8 +cash_div_flt8 +cashlarger +cashsmaller +flt8_mul_cash +cash_words +cash_div_cash +numeric +money +money +money +mod +mod +int8mod +mod +gcd +gcd +lcm +lcm +char +text +on_pl +on_sl +close_pl +close_sl +close_lb +path_inter +area +width +height +box_distance +area +box_intersect +bound_box +diagonal +path_n_lt +path_n_gt +path_n_eq +path_n_le +path_n_ge +path_length +point_ne +point_vert +point_horiz +point_distance +slope +lseg +lseg_intersect +lseg_parallel +lseg_perp +lseg_vertical +lseg_horizontal +lseg_eq +timezone +aclitemin +aclitemout +aclinsert +aclremove +aclcontains +aclitemeq +makeaclitem +acldefault +aclexplode +bpcharin +bpcharout +bpchartypmodin +bpchartypmodout +varcharin +varcharout +varchartypmodin +varchartypmodout +bpchareq +bpcharlt +bpcharle +bpchargt +bpcharge +bpcharne +bpchar_larger +bpchar_smaller +bpcharcmp +bpchar_sortsupport +hashbpchar +hashbpcharextended +format_type +date_in +date_out +date_eq +date_lt +date_le +date_gt +date_ge +date_ne +date_cmp +date_sortsupport +in_range +time_lt +time_le +time_gt +time_ge +time_ne +time_cmp +date_larger +date_smaller +date_mi +date_pli +date_mii +time_in +time_out +timetypmodin +timetypmodout +time_eq +circle_add_pt +circle_sub_pt +circle_mul_pt +circle_div_pt +timestamptz_in +timestamptz_out +timestamptztypmodin +timestamptztypmodout +timestamptz_eq +timestamptz_ne +timestamptz_lt +timestamptz_le +timestamptz_ge +timestamptz_gt +to_timestamp +timezone +interval_in +interval_out +intervaltypmodin +intervaltypmodout +interval_eq +interval_ne +interval_lt +interval_le +interval_ge +interval_gt +interval_um +interval_pl +interval_mi +date_part +extract +date_part +extract +timestamptz +justify_interval +justify_hours +justify_days +date +age +mxid_age +timestamptz_mi +timestamptz_pl_interval +timestamptz_mi_interval +timestamptz_smaller +timestamptz_larger +interval_smaller +interval_larger +age +interval_support +interval +date_trunc +date_trunc +date_trunc +int8inc +int8dec +int8inc_any +int8dec_any +int8abs +int8larger +int8smaller +texticregexeq +texticregexeq_support +texticregexne +nameicregexeq +nameicregexne +int4abs +int2abs +overlaps +datetime_pl +date_part +extract +int84pl +int84mi +int84mul +int84div +int48pl +int48mi +int48mul +int48div +int82pl +int82mi +int82mul +int82div +int28pl +int28mi +int28mul +int28div +oid +int8 +tideq +tidne +tidgt +tidlt +tidge +tidle +bttidcmp +tidlarger +tidsmaller +hashtid +hashtidextended +datetimetz_pl +now +transaction_timestamp +statement_timestamp +positionsel +positionjoinsel +contsel +contjoinsel +overlaps +overlaps +timestamp_in +timestamp_out +timestamptypmodin +timestamptypmodout +timestamptz_cmp +interval_cmp +time +length +length +xideqint4 +xidneqint4 +interval_div +dlog10 +log +log10 +ln +round +trunc +sqrt +cbrt +pow +power +exp +oidvectortypes +timetz_in +timetz_out +timetztypmodin +timetztypmodout +timetz_eq +timetz_ne +obj_description +timetz_lt +timetz_le +timetz_ge +timetz_gt +timetz_cmp +timestamptz +character_length +character_length +interval +char_length +octet_length +octet_length +time_larger +time_smaller +timetz_larger +timetz_smaller +char_length +extract +date_part +extract +timetz +isfinite +isfinite +isfinite +factorial +abs +abs +abs +abs +abs +name +varchar +current_schema +current_schemas +overlay +overlay +isvertical +ishorizontal +isparallel +isperp +isvertical +ishorizontal +isparallel +isperp +isvertical +ishorizontal +point +time +box +box_add +box_sub +box_mul +box_div +poly_contain_pt +pt_contained_poly +isclosed +isopen +path_npoints +pclose +popen +path_add +path_add_pt +path_sub_pt +path_mul_pt +path_div_pt +point +point_add +point_sub +point_mul +point_div +poly_npoints +box +path +polygon +polygon +circle_in +circle_out +circle_same +circle_contain +circle_left +circle_overleft +circle_overright +circle_right +circle_contained +circle_overlap +circle_below +circle_above +circle_eq +circle_ne +circle_lt +circle_gt +circle_le +circle_ge +area +diameter +radius +circle_distance +circle_center +circle +circle +polygon +dist_pc +circle_contain_pt +pt_contained_circle +box +circle +box +lseg_ne +lseg_lt +lseg_le +lseg_gt +lseg_ge +lseg_length +close_ls +close_lseg +line_in +line_out +line_eq +line +line_interpt +line_intersect +line_parallel +line_perp +line_vertical +line_horizontal +length +length +point +point +point +point +lseg +center +center +npoints +npoints +bit_in +bit_out +bittypmodin +bittypmodout +like +notlike +like +notlike +pg_sequence_parameters +varbit_in +varbit_out +varbittypmodin +varbittypmodout +biteq +bitne +bitge +bitgt +bitle +bitlt +bitcmp +asin +acos +atan +atan2 +sin +cos +tan +cot +asind +acosd +atand +atan2d +sind +cosd +tand +cotd +degrees +radians +pi +sinh +cosh +tanh +asinh +acosh +atanh +interval_mul +ascii +chr +repeat +similar_escape +similar_to_escape +similar_to_escape +mul_d_interval +bpcharlike +bpcharnlike +texticlike +texticlike_support +texticnlike +nameiclike +nameicnlike +like_escape +bpcharicregexeq +bpcharicregexne +bpcharregexeq +bpcharregexne +bpchariclike +bpcharicnlike +strpos +lower +upper +initcap +lpad +rpad +ltrim +rtrim +substr +translate +ltrim +rtrim +substr +btrim +btrim +substring +substring +replace +regexp_replace +regexp_replace +regexp_match +regexp_match +regexp_matches +regexp_matches +split_part +regexp_split_to_table +regexp_split_to_table +regexp_split_to_array +regexp_split_to_array +to_hex +to_hex +getdatabaseencoding +pg_client_encoding +length +convert_from +convert_to +convert +pg_char_to_encoding +pg_encoding_to_char +pg_encoding_max_length +oidgt +oidge +pg_get_ruledef +pg_get_viewdef +pg_get_viewdef +pg_get_userbyid +pg_get_indexdef +pg_get_statisticsobjdef +pg_get_statisticsobjdef_columns +pg_get_statisticsobjdef_expressions +pg_get_partkeydef +pg_get_partition_constraintdef +pg_get_triggerdef +pg_get_constraintdef +pg_get_expr +pg_get_serial_sequence +pg_get_functiondef +pg_get_function_arguments +pg_get_function_identity_arguments +pg_get_function_result +pg_get_function_arg_default +pg_get_function_sqlbody +pg_get_keywords +pg_get_catalog_foreign_keys +pg_options_to_table +pg_typeof +pg_collation_for +pg_relation_is_updatable +pg_column_is_updatable +pg_get_replica_identity_index +varbiteq +varbitne +varbitge +varbitgt +varbitle +varbitlt +varbitcmp +bitand +bitor +bitxor +bitnot +bitshiftleft +bitshiftright +bitcat +substring +length +octet_length +bit +int4 +bit +varbit_support +varbit +position +substring +overlay +overlay +get_bit +set_bit +bit_count +macaddr_in +macaddr_out +trunc +macaddr_eq +macaddr_lt +macaddr_le +macaddr_gt +macaddr_ge +macaddr_ne +macaddr_cmp +macaddr_not +macaddr_and +macaddr_or +macaddr_sortsupport +macaddr8_in +macaddr8_out +trunc +macaddr8_eq +macaddr8_lt +macaddr8_le +macaddr8_gt +macaddr8_ge +macaddr8_ne +macaddr8_cmp +macaddr8_not +macaddr8_and +macaddr8_or +macaddr8 +macaddr +macaddr8_set7bit +inet_in +inet_out +cidr_in +cidr_out +network_eq +network_lt +network_le +network_gt +network_ge +network_ne +network_larger +network_smaller +network_cmp +network_sub +network_subeq +network_sup +network_supeq +network_subset_support +network_overlap +network_sortsupport +abbrev +abbrev +set_masklen +set_masklen +family +network +netmask +masklen +broadcast +host +text +hostmask +cidr +inet_client_addr +inet_client_port +inet_server_addr +inet_server_port +inetnot +inetand +inetor +inetpl +inetmi_int8 +inetmi +inet_same_family +inet_merge +inet_gist_consistent +inet_gist_union +inet_gist_compress +inet_gist_fetch +inet_gist_penalty +inet_gist_picksplit +inet_gist_same +inet_spg_config +inet_spg_choose +inet_spg_picksplit +inet_spg_inner_consistent +inet_spg_leaf_consistent +networksel +networkjoinsel +time_mi_time +boolle +boolge +btboolcmp +time_hash +time_hash_extended +timetz_hash +timetz_hash_extended +interval_hash +interval_hash_extended +numeric_in +numeric_out +numerictypmodin +numerictypmodout +numeric_support +numeric +numeric_abs +abs +sign +round +trunc +ceil +ceiling +floor +numeric_eq +numeric_ne +numeric_gt +numeric_ge +numeric_lt +numeric_le +numeric_add +numeric_sub +numeric_mul +numeric_div +mod +numeric_mod +gcd +lcm +sqrt +numeric_sqrt +exp +numeric_exp +ln +numeric_ln +log +numeric_log +pow +power +numeric_power +scale +min_scale +trim_scale +numeric +numeric +numeric +int4 +float4 +float8 +div +numeric_div_trunc +width_bucket +time_pl_interval +time_mi_interval +timetz_pl_interval +timetz_mi_interval +numeric_inc +numeric_smaller +numeric_larger +numeric_cmp +numeric_sortsupport +numeric_uminus +int8 +numeric +numeric +int2 +pg_lsn +bool +numeric +int2 +int4 +int8 +float4 +float8 +to_char +to_char +to_char +to_char +to_char +to_char +to_number +to_timestamp +to_date +to_char +quote_ident +quote_literal +quote_literal +quote_nullable +quote_nullable +oidin +oidout +concat +concat_ws +left +right +reverse +format +format +iclikesel +icnlikesel +iclikejoinsel +icnlikejoinsel +regexeqsel +likesel +icregexeqsel +regexnesel +nlikesel +icregexnesel +regexeqjoinsel +likejoinsel +icregexeqjoinsel +regexnejoinsel +nlikejoinsel +icregexnejoinsel +prefixsel +prefixjoinsel +float8_avg +float8_var_pop +float8_var_samp +float8_stddev_pop +float8_stddev_samp +numeric_accum +numeric_combine +numeric_avg_accum +numeric_avg_combine +numeric_avg_serialize +numeric_avg_deserialize +numeric_serialize +numeric_deserialize +numeric_accum_inv +int2_accum +int4_accum +int8_accum +numeric_poly_combine +numeric_poly_serialize +numeric_poly_deserialize +int8_avg_accum +int2_accum_inv +int4_accum_inv +int8_accum_inv +int8_avg_accum_inv +int8_avg_combine +int8_avg_serialize +int8_avg_deserialize +int4_avg_combine +numeric_sum +numeric_avg +numeric_var_pop +numeric_var_samp +numeric_stddev_pop +numeric_stddev_samp +int2_sum +int4_sum +int8_sum +numeric_poly_sum +numeric_poly_avg +numeric_poly_var_pop +numeric_poly_var_samp +numeric_poly_stddev_pop +numeric_poly_stddev_samp +interval_accum +interval_combine +interval_accum_inv +interval_avg +int2_avg_accum +int4_avg_accum +int2_avg_accum_inv +int4_avg_accum_inv +int8_avg +int2int4_sum +int8inc_float8_float8 +float8_regr_accum +float8_regr_combine +float8_regr_sxx +float8_regr_syy +float8_regr_sxy +float8_regr_avgx +float8_regr_avgy +float8_regr_r2 +float8_regr_slope +float8_regr_intercept +float8_covar_pop +float8_covar_samp +float8_corr +string_agg_transfn +string_agg_finalfn +string_agg +bytea_string_agg_transfn +bytea_string_agg_finalfn +string_agg +to_ascii +to_ascii +to_ascii +int28eq +int28ne +int28lt +int28gt +int28le +int28ge +int82eq +int82ne +int82lt +int82gt +int82le +int82ge +int2and +int2or +int2xor +int2not +int2shl +int2shr +int4and +int4or +int4xor +int4not +int4shl +int4shr +int8and +int8or +int8xor +int8not +int8shl +int8shr +int8up +int2up +int4up +float4up +float8up +numeric_uplus +has_table_privilege +has_table_privilege +has_table_privilege +has_table_privilege +has_table_privilege +has_table_privilege +has_sequence_privilege +has_sequence_privilege +has_sequence_privilege +has_sequence_privilege +has_sequence_privilege +has_sequence_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_column_privilege +has_any_column_privilege +has_any_column_privilege +has_any_column_privilege +has_any_column_privilege +has_any_column_privilege +has_any_column_privilege +pg_ndistinct_in +pg_ndistinct_out +pg_ndistinct_recv +pg_ndistinct_send +pg_dependencies_in +pg_dependencies_out +pg_dependencies_recv +pg_dependencies_send +pg_mcv_list_in +pg_mcv_list_out +pg_mcv_list_recv +pg_mcv_list_send +pg_mcv_list_items +pg_stat_get_numscans +pg_stat_get_tuples_returned +pg_stat_get_tuples_fetched +pg_stat_get_tuples_inserted +pg_stat_get_tuples_updated +pg_stat_get_tuples_deleted +pg_stat_get_tuples_hot_updated +pg_stat_get_live_tuples +pg_stat_get_dead_tuples +pg_stat_get_mod_since_analyze +pg_stat_get_ins_since_vacuum +pg_stat_get_blocks_fetched +pg_stat_get_blocks_hit +pg_stat_get_last_vacuum_time +pg_stat_get_last_autovacuum_time +pg_stat_get_last_analyze_time +pg_stat_get_last_autoanalyze_time +pg_stat_get_vacuum_count +pg_stat_get_autovacuum_count +pg_stat_get_analyze_count +pg_stat_get_autoanalyze_count +pg_stat_get_backend_idset +pg_stat_get_db_tuples_deleted +pg_stat_get_db_conflict_tablespace +pg_stat_get_db_conflict_lock +pg_stat_get_db_conflict_snapshot +pg_stat_get_activity +pg_stat_get_progress_info +pg_stat_get_wal_senders +pg_stat_get_wal_receiver +pg_stat_get_replication_slot +pg_stat_get_subscription +pg_backend_pid +pg_stat_get_backend_pid +pg_stat_get_backend_dbid +pg_stat_get_backend_userid +pg_stat_get_backend_activity +pg_stat_get_backend_wait_event_type +pg_stat_get_backend_wait_event +pg_stat_get_backend_activity_start +pg_stat_get_backend_xact_start +pg_stat_get_backend_start +pg_stat_get_backend_client_addr +pg_stat_get_backend_client_port +pg_stat_get_db_numbackends +pg_stat_get_db_xact_commit +pg_stat_get_db_xact_rollback +pg_stat_get_db_blocks_fetched +pg_stat_get_db_blocks_hit +pg_stat_get_db_tuples_returned +pg_stat_get_db_tuples_fetched +pg_stat_get_db_tuples_inserted +pg_stat_get_db_tuples_updated +pg_stat_get_db_conflict_bufferpin +pg_stat_get_db_conflict_startup_deadlock +pg_stat_get_db_conflict_all +pg_stat_get_db_deadlocks +pg_stat_get_db_checksum_failures +pg_stat_get_db_checksum_last_failure +pg_stat_get_db_stat_reset_time +pg_stat_get_db_temp_files +pg_stat_get_db_temp_bytes +pg_stat_get_db_blk_read_time +pg_stat_get_db_blk_write_time +pg_stat_get_db_session_time +pg_stat_get_db_active_time +pg_stat_get_db_idle_in_transaction_time +pg_stat_get_db_sessions +pg_stat_get_db_sessions_abandoned +pg_stat_get_db_sessions_fatal +pg_stat_get_db_sessions_killed +pg_stat_get_archiver +pg_stat_get_bgwriter_timed_checkpoints +pg_stat_get_bgwriter_requested_checkpoints +pg_stat_get_bgwriter_buf_written_checkpoints +pg_stat_get_bgwriter_buf_written_clean +pg_stat_get_bgwriter_maxwritten_clean +pg_stat_get_bgwriter_stat_reset_time +pg_stat_get_checkpoint_write_time +pg_stat_get_checkpoint_sync_time +pg_stat_get_buf_written_backend +pg_stat_get_buf_fsync_backend +pg_stat_get_buf_alloc +pg_stat_get_wal +pg_stat_get_slru +pg_stat_get_function_calls +pg_stat_get_function_total_time +pg_stat_get_function_self_time +pg_stat_get_snapshot_timestamp +pg_trigger_depth +pg_tablespace_location +encode +decode +byteaeq +bytealt +byteale +byteagt +byteage +byteane +byteacmp +bytea_sortsupport +timestamp_support +time_support +timestamp +oidlarger +oidsmaller +timestamptz +time +timetz +textanycat +anytextcat +bytealike +byteanlike +like +notlike +like_escape +length +byteacat +substring +substring +substr +substr +position +btrim +ltrim +rtrim +time +date_trunc +date_bin +date_bin +date_part +extract +timestamp +timestamp +timestamp +timestamptz +date +timestamp_mi +timestamp_pl_interval +timestamp_mi_interval +timestamp_smaller +timestamp_larger +timezone +timestamp_hash +timestamp_hash_extended +overlaps +timestamp_cmp +timestamp_sortsupport +in_range +in_range +in_range +in_range +in_range +time +timetz +isfinite +to_char +timestamp_eq +timestamp_ne +timestamp_lt +timestamp_le +timestamp_ge +timestamp_gt +age +timezone +timezone +date_pl_interval +date_mi_interval +substring +bit +int8 +current_setting +current_setting +pg_show_all_settings +pg_describe_object +pg_identify_object +pg_identify_object_as_address +pg_get_object_address +pg_table_is_visible +pg_type_is_visible +pg_function_is_visible +pg_operator_is_visible +pg_opclass_is_visible +pg_opfamily_is_visible +pg_conversion_is_visible +pg_statistics_obj_is_visible +pg_ts_parser_is_visible +pg_ts_dict_is_visible +pg_ts_template_is_visible +pg_ts_config_is_visible +pg_collation_is_visible +pg_my_temp_schema +pg_is_other_temp_schema +pg_backup_start_time +pg_walfile_name_offset +pg_walfile_name +pg_wal_lsn_diff +text +avg +avg +avg +avg +avg +avg +avg +sum +sum +sum +sum +sum +sum +sum +sum +max +max +max +max +max +max +max +max +max +max +max +max +max +max +max +max +max +max +max +max +min +min +min +min +min +min +min +min +min +min +min +min +min +min +min +min +min +min +min +min +count +count +var_pop +var_pop +var_pop +var_pop +var_pop +var_pop +var_samp +var_samp +var_samp +var_samp +var_samp +var_samp +variance +variance +variance +variance +variance +variance +stddev_pop +stddev_pop +stddev_pop +stddev_pop +stddev_pop +stddev_pop +stddev_samp +stddev_samp +stddev_samp +stddev_samp +stddev_samp +stddev_samp +stddev +stddev +stddev +stddev +stddev +stddev +regr_count +regr_sxx +regr_syy +regr_sxy +regr_avgx +regr_avgy +regr_r2 +regr_slope +regr_intercept +covar_pop +covar_samp +corr +text_pattern_lt +text_pattern_le +text_pattern_ge +text_pattern_gt +bttext_pattern_cmp +bttext_pattern_sortsupport +bpchar_pattern_lt +bpchar_pattern_le +bpchar_pattern_ge +bpchar_pattern_gt +btbpchar_pattern_cmp +btbpchar_pattern_sortsupport +btint48cmp +btint84cmp +btint24cmp +btint42cmp +btint28cmp +btint82cmp +btfloat48cmp +btfloat84cmp +regprocedurein +regprocedureout +regoperin +regoperout +to_regoper +to_regoperator +regoperatorin +regoperatorout +regclassin +regclassout +to_regclass +regcollationin +regcollationout +to_regcollation +regtypein +regtypeout +to_regtype +regclass +regrolein +regroleout +to_regrole +regnamespacein +regnamespaceout +to_regnamespace +fmgr_internal_validator +fmgr_c_validator +fmgr_sql_validator +has_database_privilege +has_database_privilege +has_database_privilege +has_database_privilege +has_database_privilege +has_database_privilege +has_function_privilege +has_function_privilege +language_handler_in +has_function_privilege +has_function_privilege +has_function_privilege +has_function_privilege +has_language_privilege +has_language_privilege +has_language_privilege +has_language_privilege +has_language_privilege +has_language_privilege +has_schema_privilege +has_schema_privilege +has_schema_privilege +has_schema_privilege +has_schema_privilege +has_schema_privilege +has_tablespace_privilege +has_tablespace_privilege +has_tablespace_privilege +has_tablespace_privilege +has_tablespace_privilege +has_tablespace_privilege +has_foreign_data_wrapper_privilege +has_foreign_data_wrapper_privilege +has_foreign_data_wrapper_privilege +has_foreign_data_wrapper_privilege +has_foreign_data_wrapper_privilege +has_foreign_data_wrapper_privilege +has_server_privilege +has_server_privilege +has_server_privilege +has_server_privilege +has_server_privilege +has_server_privilege +has_type_privilege +has_type_privilege +has_type_privilege +has_type_privilege +has_type_privilege +has_type_privilege +pg_has_role +pg_has_role +pg_has_role +pg_has_role +pg_has_role +pg_has_role +pg_column_size +pg_column_compression +pg_size_pretty +pg_size_pretty +pg_size_bytes +pg_relation_filenode +pg_filenode_relation +pg_relation_filepath +postgresql_fdw_validator +record_in +record_out +cstring_in +cstring_out +any_in +any_out +anyarray_in +anyarray_out +void_in +void_out +trigger_in +trigger_out +event_trigger_in +event_trigger_out +language_handler_out +internal_in +internal_out +anyelement_in +anyelement_out +shell_in +shell_out +domain_in +domain_recv +anynonarray_in +anynonarray_out +fdw_handler_in +fdw_handler_out +index_am_handler_in +index_am_handler_out +tsm_handler_in +tsm_handler_out +table_am_handler_in +table_am_handler_out +anycompatible_in +anycompatible_out +anycompatiblearray_in +anycompatiblearray_out +anycompatiblearray_recv +anycompatiblearray_send +anycompatiblenonarray_in +anycompatiblenonarray_out +anycompatiblerange_in +anycompatiblerange_out +anycompatiblemultirange_in +anycompatiblemultirange_out +md5 +md5 +sha224 +sha256 +sha384 +sha512 +date_lt_timestamp +date_le_timestamp +date_eq_timestamp +date_gt_timestamp +date_ge_timestamp +date_ne_timestamp +date_cmp_timestamp +date_lt_timestamptz +date_le_timestamptz +date_eq_timestamptz +date_gt_timestamptz +date_ge_timestamptz +date_ne_timestamptz +date_cmp_timestamptz +timestamp_lt_date +timestamp_le_date +timestamp_eq_date +timestamp_gt_date +timestamp_ge_date +timestamp_ne_date +timestamp_cmp_date +timestamptz_lt_date +timestamptz_le_date +timestamptz_eq_date +timestamptz_gt_date +timestamptz_ge_date +timestamptz_ne_date +timestamptz_cmp_date +timestamp_lt_timestamptz +timestamp_le_timestamptz +timestamp_eq_timestamptz +timestamp_gt_timestamptz +timestamp_ge_timestamptz +timestamp_ne_timestamptz +timestamp_cmp_timestamptz +timestamptz_lt_timestamp +timestamptz_le_timestamp +timestamptz_eq_timestamp +timestamptz_gt_timestamp +timestamptz_ge_timestamp +timestamptz_ne_timestamp +timestamptz_cmp_timestamp +array_recv +array_send +record_recv +record_send +int2recv +int2send +int4recv +int4send +int8recv +int8send +int2vectorrecv +int2vectorsend +bytearecv +byteasend +textrecv +textsend +unknownrecv +unknownsend +oidrecv +oidsend +oidvectorrecv +oidvectorsend +namerecv +namesend +float4recv +float4send +float8recv +float8send +point_recv +point_send +bpcharrecv +bpcharsend +varcharrecv +varcharsend +charrecv +charsend +boolrecv +boolsend +tidrecv +tidsend +xidrecv +xidsend +cidrecv +cidsend +regprocrecv +regprocsend +regprocedurerecv +regproceduresend +regoperrecv +regopersend +regoperatorrecv +regoperatorsend +regclassrecv +regclasssend +regcollationrecv +regcollationsend +regtyperecv +regtypesend +regrolerecv +regrolesend +regnamespacerecv +regnamespacesend +bit_recv +bit_send +varbit_recv +varbit_send +numeric_recv +numeric_send +date_recv +date_send +time_recv +time_send +timetz_recv +timetz_send +timestamp_recv +timestamp_send +timestamptz_recv +timestamptz_send +interval_recv +interval_send +lseg_recv +lseg_send +path_recv +path_send +box_recv +box_send +poly_recv +poly_send +line_recv +line_send +circle_recv +circle_send +cash_recv +cash_send +macaddr_recv +macaddr_send +inet_recv +inet_send +cidr_recv +cidr_send +cstring_recv +cstring_send +anyarray_recv +anyarray_send +void_recv +void_send +macaddr8_recv +macaddr8_send +pg_get_ruledef +pg_get_viewdef +pg_get_viewdef +pg_get_viewdef +pg_get_indexdef +pg_get_constraintdef +pg_get_expr +pg_prepared_statement +pg_cursor +pg_timezone_abbrevs +pg_timezone_names +pg_get_triggerdef +pg_listening_channels +generate_series +generate_series +generate_series_int4_support +generate_series +generate_series +generate_series_int8_support +generate_series +generate_series +generate_series +generate_series +booland_statefunc +boolor_statefunc +bool_accum +bool_accum_inv +bool_alltrue +bool_anytrue +bool_and +bool_or +every +bit_and +bit_or +bit_xor +bit_and +bit_or +bit_xor +bit_and +bit_or +bit_xor +bit_and +bit_or +bit_xor +pg_tablespace_databases +bool +int4 +pg_postmaster_start_time +pg_conf_load_time +box_below +box_overbelow +box_overabove +box_above +poly_below +poly_overbelow +poly_overabove +poly_above +circle_overbelow +circle_overabove +gist_box_consistent +gist_box_penalty +gist_box_picksplit +gist_box_union +gist_box_same +gist_box_distance +gist_poly_consistent +gist_poly_compress +gist_circle_consistent +gist_circle_compress +gist_point_compress +gist_point_fetch +gist_point_consistent +gist_point_distance +gist_circle_distance +gist_poly_distance +gist_point_sortsupport +ginarrayextract +ginqueryarrayextract +ginarrayconsistent +pg_lsn_lt +ginarraytriconsistent +ginarrayextract +arrayoverlap +arraycontains +arraycontained +brin_minmax_opcinfo +brin_minmax_add_value +brin_minmax_consistent +brin_minmax_union +brin_minmax_multi_opcinfo +brin_minmax_multi_add_value +brin_minmax_multi_consistent +brin_minmax_multi_union +brin_minmax_multi_options +brin_minmax_multi_distance_int2 +brin_minmax_multi_distance_int4 +brin_minmax_multi_distance_int8 +brin_minmax_multi_distance_float4 +brin_minmax_multi_distance_float8 +brin_minmax_multi_distance_numeric +brin_minmax_multi_distance_tid +brin_minmax_multi_distance_uuid +brin_minmax_multi_distance_date +brin_minmax_multi_distance_time +brin_minmax_multi_distance_interval +brin_minmax_multi_distance_timetz +brin_minmax_multi_distance_pg_lsn +brin_minmax_multi_distance_macaddr +brin_minmax_multi_distance_macaddr8 +brin_minmax_multi_distance_inet +brin_minmax_multi_distance_timestamp +brin_inclusion_opcinfo +brin_inclusion_add_value +brin_inclusion_consistent +brin_inclusion_union +brin_bloom_opcinfo +brin_bloom_add_value +brin_bloom_consistent +brin_bloom_union +brin_bloom_options +xml_in +xml_out +xmlcomment +xml +xmlvalidate +xml_recv +xml_send +xmlconcat2 +xmlagg +text +table_to_xml +table_to_xmlschema +table_to_xml_and_xmlschema +schema_to_xml +schema_to_xmlschema +schema_to_xml_and_xmlschema +database_to_xml +database_to_xmlschema +database_to_xml_and_xmlschema +xpath +xmlexists +xpath_exists +xml_is_well_formed +xml_is_well_formed_document +xml_is_well_formed_content +json_in +json_out +json_recv +json_send +array_to_json +array_to_json +row_to_json +row_to_json +json_agg_transfn +json_agg_finalfn +json_agg +json_object_agg_transfn +json_object_agg_finalfn +json_object_agg +json_build_array +json_build_array +json_build_object +json_build_object +json_object +json_object +to_json +json_strip_nulls +json_object_field +json_object_field_text +json_array_element +json_array_element_text +json_extract_path +json_extract_path_text +json_array_elements +json_array_elements_text +json_array_length +json_object_keys +json_each +json_each_text +json_to_record +json_to_recordset +json_typeof +uuid_in +uuid_out +uuid_lt +uuid_le +uuid_eq +uuid_ge +uuid_gt +uuid_ne +uuid_cmp +uuid_sortsupport +uuid_recv +uuid_send +uuid_hash +uuid_hash_extended +pg_lsn_in +pg_lsn_out +pg_lsn_le +pg_lsn_eq +pg_lsn_ge +pg_lsn_gt +pg_lsn_ne +pg_lsn_mi +pg_lsn_recv +pg_lsn_send +pg_lsn_cmp +pg_lsn_hash +pg_lsn_hash_extended +pg_lsn_larger +pg_lsn_smaller +pg_lsn_pli +pg_lsn_mii +anyenum_in +anyenum_out +enum_in +enum_out +enum_eq +enum_ne +enum_lt +enum_gt +enum_le +enum_ge +enum_cmp +hashenum +hashenumextended +enum_smaller +enum_larger +max +min +enum_first +enum_last +enum_range +enum_range +enum_recv +enum_send +tsvectorin +tsvectorrecv +tsvectorout +tsvectorsend +tsqueryin +tsqueryrecv +tsqueryout +tsquerysend +gtsvectorin +gtsvectorout +tsvector_lt +tsvector_le +tsvector_eq +tsvector_ne +tsvector_ge +tsvector_gt +tsvector_cmp +length +strip +setweight +setweight +tsvector_concat +ts_delete +ts_delete +unnest +tsvector_to_array +array_to_tsvector +ts_filter +ts_match_vq +ts_match_qv +ts_match_tt +ts_match_tq +gtsvector_compress +gtsvector_decompress +gtsvector_picksplit +gtsvector_union +gtsvector_same +gtsvector_penalty +gtsvector_consistent +gtsvector_consistent +gtsvector_options +gin_extract_tsvector +gin_extract_tsquery +gin_tsquery_consistent +gin_tsquery_triconsistent +gin_cmp_tslexeme +gin_cmp_prefix +gin_extract_tsvector +gin_extract_tsquery +gin_tsquery_consistent +gin_extract_tsquery +gin_tsquery_consistent +tsquery_lt +tsquery_le +tsquery_eq +tsquery_ne +tsquery_ge +tsquery_gt +tsquery_cmp +tsquery_and +tsquery_or +tsquery_phrase +tsquery_phrase +tsquery_not +tsq_mcontains +tsq_mcontained +numnode +querytree +ts_rewrite +gtsquery_compress +gtsquery_picksplit +gtsquery_union +gtsquery_same +gtsquery_penalty +gtsquery_consistent +gtsquery_consistent +tsmatchsel +tsmatchjoinsel +ts_typanalyze +ts_rank +ts_rank +ts_rank +ts_rank +ts_rank_cd +ts_rank_cd +ts_rank_cd +ts_rank_cd +ts_token_type +ts_token_type +ts_parse +ts_parse +prsd_start +prsd_nexttoken +prsd_end +prsd_headline +prsd_lextype +ts_lexize +dsimple_init +dsimple_lexize +dsynonym_init +dsynonym_lexize +dispell_init +dispell_lexize +thesaurus_init +thesaurus_lexize +ts_headline +ts_headline +ts_headline +ts_headline +ts_headline +ts_headline +ts_headline +ts_headline +ts_headline +ts_headline +ts_headline +ts_headline +to_tsvector +to_tsquery +plainto_tsquery +phraseto_tsquery +websearch_to_tsquery +to_tsvector +to_tsquery +plainto_tsquery +phraseto_tsquery +websearch_to_tsquery +to_tsvector +jsonb_to_tsvector +to_tsvector +json_to_tsvector +to_tsvector +jsonb_to_tsvector +to_tsvector +json_to_tsvector +get_current_ts_config +regconfigin +regconfigout +regconfigrecv +regconfigsend +regdictionaryin +regdictionaryout +regdictionaryrecv +regdictionarysend +jsonb_in +jsonb_recv +jsonb_out +jsonb_send +jsonb_object +jsonb_object +to_jsonb +jsonb_agg_transfn +jsonb_agg_finalfn +jsonb_agg +jsonb_object_agg_transfn +jsonb_object_agg_finalfn +jsonb_object_agg +jsonb_build_array +jsonb_build_array +jsonb_build_object +jsonb_build_object +jsonb_strip_nulls +jsonb_object_field +jsonb_object_field_text +jsonb_array_element +jsonb_array_element_text +jsonb_extract_path +jsonb_extract_path_text +jsonb_array_elements +jsonb_array_elements_text +jsonb_array_length +jsonb_object_keys +jsonb_each +jsonb_each_text +jsonb_populate_record +jsonb_populate_recordset +jsonb_to_record +jsonb_to_recordset +jsonb_typeof +jsonb_ne +jsonb_lt +jsonb_gt +jsonb_le +jsonb_ge +jsonb_eq +jsonb_cmp +jsonb_hash +jsonb_hash_extended +jsonb_contains +jsonb_exists +jsonb_exists_any +jsonb_exists_all +jsonb_contained +gin_compare_jsonb +gin_extract_jsonb +gin_extract_jsonb_query +gin_consistent_jsonb +gin_triconsistent_jsonb +gin_extract_jsonb_path +gin_extract_jsonb_query_path +gin_consistent_jsonb_path +gin_triconsistent_jsonb_path +jsonb_concat +jsonb_delete +jsonb_delete +jsonb_delete +jsonb_delete_path +jsonb_pretty +jsonpath_in +jsonpath_recv +jsonpath_out +jsonpath_send +jsonb_insert +jsonb_path_query +jsonb_path_query_first +jsonb_path_query_array_tz +jsonb_path_exists_opr +jsonb_path_match_opr +txid_snapshot_in +txid_snapshot_out +txid_snapshot_recv +txid_snapshot_send +txid_current +txid_current_if_assigned +txid_current_snapshot +txid_snapshot_xmin +txid_snapshot_xmax +txid_snapshot_xip +txid_visible_in_snapshot +pg_snapshot_in +pg_snapshot_out +pg_snapshot_recv +pg_snapshot_send +pg_current_snapshot +pg_snapshot_xmin +pg_snapshot_xmax +pg_snapshot_xip +pg_visible_in_snapshot +pg_current_xact_id +pg_current_xact_id_if_assigned +record_eq +record_ne +record_lt +record_gt +record_le +record_ge +btrecordcmp +hash_record +hash_record_extended +record_image_eq +record_image_ne +record_image_lt +record_image_gt +record_image_le +record_image_ge +btrecordimagecmp +btequalimage +pg_available_extensions +pg_available_extension_versions +pg_extension_update_paths +row_number +rank +dense_rank +percent_rank +cume_dist +ntile +lag +lag +lag +lead +lead +lead +first_value +last_value +nth_value +anyrange_in +anyrange_out +range_in +range_out +range_recv +range_send +lower +upper +isempty +lower_inc +upper_inc +lower_inf +upper_inf +range_eq +range_ne +range_overlaps +range_contains_elem +range_contains +elem_contained_by_range +range_contained_by +range_adjacent +range_before +range_after +range_overleft +range_overright +range_union +range_merge +range_merge +range_intersect +range_minus +range_cmp +range_lt +range_le +range_ge +range_gt +range_gist_consistent +range_gist_union +range_gist_penalty +range_gist_picksplit +range_gist_same +multirange_gist_consistent +multirange_gist_compress +hash_range +hash_range_extended +range_typanalyze +rangesel +range_intersect_agg_transfn +range_intersect_agg +int4range_canonical +int8range_canonical +daterange_canonical +int4range_subdiff +int8range_subdiff +numrange_subdiff +daterange_subdiff +tsrange_subdiff +tstzrange_subdiff +int4range +int4range +numrange +numrange +tsrange +tsrange +tstzrange +tstzrange +daterange +daterange +int8range +int8range +anymultirange_in +anymultirange_out +multirange_in +multirange_out +multirange_recv +multirange_send +lower +upper +isempty +lower_inc +upper_inc +lower_inf +upper_inf +multirange_typanalyze +multirangesel +multirange_eq +multirange_ne +range_overlaps_multirange +multirange_overlaps_range +multirange_overlaps_multirange +multirange_contains_elem +multirange_contains_range +multirange_contains_multirange +elem_contained_by_multirange +range_contained_by_multirange +range_contains_multirange +multirange_contained_by_range +multirange_contained_by_multirange +range_adjacent_multirange +multirange_adjacent_multirange +multirange_adjacent_range +range_before_multirange +multirange_before_range +multirange_before_multirange +range_after_multirange +multirange_after_range +multirange_after_multirange +range_overleft_multirange +multirange_overleft_range +multirange_overleft_multirange +range_overright_multirange +multirange_overright_range +multirange_overright_multirange +multirange_union +multirange_minus +multirange_intersect +multirange_cmp +multirange_lt +multirange_le +multirange_ge +multirange_gt +hash_multirange +hash_multirange_extended +int4multirange +int4multirange +int4multirange +nummultirange +nummultirange +nummultirange +tsmultirange +tsmultirange +tsmultirange +tstzmultirange +tstzmultirange +spg_range_quad_inner_consistent +tstzmultirange +datemultirange +datemultirange +datemultirange +int8multirange +int8multirange +int8multirange +multirange +range_agg_transfn +range_agg_finalfn +range_agg +multirange_intersect_agg_transfn +range_intersect_agg +unnest +make_date +make_time +make_timestamp +make_timestamptz +make_timestamptz +spg_quad_config +spg_quad_choose +spg_quad_picksplit +spg_quad_inner_consistent +spg_quad_leaf_consistent +spg_kd_config +spg_kd_choose +spg_kd_picksplit +spg_kd_inner_consistent +spg_text_config +spg_text_choose +spg_text_picksplit +spg_text_inner_consistent +spg_text_leaf_consistent +spg_range_quad_config +spg_range_quad_choose +spg_range_quad_picksplit +spg_range_quad_leaf_consistent +spg_box_quad_config +spg_box_quad_choose +spg_box_quad_picksplit +spg_box_quad_inner_consistent +spg_box_quad_leaf_consistent +spg_bbox_quad_config +spg_poly_quad_compress +pg_get_replication_slots +pg_event_trigger_dropped_objects +pg_event_trigger_table_rewrite_oid +pg_event_trigger_table_rewrite_reason +pg_event_trigger_ddl_commands +ordered_set_transition +ordered_set_transition_multi +percentile_disc +percentile_disc_final +percentile_cont +percentile_cont_float8_final +percentile_cont +percentile_cont_interval_final +percentile_disc +percentile_disc_multi_final +percentile_cont +percentile_cont_float8_multi_final +percentile_cont +percentile_cont_interval_multi_final +mode +mode_final +rank +rank_final +percent_rank +percent_rank_final +cume_dist +cume_dist_final +dense_rank +dense_rank_final +koi8r_to_mic +mic_to_koi8r +iso_to_mic +mic_to_iso +win1251_to_mic +mic_to_win1251 +win866_to_mic +mic_to_win866 +koi8r_to_win1251 +win1251_to_koi8r +koi8r_to_win866 +win866_to_koi8r +win866_to_win1251 +win1251_to_win866 +iso_to_koi8r +koi8r_to_iso +iso_to_win1251 +win1251_to_iso +iso_to_win866 +win866_to_iso +euc_cn_to_mic +mic_to_euc_cn +euc_jp_to_sjis +sjis_to_euc_jp +euc_jp_to_mic +sjis_to_mic +mic_to_euc_jp +mic_to_sjis +euc_kr_to_mic +mic_to_euc_kr +euc_tw_to_big5 +big5_to_euc_tw +euc_tw_to_mic +big5_to_mic +mic_to_euc_tw +mic_to_big5 +latin2_to_mic +mic_to_latin2 +win1250_to_mic +mic_to_win1250 +latin2_to_win1250 +win1250_to_latin2 +latin1_to_mic +mic_to_latin1 +latin3_to_mic +mic_to_latin3 +latin4_to_mic +mic_to_latin4 +big5_to_utf8 +utf8_to_big5 +utf8_to_koi8r +koi8r_to_utf8 +utf8_to_koi8u +koi8u_to_utf8 +utf8_to_win +win_to_utf8 +euc_cn_to_utf8 +utf8_to_euc_cn +euc_jp_to_utf8 +utf8_to_euc_jp +euc_kr_to_utf8 +utf8_to_euc_kr +euc_tw_to_utf8 +utf8_to_euc_tw +gb18030_to_utf8 +utf8_to_gb18030 +gbk_to_utf8 +utf8_to_gbk +utf8_to_iso8859 +iso8859_to_utf8 +iso8859_1_to_utf8 +utf8_to_iso8859_1 +johab_to_utf8 +utf8_to_johab +sjis_to_utf8 +utf8_to_sjis +uhc_to_utf8 +utf8_to_uhc +euc_jis_2004_to_utf8 +utf8_to_euc_jis_2004 +shift_jis_2004_to_utf8 +utf8_to_shift_jis_2004 +euc_jis_2004_to_shift_jis_2004 +shift_jis_2004_to_euc_jis_2004 +matchingsel +matchingjoinsel +pg_replication_origin_oid +pg_get_publication_tables +pg_relation_is_publishable +row_security_active +row_security_active +array_subscript_handler +raw_array_subscript_handler +jsonb_subscript_handler +satisfies_hash_partition +pg_partition_root +unistr +brin_bloom_summary_in +brin_bloom_summary_out +brin_bloom_summary_recv +pg_config +brin_bloom_summary_send +brin_minmax_multi_summary_in +brin_minmax_multi_summary_out +brin_minmax_multi_summary_recv +brin_minmax_multi_summary_send +lpad +rpad +substring +bit_length +trunc +bit_length +bit_length +log +log10 +round +numeric_pl_pg_lsn +path_contain_pt +polygon +age +age +interval_pl_timetz +date_part +timestamptz +timedate_pl +timetzdate_pl +interval_pl_time +interval_pl_date +interval_pl_timestamp +interval_pl_timestamptz +integer_pl_date +overlaps +overlaps +overlaps +overlaps +overlaps +overlaps +overlaps +overlaps +overlaps +int8pl_inet +xpath +xpath_exists +obj_description +shobj_description +col_description +ts_debug +ts_debug +json_populate_record +json_populate_recordset +make_interval +jsonb_set +jsonb_set_lax +parse_ident +jsonb_path_exists +jsonb_path_match +jsonb_path_query_array +jsonb_path_exists_tz +jsonb_path_match_tz +jsonb_path_query_tz +jsonb_path_query_first_tz +normalize +is_normalized +_pg_expandarray +_pg_index_position +_pg_truetypid +_pg_truetypmod +_pg_char_max_length +_pg_char_octet_length +_pg_numeric_precision +_pg_numeric_precision_radix +_pg_numeric_scale +_pg_datetime_precision +_pg_interval_type diff --git a/crates/squawk_linter/src/rules/prefer_big_int.rs b/crates/squawk_linter/src/rules/prefer_big_int.rs new file mode 100644 index 00000000..c312bac7 --- /dev/null +++ b/crates/squawk_linter/src/rules/prefer_big_int.rs @@ -0,0 +1,231 @@ +use std::collections::HashSet; + +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{text::trim_quotes, ErrorCode, Linter, Violation}; +use lazy_static::lazy_static; + +lazy_static! { + static ref SMALL_INT_TYPES: HashSet<&'static str> = HashSet::from([ + "smallint", + "integer", + "int2", + "int4", + "serial", + "serial2", + "serial4", + "smallserial", + ]); +} + +pub(crate) fn is_not_valid_int_type(ty: &ast::Type, invalid_type_names: &HashSet<&str>) -> bool { + match ty { + ast::Type::ArrayType(array_type) => { + if let Some(ty) = array_type.ty() { + is_not_valid_int_type(&ty, invalid_type_names) + } else { + false + } + } + ast::Type::PercentType(_) => false, + ast::Type::PathType(path_type) => { + let Some(ty_name) = path_type + .path() + .and_then(|x| x.segment()) + .and_then(|x| x.name_ref()) + .map(|x| x.text().to_string()) + else { + return false; + }; + let name = trim_quotes(ty_name.as_str()); + invalid_type_names.contains(name) + } + ast::Type::CharType(_) => false, + ast::Type::BitType(_) => false, + ast::Type::DoubleType(_) => false, + ast::Type::TimeType(_) => false, + ast::Type::IntervalType(_) => false, + } +} + +pub(crate) fn check_not_allowed_types( + ctx: &mut Linter, + file: &ast::SourceFile, + check_ty: impl Fn(&mut Linter, Option), +) { + for item in file.items() { + match item { + ast::Item::CreateTable(create_table) => { + if let Some(table_args) = create_table.table_args() { + for arg in table_args.args() { + if let ast::TableArg::Column(column) = arg { + check_ty(ctx, column.ty()); + } + } + } + } + ast::Item::AlterTable(alter_table) => { + for action in alter_table.actions() { + match action { + ast::AlterTableAction::AddColumn(add_column) => { + check_ty(ctx, add_column.ty()); + } + ast::AlterTableAction::AlterColumn(alter_column) => { + if let Some(ast::AlterColumnOption::SetType(set_type)) = + alter_column.option() + { + check_ty(ctx, set_type.ty()); + } + } + _ => (), + } + } + } + _ => (), + } + } +} + +fn check_ty_for_big_int(ctx: &mut Linter, ty: Option) { + if let Some(ty) = ty { + if is_not_valid_int_type(&ty, &SMALL_INT_TYPES) { + ctx.report(Violation::new( + ErrorCode::PreferBigInt, + "Using 32-bit integer fields can result in hitting the max `int` limit.".into(), + ty.syntax().text_range(), + None, + )); + }; + } +} + +pub(crate) fn prefer_big_int(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + check_not_allowed_types(ctx, &file, check_ty_for_big_int); +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{ErrorCode, Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +create table users ( + id smallint +); +create table users ( + id int2 +); +create table users ( + id integer +); +create table users ( + id int4 +); +create table users ( + id serial +); +create table users ( + id serial2 +); +create table users ( + id serial4 +); +create table users ( + id smallserial +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigInt]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_eq!(errors.len(), 8); + assert_eq!( + errors + .iter() + .filter(|x| x.code == ErrorCode::PreferBigInt) + .count(), + 8 + ); + assert_debug_snapshot!(errors); + } + + #[test] + fn ok() { + let sql = r#" +create table users ( + id bigint +); +create table users ( + id int8 +); +create table users ( + id bigserial +); +create table users ( + id serial8 +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigInt]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn create_table_many_err() { + let sql = r#" +create table users ( + foo integer, + bar serial +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigInt]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_add_column_err() { + let sql = r#" +alter table t add column c integer; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigInt]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_alter_column_type_err() { + let sql = r#" +alter table t alter column c type integer; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigInt]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_alter_column_type_with_quotes_err() { + let sql = r#" +alter table t alter column c type "integer"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigInt]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs b/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs new file mode 100644 index 00000000..73a6269a --- /dev/null +++ b/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs @@ -0,0 +1,107 @@ +use std::collections::HashSet; + +use syntax::ast::AstNode; +use syntax::{ast, Parse, SourceFile}; + +use crate::{ErrorCode, Linter, Violation}; + +use crate::prefer_big_int::check_not_allowed_types; +use crate::prefer_big_int::is_not_valid_int_type; + +use lazy_static::lazy_static; + +lazy_static! { + static ref INT_TYPES: HashSet<&'static str> = + HashSet::from(["integer", "int4", "serial", "serial4",]); +} + +fn check_ty_for_big_int(ctx: &mut Linter, ty: Option) { + if let Some(ty) = ty { + if is_not_valid_int_type(&ty, &INT_TYPES) { + ctx.report(Violation::new( + ErrorCode::PreferBigintOverInt, + "Using 32-bit integer fields can result in hitting the max `int` limit.".into(), + ty.syntax().text_range(), + None, + )); + }; + } +} + +// TODO: we should have this be a config option instead of having a bunch of prefer_$int rules +pub(crate) fn prefer_bigint_over_int(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + check_not_allowed_types(ctx, &file, check_ty_for_big_int); +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{ErrorCode, Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +create table users ( + id integer +); +create table users ( + id int4 +); +create table users ( + id serial +); +create table users ( + id serial4 +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigintOverInt]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_eq!(errors.len(), 4); + assert_eq!( + errors + .iter() + .filter(|x| x.code == ErrorCode::PreferBigintOverInt) + .count(), + 4 + ); + assert_debug_snapshot!(errors); + } + + #[test] + fn ok() { + let sql = r#" +create table users ( + id bigint +); +create table users ( + id int8 +); +create table users ( + id bigserial +); +create table users ( + id serial8 +); +create table users ( + id smallint +); +create table users ( + id int2 +); +create table users ( + id smallserial +); +create table users ( + id serial2 +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigintOverInt]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs b/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs new file mode 100644 index 00000000..87e1413c --- /dev/null +++ b/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs @@ -0,0 +1,106 @@ +use std::collections::HashSet; + +use syntax::ast::AstNode; +use syntax::{ast, Parse, SourceFile}; + +use crate::{ErrorCode, Linter, Violation}; + +use crate::prefer_big_int::check_not_allowed_types; +use crate::prefer_big_int::is_not_valid_int_type; + +use lazy_static::lazy_static; + +lazy_static! { + static ref SMALL_INT_TYPES: HashSet<&'static str> = + HashSet::from(["smallint", "int2", "smallserial", "serial2",]); +} + +fn check_ty_for_small_int(ctx: &mut Linter, ty: Option) { + if let Some(ty) = ty { + if is_not_valid_int_type(&ty, &SMALL_INT_TYPES) { + ctx.report(Violation::new( + ErrorCode::PreferBigintOverSmallint, + "Using 16-bit integer fields can result in hitting the max `int` limit.".into(), + ty.syntax().text_range(), + None, + )); + }; + } +} + +pub(crate) fn prefer_bigint_over_smallint(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + check_not_allowed_types(ctx, &file, check_ty_for_small_int); +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{ErrorCode, Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +create table users ( + id smallint +); +create table users ( + id int2 +); +create table users ( + id smallserial +); +create table users ( + id serial2 +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigintOverSmallint]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_eq!(errors.len(), 4); + assert_eq!( + errors + .iter() + .filter(|x| x.code == ErrorCode::PreferBigintOverSmallint) + .count(), + 4 + ); + assert_debug_snapshot!(errors); + } + + #[test] + fn ok() { + let sql = r#" +create table users ( + id bigint +); +create table users ( + id int8 +); +create table users ( + id bigserial +); +create table users ( + id serial8 +); +create table users ( + id integer +); +create table users ( + id int4 +); +create table users ( + id serial +); +create table users ( + id serial4 +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferBigintOverSmallint]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/prefer_identity.rs b/crates/squawk_linter/src/rules/prefer_identity.rs new file mode 100644 index 00000000..1c15e031 --- /dev/null +++ b/crates/squawk_linter/src/rules/prefer_identity.rs @@ -0,0 +1,101 @@ +use std::collections::HashSet; + +use syntax::{ + ast::{self, AstNode}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +use lazy_static::lazy_static; + +use crate::prefer_big_int::{check_not_allowed_types, is_not_valid_int_type}; + +lazy_static! { + static ref SERIAL_TYPES: HashSet<&'static str> = HashSet::from([ + "serial", + "serial2", + "serial4", + "serial8", + "smallserial", + "bigserial", + ]); +} + +fn check_ty_for_serial(ctx: &mut Linter, ty: Option) { + if let Some(ty) = ty { + if is_not_valid_int_type(&ty, &SERIAL_TYPES) { + ctx.report(Violation::new( + ErrorCode::PreferIdentity, + "Serial types make permissions and schema management difficult. Identity columns are standard SQL and have more features and better usability.".into(), + ty.syntax().text_range(), + None, + )); + }; + } +} + +pub(crate) fn prefer_identity(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + check_not_allowed_types(ctx, &file, check_ty_for_serial); +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{ErrorCode, Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +create table users ( + id serial +); +create table users ( + id serial2 +); +create table users ( + id serial4 +); +create table users ( + id serial8 +); +create table users ( + id smallserial +); +create table users ( + id bigserial +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferIdentity]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_eq!(errors.len(), 6); + assert_eq!( + errors + .iter() + .filter(|x| x.code == ErrorCode::PreferIdentity) + .count(), + 6 + ); + assert_debug_snapshot!(errors); + } + + #[test] + fn ok() { + let sql = r#" +create table users ( + id bigint generated by default as identity primary key +); +create table users ( + id bigint generated always as identity primary key +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferIdentity]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/prefer_robust_stmts.rs b/crates/squawk_linter/src/rules/prefer_robust_stmts.rs new file mode 100644 index 00000000..4e4c3f89 --- /dev/null +++ b/crates/squawk_linter/src/rules/prefer_robust_stmts.rs @@ -0,0 +1,571 @@ +use std::collections::HashMap; + +use syntax::{ + ast::{self, AstNode, HasIfExists, HasIfNotExists, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{text::trim_quotes, ErrorCode, Linter, Violation}; + +#[derive(PartialEq)] +enum Constraint { + Dropped, + Added, +} + +pub(crate) fn prefer_robust_stmts(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + let mut inside_transaction = ctx.settings.assume_in_transaction; + let mut constraint_names: HashMap = HashMap::new(); + + let mut total_stmts = 0; + for _ in file.items() { + total_stmts += 1; + if total_stmts > 1 { + break; + } + } + if total_stmts <= 1 { + // single stmts are fine + return; + } + + for item in file.items() { + match item { + ast::Item::Begin(_) => { + inside_transaction = true; + } + ast::Item::Commit(_) => { + inside_transaction = false; + } + ast::Item::AlterTable(alter_table) => { + for action in alter_table.actions() { + match &action { + ast::AlterTableAction::DropConstraint(drop_constraint) => { + if let Some(constraint_name) = drop_constraint.name_ref() { + constraint_names.insert( + trim_quotes(constraint_name.text().as_str()).to_string(), + Constraint::Dropped, + ); + } + if drop_constraint.if_exists().is_some() { + continue; + } + } + ast::AlterTableAction::AddColumn(add_column) => { + if add_column.if_not_exists().is_some() { + continue; + } + } + ast::AlterTableAction::ValidateConstraint(validate_constraint) => { + if let Some(constraint_name) = validate_constraint.name_ref() { + if constraint_names + .contains_key(trim_quotes(constraint_name.text().as_str())) + { + continue; + } + } + } + ast::AlterTableAction::AddConstraint(add_constraint) => { + let constraint = add_constraint.constraint(); + if let Some(constraint_name) = constraint.and_then(|x| x.name()) { + let name_text = constraint_name.text(); + let name = trim_quotes(name_text.as_str()); + if let Some(constraint) = constraint_names.get_mut(name) { + if *constraint == Constraint::Dropped { + *constraint = Constraint::Added; + continue; + } + } + } + } + ast::AlterTableAction::DropColumn(drop_column) => { + if drop_column.if_exists().is_some() { + continue; + } + } + _ => (), + } + + if inside_transaction { + continue; + } + + ctx.report(Violation::new( + ErrorCode::PreferRobustStmts, + "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.".into(), + action.syntax().text_range(), + None, + )); + } + } + ast::Item::CreateIndex(create_index) + if create_index.if_not_exists().is_none() + && (create_index.concurrently_token().is_some() || !inside_transaction) => + { + ctx.report(Violation::new( + ErrorCode::PreferRobustStmts, + "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.".into(), + create_index.syntax().text_range(), + vec!["Use an explicit name for a concurrently created index".into()], + )); + } + ast::Item::CreateTable(create_table) + if create_table.if_not_exists().is_none() && !inside_transaction => + { + ctx.report(Violation::new( + ErrorCode::PreferRobustStmts, + "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.".into(), + create_table.syntax().text_range(), + None, + )); + } + ast::Item::DropIndex(drop_index) + if drop_index.if_exists().is_none() && !inside_transaction => + { + ctx.report(Violation::new( + ErrorCode::PreferRobustStmts, + "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.".into(), + drop_index.syntax().text_range(), + None, + )); + } + ast::Item::DropTable(drop_table) + if drop_table.if_exists().is_none() && !inside_transaction => + { + ctx.report(Violation::new( + ErrorCode::PreferRobustStmts, + "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.".into(), + drop_table.syntax().text_range(), + None, + )); + } + ast::Item::DropType(drop_type) + if drop_type.if_exists().is_none() && !inside_transaction => + { + ctx.report(Violation::new( + ErrorCode::PreferRobustStmts, + "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.".into(), + drop_type.syntax().text_range(), + None, + )); + } + _ => (), + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn drop_before_end_ok() { + let sql = r#" +ALTER TABLE "app_email" DROP CONSTRAINT IF EXISTS "email_uniq"; +ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn drop_index_if_exists_ok() { + let sql = r#" +select 1; -- so we don't skip checking +DROP INDEX CONCURRENTLY IF EXISTS "email_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn drop_before_add_foreign_key_ok() { + let sql = r#" +ALTER TABLE "app_email" DROP CONSTRAINT IF EXISTS "fk_user"; +ALTER TABLE "app_email" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "app_user" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID; +ALTER TABLE "app_email" VALIDATE CONSTRAINT "fk_user"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn prefer_robust_stmt_ok() { + let sql = r#" +BEGIN; +ALTER TABLE "core_foo" ADD COLUMN "answer_id" integer NULL; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn prefer_robust_stmt_part_2_ok() { + let sql = r#" +select 1; -- so we don't skip checking +ALTER TABLE "core_foo" ADD COLUMN IF NOT EXISTS "answer_id" integer NULL; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn prefer_robust_stmt_part_3_ok() { + let sql = r#" +select 1; -- so we don't skip checking +CREATE INDEX CONCURRENTLY IF NOT EXISTS "core_foo_idx" ON "core_foo" ("answer_id"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn prefer_robust_stmt_part_4_ok() { + let sql = r#" +BEGIN; +CREATE TABLE "core_bar" ( + "id" serial NOT NULL PRIMARY KEY, + "bravo" text NOT NULL +); +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn prefer_robust_stmt_part_5_ok() { + let sql = r#" +CREATE TABLE IF NOT EXISTS "core_bar" ( + "id" serial NOT NULL PRIMARY KEY, + "bravo" text NOT NULL +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn prefer_robust_stmt_part_6_ok() { + // If done in a transaction, most forms of drop are fine + let sql = r#" +BEGIN; +DROP INDEX "core_bar_foo_id_idx"; +DROP TABLE "core_bar"; +DROP TYPE foo; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn select_ok() { + // select is fine, we're only interested in modifications to the tables + let sql = r#" +select 1; -- so we don't skip checking +SELECT 1; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn insert_ok() { + // select is fine, we're only interested in modifications to the tables + let sql = r#" +select 1; -- so we don't skip checking +INSERT INTO tbl VALUES (a); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn alter_table_ok() { + // select is fine, we're only interested in modifications to the tables + let sql = r#" +select 1; -- so we don't skip checking +ALTER TABLE "core_foo" DROP CONSTRAINT IF EXISTS "core_foo_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn assume_in_transaction_add_column_ok() { + let sql = r#" +select 1; -- so we don't skip checking +ALTER TABLE "core_foo" ADD COLUMN "answer_id" integer NULL; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn assume_in_transaction_drop_table_ok() { + let sql = r#" +DROP INDEX "core_bar_foo_id_idx"; +DROP TABLE "core_bar"; +DROP TYPE foo; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn assume_in_transaction_create_table_ok() { + let sql = r#" +CREATE TABLE "core_bar" ( + "id" serial NOT NULL PRIMARY KEY, + "bravo" text NOT NULL +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn ignore_single_stmts_ok() { + // we don't include a placeholder stmt because we're actually checking + // for the single stmt behavior here + let sql = r#" +CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn create_index_concurrently_muli_stmts_err() { + let sql = r#" +CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); +CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn start_transaction_ok() { + let sql = r#" +START TRANSACTION; + +ALTER TABLE "A" DROP CONSTRAINT "UQ_c4fb579a038211909ee524ccf29"; + +ALTER TABLE "B" DROP CONSTRAINT "UQ_791c01fe9438d66a94490d0da28"; + +ALTER TABLE "C" DROP CONSTRAINT "UQ_23fbf20e8ab4e806941359f4f79"; + +ALTER TABLE "D" DROP CONSTRAINT "UQ_468cad3743146a81c94b0b114ac"; + +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn alter_table_err() { + let sql = r#" +select 1; -- so we don't skip checking +ALTER TABLE "core_foo" ADD COLUMN "answer_id" integer NULL; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn create_index_concurrently_err() { + let sql = r#" +select 1; -- so we don't skip checking +CREATE INDEX CONCURRENTLY "core_foo_idx" ON "core_foo" ("answer_id"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_drop_column_err() { + let sql = r#" +select 1; -- so we don't skip checking +alter table t drop column c cascade; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_drop_column_ok() { + let sql = r#" +select 1; -- so we don't skip checking +alter table t drop column if exists c cascade; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn create_table_err() { + let sql = r#" +select 1; -- so we don't skip checking +CREATE TABLE "core_bar" ( "id" serial NOT NULL PRIMARY KEY, "bravo" text NOT NULL); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_drop_constraint_err() { + let sql = r#" +select 1; -- so we don't skip checking +ALTER TABLE "core_foo" DROP CONSTRAINT "core_foo_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn create_index_concurrently_unnamed_err() { + let sql = r#" +select 1; -- so we don't skip checking +CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn enable_row_level_security_err() { + let sql = r#" +CREATE TABLE IF NOT EXISTS test(); +ALTER TABLE IF EXISTS test ENABLE ROW LEVEL SECURITY; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn enable_row_level_security_without_exists_check_err() { + let sql = r#" +CREATE TABLE IF NOT EXISTS test(); +ALTER TABLE test ENABLE ROW LEVEL SECURITY; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn disable_row_level_security_err() { + let sql = r#" +CREATE TABLE IF NOT EXISTS test(); +ALTER TABLE IF EXISTS test DISABLE ROW LEVEL SECURITY; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn double_add_after_drop_err() { + let sql = r#" +ALTER TABLE "app_email" DROP CONSTRAINT IF EXISTS "email_uniq"; +ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_idx"; +-- this second add constraint should error because it's not robust +ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn drop_index_err() { + let sql = r#" +select 1; -- so we don't skip checking +DROP INDEX CONCURRENTLY "email_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferRobustStmts]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/prefer_text_field.rs b/crates/squawk_linter/src/rules/prefer_text_field.rs new file mode 100644 index 00000000..b45c77ce --- /dev/null +++ b/crates/squawk_linter/src/rules/prefer_text_field.rs @@ -0,0 +1,175 @@ +use std::collections::HashSet; + +use syntax::{ + ast::{self, AstNode, HasArgList}, + Parse, SourceFile, +}; + +use crate::{text::trim_quotes, ErrorCode, Linter, Violation}; + +use crate::prefer_big_int::check_not_allowed_types; + +use lazy_static::lazy_static; + +lazy_static! { + static ref VARCHAR_TYPE_NAMES: HashSet<&'static str> = HashSet::from(["varchar"]); +} + +fn is_not_allowed_varchar(ty: &ast::Type) -> bool { + match ty { + ast::Type::ArrayType(array_type) => { + if let Some(ty) = array_type.ty() { + is_not_allowed_varchar(&ty) + } else { + false + } + } + ast::Type::PercentType(_) => false, + ast::Type::PathType(path_type) => { + let Some(ty_name) = path_type + .path() + .and_then(|x| x.segment()) + .and_then(|x| x.name_ref()) + .map(|x| x.text().to_string()) + else { + return false; + }; + // if we don't have any args, then it's the same as `text` + trim_quotes(ty_name.as_str()) == "varchar" && path_type.arg_list().is_some() + } + ast::Type::CharType(char_type) => { + trim_quotes(&char_type.text()) == "varchar" && char_type.arg_list().is_some() + } + ast::Type::BitType(_) => false, + ast::Type::DoubleType(_) => false, + ast::Type::TimeType(_) => false, + ast::Type::IntervalType(_) => false, + } +} + +fn check_ty_for_varchar(ctx: &mut Linter, ty: Option) { + if let Some(ty) = ty { + if is_not_allowed_varchar(&ty) { + ctx.report(Violation::new( + ErrorCode::PreferTextField, + "Changing the size of a `varchar` field requires an `ACCESS EXCLUSIVE` lock, that will prevent all reads and writes to the table.".to_string(), + ty.syntax().text_range(), + None, + )); + }; + } +} + +pub(crate) fn prefer_text_field(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + check_not_allowed_types(ctx, &file, check_ty_for_varchar); +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + /// Changing a column of varchar(255) to varchar(1000) requires an ACCESS + /// EXCLUSIVE lock + #[test] + fn increase_varchar_size_err() { + let sql = r#" +BEGIN; +-- +-- Alter field kind on foo +-- +ALTER TABLE "core_foo" ALTER COLUMN "kind" TYPE varchar(1000) USING "kind"::varchar(1000); +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTextField]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn create_table_with_varchar_err() { + let sql = r#" +BEGIN; +-- +-- Create model Bar +-- +CREATE TABLE "core_bar" ( + "id" serial NOT NULL PRIMARY KEY, + "alpha" varchar(100) NOT NULL +); +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTextField]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn create_table_with_pgcatalog_varchar_err() { + let sql = r#" +create table t ( + "id" serial NOT NULL PRIMARY KEY, + "alpha" pg_catalog.varchar(100) NOT NULL +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTextField]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn adding_column_non_text_err() { + let sql = r#" +BEGIN; +ALTER TABLE "foo_table" ADD COLUMN "foo_column" varchar(256) NULL; +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTextField]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn varchar_without_specified_limit_ok() { + let sql = r#" +CREATE TABLE IF NOT EXISTS foo_table(bar_col varchar); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTextField]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn create_table_with_text_ok() { + let sql = r#" +BEGIN; +-- +-- Create model Bar +-- +CREATE TABLE "core_bar" ( + "id" serial NOT NULL PRIMARY KEY, + "bravo" text NOT NULL +); +-- +-- Create constraint text_size on model bar +-- +ALTER TABLE "core_bar" ADD CONSTRAINT "text_size" CHECK (LENGTH("bravo") <= 100); +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTextField]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/prefer_timestamptz.rs b/crates/squawk_linter/src/rules/prefer_timestamptz.rs new file mode 100644 index 00000000..3f260b9c --- /dev/null +++ b/crates/squawk_linter/src/rules/prefer_timestamptz.rs @@ -0,0 +1,139 @@ +use syntax::{ + ast::{self, AstNode, HasArgList}, + Parse, SourceFile, +}; + +use crate::{prefer_big_int::check_not_allowed_types, text::trim_quotes}; +use crate::{ErrorCode, Linter, Violation}; + +pub fn is_not_allowed_timestamp(ty: &ast::Type) -> bool { + match ty { + ast::Type::ArrayType(array_type) => { + if let Some(ty) = array_type.ty() { + is_not_allowed_timestamp(&ty) + } else { + false + } + } + ast::Type::PercentType(_) => false, + ast::Type::PathType(path_type) => { + let Some(ty_name) = path_type + .path() + .and_then(|x| x.segment()) + .and_then(|x| x.name_ref()) + .map(|x| x.text().to_string()) + else { + return false; + }; + // if we don't have any args, then it's the same as `text` + trim_quotes(ty_name.as_str()) == "varchar" && path_type.arg_list().is_some() + } + ast::Type::CharType(_) => false, + ast::Type::BitType(_) => false, + ast::Type::DoubleType(_) => false, + ast::Type::TimeType(time_type) => { + if let Some(ty_name) = time_type.name_ref() { + if ty_name.text() == "timestamp" && time_type.with_timezone().is_none() { + return true; + } + } + false + } + ast::Type::IntervalType(_) => false, + } +} + +fn check_ty_for_timestamp(ctx: &mut Linter, ty: Option) { + if let Some(ty) = ty { + if is_not_allowed_timestamp(&ty) { + ctx.report(Violation::new( + ErrorCode::PreferTimestampTz, + "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.".into(), + ty.syntax().text_range(), + None, + )); + }; + } +} + +pub(crate) fn prefer_timestamptz(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + check_not_allowed_types(ctx, &file, check_ty_for_timestamp); +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn create_table_with_timestamp_err() { + let sql = r#" +create table app.users +( + created_ts timestamp +); +create table app.accounts +( + created_ts timestamp without time zone +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTimestamptz]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_with_timestamp_err() { + let sql = r#" +alter table app.users + alter column created_ts type timestamp; +alter table app.accounts + alter column created_ts type timestamp without time zone; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTimestamptz]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn alter_table_with_time_zone_ok() { + let sql = r#" +create table app.users +( + created_ts timestamptz +); +create table app.accounts +( + created_ts timestamp with time zone +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTimestamptz]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn create_table_with_time_zone_ok() { + let sql = r#" +create table app.users +( + created_ts timestamptz +); +create table app.accounts +( + created_ts timestamp with time zone +); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::PreferTimestamptz]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/renaming_column.rs b/crates/squawk_linter/src/rules/renaming_column.rs new file mode 100644 index 00000000..61c7b839 --- /dev/null +++ b/crates/squawk_linter/src/rules/renaming_column.rs @@ -0,0 +1,43 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn renaming_column(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + if let ast::AlterTableAction::RenameColumn(rename_column) = action { + ctx.report(Violation::new( + ErrorCode::RenamingColumn, + "Renaming a column may break existing clients.".into(), + rename_column.syntax().text_range(), + None, + )); + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +ALTER TABLE "table_name" RENAME COLUMN "column_name" TO "new_column_name"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RenamingColumn]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/renaming_table.rs b/crates/squawk_linter/src/rules/renaming_table.rs new file mode 100644 index 00000000..204c3215 --- /dev/null +++ b/crates/squawk_linter/src/rules/renaming_table.rs @@ -0,0 +1,43 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn renaming_table(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::AlterTable(alter_table) = item { + for action in alter_table.actions() { + if let ast::AlterTableAction::RenameTable(rename_table) = action { + ctx.report(Violation::new( + ErrorCode::RenamingTable, + "Renaming a table may break existing clients.".into(), + rename_table.syntax().text_range(), + None, + )); + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + #[test] + fn err() { + let sql = r#" +ALTER TABLE "table_name" RENAME TO "new_table_name"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RenamingTable]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } +} diff --git a/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs b/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs new file mode 100644 index 00000000..ab5fcf80 --- /dev/null +++ b/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs @@ -0,0 +1,106 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{text::trim_quotes, ErrorCode, Linter, Violation}; + +use super::constraint_missing_not_valid::tables_created_in_transaction; + +pub(crate) fn require_concurrent_index_creation(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + let tables_created = tables_created_in_transaction(ctx.settings.assume_in_transaction, &file); + for item in file.items() { + if let ast::Item::CreateIndex(create_index) = item { + if let Some(table_name) = create_index + .path() + .and_then(|x| x.segment()) + .and_then(|x| x.name_ref()) + { + if create_index.concurrently_token().is_none() + && !tables_created.contains(trim_quotes(table_name.text().as_str())) + { + ctx.report(Violation::new( + ErrorCode::RequireConcurrentIndexCreation, +"During a normal index creation, table updates are blocked, but reads are still allowed. `CONCURRENTLY` avoids locking the table against writes during index creation.".into(), + + + create_index.syntax().text_range(), + None, + )); + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{Linter, Rule}; + + /// ```sql + /// -- instead of + /// CREATE INDEX "field_name_idx" ON "table_name" ("field_name"); + /// -- use CONCURRENTLY + /// CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); + /// ``` + #[test] + fn adding_index_non_concurrently_err() { + let sql = r#" +-- instead of +CREATE INDEX "field_name_idx" ON "table_name" ("field_name"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexCreation]); + let errors = linter.lint(file, sql); + assert_ne!(errors.len(), 0); + assert_debug_snapshot!(errors); + } + + #[test] + fn adding_index_concurrently_ok() { + let sql = r#" +-- use CONCURRENTLY +CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexCreation]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn new_table_ok() { + let sql = r#" +BEGIN; +CREATE TABLE "core_foo" ( +"id" serial NOT NULL PRIMARY KEY, +"tenant_id" integer NULL +); +CREATE INDEX "core_foo_tenant_id_4d397ef9" ON "core_foo" ("tenant_id"); +COMMIT; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexCreation]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn new_table_in_assume_transaction_ok() { + let sql = r#" +CREATE TABLE "core_foo" ( +"id" serial NOT NULL PRIMARY KEY, +"tenant_id" integer NULL +); +CREATE INDEX "core_foo_tenant_id_4d397ef9" ON "core_foo" ("tenant_id"); + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexCreation]); + linter.settings.assume_in_transaction = true; + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs b/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs new file mode 100644 index 00000000..3f4a337f --- /dev/null +++ b/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs @@ -0,0 +1,87 @@ +use syntax::{ + ast::{self, AstNode, HasModuleItem}, + Parse, SourceFile, +}; + +use crate::{ErrorCode, Linter, Violation}; + +pub(crate) fn require_concurrent_index_deletion(ctx: &mut Linter, parse: &Parse) { + let file = parse.tree(); + for item in file.items() { + if let ast::Item::DropIndex(drop_index) = item { + if drop_index.concurrently_token().is_none() { + ctx.report(Violation::new( + ErrorCode::RequireConcurrentIndexDeletion, +"A normal `DROP INDEX` acquires an `ACCESS EXCLUSIVE` lock on the table, blocking other accesses until the index drop can be completed.".into(), + drop_index.syntax().text_range(), + None, + )); + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_debug_snapshot; + + use crate::{ErrorCode, Linter, Rule}; + + #[test] + fn drop_index_missing_concurrently_err() { + let sql = r#" + -- instead of + DROP INDEX IF EXISTS "field_name_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].code, ErrorCode::RequireConcurrentIndexDeletion); + assert_debug_snapshot!(errors); + } + + #[test] + fn drop_index_concurrently_ok() { + let sql = r#" +DROP INDEX CONCURRENTLY IF EXISTS "field_name_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn regression_false_positive_drop_type_ok() { + let sql = r#" +DROP INDEX CONCURRENTLY IF EXISTS "field_name_idx"; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn regression_false_positive_drop_table_ok() { + let sql = r#" +DROP TABLE IF EXISTS some_table; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } + + #[test] + fn regression_false_positive_drop_trigger_ok() { + let sql = r#" +DROP TRIGGER IF EXISTS trigger on foo_table; + "#; + let file = syntax::SourceFile::parse(sql); + let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); + let errors = linter.lint(file, sql); + assert_eq!(errors.len(), 0); + } +} diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__add_numbers_ok.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__add_numbers_ok.snap new file mode 100644 index 00000000..5563b6de --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__add_numbers_ok.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__arbitrary_func_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__arbitrary_func_err.snap new file mode 100644 index 00000000..0d76ee71 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__arbitrary_func_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[ + Violation { + code: AddingFieldWithDefault, + message: "Adding a generated column requires a table rewrite with an `ACCESS EXCLUSIVE` lock.", + text_range: 74..83, + messages: [ + "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_bool_ok.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_bool_ok.snap new file mode 100644 index 00000000..5563b6de --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_bool_ok.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_enum_ok.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_enum_ok.snap new file mode 100644 index 00000000..5563b6de --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_enum_ok.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_func_now_ok.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_func_now_ok.snap new file mode 100644 index 00000000..5563b6de --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_func_now_ok.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_jsonb_ok.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_jsonb_ok.snap new file mode 100644 index 00000000..5563b6de --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_jsonb_ok.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_random_with_args_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_random_with_args_err.snap new file mode 100644 index 00000000..fea9efa4 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_random_with_args_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[ + Violation { + code: AddingFieldWithDefault, + message: "Adding a generated column requires a table rewrite with an `ACCESS EXCLUSIVE` lock.", + text_range: 80..88, + messages: [ + "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_str_ok.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_str_ok.snap new file mode 100644 index 00000000..5563b6de --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_str_ok.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_uuid_error.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_uuid_error.snap new file mode 100644 index 00000000..01326346 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_uuid_error.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[ + Violation { + code: AddingFieldWithDefault, + message: "Adding a generated column requires a table rewrite with an `ACCESS EXCLUSIVE` lock.", + text_range: 60..66, + messages: [ + "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_uuid_error_multi_stmt.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_uuid_error_multi_stmt.snap new file mode 100644 index 00000000..047703a5 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_uuid_error_multi_stmt.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[ + Violation { + code: AddingFieldWithDefault, + message: "Adding a generated column requires a table rewrite with an `ACCESS EXCLUSIVE` lock.", + text_range: 56..62, + messages: [ + "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_volatile_func_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_volatile_func_err.snap new file mode 100644 index 00000000..3efb5b3f --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__default_volatile_func_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[ + Violation { + code: AddingFieldWithDefault, + message: "Adding a generated column requires a table rewrite with an `ACCESS EXCLUSIVE` lock.", + text_range: 76..84, + messages: [ + "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__docs_example_ok.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__docs_example_ok.snap new file mode 100644 index 00000000..5563b6de --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__docs_example_ok.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__docs_example_ok_post_pg_11.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__docs_example_ok_post_pg_11.snap new file mode 100644 index 00000000..5563b6de --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__docs_example_ok_post_pg_11.snap @@ -0,0 +1,5 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__generated_stored_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__generated_stored_err.snap new file mode 100644 index 00000000..951b257a --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_field_with_default__test__generated_stored_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/adding_field_with_default.rs +expression: errors +--- +[ + Violation { + code: AddingFieldWithDefault, + message: "Adding a generated column requires a table rewrite with an `ACCESS EXCLUSIVE` lock.", + text_range: 40..78, + messages: [ + "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_not_null_field__test__set_not_null.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_not_null_field__test__set_not_null.snap new file mode 100644 index 00000000..269d3ecf --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_not_null_field__test__set_not_null.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/adding_not_null_field.rs +expression: errors +--- +[ + Violation { + code: AddingNotNullableField, + message: "Setting a column NOT NULL blocks reads while the table is scanned.", + text_range: 46..58, + messages: [ + "Use a check constraint instead.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_primary_key_constraint__test__plain_primary_key.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_primary_key_constraint__test__plain_primary_key.snap new file mode 100644 index 00000000..a71a4f98 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_primary_key_constraint__test__plain_primary_key.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/adding_primary_key_constraint.rs +expression: errors +--- +[ + Violation { + code: AddingSerialPrimaryKeyField, + message: "Adding a primary key constraint requires an `ACCESS EXCLUSIVE` lock that will block all reads and writes to the table while the primary key index is built.", + text_range: 23..39, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_primary_key_constraint__test__serial_primary_key.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_primary_key_constraint__test__serial_primary_key.snap new file mode 100644 index 00000000..00defb2b --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_primary_key_constraint__test__serial_primary_key.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/adding_primary_key_constraint.rs +expression: errors +--- +[ + Violation { + code: AddingSerialPrimaryKeyField, + message: "Adding a primary key constraint requires an `ACCESS EXCLUSIVE` lock that will block all reads and writes to the table while the primary key index is built.", + text_range: 43..54, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_required_field__test__not_null_without_default.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_required_field__test__not_null_without_default.snap new file mode 100644 index 00000000..939eb3b5 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__adding_required_field__test__not_null_without_default.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/adding_required_field.rs +expression: errors +--- +[ + Violation { + code: AddingRequiredField, + message: "Adding a new column that is `NOT NULL` and has no default value to an existing table effectively makes it required.", + text_range: 22..58, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_alter_domain_with_add_constraint__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_alter_domain_with_add_constraint__test__err.snap new file mode 100644 index 00000000..1104e1f9 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_alter_domain_with_add_constraint__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs +expression: errors +--- +[ + Violation { + code: BanAlterDomainWithAddConstraint, + message: "Domains with constraints have poor support for online migrations. Use table and column constraints instead.", + text_range: 31..79, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__all_the_types.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__all_the_types.snap new file mode 100644 index 00000000..2bcc9a03 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__all_the_types.snap @@ -0,0 +1,42 @@ +--- +source: crates/squawk_linter/src/rules/ban_char_field.rs +expression: errors +--- +[ + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 59..68, + messages: [], + }, + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 76..90, + messages: [], + }, + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 98..102, + messages: [], + }, + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 110..119, + messages: [], + }, + Violation { + code: BanCharField, + message: "Using `character` is likely a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 265..280, + messages: [], + }, + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 288..292, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__alter_table_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__alter_table_err.snap new file mode 100644 index 00000000..7c5a941c --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__alter_table_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_char_field.rs +expression: errors +--- +[ + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 28..32, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__array_char_type_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__array_char_type_err.snap new file mode 100644 index 00000000..f486d0d3 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__array_char_type_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_char_field.rs +expression: errors +--- +[ + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 22..26, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__creating_table_with_char_errors.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__creating_table_with_char_errors.snap new file mode 100644 index 00000000..b813748f --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_char_field__test__creating_table_with_char_errors.snap @@ -0,0 +1,30 @@ +--- +source: crates/squawk_linter/src/rules/ban_char_field.rs +expression: errors +--- +[ + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 77..86, + messages: [], + }, + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 108..122, + messages: [], + }, + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 147..151, + messages: [], + }, + Violation { + code: BanCharField, + message: "Using `character` is likey a mistake and should almost always be replaced by `text` or `varchar`.", + text_range: 174..183, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_concurrent_index_creation_in_transaction__test__assuming_in_transaction_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_concurrent_index_creation_in_transaction__test__assuming_in_transaction_err.snap new file mode 100644 index 00000000..69fd9180 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_concurrent_index_creation_in_transaction__test__assuming_in_transaction_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs +expression: errors +--- +[ + Violation { + code: BanConcurrentIndexCreationInTransaction, + message: "While regular index creation can happen inside a transaction, this is not allowed when the CONCURRENTLY option is used.", + text_range: 39..51, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_concurrent_index_creation_in_transaction__test__ban_concurrent_index_creation_in_transaction_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_concurrent_index_creation_in_transaction__test__ban_concurrent_index_creation_in_transaction_err.snap new file mode 100644 index 00000000..eef7e768 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_concurrent_index_creation_in_transaction__test__ban_concurrent_index_creation_in_transaction_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs +expression: errors +--- +[ + Violation { + code: BanConcurrentIndexCreationInTransaction, + message: "While regular index creation can happen inside a transaction, this is not allowed when the CONCURRENTLY option is used.", + text_range: 59..71, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_create_domain_with_constraint__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_create_domain_with_constraint__test__err.snap new file mode 100644 index 00000000..062476c8 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_create_domain_with_constraint__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs +expression: errors +--- +[ + Violation { + code: BanCreateDomainWithConstraint, + message: "Domains with constraints have poor support for online migrations. Use table and column constraints instead.", + text_range: 46..63, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_create_domain_with_constraint__test__err_with_multiple_constraints.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_create_domain_with_constraint__test__err_with_multiple_constraints.snap new file mode 100644 index 00000000..47dcc10e --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_create_domain_with_constraint__test__err_with_multiple_constraints.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs +expression: errors +--- +[ + Violation { + code: BanCreateDomainWithConstraint, + message: "Domains with constraints have poor support for online migrations. Use table and column constraints instead.", + text_range: 22..48, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_column__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_column__test__err.snap new file mode 100644 index 00000000..e23bddfa --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_column__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_column.rs +expression: errors +--- +[ + Violation { + code: BanDropColumn, + message: "Dropping a column may break existing clients.", + text_range: 23..52, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_database__test__ban_drop_database.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_database__test__ban_drop_database.snap new file mode 100644 index 00000000..998372ec --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_database__test__ban_drop_database.snap @@ -0,0 +1,24 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_database.rs +expression: errors +--- +[ + Violation { + code: BanDropDatabase, + message: "Dropping a database may break existing clients.", + text_range: 9..35, + messages: [], + }, + Violation { + code: BanDropDatabase, + message: "Dropping a database may break existing clients.", + text_range: 45..81, + messages: [], + }, + Violation { + code: BanDropDatabase, + message: "Dropping a database may break existing clients.", + text_range: 91..127, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_not_null__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_not_null__test__err.snap new file mode 100644 index 00000000..4b2ffd58 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_not_null__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_not_null.rs +expression: errors +--- +[ + Violation { + code: BanDropNotNull, + message: "Dropping a NOT NULL constraint may break existing clients.", + text_range: 46..59, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_table__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_table__test__err.snap new file mode 100644 index 00000000..bf40fd30 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_table__test__err.snap @@ -0,0 +1,24 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_table.rs +expression: errors +--- +[ + Violation { + code: BanDropTable, + message: "Dropping a table may break existing clients.", + text_range: 1..24, + messages: [], + }, + Violation { + code: BanDropTable, + message: "Dropping a table may break existing clients.", + text_range: 26..59, + messages: [], + }, + Violation { + code: BanDropTable, + message: "Dropping a table may break existing clients.", + text_range: 61..94, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__changing_column_type__test__another_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__changing_column_type__test__another_err.snap new file mode 100644 index 00000000..94f8ffff --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__changing_column_type__test__another_err.snap @@ -0,0 +1,18 @@ +--- +source: crates/squawk_linter/src/rules/changing_column_type.rs +expression: errors +--- +[ + Violation { + code: ChangingColumnType, + message: "Changing a column type requires an `ACCESS EXCLUSIVE` lock on the table which blocks reads and writes while the table is rewritten. Changing the type of the column may also break other clients reading from the table.", + text_range: 88..131, + messages: [], + }, + Violation { + code: ChangingColumnType, + message: "Changing a column type requires an `ACCESS EXCLUSIVE` lock on the table which blocks reads and writes while the table is rewritten. Changing the type of the column may also break other clients reading from the table.", + text_range: 178..205, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__changing_column_type__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__changing_column_type__test__err.snap new file mode 100644 index 00000000..65473fe3 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__changing_column_type__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/changing_column_type.rs +expression: errors +--- +[ + Violation { + code: ChangingColumnType, + message: "Changing a column type requires an `ACCESS EXCLUSIVE` lock on the table which blocks reads and writes while the table is rewritten. Changing the type of the column may also break other clients reading from the table.", + text_range: 92..121, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__adding_check_constraint_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__adding_check_constraint_err.snap new file mode 100644 index 00000000..f52b2f84 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__adding_check_constraint_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/constraint_missing_not_valid.rs +expression: errors +--- +[ + Violation { + code: ConstraintMissingNotValid, + message: "By default new constraints require a table scan and block writes to the table while that scan occurs.", + text_range: 38..94, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__adding_fk_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__adding_fk_err.snap new file mode 100644 index 00000000..c9cb5cd5 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__adding_fk_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/constraint_missing_not_valid.rs +expression: errors +--- +[ + Violation { + code: ConstraintMissingNotValid, + message: "By default new constraints require a table scan and block writes to the table while that scan occurs.", + text_range: 40..114, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_assume_transaction_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_assume_transaction_err.snap new file mode 100644 index 00000000..cb28dfb9 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_assume_transaction_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/constraint_missing_not_valid.rs +expression: errors +--- +[ + Violation { + code: ConstraintMissingNotValid, + message: "Using NOT VALID and VALIDATE CONSTRAINT in the same transaction will block all reads while the constraint is validated.", + text_range: 134..163, + messages: [ + "Add constraint as NOT VALID in one transaction and VALIDATE CONSTRAINT in a separate transaction.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_transaction_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_transaction_err.snap new file mode 100644 index 00000000..bc72d2a7 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_transaction_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/constraint_missing_not_valid.rs +expression: errors +--- +[ + Violation { + code: ConstraintMissingNotValid, + message: "Using NOT VALID and VALIDATE CONSTRAINT in the same transaction will block all reads while the constraint is validated.", + text_range: 141..170, + messages: [ + "Add constraint as NOT VALID in one transaction and VALIDATE CONSTRAINT in a separate transaction.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_with_assume_in_transaction_with_explicit_commit_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_with_assume_in_transaction_with_explicit_commit_err.snap new file mode 100644 index 00000000..cb28dfb9 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__constraint_missing_not_valid__test__not_valid_validate_with_assume_in_transaction_with_explicit_commit_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/constraint_missing_not_valid.rs +expression: errors +--- +[ + Violation { + code: ConstraintMissingNotValid, + message: "Using NOT VALID and VALIDATE CONSTRAINT in the same transaction will block all reads while the constraint is validated.", + text_range: 134..163, + messages: [ + "Add constraint as NOT VALID in one transaction and VALIDATE CONSTRAINT in a separate transaction.", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__adding_unique_constraint_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__adding_unique_constraint_err.snap new file mode 100644 index 00000000..f7f8d32b --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__adding_unique_constraint_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/disallow_unique_constraint.rs +expression: errors +--- +[ + Violation { + code: DisallowedUniqueConstraint, + message: "Adding a `UNIQUE` constraint requires an `ACCESS EXCLUSIVE` lock which blocks reads and writes to the table while the index is built.", + text_range: 28..80, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__unique_constraint_inline_add_column_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__unique_constraint_inline_add_column_err.snap new file mode 100644 index 00000000..4f784138 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__unique_constraint_inline_add_column_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/disallow_unique_constraint.rs +expression: errors +--- +[ + Violation { + code: DisallowedUniqueConstraint, + message: "Adding a `UNIQUE` constraint requires an `ACCESS EXCLUSIVE` lock which blocks reads and writes to the table while the index is built.", + text_range: 37..69, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__unique_constraint_inline_add_column_unique_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__unique_constraint_inline_add_column_unique_err.snap new file mode 100644 index 00000000..6b96c7fe --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__disallow_unique_constraint__test__unique_constraint_inline_add_column_unique_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/disallow_unique_constraint.rs +expression: errors +--- +[ + Violation { + code: DisallowedUniqueConstraint, + message: "Adding a `UNIQUE` constraint requires an `ACCESS EXCLUSIVE` lock which blocks reads and writes to the table while the index is built.", + text_range: 37..43, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_add_column_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_add_column_err.snap new file mode 100644 index 00000000..11e530a3 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_add_column_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_big_int.rs +expression: errors +--- +[ + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 28..35, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_alter_column_type_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_alter_column_type_err.snap new file mode 100644 index 00000000..79e843cd --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_alter_column_type_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_big_int.rs +expression: errors +--- +[ + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 35..42, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_alter_column_type_with_quotes_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_alter_column_type_with_quotes_err.snap new file mode 100644 index 00000000..4a698fcf --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__alter_table_alter_column_type_with_quotes_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_big_int.rs +expression: errors +--- +[ + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 35..44, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__create_table_many_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__create_table_many_err.snap new file mode 100644 index 00000000..e6fdb760 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__create_table_many_err.snap @@ -0,0 +1,18 @@ +--- +source: crates/squawk_linter/src/rules/prefer_big_int.rs +expression: errors +--- +[ + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 30..37, + messages: [], + }, + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 47..53, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__err.snap new file mode 100644 index 00000000..b922a468 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_big_int__test__err.snap @@ -0,0 +1,54 @@ +--- +source: crates/squawk_linter/src/rules/prefer_big_int.rs +expression: errors +--- +[ + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 29..37, + messages: [], + }, + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 69..73, + messages: [], + }, + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 105..112, + messages: [], + }, + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 144..148, + messages: [], + }, + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 180..186, + messages: [], + }, + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 218..225, + messages: [], + }, + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 257..264, + messages: [], + }, + Violation { + code: PreferBigInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 296..307, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_bigint_over_int__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_bigint_over_int__test__err.snap new file mode 100644 index 00000000..ca6e5e0d --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_bigint_over_int__test__err.snap @@ -0,0 +1,30 @@ +--- +source: crates/squawk_linter/src/rules/prefer_bigint_over_int.rs +expression: errors +--- +[ + Violation { + code: PreferBigintOverInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 29..36, + messages: [], + }, + Violation { + code: PreferBigintOverInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 68..72, + messages: [], + }, + Violation { + code: PreferBigintOverInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 104..110, + messages: [], + }, + Violation { + code: PreferBigintOverInt, + message: "Using 32-bit integer fields can result in hitting the max `int` limit.", + text_range: 142..149, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_bigint_over_smallint__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_bigint_over_smallint__test__err.snap new file mode 100644 index 00000000..15c1c290 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_bigint_over_smallint__test__err.snap @@ -0,0 +1,30 @@ +--- +source: crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs +expression: errors +--- +[ + Violation { + code: PreferBigintOverSmallint, + message: "Using 16-bit integer fields can result in hitting the max `int` limit.", + text_range: 29..37, + messages: [], + }, + Violation { + code: PreferBigintOverSmallint, + message: "Using 16-bit integer fields can result in hitting the max `int` limit.", + text_range: 69..73, + messages: [], + }, + Violation { + code: PreferBigintOverSmallint, + message: "Using 16-bit integer fields can result in hitting the max `int` limit.", + text_range: 105..116, + messages: [], + }, + Violation { + code: PreferBigintOverSmallint, + message: "Using 16-bit integer fields can result in hitting the max `int` limit.", + text_range: 148..155, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_identity__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_identity__test__err.snap new file mode 100644 index 00000000..af7027c3 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_identity__test__err.snap @@ -0,0 +1,42 @@ +--- +source: crates/squawk_linter/src/rules/prefer_identity.rs +expression: errors +--- +[ + Violation { + code: PreferIdentity, + message: "Serial types make permissions and schema management difficult. Identity columns are standard SQL and have more features and better usability.", + text_range: 29..35, + messages: [], + }, + Violation { + code: PreferIdentity, + message: "Serial types make permissions and schema management difficult. Identity columns are standard SQL and have more features and better usability.", + text_range: 67..74, + messages: [], + }, + Violation { + code: PreferIdentity, + message: "Serial types make permissions and schema management difficult. Identity columns are standard SQL and have more features and better usability.", + text_range: 106..113, + messages: [], + }, + Violation { + code: PreferIdentity, + message: "Serial types make permissions and schema management difficult. Identity columns are standard SQL and have more features and better usability.", + text_range: 145..152, + messages: [], + }, + Violation { + code: PreferIdentity, + message: "Serial types make permissions and schema management difficult. Identity columns are standard SQL and have more features and better usability.", + text_range: 184..195, + messages: [], + }, + Violation { + code: PreferIdentity, + message: "Serial types make permissions and schema management difficult. Identity columns are standard SQL and have more features and better usability.", + text_range: 227..236, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_drop_column_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_drop_column_err.snap new file mode 100644 index 00000000..4c571169 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_drop_column_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 54..75, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_drop_constraint_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_drop_constraint_err.snap new file mode 100644 index 00000000..16223bdf --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_drop_constraint_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 63..93, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_err.snap new file mode 100644 index 00000000..386a33b9 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__alter_table_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 63..98, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_err.snap new file mode 100644 index 00000000..f336aa71 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 40..108, + messages: [ + "Use an explicit name for a concurrently created index", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_muli_stmts_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_muli_stmts_err.snap new file mode 100644 index 00000000..36322423 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_muli_stmts_err.snap @@ -0,0 +1,22 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 1..57, + messages: [ + "Use an explicit name for a concurrently created index", + ], + }, + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 59..115, + messages: [ + "Use an explicit name for a concurrently created index", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_unnamed_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_unnamed_err.snap new file mode 100644 index 00000000..98161f4b --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_index_concurrently_unnamed_err.snap @@ -0,0 +1,14 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 40..96, + messages: [ + "Use an explicit name for a concurrently created index", + ], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_table_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_table_err.snap new file mode 100644 index 00000000..9f86e90a --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__create_table_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 11..122, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__disable_row_level_security_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__disable_row_level_security_err.snap new file mode 100644 index 00000000..61304942 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__disable_row_level_security_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 63..89, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__double_add_after_drop_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__double_add_after_drop_err.snap new file mode 100644 index 00000000..f87b67c4 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__double_add_after_drop_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 240..298, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__drop_index_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__drop_index_err.snap new file mode 100644 index 00000000..8618647f --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__drop_index_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 40..75, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__enable_row_level_security_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__enable_row_level_security_err.snap new file mode 100644 index 00000000..26d66eca --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__enable_row_level_security_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 63..88, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__enable_row_level_security_without_exists_check_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__enable_row_level_security_without_exists_check_err.snap new file mode 100644 index 00000000..b4b79ee2 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_robust_stmts__test__enable_row_level_security_without_exists_check_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_robust_stmts.rs +expression: errors +--- +[ + Violation { + code: PreferRobustStmts, + message: "Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.", + text_range: 53..78, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__adding_column_non_text_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__adding_column_non_text_err.snap new file mode 100644 index 00000000..bf23edd6 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__adding_column_non_text_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_text_field.rs +expression: errors +--- +[ + Violation { + code: PreferTextField, + message: "Changing the size of a `varchar` field requires an `ACCESS EXCLUSIVE` lock, that will prevent all reads and writes to the table.", + text_range: 56..68, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__create_table_with_pgcatalog_varchar_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__create_table_with_pgcatalog_varchar_err.snap new file mode 100644 index 00000000..9719e0a1 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__create_table_with_pgcatalog_varchar_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_text_field.rs +expression: errors +--- +[ + Violation { + code: PreferTextField, + message: "Changing the size of a `varchar` field requires an `ACCESS EXCLUSIVE` lock, that will prevent all reads and writes to the table.", + text_range: 69..92, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__create_table_with_varchar_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__create_table_with_varchar_err.snap new file mode 100644 index 00000000..2970531d --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__create_table_with_varchar_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_text_field.rs +expression: errors +--- +[ + Violation { + code: PreferTextField, + message: "Changing the size of a `varchar` field requires an `ACCESS EXCLUSIVE` lock, that will prevent all reads and writes to the table.", + text_range: 111..123, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__increase_varchar_size_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__increase_varchar_size_err.snap new file mode 100644 index 00000000..e4137151 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_text_field__test__increase_varchar_size_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/prefer_text_field.rs +expression: errors +--- +[ + Violation { + code: PreferTextField, + message: "Changing the size of a `varchar` field requires an `ACCESS EXCLUSIVE` lock, that will prevent all reads and writes to the table.", + text_range: 89..102, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__alter_table_with_timestamp_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__alter_table_with_timestamp_err.snap new file mode 100644 index 00000000..9913c5a9 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__alter_table_with_timestamp_err.snap @@ -0,0 +1,18 @@ +--- +source: crates/squawk_linter/src/rules/prefer_timestamptz.rs +expression: errors +--- +[ + Violation { + code: PreferTimestampTz, + message: "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.", + text_range: 56..65, + messages: [], + }, + Violation { + code: PreferTimestampTz, + message: "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.", + text_range: 125..152, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__create_table_with_timestamp_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__create_table_with_timestamp_err.snap new file mode 100644 index 00000000..4bbc5ea7 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__create_table_with_timestamp_err.snap @@ -0,0 +1,18 @@ +--- +source: crates/squawk_linter/src/rules/prefer_timestamptz.rs +expression: errors +--- +[ + Violation { + code: PreferTimestampTz, + message: "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.", + text_range: 43..52, + messages: [], + }, + Violation { + code: PreferTimestampTz, + message: "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.", + text_range: 99..126, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__renaming_column__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__renaming_column__test__err.snap new file mode 100644 index 00000000..e8f4343c --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__renaming_column__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/renaming_column.rs +expression: errors +--- +[ + Violation { + code: RenamingColumn, + message: "Renaming a column may break existing clients.", + text_range: 26..74, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__renaming_table__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__renaming_table__test__err.snap new file mode 100644 index 00000000..d94c68e2 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__renaming_table__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/renaming_table.rs +expression: errors +--- +[ + Violation { + code: RenamingTable, + message: "Renaming a table may break existing clients.", + text_range: 26..52, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__require_concurrent_index_creation__test__adding_index_non_concurrently_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__require_concurrent_index_creation__test__adding_index_non_concurrently_err.snap new file mode 100644 index 00000000..219b5629 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__require_concurrent_index_creation__test__adding_index_non_concurrently_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/require_concurrent_index_creation.rs +expression: errors +--- +[ + Violation { + code: RequireConcurrentIndexCreation, + message: "During a normal index creation, table updates are blocked, but reads are still allowed. `CONCURRENTLY` avoids locking the table against writes during index creation.", + text_range: 15..75, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__require_concurrent_index_deletion__test__drop_index_missing_concurrently_err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__require_concurrent_index_deletion__test__drop_index_missing_concurrently_err.snap new file mode 100644 index 00000000..688b409f --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__require_concurrent_index_deletion__test__drop_index_missing_concurrently_err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs +expression: errors +--- +[ + Violation { + code: RequireConcurrentIndexDeletion, + message: "A normal `DROP INDEX` acquires an `ACCESS EXCLUSIVE` lock on the table, blocking other accesses until the index drop can be completed.", + text_range: 19..56, + messages: [], + }, +] diff --git a/crates/squawk_linter/src/text.rs b/crates/squawk_linter/src/text.rs new file mode 100644 index 00000000..b9a6c5fb --- /dev/null +++ b/crates/squawk_linter/src/text.rs @@ -0,0 +1,8 @@ +// TODO: figure out a better way to handle quoted and unquoted idents +pub(crate) fn trim_quotes(s: &str) -> &str { + if s.starts_with('"') && s.ends_with('"') { + &s[1..s.len() - 1] + } else { + s + } +} From be79eeb1fe1155076c037c129806eaa0d201d85f Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 3 May 2025 19:10:19 -0400 Subject: [PATCH 2/7] fix --- crates/squawk_linter/Cargo.toml | 5 ++--- crates/squawk_linter/src/ignore.rs | 2 +- crates/squawk_linter/src/lib.rs | 2 +- crates/squawk_linter/src/rules/adding_field_with_default.rs | 6 +++--- .../src/rules/adding_foreign_key_constraint.rs | 2 +- crates/squawk_linter/src/rules/adding_not_null_field.rs | 2 +- .../src/rules/adding_primary_key_constraint.rs | 2 +- crates/squawk_linter/src/rules/adding_required_field.rs | 2 +- .../src/rules/ban_alter_domain_with_add_constraint.rs | 2 +- crates/squawk_linter/src/rules/ban_char_field.rs | 2 +- .../rules/ban_concurrent_index_creation_in_transaction.rs | 2 +- .../src/rules/ban_create_domain_with_constraint.rs | 2 +- crates/squawk_linter/src/rules/ban_drop_column.rs | 2 +- crates/squawk_linter/src/rules/ban_drop_database.rs | 2 +- crates/squawk_linter/src/rules/ban_drop_not_null.rs | 2 +- crates/squawk_linter/src/rules/ban_drop_table.rs | 2 +- crates/squawk_linter/src/rules/changing_column_type.rs | 2 +- .../squawk_linter/src/rules/constraint_missing_not_valid.rs | 2 +- .../squawk_linter/src/rules/disallow_unique_constraint.rs | 2 +- crates/squawk_linter/src/rules/prefer_big_int.rs | 2 +- crates/squawk_linter/src/rules/prefer_bigint_over_int.rs | 4 ++-- .../squawk_linter/src/rules/prefer_bigint_over_smallint.rs | 4 ++-- crates/squawk_linter/src/rules/prefer_identity.rs | 2 +- crates/squawk_linter/src/rules/prefer_robust_stmts.rs | 2 +- crates/squawk_linter/src/rules/prefer_text_field.rs | 2 +- crates/squawk_linter/src/rules/prefer_timestamptz.rs | 2 +- crates/squawk_linter/src/rules/renaming_column.rs | 2 +- crates/squawk_linter/src/rules/renaming_table.rs | 2 +- .../src/rules/require_concurrent_index_creation.rs | 2 +- .../src/rules/require_concurrent_index_deletion.rs | 2 +- 30 files changed, 35 insertions(+), 36 deletions(-) diff --git a/crates/squawk_linter/Cargo.toml b/crates/squawk_linter/Cargo.toml index bb60c196..600aaa3f 100644 --- a/crates/squawk_linter/Cargo.toml +++ b/crates/squawk_linter/Cargo.toml @@ -7,10 +7,9 @@ authors.workspace = true license.workspace = true [dependencies] -syntax.workspace = true -lexer.workspace = true -rowan.workspace = true +squawk_syntax.workspace = true +rowan.workspace = true serde.workspace = true lazy_static.workspace = true insta.workspace = true diff --git a/crates/squawk_linter/src/ignore.rs b/crates/squawk_linter/src/ignore.rs index 902115f2..876fca59 100644 --- a/crates/squawk_linter/src/ignore.rs +++ b/crates/squawk_linter/src/ignore.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use rowan::{NodeOrToken, TextRange, TextSize}; -use syntax::{SyntaxKind, SyntaxNode, SyntaxToken}; +use squawk_syntax::{SyntaxKind, SyntaxNode, SyntaxToken}; use crate::{ErrorCode, Linter, Violation}; diff --git a/crates/squawk_linter/src/lib.rs b/crates/squawk_linter/src/lib.rs index d8f2d80e..f336b0bb 100644 --- a/crates/squawk_linter/src/lib.rs +++ b/crates/squawk_linter/src/lib.rs @@ -10,7 +10,7 @@ use lazy_static::lazy_static; use rowan::TextRange; use serde::{Deserialize, Serialize}; -use syntax::{Parse, SourceFile}; +use squawk_syntax::{Parse, SourceFile}; mod ignore; mod ignore_index; diff --git a/crates/squawk_linter/src/rules/adding_field_with_default.rs b/crates/squawk_linter/src/rules/adding_field_with_default.rs index a7bd25ac..7f88a44a 100644 --- a/crates/squawk_linter/src/rules/adding_field_with_default.rs +++ b/crates/squawk_linter/src/rules/adding_field_with_default.rs @@ -1,9 +1,9 @@ use lazy_static::lazy_static; use std::collections::HashSet; -use syntax::ast; -use syntax::ast::{AstNode, HasArgList}; -use syntax::{ast::HasModuleItem, Parse, SourceFile}; +use squawk_syntax::ast; +use squawk_syntax::ast::{AstNode, HasArgList}; +use squawk_syntax::{ast::HasModuleItem, Parse, SourceFile}; use crate::{ErrorCode, Linter, Violation}; diff --git a/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs b/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs index f46cb18d..6967644d 100644 --- a/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs +++ b/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/adding_not_null_field.rs b/crates/squawk_linter/src/rules/adding_not_null_field.rs index e7490d82..b3987ef2 100644 --- a/crates/squawk_linter/src/rules/adding_not_null_field.rs +++ b/crates/squawk_linter/src/rules/adding_not_null_field.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs b/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs index b3e8a7b0..048b3bde 100644 --- a/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs +++ b/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/adding_required_field.rs b/crates/squawk_linter/src/rules/adding_required_field.rs index 7c3836ee..8c92d6a8 100644 --- a/crates/squawk_linter/src/rules/adding_required_field.rs +++ b/crates/squawk_linter/src/rules/adding_required_field.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs b/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs index 0aae03aa..db4715d2 100644 --- a/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs +++ b/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/ban_char_field.rs b/crates/squawk_linter/src/rules/ban_char_field.rs index e2c58106..075084e9 100644 --- a/crates/squawk_linter/src/rules/ban_char_field.rs +++ b/crates/squawk_linter/src/rules/ban_char_field.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode}, Parse, SourceFile, TokenText, }; diff --git a/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs b/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs index 2319c2bb..35ebd2e5 100644 --- a/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs +++ b/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs b/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs index 198694bd..3674924c 100644 --- a/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs +++ b/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs @@ -1,5 +1,5 @@ use rowan::TextRange; -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/ban_drop_column.rs b/crates/squawk_linter/src/rules/ban_drop_column.rs index cfbcee9a..fc431bce 100644 --- a/crates/squawk_linter/src/rules/ban_drop_column.rs +++ b/crates/squawk_linter/src/rules/ban_drop_column.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/ban_drop_database.rs b/crates/squawk_linter/src/rules/ban_drop_database.rs index 6d2a91d5..62b6b483 100644 --- a/crates/squawk_linter/src/rules/ban_drop_database.rs +++ b/crates/squawk_linter/src/rules/ban_drop_database.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/ban_drop_not_null.rs b/crates/squawk_linter/src/rules/ban_drop_not_null.rs index a0091b33..8253626b 100644 --- a/crates/squawk_linter/src/rules/ban_drop_not_null.rs +++ b/crates/squawk_linter/src/rules/ban_drop_not_null.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/ban_drop_table.rs b/crates/squawk_linter/src/rules/ban_drop_table.rs index 418eb790..20235da8 100644 --- a/crates/squawk_linter/src/rules/ban_drop_table.rs +++ b/crates/squawk_linter/src/rules/ban_drop_table.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/changing_column_type.rs b/crates/squawk_linter/src/rules/changing_column_type.rs index d6095d12..7e173687 100644 --- a/crates/squawk_linter/src/rules/changing_column_type.rs +++ b/crates/squawk_linter/src/rules/changing_column_type.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs b/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs index 52a5889c..67bf4ff8 100644 --- a/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs +++ b/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/disallow_unique_constraint.rs b/crates/squawk_linter/src/rules/disallow_unique_constraint.rs index b56d23f0..57e0a4ec 100644 --- a/crates/squawk_linter/src/rules/disallow_unique_constraint.rs +++ b/crates/squawk_linter/src/rules/disallow_unique_constraint.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/prefer_big_int.rs b/crates/squawk_linter/src/rules/prefer_big_int.rs index c312bac7..7987c5f6 100644 --- a/crates/squawk_linter/src/rules/prefer_big_int.rs +++ b/crates/squawk_linter/src/rules/prefer_big_int.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs b/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs index 73a6269a..5fc5d5ae 100644 --- a/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs +++ b/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; -use syntax::ast::AstNode; -use syntax::{ast, Parse, SourceFile}; +use squawk_syntax::ast::AstNode; +use squawk_syntax::{ast, Parse, SourceFile}; use crate::{ErrorCode, Linter, Violation}; diff --git a/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs b/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs index 87e1413c..5102fb60 100644 --- a/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs +++ b/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; -use syntax::ast::AstNode; -use syntax::{ast, Parse, SourceFile}; +use squawk_syntax::ast::AstNode; +use squawk_syntax::{ast, Parse, SourceFile}; use crate::{ErrorCode, Linter, Violation}; diff --git a/crates/squawk_linter/src/rules/prefer_identity.rs b/crates/squawk_linter/src/rules/prefer_identity.rs index 1c15e031..9d86702c 100644 --- a/crates/squawk_linter/src/rules/prefer_identity.rs +++ b/crates/squawk_linter/src/rules/prefer_identity.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/prefer_robust_stmts.rs b/crates/squawk_linter/src/rules/prefer_robust_stmts.rs index 4e4c3f89..1167ccc6 100644 --- a/crates/squawk_linter/src/rules/prefer_robust_stmts.rs +++ b/crates/squawk_linter/src/rules/prefer_robust_stmts.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasIfExists, HasIfNotExists, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/prefer_text_field.rs b/crates/squawk_linter/src/rules/prefer_text_field.rs index b45c77ce..8802ea05 100644 --- a/crates/squawk_linter/src/rules/prefer_text_field.rs +++ b/crates/squawk_linter/src/rules/prefer_text_field.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasArgList}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/prefer_timestamptz.rs b/crates/squawk_linter/src/rules/prefer_timestamptz.rs index 3f260b9c..c8db91ca 100644 --- a/crates/squawk_linter/src/rules/prefer_timestamptz.rs +++ b/crates/squawk_linter/src/rules/prefer_timestamptz.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasArgList}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/renaming_column.rs b/crates/squawk_linter/src/rules/renaming_column.rs index 61c7b839..e244d32e 100644 --- a/crates/squawk_linter/src/rules/renaming_column.rs +++ b/crates/squawk_linter/src/rules/renaming_column.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/renaming_table.rs b/crates/squawk_linter/src/rules/renaming_table.rs index 204c3215..e6d2982b 100644 --- a/crates/squawk_linter/src/rules/renaming_table.rs +++ b/crates/squawk_linter/src/rules/renaming_table.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs b/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs index ab5fcf80..4339bf33 100644 --- a/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs +++ b/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; diff --git a/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs b/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs index 3f4a337f..ea615056 100644 --- a/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs +++ b/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs @@ -1,4 +1,4 @@ -use syntax::{ +use squawk_syntax::{ ast::{self, AstNode, HasModuleItem}, Parse, SourceFile, }; From e774cff2405e52e69af7d7b778e0bee5a51557f8 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 3 May 2025 19:12:27 -0400 Subject: [PATCH 3/7] fix --- Cargo.lock | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + 2 files changed, 117 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d334374a..d8623061 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "cfg_aliases", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -222,6 +231,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clang-sys" version = "1.8.1" @@ -330,6 +345,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "crc32fast" version = "1.3.2" @@ -447,6 +468,26 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enum-iterator" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -621,6 +662,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.3" @@ -906,6 +953,16 @@ dependencies = [ "make-cmd", ] +[[package]] +name = "line-index" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e27e0ed5a392a7f5ba0b3808a2afccff16c64933312c84b57618b49d1209bd2" +dependencies = [ + "nohash-hasher", + "text-size", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1057,6 +1114,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -1604,6 +1667,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rowan" +version = "0.15.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a542b0253fa46e632d27a1dc5cf7b930de4df8659dc6e720b647fc72147ae3d" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash", + "text-size", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -1811,6 +1886,16 @@ dependencies = [ "maybe-uninit", ] +[[package]] +name = "smol_str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" +dependencies = [ + "borsh", + "serde", +] + [[package]] name = "spin" version = "0.5.2" @@ -1879,6 +1964,19 @@ dependencies = [ "insta", ] +[[package]] +name = "squawk_linter" +version = "0.0.0" +dependencies = [ + "enum-iterator", + "insta", + "lazy_static", + "line-index", + "rowan", + "serde", + "squawk_syntax", +] + [[package]] name = "squawk_parser" version = "0.0.0" @@ -1891,6 +1989,18 @@ dependencies = [ "squawk_lexer", ] +[[package]] +name = "squawk_syntax" +version = "0.0.0" +dependencies = [ + "camino", + "dir-test", + "insta", + "rowan", + "smol_str", + "squawk_parser", +] + [[package]] name = "string" version = "0.2.1" @@ -2006,6 +2116,12 @@ dependencies = [ "libc", ] +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index ed31bbb9..163f4247 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ squawk-linter = { version = "0.0.0", path = "./crates/linter" } squawk-github = { version = "0.0.0", path = "./crates/github" } squawk_lexer = { version = "0.0.0", path = "./crates/squawk_lexer" } squawk_parser = { version = "0.0.0", path = "./crates/squawk_parser" } +squawk_syntax = { version = "0.0.0", path = "./crates/squawk_syntax" } [workspace.lints.clippy] collapsible_else_if = "allow" From e3f5dc59ac8a0e30214d8bc27928d87fabcd75e0 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 3 May 2025 19:14:59 -0400 Subject: [PATCH 4/7] fmt --- crates/squawk_linter/src/rules/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/squawk_linter/src/rules/mod.rs b/crates/squawk_linter/src/rules/mod.rs index 934e70c1..f8f69e3f 100644 --- a/crates/squawk_linter/src/rules/mod.rs +++ b/crates/squawk_linter/src/rules/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod adding_foreign_key_constraint; pub(crate) mod adding_not_null_field; pub(crate) mod adding_primary_key_constraint; pub(crate) mod adding_required_field; +pub(crate) mod ban_alter_domain_with_add_constraint; pub(crate) mod ban_char_field; pub(crate) mod ban_concurrent_index_creation_in_transaction; pub(crate) mod ban_create_domain_with_constraint; @@ -24,7 +25,6 @@ pub(crate) mod renaming_column; pub(crate) mod renaming_table; pub(crate) mod require_concurrent_index_creation; pub(crate) mod require_concurrent_index_deletion; -pub(crate) mod ban_alter_domain_with_add_constraint; // xtask:new-lint:mod-decl pub(crate) use adding_field_with_default::adding_field_with_default; @@ -32,6 +32,7 @@ pub(crate) use adding_foreign_key_constraint::adding_foreign_key_constraint; pub(crate) use adding_not_null_field::adding_not_null_field; pub(crate) use adding_primary_key_constraint::adding_primary_key_constraint; pub(crate) use adding_required_field::adding_required_field; +pub(crate) use ban_alter_domain_with_add_constraint::ban_alter_domain_with_add_constraint; pub(crate) use ban_char_field::ban_char_field; pub(crate) use ban_concurrent_index_creation_in_transaction::ban_concurrent_index_creation_in_transaction; pub(crate) use ban_create_domain_with_constraint::ban_create_domain_with_constraint; @@ -53,5 +54,4 @@ pub(crate) use renaming_column::renaming_column; pub(crate) use renaming_table::renaming_table; pub(crate) use require_concurrent_index_creation::require_concurrent_index_creation; pub(crate) use require_concurrent_index_deletion::require_concurrent_index_deletion; -pub(crate) use ban_alter_domain_with_add_constraint::ban_alter_domain_with_add_constraint; // xtask:new-lint:export From a6d71498e073703d780ff55b1d87d149e3a9c2dd Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 3 May 2025 19:15:51 -0400 Subject: [PATCH 5/7] rename --- crates/squawk_linter/src/ignore.rs | 12 ++-- .../src/rules/adding_field_with_default.rs | 28 ++++----- .../rules/adding_foreign_key_constraint.rs | 8 +-- .../src/rules/adding_not_null_field.rs | 8 +-- .../rules/adding_primary_key_constraint.rs | 6 +- .../src/rules/adding_required_field.rs | 10 ++-- .../ban_alter_domain_with_add_constraint.rs | 4 +- .../squawk_linter/src/rules/ban_char_field.rs | 10 ++-- ...oncurrent_index_creation_in_transaction.rs | 10 ++-- .../ban_create_domain_with_constraint.rs | 6 +- .../src/rules/ban_drop_column.rs | 2 +- .../src/rules/ban_drop_database.rs | 2 +- .../src/rules/ban_drop_not_null.rs | 2 +- .../squawk_linter/src/rules/ban_drop_table.rs | 2 +- .../src/rules/changing_column_type.rs | 4 +- .../src/rules/constraint_missing_not_valid.rs | 22 +++---- .../src/rules/disallow_unique_constraint.rs | 16 ++--- .../squawk_linter/src/rules/prefer_big_int.rs | 12 ++-- .../src/rules/prefer_bigint_over_int.rs | 4 +- .../src/rules/prefer_bigint_over_smallint.rs | 4 +- .../src/rules/prefer_identity.rs | 4 +- .../src/rules/prefer_robust_stmts.rs | 60 +++++++++---------- .../src/rules/prefer_text_field.rs | 12 ++-- .../src/rules/prefer_timestamptz.rs | 8 +-- .../src/rules/renaming_column.rs | 2 +- .../squawk_linter/src/rules/renaming_table.rs | 2 +- .../require_concurrent_index_creation.rs | 8 +-- .../require_concurrent_index_deletion.rs | 10 ++-- 28 files changed, 139 insertions(+), 139 deletions(-) diff --git a/crates/squawk_linter/src/ignore.rs b/crates/squawk_linter/src/ignore.rs index 876fca59..0fb50618 100644 --- a/crates/squawk_linter/src/ignore.rs +++ b/crates/squawk_linter/src/ignore.rs @@ -116,7 +116,7 @@ mod test { -- squawk-ignore ban-drop-column alter table t drop column c cascade; "#; - let parse = syntax::SourceFile::parse(sql); + let parse = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([]); find_ignores(&mut linter, &parse.syntax_node()); @@ -132,7 +132,7 @@ alter table t drop column c cascade; /* squawk-ignore ban-drop-column */ alter table t drop column c cascade; "#; - let parse = syntax::SourceFile::parse(sql); + let parse = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([]); @@ -149,7 +149,7 @@ alter table t drop column c cascade; -- squawk-ignore ban-drop-column, renaming-column,ban-drop-database alter table t drop column c cascade; "#; - let parse = syntax::SourceFile::parse(sql); + let parse = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([]); @@ -168,7 +168,7 @@ alter table t drop column c cascade; /* squawk-ignore ban-drop-column, renaming-column,ban-drop-database */ alter table t drop column c cascade; "#; - let parse = syntax::SourceFile::parse(sql); + let parse = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([]); @@ -198,7 +198,7 @@ create table users ( ); "#; - let parse = syntax::SourceFile::parse(sql); + let parse = squawk_syntax::SourceFile::parse(sql); let errors: Vec<&Violation> = linter.lint(parse, sql); assert_eq!(errors.len(), 0); } @@ -208,7 +208,7 @@ create table users ( let mut linter = Linter::with_all_rules(); let sql = r#"alter table t add column c char;"#; - let parse = syntax::SourceFile::parse(sql); + let parse = squawk_syntax::SourceFile::parse(sql); let errors: Vec<&Violation> = linter.lint(parse, sql); assert_eq!(errors.len(), 1); } diff --git a/crates/squawk_linter/src/rules/adding_field_with_default.rs b/crates/squawk_linter/src/rules/adding_field_with_default.rs index 7f88a44a..ba8a6fa1 100644 --- a/crates/squawk_linter/src/rules/adding_field_with_default.rs +++ b/crates/squawk_linter/src/rules/adding_field_with_default.rs @@ -111,7 +111,7 @@ mod test { ALTER TABLE "core_recipe" ADD COLUMN "foo" integer DEFAULT 10; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -128,7 +128,7 @@ ALTER TABLE "core_recipe" ALTER COLUMN "foo" SET DEFAULT 10; -- remove nullability "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -141,7 +141,7 @@ ALTER TABLE "core_recipe" ALTER COLUMN "foo" SET DEFAULT 10; alter table t set logged, add column c integer default uuid(); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); @@ -154,7 +154,7 @@ alter table t set logged, add column c integer default uuid(); ALTER TABLE "core_recipe" ADD COLUMN "foo" integer DEFAULT uuid(); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); @@ -168,7 +168,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" integer DEFAULT uuid(); ALTER TABLE "core_recipe" ADD COLUMN "foo" boolean DEFAULT random(); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); @@ -182,7 +182,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" boolean DEFAULT random(); ALTER TABLE "core_recipe" ADD COLUMN "foo" boolean DEFAULT true; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -196,7 +196,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" boolean DEFAULT true; ALTER TABLE "core_recipe" ADD COLUMN "foo" text DEFAULT 'some-str'; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -210,7 +210,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" text DEFAULT 'some-str'; ALTER TABLE "core_recipe" ADD COLUMN "foo" some_enum_type DEFAULT 'my-enum-variant'; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -224,7 +224,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" some_enum_type DEFAULT 'my-enum-varia ALTER TABLE "core_recipe" ADD COLUMN "foo" jsonb DEFAULT '{}'::jsonb; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -238,7 +238,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" jsonb DEFAULT '{}'::jsonb; ALTER TABLE "core_recipe" ADD COLUMN "foo" jsonb DEFAULT myjsonb(); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); @@ -252,7 +252,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" jsonb DEFAULT myjsonb(); ALTER TABLE "core_recipe" ADD COLUMN "foo" timestamptz DEFAULT now(123); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); @@ -265,7 +265,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" timestamptz DEFAULT now(123); ALTER TABLE "core_recipe" ADD COLUMN "foo" timestamptz DEFAULT now(); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -278,7 +278,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" timestamptz DEFAULT now(); alter table account_metadata add column blah integer default 2 + 2; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -292,7 +292,7 @@ ALTER TABLE foo ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingFieldWithDefault]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); diff --git a/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs b/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs index 6967644d..f801612e 100644 --- a/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs +++ b/crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs @@ -75,7 +75,7 @@ mod test { COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingForeignKeyConstraint]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -91,7 +91,7 @@ ALTER TABLE "email" VALIDATE CONSTRAINT "fk_user"; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingForeignKeyConstraint]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -106,7 +106,7 @@ ALTER TABLE "email" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingForeignKeyConstraint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 1); @@ -121,7 +121,7 @@ ALTER TABLE "emails" ADD COLUMN "user_id" INT REFERENCES "user" ("id"); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingForeignKeyConstraint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 1); diff --git a/crates/squawk_linter/src/rules/adding_not_null_field.rs b/crates/squawk_linter/src/rules/adding_not_null_field.rs index b3987ef2..9ce8394d 100644 --- a/crates/squawk_linter/src/rules/adding_not_null_field.rs +++ b/crates/squawk_linter/src/rules/adding_not_null_field.rs @@ -44,7 +44,7 @@ mod test { let sql = r#" ALTER TABLE "core_recipe" ALTER COLUMN "foo" SET NOT NULL; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingNotNullField]); linter.settings.pg_version = Version::new(10, 0, 0); let errors = linter.lint(file, sql); @@ -62,7 +62,7 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" integer DEFAULT 10 NOT NULL; ALTER TABLE "core_recipe" ALTER COLUMN "foo" DROP DEFAULT; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingNotNullField]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -74,7 +74,7 @@ COMMIT; -- This won't work if the table is populated, but that error is caught by adding-required-field. ALTER TABLE "core_recipe" ADD COLUMN "foo" integer NOT NULL; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingNotNullField]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -90,7 +90,7 @@ BEGIN; ALTER TABLE "core_recipe" ADD COLUMN "foo" integer NOT NULL DEFAULT 10; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingNotNullField]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); diff --git a/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs b/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs index 048b3bde..58b9616b 100644 --- a/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs +++ b/crates/squawk_linter/src/rules/adding_primary_key_constraint.rs @@ -59,7 +59,7 @@ mod test { let sql = r#" ALTER TABLE a ADD COLUMN b SERIAL PRIMARY KEY; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingPrimaryKeyConstraint]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); @@ -71,7 +71,7 @@ mod test { let sql = r#" ALTER TABLE items ADD PRIMARY KEY (id); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingPrimaryKeyConstraint]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); @@ -84,7 +84,7 @@ ALTER TABLE items ADD PRIMARY KEY (id); ALTER TABLE items ADD CONSTRAINT items_pk PRIMARY KEY USING INDEX items_pk; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingPrimaryKeyConstraint]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); diff --git a/crates/squawk_linter/src/rules/adding_required_field.rs b/crates/squawk_linter/src/rules/adding_required_field.rs index 8c92d6a8..0dfb1831 100644 --- a/crates/squawk_linter/src/rules/adding_required_field.rs +++ b/crates/squawk_linter/src/rules/adding_required_field.rs @@ -65,7 +65,7 @@ mod test { let sql = r#" ALTER TABLE "recipe" ADD COLUMN "public" boolean; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingRequiredField]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -76,7 +76,7 @@ ALTER TABLE "recipe" ADD COLUMN "public" boolean; let sql = r#" ALTER TABLE "recipe" ADD COLUMN "public" boolean NOT NULL DEFAULT true; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingRequiredField]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -87,7 +87,7 @@ ALTER TABLE "recipe" ADD COLUMN "public" boolean NOT NULL DEFAULT true; let sql = r#" ALTER TABLE "recipe" ADD COLUMN "public" boolean NOT NULL; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingRequiredField]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); @@ -100,7 +100,7 @@ ALTER TABLE "recipe" ADD COLUMN "public" boolean NOT NULL; ALTER TABLE foo ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED NOT NULL; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingRequiredField]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); @@ -112,7 +112,7 @@ ALTER TABLE foo ALTER TABLE foo ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED ; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::AddingRequiredField]); let errors = linter.lint(file, sql); assert!(errors.is_empty()); diff --git a/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs b/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs index db4715d2..b21b2413 100644 --- a/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs +++ b/crates/squawk_linter/src/rules/ban_alter_domain_with_add_constraint.rs @@ -35,7 +35,7 @@ mod test { let sql = r#" ALTER DOMAIN domain_name ADD CONSTRAINT constraint_name CHECK (value > 0); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanAlterDomainWithAddConstraint]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -53,7 +53,7 @@ mod test { ALTER DOMAIN domain_name_7 OWNER TO you; ALTER DOMAIN domain_name_8 SET SCHEMA foo; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanAlterDomainWithAddConstraint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/ban_char_field.rs b/crates/squawk_linter/src/rules/ban_char_field.rs index 075084e9..4cfb2e13 100644 --- a/crates/squawk_linter/src/rules/ban_char_field.rs +++ b/crates/squawk_linter/src/rules/ban_char_field.rs @@ -84,7 +84,7 @@ CREATE TABLE "core_bar" ( "delta" character NOT NULL ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanCharField]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -100,7 +100,7 @@ CREATE TABLE "core_bar" ( "beta" text NOT NULL ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanCharField]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -126,7 +126,7 @@ create table t ( p char[] ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanCharField]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -140,7 +140,7 @@ create table t ( a char[] ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanCharField]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -152,7 +152,7 @@ create table t ( let sql = r#" alter table t add column c char; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanCharField]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs b/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs index 35ebd2e5..d7d42393 100644 --- a/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs +++ b/crates/squawk_linter/src/rules/ban_concurrent_index_creation_in_transaction.rs @@ -58,7 +58,7 @@ mod test { CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -71,7 +71,7 @@ mod test { -- run outside a transaction CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -84,7 +84,7 @@ mod test { CREATE UNIQUE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); ALTER TABLE "table_name" ADD CONSTRAINT "field_name_id" UNIQUE USING INDEX "field_name_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -98,7 +98,7 @@ mod test { -- run index creation in a standalone migration CREATE UNIQUE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -114,7 +114,7 @@ mod test { BEGIN; ALTER TABLE "table_name" ADD CONSTRAINT "field_name_id" UNIQUE USING INDEX "field_name_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanConcurrentIndexCreationInTransaction]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); diff --git a/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs b/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs index 3674924c..dc620f0c 100644 --- a/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs +++ b/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs @@ -47,7 +47,7 @@ mod test { let sql = r#" CREATE DOMAIN domain_name_3 AS NUMERIC(15,5) CHECK (value > 0); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanCreateDomainWithConstraint]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -60,7 +60,7 @@ CREATE DOMAIN domain_name_3 AS NUMERIC(15,5) CHECK (value > 0); let sql = r#" create domain d as t check (value > 0) not null; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanCreateDomainWithConstraint]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -74,7 +74,7 @@ create domain d as t check (value > 0) not null; CREATE DOMAIN domain_name_1 AS TEXT; CREATE DOMAIN domain_name_2 AS CHARACTER VARYING; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanCreateDomainWithConstraint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/ban_drop_column.rs b/crates/squawk_linter/src/rules/ban_drop_column.rs index fc431bce..08cc4113 100644 --- a/crates/squawk_linter/src/rules/ban_drop_column.rs +++ b/crates/squawk_linter/src/rules/ban_drop_column.rs @@ -34,7 +34,7 @@ mod test { let sql = r#" ALTER TABLE "bar_tbl" DROP COLUMN "foo_col" CASCADE; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanDropColumn]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/ban_drop_database.rs b/crates/squawk_linter/src/rules/ban_drop_database.rs index 62b6b483..1799571d 100644 --- a/crates/squawk_linter/src/rules/ban_drop_database.rs +++ b/crates/squawk_linter/src/rules/ban_drop_database.rs @@ -33,7 +33,7 @@ mod test { DROP DATABASE IF EXISTS "table_name"; DROP DATABASE IF EXISTS "table_name" "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanDropDatabase]); let errors = linter.lint(file, sql); assert!(!errors.is_empty()); diff --git a/crates/squawk_linter/src/rules/ban_drop_not_null.rs b/crates/squawk_linter/src/rules/ban_drop_not_null.rs index 8253626b..9f1f06a9 100644 --- a/crates/squawk_linter/src/rules/ban_drop_not_null.rs +++ b/crates/squawk_linter/src/rules/ban_drop_not_null.rs @@ -38,7 +38,7 @@ mod test { let sql = r#" ALTER TABLE "bar_tbl" ALTER COLUMN "foo_col" DROP NOT NULL; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanDropNotNull]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/ban_drop_table.rs b/crates/squawk_linter/src/rules/ban_drop_table.rs index 20235da8..1ca25248 100644 --- a/crates/squawk_linter/src/rules/ban_drop_table.rs +++ b/crates/squawk_linter/src/rules/ban_drop_table.rs @@ -32,7 +32,7 @@ DROP TABLE "table_name"; DROP TABLE IF EXISTS "table_name"; DROP TABLE IF EXISTS "table_name" "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::BanDropTable]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/changing_column_type.rs b/crates/squawk_linter/src/rules/changing_column_type.rs index 7e173687..ff91c4ee 100644 --- a/crates/squawk_linter/src/rules/changing_column_type.rs +++ b/crates/squawk_linter/src/rules/changing_column_type.rs @@ -41,7 +41,7 @@ BEGIN; ALTER TABLE "core_recipe" ALTER COLUMN "edits" TYPE text USING "edits"::text; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ChangingColumnType]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -59,7 +59,7 @@ ALTER TABLE "core_recipe" ALTER COLUMN "foo" TYPE varchar(255) USING "foo"::varc ALTER TABLE "core_recipe" ALTER COLUMN "foo" TYPE text USING "foo"::text; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ChangingColumnType]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs b/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs index 67bf4ff8..d42356ba 100644 --- a/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs +++ b/crates/squawk_linter/src/rules/constraint_missing_not_valid.rs @@ -156,7 +156,7 @@ ALTER TABLE "app_email" ADD CONSTRAINT "fk_user" FOREIGN KEY (user_id) REFERENCE ALTER TABLE "app_email" VALIDATE CONSTRAINT "fk_user"; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -169,7 +169,7 @@ COMMIT; ALTER TABLE "app_email" ADD CONSTRAINT "fk_user" FOREIGN KEY (user_id) REFERENCES "app_user" (id) NOT VALID; ALTER TABLE "app_email" VALIDATE CONSTRAINT "fk_user"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -184,7 +184,7 @@ ALTER TABLE "app_email" ADD CONSTRAINT "fk_user" FOREIGN KEY (user_id) REFERENCE ALTER TABLE "app_email" VALIDATE CONSTRAINT "fk_user"; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -198,7 +198,7 @@ COMMIT; -- instead of ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -212,7 +212,7 @@ ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address) NOT VALID; ALTER TABLE distributors VALIDATE CONSTRAINT distfk; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -224,7 +224,7 @@ ALTER TABLE distributors VALIDATE CONSTRAINT distfk; -- instead of ALTER TABLE "accounts" ADD CONSTRAINT "positive_balance" CHECK ("balance" >= 0); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -239,7 +239,7 @@ ALTER TABLE "accounts" ADD CONSTRAINT "positive_balance" CHECK ("balance" >= 0); ALTER TABLE "accounts" ADD CONSTRAINT "positive_balance" CHECK ("balance" >= 0) NOT VALID; ALTER TABLE accounts VALIDATE CONSTRAINT positive_balance; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -256,7 +256,7 @@ CREATE TABLE "core_foo" ( ALTER TABLE "core_foo" ADD CONSTRAINT "age_restriction" CHECK ("age" >= 25); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -271,7 +271,7 @@ CREATE TABLE "core_foo" ( ); ALTER TABLE "core_foo" ADD CONSTRAINT "age_restriction" CHECK ("age" >= 25); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -287,7 +287,7 @@ CREATE TABLE "core_foo" ( ); ALTER TABLE "core_foo" ADD CONSTRAINT "age_restriction" CHECK ("age" >= 25); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -299,7 +299,7 @@ ALTER TABLE "core_foo" ADD CONSTRAINT "age_restriction" CHECK ("age" >= 25); let sql = r#" ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::ConstraintMissingNotValid]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/disallow_unique_constraint.rs b/crates/squawk_linter/src/rules/disallow_unique_constraint.rs index 57e0a4ec..442226f8 100644 --- a/crates/squawk_linter/src/rules/disallow_unique_constraint.rs +++ b/crates/squawk_linter/src/rules/disallow_unique_constraint.rs @@ -69,7 +69,7 @@ mod test { let sql = r#" ALTER TABLE table_name ADD CONSTRAINT field_name_constraint UNIQUE (field_name); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -81,7 +81,7 @@ ALTER TABLE table_name ADD CONSTRAINT field_name_constraint UNIQUE (field_name); let sql = r#" ALTER TABLE table_name DROP CONSTRAINT field_name_constraint; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -94,7 +94,7 @@ CREATE UNIQUE INDEX CONCURRENTLY dist_id_temp_idx ON distributors (dist_id); ALTER TABLE distributors DROP CONSTRAINT distributors_pkey, ADD CONSTRAINT distributors_pkey PRIMARY KEY USING INDEX dist_id_temp_idx; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -107,7 +107,7 @@ CREATE UNIQUE INDEX CONCURRENTLY "legacy_questiongrouppg_mongo_id_1f8f47d9_uniq_ ON "legacy_questiongrouppg" ("mongo_id"); ALTER TABLE "legacy_questiongrouppg" ADD CONSTRAINT "legacy_questiongrouppg_mongo_id_1f8f47d9_uniq" UNIQUE USING INDEX "legacy_questiongrouppg_mongo_id_1f8f47d9_uniq_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -124,7 +124,7 @@ CREATE TABLE products ( ALTER TABLE products ADD CONSTRAINT sku_constraint UNIQUE (sku); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -139,7 +139,7 @@ CREATE TABLE products ( ); ALTER TABLE products ADD CONSTRAINT sku_constraint UNIQUE (sku); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -151,7 +151,7 @@ ALTER TABLE products ADD CONSTRAINT sku_constraint UNIQUE (sku); let sql = r#" ALTER TABLE foo ADD COLUMN bar text CONSTRAINT foo_bar_unique UNIQUE; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -163,7 +163,7 @@ ALTER TABLE foo ADD COLUMN bar text CONSTRAINT foo_bar_unique UNIQUE; let sql = r#" ALTER TABLE foo ADD COLUMN bar text UNIQUE; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::DisallowUniqueConstraint]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/prefer_big_int.rs b/crates/squawk_linter/src/rules/prefer_big_int.rs index 7987c5f6..526ce8ea 100644 --- a/crates/squawk_linter/src/rules/prefer_big_int.rs +++ b/crates/squawk_linter/src/rules/prefer_big_int.rs @@ -141,7 +141,7 @@ create table users ( id smallserial ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigInt]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -172,7 +172,7 @@ create table users ( id serial8 ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigInt]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -186,7 +186,7 @@ create table users ( bar serial ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigInt]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -198,7 +198,7 @@ create table users ( let sql = r#" alter table t add column c integer; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigInt]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -210,7 +210,7 @@ alter table t add column c integer; let sql = r#" alter table t alter column c type integer; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigInt]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -222,7 +222,7 @@ alter table t alter column c type integer; let sql = r#" alter table t alter column c type "integer"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigInt]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs b/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs index 5fc5d5ae..2d3e28f8 100644 --- a/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs +++ b/crates/squawk_linter/src/rules/prefer_bigint_over_int.rs @@ -56,7 +56,7 @@ create table users ( id serial4 ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigintOverInt]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -99,7 +99,7 @@ create table users ( id serial2 ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigintOverInt]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs b/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs index 5102fb60..277ddae3 100644 --- a/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs +++ b/crates/squawk_linter/src/rules/prefer_bigint_over_smallint.rs @@ -55,7 +55,7 @@ create table users ( id serial2 ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigintOverSmallint]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -98,7 +98,7 @@ create table users ( id serial4 ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferBigintOverSmallint]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/prefer_identity.rs b/crates/squawk_linter/src/rules/prefer_identity.rs index 9d86702c..07760da8 100644 --- a/crates/squawk_linter/src/rules/prefer_identity.rs +++ b/crates/squawk_linter/src/rules/prefer_identity.rs @@ -68,7 +68,7 @@ create table users ( id bigserial ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferIdentity]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -93,7 +93,7 @@ create table users ( id bigint generated always as identity primary key ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferIdentity]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/prefer_robust_stmts.rs b/crates/squawk_linter/src/rules/prefer_robust_stmts.rs index 1167ccc6..1cc39781 100644 --- a/crates/squawk_linter/src/rules/prefer_robust_stmts.rs +++ b/crates/squawk_linter/src/rules/prefer_robust_stmts.rs @@ -167,7 +167,7 @@ mod test { ALTER TABLE "app_email" DROP CONSTRAINT IF EXISTS "email_uniq"; ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -179,7 +179,7 @@ ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_id select 1; -- so we don't skip checking DROP INDEX CONCURRENTLY IF EXISTS "email_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -192,7 +192,7 @@ ALTER TABLE "app_email" DROP CONSTRAINT IF EXISTS "fk_user"; ALTER TABLE "app_email" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "app_user" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID; ALTER TABLE "app_email" VALIDATE CONSTRAINT "fk_user"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -205,7 +205,7 @@ BEGIN; ALTER TABLE "core_foo" ADD COLUMN "answer_id" integer NULL; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -217,7 +217,7 @@ COMMIT; select 1; -- so we don't skip checking ALTER TABLE "core_foo" ADD COLUMN IF NOT EXISTS "answer_id" integer NULL; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -229,7 +229,7 @@ ALTER TABLE "core_foo" ADD COLUMN IF NOT EXISTS "answer_id" integer NULL; select 1; -- so we don't skip checking CREATE INDEX CONCURRENTLY IF NOT EXISTS "core_foo_idx" ON "core_foo" ("answer_id"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -245,7 +245,7 @@ CREATE TABLE "core_bar" ( ); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -259,7 +259,7 @@ CREATE TABLE IF NOT EXISTS "core_bar" ( "bravo" text NOT NULL ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -275,7 +275,7 @@ DROP TABLE "core_bar"; DROP TYPE foo; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -288,7 +288,7 @@ COMMIT; select 1; -- so we don't skip checking SELECT 1; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -301,7 +301,7 @@ SELECT 1; select 1; -- so we don't skip checking INSERT INTO tbl VALUES (a); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -314,7 +314,7 @@ INSERT INTO tbl VALUES (a); select 1; -- so we don't skip checking ALTER TABLE "core_foo" DROP CONSTRAINT IF EXISTS "core_foo_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -326,7 +326,7 @@ ALTER TABLE "core_foo" DROP CONSTRAINT IF EXISTS "core_foo_idx"; select 1; -- so we don't skip checking ALTER TABLE "core_foo" ADD COLUMN "answer_id" integer NULL; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -340,7 +340,7 @@ DROP INDEX "core_bar_foo_id_idx"; DROP TABLE "core_bar"; DROP TYPE foo; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -355,7 +355,7 @@ CREATE TABLE "core_bar" ( "bravo" text NOT NULL ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -369,7 +369,7 @@ CREATE TABLE "core_bar" ( let sql = r#" CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -382,7 +382,7 @@ CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -405,7 +405,7 @@ ALTER TABLE "D" DROP CONSTRAINT "UQ_468cad3743146a81c94b0b114ac"; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); @@ -418,7 +418,7 @@ COMMIT; select 1; -- so we don't skip checking ALTER TABLE "core_foo" ADD COLUMN "answer_id" integer NULL; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -431,7 +431,7 @@ ALTER TABLE "core_foo" ADD COLUMN "answer_id" integer NULL; select 1; -- so we don't skip checking CREATE INDEX CONCURRENTLY "core_foo_idx" ON "core_foo" ("answer_id"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -444,7 +444,7 @@ CREATE INDEX CONCURRENTLY "core_foo_idx" ON "core_foo" ("answer_id"); select 1; -- so we don't skip checking alter table t drop column c cascade; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -457,7 +457,7 @@ alter table t drop column c cascade; select 1; -- so we don't skip checking alter table t drop column if exists c cascade; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -469,7 +469,7 @@ alter table t drop column if exists c cascade; select 1; -- so we don't skip checking CREATE TABLE "core_bar" ( "id" serial NOT NULL PRIMARY KEY, "bravo" text NOT NULL); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -482,7 +482,7 @@ CREATE TABLE "core_bar" ( "id" serial NOT NULL PRIMARY KEY, "bravo" text NOT NUL select 1; -- so we don't skip checking ALTER TABLE "core_foo" DROP CONSTRAINT "core_foo_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -495,7 +495,7 @@ ALTER TABLE "core_foo" DROP CONSTRAINT "core_foo_idx"; select 1; -- so we don't skip checking CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -508,7 +508,7 @@ CREATE INDEX CONCURRENTLY ON "table_name" ("field_name"); CREATE TABLE IF NOT EXISTS test(); ALTER TABLE IF EXISTS test ENABLE ROW LEVEL SECURITY; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -521,7 +521,7 @@ ALTER TABLE IF EXISTS test ENABLE ROW LEVEL SECURITY; CREATE TABLE IF NOT EXISTS test(); ALTER TABLE test ENABLE ROW LEVEL SECURITY; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -534,7 +534,7 @@ ALTER TABLE test ENABLE ROW LEVEL SECURITY; CREATE TABLE IF NOT EXISTS test(); ALTER TABLE IF EXISTS test DISABLE ROW LEVEL SECURITY; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -549,7 +549,7 @@ ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_id -- this second add constraint should error because it's not robust ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -562,7 +562,7 @@ ALTER TABLE "app_email" ADD CONSTRAINT "email_uniq" UNIQUE USING INDEX "email_id select 1; -- so we don't skip checking DROP INDEX CONCURRENTLY "email_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferRobustStmts]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/prefer_text_field.rs b/crates/squawk_linter/src/rules/prefer_text_field.rs index 8802ea05..08342d6d 100644 --- a/crates/squawk_linter/src/rules/prefer_text_field.rs +++ b/crates/squawk_linter/src/rules/prefer_text_field.rs @@ -83,7 +83,7 @@ BEGIN; ALTER TABLE "core_foo" ALTER COLUMN "kind" TYPE varchar(1000) USING "kind"::varchar(1000); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTextField]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -103,7 +103,7 @@ CREATE TABLE "core_bar" ( ); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTextField]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -118,7 +118,7 @@ create table t ( "alpha" pg_catalog.varchar(100) NOT NULL ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTextField]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -132,7 +132,7 @@ BEGIN; ALTER TABLE "foo_table" ADD COLUMN "foo_column" varchar(256) NULL; COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTextField]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -144,7 +144,7 @@ COMMIT; let sql = r#" CREATE TABLE IF NOT EXISTS foo_table(bar_col varchar); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTextField]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -167,7 +167,7 @@ CREATE TABLE "core_bar" ( ALTER TABLE "core_bar" ADD CONSTRAINT "text_size" CHECK (LENGTH("bravo") <= 100); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTextField]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/prefer_timestamptz.rs b/crates/squawk_linter/src/rules/prefer_timestamptz.rs index c8db91ca..8677a238 100644 --- a/crates/squawk_linter/src/rules/prefer_timestamptz.rs +++ b/crates/squawk_linter/src/rules/prefer_timestamptz.rs @@ -79,7 +79,7 @@ create table app.accounts created_ts timestamp without time zone ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTimestamptz]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -94,7 +94,7 @@ alter table app.users alter table app.accounts alter column created_ts type timestamp without time zone; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTimestamptz]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -113,7 +113,7 @@ create table app.accounts created_ts timestamp with time zone ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTimestamptz]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -131,7 +131,7 @@ create table app.accounts created_ts timestamp with time zone ); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::PreferTimestamptz]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/renaming_column.rs b/crates/squawk_linter/src/rules/renaming_column.rs index e244d32e..37c00d3b 100644 --- a/crates/squawk_linter/src/rules/renaming_column.rs +++ b/crates/squawk_linter/src/rules/renaming_column.rs @@ -34,7 +34,7 @@ mod test { let sql = r#" ALTER TABLE "table_name" RENAME COLUMN "column_name" TO "new_column_name"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RenamingColumn]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/renaming_table.rs b/crates/squawk_linter/src/rules/renaming_table.rs index e6d2982b..73113dc5 100644 --- a/crates/squawk_linter/src/rules/renaming_table.rs +++ b/crates/squawk_linter/src/rules/renaming_table.rs @@ -34,7 +34,7 @@ mod test { let sql = r#" ALTER TABLE "table_name" RENAME TO "new_table_name"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RenamingTable]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); diff --git a/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs b/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs index 4339bf33..cd3041a4 100644 --- a/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs +++ b/crates/squawk_linter/src/rules/require_concurrent_index_creation.rs @@ -52,7 +52,7 @@ mod test { -- instead of CREATE INDEX "field_name_idx" ON "table_name" ("field_name"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexCreation]); let errors = linter.lint(file, sql); assert_ne!(errors.len(), 0); @@ -65,7 +65,7 @@ CREATE INDEX "field_name_idx" ON "table_name" ("field_name"); -- use CONCURRENTLY CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexCreation]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -82,7 +82,7 @@ CREATE TABLE "core_foo" ( CREATE INDEX "core_foo_tenant_id_4d397ef9" ON "core_foo" ("tenant_id"); COMMIT; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexCreation]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -97,7 +97,7 @@ CREATE TABLE "core_foo" ( ); CREATE INDEX "core_foo_tenant_id_4d397ef9" ON "core_foo" ("tenant_id"); "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexCreation]); linter.settings.assume_in_transaction = true; let errors = linter.lint(file, sql); diff --git a/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs b/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs index ea615056..875a95f6 100644 --- a/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs +++ b/crates/squawk_linter/src/rules/require_concurrent_index_deletion.rs @@ -33,7 +33,7 @@ mod test { -- instead of DROP INDEX IF EXISTS "field_name_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 1); @@ -46,7 +46,7 @@ mod test { let sql = r#" DROP INDEX CONCURRENTLY IF EXISTS "field_name_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -57,7 +57,7 @@ DROP INDEX CONCURRENTLY IF EXISTS "field_name_idx"; let sql = r#" DROP INDEX CONCURRENTLY IF EXISTS "field_name_idx"; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -68,7 +68,7 @@ DROP INDEX CONCURRENTLY IF EXISTS "field_name_idx"; let sql = r#" DROP TABLE IF EXISTS some_table; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); @@ -79,7 +79,7 @@ DROP TABLE IF EXISTS some_table; let sql = r#" DROP TRIGGER IF EXISTS trigger on foo_table; "#; - let file = syntax::SourceFile::parse(sql); + let file = squawk_syntax::SourceFile::parse(sql); let mut linter = Linter::from([Rule::RequireConcurrentIndexDeletion]); let errors = linter.lint(file, sql); assert_eq!(errors.len(), 0); From eb50d96b3330a0b4c690c39ad261e77c454d6b70 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 3 May 2025 19:26:30 -0400 Subject: [PATCH 6/7] fix From b63562881799cd7cb2689214947412ee156930d2 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 3 May 2025 19:30:40 -0400 Subject: [PATCH 7/7] fix --- crates/squawk_linter/src/lib.rs | 2 +- .../ban_create_domain_with_constraint.rs | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/crates/squawk_linter/src/lib.rs b/crates/squawk_linter/src/lib.rs index f336b0bb..7209a899 100644 --- a/crates/squawk_linter/src/lib.rs +++ b/crates/squawk_linter/src/lib.rs @@ -387,7 +387,7 @@ pub enum ViolationMessage<'a> { #[derive(Debug)] pub struct ViolationMeta<'a> { /// A description of the rule that's used when rendering the error message - /// in on the CLI. It should be a slightly expanded version of the [ViolationName] + /// in on the CLI. It should be a slightly expanded version of the [`ViolationName`] pub title: String, /// Messages rendered for each error to provide context and offer advice on how to fix. pub messages: Vec>, diff --git a/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs b/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs index dc620f0c..77ad1c3b 100644 --- a/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs +++ b/crates/squawk_linter/src/rules/ban_create_domain_with_constraint.rs @@ -9,29 +9,27 @@ use crate::{ErrorCode, Linter, Violation}; pub(crate) fn ban_create_domain_with_constraint(ctx: &mut Linter, parse: &Parse) { let file = parse.tree(); for item in file.items() { - match item { - ast::Item::CreateDomain(domain) => { - let range = domain.constraints().map(|c| c.syntax().text_range()).fold( - None, - |prev, cur| match prev { + if let ast::Item::CreateDomain(domain) = item { + let range = + domain + .constraints() + .map(|c| c.syntax().text_range()) + .fold(None, |prev, cur| match prev { None => Some(cur), Some(prev) => { let new_start = prev.start().min(cur.start()); let new_end = prev.end().max(cur.end()); Some(TextRange::new(new_start, new_end)) } - }, - ); - if let Some(range) = range { - ctx.report(Violation::new( - ErrorCode::BanCreateDomainWithConstraint, - "Domains with constraints have poor support for online migrations. Use table and column constraints instead.".into(), - range, - None, - )) - } + }); + if let Some(range) = range { + ctx.report(Violation::new( + ErrorCode::BanCreateDomainWithConstraint, + "Domains with constraints have poor support for online migrations. Use table and column constraints instead.".into(), + range, + None, + )) } - _ => (), } } }