From c64ceafce14c2de416b8dc6c0258c8e2ec957e23 Mon Sep 17 00:00:00 2001 From: joshwd36 Date: Tue, 6 Apr 2021 21:02:50 +0100 Subject: [PATCH 1/4] Add referential actions to TableConstraint foreign key --- src/ast/ddl.rs | 34 +++++++++++++++++++++++++--------- src/parser.rs | 15 +++++++++++++++ tests/sqlparser_common.rs | 34 +++++++++++++++++++++++++++++----- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 67dc2e322..306d4d80f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -132,12 +132,17 @@ pub enum TableConstraint { is_primary: bool, }, /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () - /// REFERENCES ()`) + /// REFERENCES () + /// { [ON DELETE ] [ON UPDATE ] | + /// [ON UPDATE ] [ON DELETE ] + /// }`). ForeignKey { name: Option, columns: Vec, foreign_table: ObjectName, referred_columns: Vec, + on_delete: Option, + on_update: Option, }, /// `[ CONSTRAINT ] CHECK ()` Check { @@ -165,14 +170,25 @@ impl fmt::Display for TableConstraint { columns, foreign_table, referred_columns, - } => write!( - f, - "{}FOREIGN KEY ({}) REFERENCES {}({})", - display_constraint_name(name), - display_comma_separated(columns), - foreign_table, - display_comma_separated(referred_columns) - ), + on_delete, + on_update, + } => { + write!( + f, + "{}FOREIGN KEY ({}) REFERENCES {}({})", + display_constraint_name(name), + display_comma_separated(columns), + foreign_table, + display_comma_separated(referred_columns), + )?; + if let Some(action) = on_delete { + write!(f, " ON DELETE {}", action)?; + } + if let Some(action) = on_update { + write!(f, " ON UPDATE {}", action)?; + } + Ok(()) + } TableConstraint::Check { name, expr } => { write!(f, "{}CHECK ({})", display_constraint_name(name), expr) } diff --git a/src/parser.rs b/src/parser.rs index 863fc66d0..60482591e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1686,11 +1686,26 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name()?; let referred_columns = self.parse_parenthesized_column_list(Mandatory)?; + let mut on_delete = None; + let mut on_update = None; + loop { + if on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) { + on_delete = Some(self.parse_referential_action()?); + } else if on_update.is_none() + && self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) + { + on_update = Some(self.parse_referential_action()?); + } else { + break; + } + } Ok(Some(TableConstraint::ForeignKey { name, columns, foreign_table, referred_columns, + on_delete, + on_update, })) } Token::Word(w) if w.keyword == Keyword::CHECK => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e43bd12ce..346e19009 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1,4 +1,4 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); +// Licensed under the Apache License, Version 2. name: (), columns: (), f columns: (), foreign_table: (), referred_columns: (), on_delete: (), on_update: ()oreign_table: (), referred_columns: (), on_delete: (), on_update: () name: (), columns: (), foreign_table: (), referred_columns: (), on_delete: (), on_update: ()0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // @@ -109,7 +109,7 @@ fn parse_insert_sqlite() { .unwrap() { Statement::Insert { or, .. } => assert_eq!(or, expected_action), - _ => panic!(sql.to_string()), + _ => panic!("{}", sql.to_string()), }; let sql = "INSERT INTO test_table(id) VALUES(1)"; @@ -1142,7 +1142,9 @@ fn parse_create_table() { lng DOUBLE, constrained INT NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0), ref INT REFERENCES othertable (a, b),\ - ref2 INT references othertable2 on delete cascade on update no action\ + ref2 INT references othertable2 on delete cascade on update no action,\ + constraint fkey foreign key (lat) references othertable3 (lat) on delete restrict,\ + FOREIGN KEY (lng) REFERENCES othertable4 (longitude) ON UPDATE SET NULL )"; let ast = one_statement_parses_to( sql, @@ -1152,7 +1154,9 @@ fn parse_create_table() { lng DOUBLE, \ constrained INT NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0), \ ref INT REFERENCES othertable (a, b), \ - ref2 INT REFERENCES othertable2 ON DELETE CASCADE ON UPDATE NO ACTION)", + ref2 INT REFERENCES othertable2 ON DELETE CASCADE ON UPDATE NO ACTION, \ + CONSTRAINT fkey FOREIGN KEY (lat) REFERENCES othertable3(lat) ON DELETE RESTRICT, \ + FOREIGN KEY (lng) REFERENCES othertable4(longitude) ON UPDATE SET NULL)", ); match ast { Statement::CreateTable { @@ -1251,7 +1255,27 @@ fn parse_create_table() { } ] ); - assert!(constraints.is_empty()); + assert_eq!( + constraints, + vec![ + TableConstraint::ForeignKey { + name: Some("fkey".into()), + columns: vec!["lat".into()], + foreign_table: ObjectName(vec!["othertable3".into()]), + referred_columns: vec!["lat".into()], + on_delete: Some(ReferentialAction::Restrict), + on_update: None + }, + TableConstraint::ForeignKey { + name: None, + columns: vec!["lng".into()], + foreign_table: ObjectName(vec!["othertable4".into()]), + referred_columns: vec!["longitude".into()], + on_delete: None, + on_update: Some(ReferentialAction::SetNull) + }, + ] + ); assert_eq!(with_options, vec![]); } _ => unreachable!(), From c4add38d93145b39991b6a40f55dca12223ca950 Mon Sep 17 00:00:00 2001 From: joshwd36 Date: Tue, 6 Apr 2021 21:08:58 +0100 Subject: [PATCH 2/4] Remove copy/paste error --- tests/sqlparser_common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 346e19009..21588968b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1,4 +1,4 @@ -// Licensed under the Apache License, Version 2. name: (), columns: (), f columns: (), foreign_table: (), referred_columns: (), on_delete: (), on_update: ()oreign_table: (), referred_columns: (), on_delete: (), on_update: () name: (), columns: (), foreign_table: (), referred_columns: (), on_delete: (), on_update: ()0 (the "License"); +// Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // From f1dab3abdb5b6733aec7e32d5a083ce04b488a7b Mon Sep 17 00:00:00 2001 From: joshwd36 Date: Tue, 6 Apr 2021 21:02:50 +0100 Subject: [PATCH 3/4] Add referential actions to TableConstraint foreign key --- src/ast/ddl.rs | 34 +++++++++++++++++++++++++--------- src/parser.rs | 15 +++++++++++++++ tests/sqlparser_common.rs | 30 +++++++++++++++++++++++++++--- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1d64abaa2..a4a7b2f29 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -136,12 +136,17 @@ pub enum TableConstraint { is_primary: bool, }, /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () - /// REFERENCES ()`) + /// REFERENCES () + /// { [ON DELETE ] [ON UPDATE ] | + /// [ON UPDATE ] [ON DELETE ] + /// }`). ForeignKey { name: Option, columns: Vec, foreign_table: ObjectName, referred_columns: Vec, + on_delete: Option, + on_update: Option, }, /// `[ CONSTRAINT ] CHECK ()` Check { @@ -169,14 +174,25 @@ impl fmt::Display for TableConstraint { columns, foreign_table, referred_columns, - } => write!( - f, - "{}FOREIGN KEY ({}) REFERENCES {}({})", - display_constraint_name(name), - display_comma_separated(columns), - foreign_table, - display_comma_separated(referred_columns) - ), + on_delete, + on_update, + } => { + write!( + f, + "{}FOREIGN KEY ({}) REFERENCES {}({})", + display_constraint_name(name), + display_comma_separated(columns), + foreign_table, + display_comma_separated(referred_columns), + )?; + if let Some(action) = on_delete { + write!(f, " ON DELETE {}", action)?; + } + if let Some(action) = on_update { + write!(f, " ON UPDATE {}", action)?; + } + Ok(()) + } TableConstraint::Check { name, expr } => { write!(f, "{}CHECK ({})", display_constraint_name(name), expr) } diff --git a/src/parser.rs b/src/parser.rs index c6e306ab5..5ef252301 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1741,11 +1741,26 @@ impl<'a> Parser<'a> { self.expect_keyword(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name()?; let referred_columns = self.parse_parenthesized_column_list(Mandatory)?; + let mut on_delete = None; + let mut on_update = None; + loop { + if on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) { + on_delete = Some(self.parse_referential_action()?); + } else if on_update.is_none() + && self.parse_keywords(&[Keyword::ON, Keyword::UPDATE]) + { + on_update = Some(self.parse_referential_action()?); + } else { + break; + } + } Ok(Some(TableConstraint::ForeignKey { name, columns, foreign_table, referred_columns, + on_delete, + on_update, })) } Token::Word(w) if w.keyword == Keyword::CHECK => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index dbd28d12b..a7faa5ffa 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1162,7 +1162,9 @@ fn parse_create_table() { lng DOUBLE, constrained INT NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0), ref INT REFERENCES othertable (a, b),\ - ref2 INT references othertable2 on delete cascade on update no action\ + ref2 INT references othertable2 on delete cascade on update no action,\ + constraint fkey foreign key (lat) references othertable3 (lat) on delete restrict,\ + FOREIGN KEY (lng) REFERENCES othertable4 (longitude) ON UPDATE SET NULL )"; let ast = one_statement_parses_to( sql, @@ -1172,7 +1174,9 @@ fn parse_create_table() { lng DOUBLE, \ constrained INT NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0), \ ref INT REFERENCES othertable (a, b), \ - ref2 INT REFERENCES othertable2 ON DELETE CASCADE ON UPDATE NO ACTION)", + ref2 INT REFERENCES othertable2 ON DELETE CASCADE ON UPDATE NO ACTION, \ + CONSTRAINT fkey FOREIGN KEY (lat) REFERENCES othertable3(lat) ON DELETE RESTRICT, \ + FOREIGN KEY (lng) REFERENCES othertable4(longitude) ON UPDATE SET NULL)", ); match ast { Statement::CreateTable { @@ -1271,7 +1275,27 @@ fn parse_create_table() { } ] ); - assert!(constraints.is_empty()); + assert_eq!( + constraints, + vec![ + TableConstraint::ForeignKey { + name: Some("fkey".into()), + columns: vec!["lat".into()], + foreign_table: ObjectName(vec!["othertable3".into()]), + referred_columns: vec!["lat".into()], + on_delete: Some(ReferentialAction::Restrict), + on_update: None + }, + TableConstraint::ForeignKey { + name: None, + columns: vec!["lng".into()], + foreign_table: ObjectName(vec!["othertable4".into()]), + referred_columns: vec!["longitude".into()], + on_delete: None, + on_update: Some(ReferentialAction::SetNull) + }, + ] + ); assert_eq!(with_options, vec![]); } _ => unreachable!(), From 833f4df93cb75fd98ab88f13e3ca1c05aec19835 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 25 Aug 2021 13:30:38 +0100 Subject: [PATCH 4/4] Add additional tests --- tests/sqlparser_common.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a7faa5ffa..05dde8aa8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1164,6 +1164,8 @@ fn parse_create_table() { ref INT REFERENCES othertable (a, b),\ ref2 INT references othertable2 on delete cascade on update no action,\ constraint fkey foreign key (lat) references othertable3 (lat) on delete restrict,\ + constraint fkey2 foreign key (lat) references othertable4(lat) on delete no action on update restrict, \ + foreign key (lat) references othertable4(lat) on update set default on delete cascade, \ FOREIGN KEY (lng) REFERENCES othertable4 (longitude) ON UPDATE SET NULL )"; let ast = one_statement_parses_to( @@ -1176,6 +1178,8 @@ fn parse_create_table() { ref INT REFERENCES othertable (a, b), \ ref2 INT REFERENCES othertable2 ON DELETE CASCADE ON UPDATE NO ACTION, \ CONSTRAINT fkey FOREIGN KEY (lat) REFERENCES othertable3(lat) ON DELETE RESTRICT, \ + CONSTRAINT fkey2 FOREIGN KEY (lat) REFERENCES othertable4(lat) ON DELETE NO ACTION ON UPDATE RESTRICT, \ + FOREIGN KEY (lat) REFERENCES othertable4(lat) ON DELETE CASCADE ON UPDATE SET DEFAULT, \ FOREIGN KEY (lng) REFERENCES othertable4(longitude) ON UPDATE SET NULL)", ); match ast { @@ -1286,6 +1290,22 @@ fn parse_create_table() { on_delete: Some(ReferentialAction::Restrict), on_update: None }, + TableConstraint::ForeignKey { + name: Some("fkey2".into()), + columns: vec!["lat".into()], + foreign_table: ObjectName(vec!["othertable4".into()]), + referred_columns: vec!["lat".into()], + on_delete: Some(ReferentialAction::NoAction), + on_update: Some(ReferentialAction::Restrict) + }, + TableConstraint::ForeignKey { + name: None, + columns: vec!["lat".into()], + foreign_table: ObjectName(vec!["othertable4".into()]), + referred_columns: vec!["lat".into()], + on_delete: Some(ReferentialAction::Cascade), + on_update: Some(ReferentialAction::SetDefault) + }, TableConstraint::ForeignKey { name: None, columns: vec!["lng".into()], @@ -1314,6 +1334,18 @@ fn parse_create_table() { .contains("Expected constraint details after CONSTRAINT ")); } +#[test] +fn parse_create_table_with_multiple_on_delete_in_constraint_fails() { + parse_sql_statements( + "\ + create table X (\ + y_id int, \ + foreign key (y_id) references Y (id) on delete cascade on update cascade on delete no action\ + )", + ) + .expect_err("should have failed"); +} + #[test] fn parse_create_table_with_multiple_on_delete_fails() { parse_sql_statements(