From fdee046196fbe3220590bd013e89d17a8ce782ca Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 3 Oct 2025 23:21:35 -0400 Subject: [PATCH 1/3] linter: fix require-timeout-settings --- crates/squawk_linter/src/analyze.rs | 48 +++++- .../squawk_syntax/src/ast/generated/nodes.rs | 138 ++++++++++++++++++ crates/squawk_syntax/src/postgresql.ungram | 21 ++- 3 files changed, 205 insertions(+), 2 deletions(-) diff --git a/crates/squawk_linter/src/analyze.rs b/crates/squawk_linter/src/analyze.rs index f48b325f..9ba3884d 100644 --- a/crates/squawk_linter/src/analyze.rs +++ b/crates/squawk_linter/src/analyze.rs @@ -1,5 +1,26 @@ use squawk_syntax::ast; +fn has_foreign_key_constraint(create_table: &ast::CreateTable) -> bool { + if let Some(table_arg_list) = create_table.table_arg_list() { + for arg in table_arg_list.args() { + match arg { + ast::TableArg::TableConstraint(ast::TableConstraint::ForeignKeyConstraint(_)) => { + return true; + } + ast::TableArg::Column(column) => { + if let Some(ast::ColumnConstraint::ForeignKeyConstraint(_)) = + column.constraint() + { + return true; + } + } + _ => {} + } + } + } + false +} + /// Returns `true` if the statement might impede normal database queries. pub fn possibly_slow_stmt(stmt: &ast::Stmt) -> bool { // We assume all DDL like Alter, Create, Drop could affect queries. @@ -8,6 +29,8 @@ pub fn possibly_slow_stmt(stmt: &ast::Stmt) -> bool { // insert, update, delete, etc. This allows using squawk for normal SQL // editing. match stmt { + // Without a foreign key constraint, creating a new table should be fast + ast::Stmt::CreateTable(create_table) => has_foreign_key_constraint(create_table), | ast::Stmt::AlterAggregate(_) | ast::Stmt::AlterCollation(_) | ast::Stmt::AlterConversion(_) @@ -79,7 +102,6 @@ pub fn possibly_slow_stmt(stmt: &ast::Stmt) -> bool { | ast::Stmt::CreateServer(_) | ast::Stmt::CreateStatistics(_) | ast::Stmt::CreateSubscription(_) - | ast::Stmt::CreateTable(_) | ast::Stmt::CreateTableAs(_) | ast::Stmt::CreateTablespace(_) | ast::Stmt::CreateTextSearchConfiguration(_) @@ -211,4 +233,28 @@ mod tests { let stmts = file.tree().stmts().next().unwrap(); assert!(!possibly_slow_stmt(&stmts)); } + + #[test] + fn create_table_without_foreign_key() { + let sql = "create table foo (id integer generated by default as identity primary key);"; + let file = SourceFile::parse(sql); + let stmts = file.tree().stmts().next().unwrap(); + assert!(!possibly_slow_stmt(&stmts)); + } + + #[test] + fn create_table_with_foreign_key() { + let sql = "create table foo (id integer, user_id integer references users(id));"; + let file = SourceFile::parse(sql); + let stmts = file.tree().stmts().next().unwrap(); + assert!(possibly_slow_stmt(&stmts)); + } + + #[test] + fn create_table_with_table_level_foreign_key() { + let sql = "create table foo (id integer, user_id integer, foreign key (user_id) references users(id));"; + let file = SourceFile::parse(sql); + let stmts = file.tree().stmts().next().unwrap(); + assert!(possibly_slow_stmt(&stmts)); + } } diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 4bf4d572..c2e6be93 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -1492,6 +1492,10 @@ impl CheckConstraint { support::child(&self.syntax) } #[inline] + pub fn no_inherit(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn l_paren_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::L_PAREN) } @@ -1616,14 +1620,46 @@ impl Column { support::child(&self.syntax) } #[inline] + pub fn constraint(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn deferrable_constraint_option(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn enforced(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn index_expr(&self) -> Option { support::child(&self.syntax) } #[inline] + pub fn initially_deferred_constraint_option( + &self, + ) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn initially_immediate_constraint_option( + &self, + ) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn name(&self) -> Option { support::child(&self.syntax) } #[inline] + pub fn not_deferrable_constraint_option(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn not_enforced(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn storage(&self) -> Option { support::child(&self.syntax) } @@ -6657,6 +6693,10 @@ impl NotNullConstraint { support::child(&self.syntax) } #[inline] + pub fn no_inherit(&self) -> Option { + support::child(&self.syntax) + } + #[inline] pub fn constraint_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::CONSTRAINT_KW) } @@ -10041,6 +10081,16 @@ pub enum AlterTableAction { ValidateConstraint(ValidateConstraint), } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ColumnConstraint { + CheckConstraint(CheckConstraint), + ExcludeConstraint(ExcludeConstraint), + ForeignKeyConstraint(ForeignKeyConstraint), + NotNullConstraint(NotNullConstraint), + PrimaryKeyConstraint(PrimaryKeyConstraint), + UniqueConstraint(UniqueConstraint), +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Constraint { CheckConstraint(CheckConstraint), @@ -19919,6 +19969,94 @@ impl From for AlterTableAction { AlterTableAction::ValidateConstraint(node) } } +impl AstNode for ColumnConstraint { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + matches!( + kind, + SyntaxKind::CHECK_CONSTRAINT + | SyntaxKind::EXCLUDE_CONSTRAINT + | SyntaxKind::FOREIGN_KEY_CONSTRAINT + | SyntaxKind::NOT_NULL_CONSTRAINT + | SyntaxKind::PRIMARY_KEY_CONSTRAINT + | SyntaxKind::UNIQUE_CONSTRAINT + ) + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + let res = match syntax.kind() { + SyntaxKind::CHECK_CONSTRAINT => { + ColumnConstraint::CheckConstraint(CheckConstraint { syntax }) + } + SyntaxKind::EXCLUDE_CONSTRAINT => { + ColumnConstraint::ExcludeConstraint(ExcludeConstraint { syntax }) + } + SyntaxKind::FOREIGN_KEY_CONSTRAINT => { + ColumnConstraint::ForeignKeyConstraint(ForeignKeyConstraint { syntax }) + } + SyntaxKind::NOT_NULL_CONSTRAINT => { + ColumnConstraint::NotNullConstraint(NotNullConstraint { syntax }) + } + SyntaxKind::PRIMARY_KEY_CONSTRAINT => { + ColumnConstraint::PrimaryKeyConstraint(PrimaryKeyConstraint { syntax }) + } + SyntaxKind::UNIQUE_CONSTRAINT => { + ColumnConstraint::UniqueConstraint(UniqueConstraint { syntax }) + } + _ => { + return None; + } + }; + Some(res) + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + match self { + ColumnConstraint::CheckConstraint(it) => &it.syntax, + ColumnConstraint::ExcludeConstraint(it) => &it.syntax, + ColumnConstraint::ForeignKeyConstraint(it) => &it.syntax, + ColumnConstraint::NotNullConstraint(it) => &it.syntax, + ColumnConstraint::PrimaryKeyConstraint(it) => &it.syntax, + ColumnConstraint::UniqueConstraint(it) => &it.syntax, + } + } +} +impl From for ColumnConstraint { + #[inline] + fn from(node: CheckConstraint) -> ColumnConstraint { + ColumnConstraint::CheckConstraint(node) + } +} +impl From for ColumnConstraint { + #[inline] + fn from(node: ExcludeConstraint) -> ColumnConstraint { + ColumnConstraint::ExcludeConstraint(node) + } +} +impl From for ColumnConstraint { + #[inline] + fn from(node: ForeignKeyConstraint) -> ColumnConstraint { + ColumnConstraint::ForeignKeyConstraint(node) + } +} +impl From for ColumnConstraint { + #[inline] + fn from(node: NotNullConstraint) -> ColumnConstraint { + ColumnConstraint::NotNullConstraint(node) + } +} +impl From for ColumnConstraint { + #[inline] + fn from(node: PrimaryKeyConstraint) -> ColumnConstraint { + ColumnConstraint::PrimaryKeyConstraint(node) + } +} +impl From for ColumnConstraint { + #[inline] + fn from(node: UniqueConstraint) -> ColumnConstraint { + ColumnConstraint::UniqueConstraint(node) + } +} impl AstNode for Constraint { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 2e20aa77..55490714 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -289,6 +289,7 @@ Role = CheckConstraint = ('constraint' NameRef) 'check' '(' Expr ')' + NoInherit? UsingIndex = 'using' 'index' NameRef @@ -304,7 +305,24 @@ CompressionMethod = Column = 'period'? - (Name WithOptions? | Name Type Storage? CompressionMethod? Collate? | IndexExpr) + ( + Name WithOptions? constraint:ColumnConstraint? + DeferrableConstraintOption? NotDeferrableConstraintOption? + InitiallyDeferredConstraintOption? InitiallyImmediateConstraintOption? + NotEnforced? Enforced? + | Name Type Storage? CompressionMethod? Collate? constraint:ColumnConstraint? + DeferrableConstraintOption? NotDeferrableConstraintOption? + InitiallyDeferredConstraintOption? InitiallyImmediateConstraintOption? + NotEnforced? Enforced? + | IndexExpr + ) +ColumnConstraint = + CheckConstraint +| NotNullConstraint +| UniqueConstraint +| PrimaryKeyConstraint +| ExcludeConstraint +| ForeignKeyConstraint NullsDistinct = 'nulls' 'distinct' @@ -396,6 +414,7 @@ Enforced = NotNullConstraint = ('constraint' NameRef) 'not' 'null' + NoInherit NullConstraint = ('constraint' NameRef) From 81b7c6ab8e038c93bbd866d09d199ded0d5c749f Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Fri, 3 Oct 2025 23:23:16 -0400 Subject: [PATCH 2/3] fix test --- crates/squawk/src/snapshots/example.svg | 170 +++++++++++------------- 1 file changed, 79 insertions(+), 91 deletions(-) diff --git a/crates/squawk/src/snapshots/example.svg b/crates/squawk/src/snapshots/example.svg index 1ca46f85..0b03eafa 100644 --- a/crates/squawk/src/snapshots/example.svg +++ b/crates/squawk/src/snapshots/example.svg @@ -1,4 +1,4 @@ - +