From 953dc80aac4cd45b3b2767e4abceada00d25dc95 Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Thu, 12 Mar 2026 11:20:09 -0400 Subject: [PATCH] linter: add file-level override for `assume_in_transaction` via comments Add `-- squawk-disable-assume-in-transaction` and `-- squawk-enable-assume-in-transaction` comments to override the global `assume_in_transaction` setting on a per-file basis. This is useful when a migration tool wraps files in transactions by default but specific files need to opt out (e.g., for `CREATE INDEX CONCURRENTLY`). Closes #990 Co-Authored-By: Claude Opus 4.6 --- crates/squawk_linter/src/ignore.rs | 70 ++++++++++++++++++- crates/squawk_linter/src/lib.rs | 5 ++ ...oncurrent_index_creation_in_transaction.rs | 16 +++++ ...ble_assume_in_transaction_flags_begin.snap | 17 +++++ .../src/rules/transaction_nesting.rs | 32 +++++++++ docs/docs/cli.md | 16 +++++ 6 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__transaction_nesting__test__squawk_enable_assume_in_transaction_flags_begin.snap diff --git a/crates/squawk_linter/src/ignore.rs b/crates/squawk_linter/src/ignore.rs index 01ce5c2b..63ff8519 100644 --- a/crates/squawk_linter/src/ignore.rs +++ b/crates/squawk_linter/src/ignore.rs @@ -19,7 +19,7 @@ pub struct Ignore { pub kind: IgnoreKind, } -fn comment_body(token: &SyntaxToken) -> Option<(&str, TextRange)> { +pub(crate) fn comment_body(token: &SyntaxToken) -> Option<(&str, TextRange)> { let range = token.text_range(); if token.kind() == SyntaxKind::COMMENT { let text = token.text(); @@ -128,6 +128,34 @@ pub(crate) fn find_ignores(ctx: &mut Linter, file: &SyntaxNode) { } } +const DISABLE_ASSUME_IN_TRANSACTION: &str = "squawk-disable-assume-in-transaction"; +const ENABLE_ASSUME_IN_TRANSACTION: &str = "squawk-enable-assume-in-transaction"; + +pub fn find_transaction_override(file: &SyntaxNode) -> Option { + for event in file.preorder_with_tokens() { + match event { + rowan::WalkEvent::Enter(NodeOrToken::Token(token)) + if token.kind() == SyntaxKind::COMMENT => + { + if let Some((body, _range)) = comment_body(&token) { + let trimmed = body.trim(); + let trimmed = trimmed + .find("--") + .map_or(trimmed, |idx| trimmed[..idx].trim_end()); + if trimmed == DISABLE_ASSUME_IN_TRANSACTION { + return Some(false); + } + if trimmed == ENABLE_ASSUME_IN_TRANSACTION { + return Some(true); + } + } + } + _ => (), + } + } + None +} + #[cfg(test)] mod test { @@ -612,4 +640,44 @@ alter table t drop column c cascade; ] "); } + + #[test] + fn disable_assume_in_transaction() { + use super::find_transaction_override; + let sql = "-- squawk-disable-assume-in-transaction\nSELECT 1;"; + let parse = squawk_syntax::SourceFile::parse(sql); + assert_eq!(find_transaction_override(&parse.syntax_node()), Some(false)); + } + + #[test] + fn enable_assume_in_transaction() { + use super::find_transaction_override; + let sql = "-- squawk-enable-assume-in-transaction\nSELECT 1;"; + let parse = squawk_syntax::SourceFile::parse(sql); + assert_eq!(find_transaction_override(&parse.syntax_node()), Some(true)); + } + + #[test] + fn disable_assume_in_transaction_c_style_comment() { + use super::find_transaction_override; + let sql = "/* squawk-disable-assume-in-transaction */\nSELECT 1;"; + let parse = squawk_syntax::SourceFile::parse(sql); + assert_eq!(find_transaction_override(&parse.syntax_node()), Some(false)); + } + + #[test] + fn disable_assume_in_transaction_with_trailing_comment() { + use super::find_transaction_override; + let sql = "-- squawk-disable-assume-in-transaction -- not in a transaction\nSELECT 1;"; + let parse = squawk_syntax::SourceFile::parse(sql); + assert_eq!(find_transaction_override(&parse.syntax_node()), Some(false)); + } + + #[test] + fn transaction_override_none_when_absent() { + use super::find_transaction_override; + let sql = "SELECT 1;"; + let parse = squawk_syntax::SourceFile::parse(sql); + assert_eq!(find_transaction_override(&parse.syntax_node()), None); + } } diff --git a/crates/squawk_linter/src/lib.rs b/crates/squawk_linter/src/lib.rs index e72654b6..ca7b4524 100644 --- a/crates/squawk_linter/src/lib.rs +++ b/crates/squawk_linter/src/lib.rs @@ -5,6 +5,7 @@ use enum_iterator::Sequence; use enum_iterator::all; pub use ignore::Ignore; use ignore::find_ignores; +use ignore::find_transaction_override; use ignore_index::IgnoreIndex; use rowan::TextRange; use rowan::TextSize; @@ -332,6 +333,10 @@ impl Linter { #[must_use] pub fn lint(&mut self, file: &Parse, text: &str) -> Vec { + if let Some(override_value) = find_transaction_override(&file.syntax_node()) { + self.settings.assume_in_transaction = override_value; + } + if self.rules.contains(&Rule::AddingFieldWithDefault) { adding_field_with_default(self, file); } 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 fe556653..164e2bda 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 @@ -134,4 +134,20 @@ mod test { }, ); } + + #[test] + fn squawk_disable_assume_in_transaction_overrides() { + let sql = r#" +-- squawk-disable-assume-in-transaction +CREATE 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"; + "#; + lint_ok_with( + sql, + LinterSettings { + assume_in_transaction: true, + ..Default::default() + }, + ); + } } diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__transaction_nesting__test__squawk_enable_assume_in_transaction_flags_begin.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__transaction_nesting__test__squawk_enable_assume_in_transaction_flags_begin.snap new file mode 100644 index 00000000..f95ebe4c --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__transaction_nesting__test__squawk_enable_assume_in_transaction_flags_begin.snap @@ -0,0 +1,17 @@ +--- +source: crates/squawk_linter/src/rules/transaction_nesting.rs +assertion_line: 195 +expression: "lint_errors_with(sql, LinterSettings\n{ assume_in_transaction: false, ..Default::default() },)" +--- +warning[transaction-nesting]: There is an existing transaction already in progress, managed by your migration tool. + ╭▸ +3 │ BEGIN; + │ ━━━━━ + │ + ╰ help: Put migration statements in separate files to have them be in separate transactions or don't use the assume-in-transaction setting. +warning[transaction-nesting]: Attempting to end the transaction that is managed by your migration tool + ╭▸ +5 │ COMMIT; + │ ━━━━━━ + │ + ╰ help: Put migration statements in separate files to have them be in separate transactions or don't use the assume-in-transaction setting. diff --git a/crates/squawk_linter/src/rules/transaction_nesting.rs b/crates/squawk_linter/src/rules/transaction_nesting.rs index f63239f9..39100ebf 100644 --- a/crates/squawk_linter/src/rules/transaction_nesting.rs +++ b/crates/squawk_linter/src/rules/transaction_nesting.rs @@ -168,4 +168,36 @@ SELECT 1; }; lint_ok_with(sql, settings); } + + #[test] + fn squawk_disable_assume_in_transaction_allows_begin_commit() { + let sql = r#" +-- squawk-disable-assume-in-transaction +BEGIN; +SELECT 1; +COMMIT; + "#; + let settings = LinterSettings { + assume_in_transaction: true, + ..Default::default() + }; + lint_ok_with(sql, settings); + } + + #[test] + fn squawk_enable_assume_in_transaction_flags_begin() { + let sql = r#" +-- squawk-enable-assume-in-transaction +BEGIN; +SELECT 1; +COMMIT; + "#; + assert_snapshot!(lint_errors_with( + sql, + LinterSettings { + assume_in_transaction: false, + ..Default::default() + }, + )); + } } diff --git a/docs/docs/cli.md b/docs/docs/cli.md index 208e1af4..58e18b8e 100644 --- a/docs/docs/cli.md +++ b/docs/docs/cli.md @@ -54,6 +54,22 @@ alter table t drop column c cascade; create table t (a int); ``` +### Overriding `assume_in_transaction` via comments + +If you have `assume_in_transaction = true` set globally (via config or CLI flag), you can disable it for a specific file with `squawk-disable-assume-in-transaction`: + +```sql +-- squawk-disable-assume-in-transaction +CREATE INDEX CONCURRENTLY IF NOT EXISTS my_idx ON my_table (col); +``` + +Similarly, you can enable it for a specific file with `squawk-enable-assume-in-transaction`: + +```sql +-- squawk-enable-assume-in-transaction +ALTER TABLE my_table ADD COLUMN my_col integer; +``` + ## Files Files can be excluded from linting via the `--exclude-path` flag. Glob matching is supported and the flag can be provided multiple times.