11use squawk_syntax:: {
22 Parse , SourceFile ,
33 ast:: { self , AstNode } ,
4+ identifier:: Identifier ,
45} ;
56
6- use crate :: { Linter , Rule , Violation } ;
7+ use crate :: {
8+ Linter , Rule , Violation , rules:: constraint_missing_not_valid:: tables_created_in_transaction,
9+ } ;
710
811pub ( crate ) fn adding_foreign_key_constraint ( ctx : & mut Linter , parse : & Parse < SourceFile > ) {
912 let message = "Adding a foreign key constraint requires a table scan and a `SHARE ROW EXCLUSIVE` lock on both tables, which blocks writes to each table." ;
1013 let help = "Add `NOT VALID` to the constraint in one transaction and then VALIDATE the constraint in a separate transaction." ;
1114 let file = parse. tree ( ) ;
15+ let tables_created = tables_created_in_transaction ( ctx. settings . assume_in_transaction , & file) ;
1216 // TODO: use match_ast! like in #api_walkthrough
1317 for stmt in file. stmts ( ) {
1418 if let ast:: Stmt :: AlterTable ( alter_table) = stmt {
15- for action in alter_table. actions ( ) {
16- match action {
17- ast:: AlterTableAction :: AddConstraint ( add_constraint) => {
18- if add_constraint. not_valid ( ) . is_some ( ) {
19- // Adding foreign key is okay when NOT VALID is specified.
20- continue ;
21- }
22- if let Some ( constraint) = add_constraint. constraint ( ) {
23- if matches ! (
24- constraint,
25- ast:: Constraint :: ForeignKeyConstraint ( _)
26- | ast:: Constraint :: ReferencesConstraint ( _)
27- ) {
28- ctx. report (
29- Violation :: for_node (
30- Rule :: AddingForeignKeyConstraint ,
31- message. into ( ) ,
32- constraint. syntax ( ) ,
19+ if let Some ( table_name) = alter_table
20+ . relation_name ( )
21+ . and_then ( |x| x. path ( ) )
22+ . and_then ( |x| x. segment ( ) )
23+ . and_then ( |x| x. name_ref ( ) )
24+ {
25+ for action in alter_table. actions ( ) {
26+ match action {
27+ ast:: AlterTableAction :: AddConstraint ( add_constraint) => {
28+ if add_constraint. not_valid ( ) . is_some ( )
29+ || tables_created. contains ( & Identifier :: new ( & table_name. text ( ) ) )
30+ {
31+ // Adding foreign key is okay when:
32+ // - NOT VALID is specified.
33+ // - The table is created in the same transaction
34+ continue ;
35+ }
36+ if let Some ( constraint) = add_constraint. constraint ( ) {
37+ if matches ! (
38+ constraint,
39+ ast:: Constraint :: ForeignKeyConstraint ( _)
40+ | ast:: Constraint :: ReferencesConstraint ( _)
41+ ) {
42+ ctx. report (
43+ Violation :: for_node (
44+ Rule :: AddingForeignKeyConstraint ,
45+ message. into ( ) ,
46+ constraint. syntax ( ) ,
47+ )
48+ . help ( help) ,
3349 )
34- . help ( help) ,
35- )
50+ }
3651 }
3752 }
38- }
39- ast:: AlterTableAction :: AddColumn ( add_column) => {
40- for constraint in add_column. constraints ( ) {
41- if matches ! (
42- constraint,
43- ast:: Constraint :: ForeignKeyConstraint ( _)
44- | ast:: Constraint :: ReferencesConstraint ( _)
45- ) {
46- ctx. report (
47- Violation :: for_node (
48- Rule :: AddingForeignKeyConstraint ,
49- message. into ( ) ,
50- constraint. syntax ( ) ,
53+ ast:: AlterTableAction :: AddColumn ( add_column) => {
54+ for constraint in add_column. constraints ( ) {
55+ if matches ! (
56+ constraint,
57+ ast:: Constraint :: ForeignKeyConstraint ( _)
58+ | ast:: Constraint :: ReferencesConstraint ( _)
59+ ) {
60+ ctx. report (
61+ Violation :: for_node (
62+ Rule :: AddingForeignKeyConstraint ,
63+ message. into ( ) ,
64+ constraint. syntax ( ) ,
65+ )
66+ . help ( help) ,
5167 )
52- . help ( help) ,
53- )
68+ }
5469 }
5570 }
71+ _ => continue ,
5672 }
57- _ => continue ,
5873 }
5974 }
6075 }
@@ -64,7 +79,7 @@ pub(crate) fn adding_foreign_key_constraint(ctx: &mut Linter, parse: &Parse<Sour
6479#[ cfg( test) ]
6580mod test {
6681 use crate :: Rule ;
67- use crate :: test_utils:: lint;
82+ use crate :: test_utils:: { lint, lint_with_assume_in_transaction } ;
6883
6984 #[ test]
7085 fn create_table_with_foreign_key_constraint ( ) {
@@ -86,6 +101,30 @@ mod test {
86101 assert ! ( errors. is_empty( ) ) ;
87102 }
88103
104+ #[ test]
105+ fn alter_table_foreign_key_assume_transaction ( ) {
106+ let sql = r#"
107+ CREATE TABLE "emails" ("id" UUID NOT NULL, "user_id" UUID NOT NULL);
108+ ALTER TABLE "emails" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id");
109+ "# ;
110+
111+ let errors = lint_with_assume_in_transaction ( sql, Rule :: AddingForeignKeyConstraint ) ;
112+ assert ! ( errors. is_empty( ) ) ;
113+ }
114+
115+ #[ test]
116+ fn alter_table_foreign_key_in_transaction ( ) {
117+ let sql = r#"
118+ BEGIN;
119+ CREATE TABLE "emails" ("id" UUID NOT NULL, "user_id" UUID NOT NULL);
120+ ALTER TABLE "emails" ADD CONSTRAINT "fk_user" FOREIGN KEY ("user_id") REFERENCES "users" ("id");
121+ COMMIT;
122+ "# ;
123+
124+ let errors = lint ( sql, Rule :: AddingForeignKeyConstraint ) ;
125+ assert ! ( errors. is_empty( ) ) ;
126+ }
127+
89128 #[ test]
90129 fn add_foreign_key_constraint_not_valid_validate ( ) {
91130 let sql = r#"
0 commit comments