From d6a7fd461277ed0d81ec42bc5c30829ae4a9f405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20M=C3=BCller?= Date: Mon, 25 May 2020 00:00:00 +0000 Subject: [PATCH 1/7] + support for "on delete cascade" option --- src/ast/ddl.rs | 54 ++++++++++++++++++++++++++++++++++----- src/ast/mod.rs | 1 + src/dialect/keywords.rs | 1 + src/parser.rs | 39 +++++++++++++++++++++++++++- tests/sqlparser_common.rs | 25 ++++++++++++++++-- 5 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 7333ad287..6c7bab984 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -155,10 +155,16 @@ pub enum ColumnOption { is_primary: bool, }, /// A referential integrity constraint (`[FOREIGN KEY REFERENCES - /// ()`). + /// () + /// [ON DELETE ] + /// [ON UPDATE ]`). + /// + /// The `ON DELETE` option may be placed after the `ON UPDATE` option. ForeignKey { foreign_table: ObjectName, referred_columns: Vec, + on_delete: Option, + on_update: Option, }, // `CHECK ()` Check(Expr), @@ -177,12 +183,21 @@ impl fmt::Display for ColumnOption { ForeignKey { foreign_table, referred_columns, - } => write!( - f, - "REFERENCES {} ({})", - foreign_table, - display_comma_separated(referred_columns) - ), + on_delete, + on_update, + } => { + write!(f, "REFERENCES {}", foreign_table)?; + if referred_columns.len() > 0 { + write!(f, " ({})", 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(()) + } Check(expr) => write!(f, "CHECK ({})", expr), } } @@ -200,3 +215,28 @@ fn display_constraint_name<'a>(name: &'a Option) -> impl fmt::Display + ' } ConstraintName(name) } + +/// ` = +/// { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` +/// +/// Used in foreign key constraints in `ON UPDATE` and `ON DELETE` options. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ReferentialAction { + Restrict, + Cascade, + SetNull, + NoAction, + SetDefault, +} + +impl fmt::Display for ReferentialAction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + ReferentialAction::Restrict => "RESTRICT", + ReferentialAction::Cascade => "CASCADE", + ReferentialAction::SetNull => "SET NULL", + ReferentialAction::NoAction => "NO ACTION", + ReferentialAction::SetDefault => "SET DEFAULT", + }) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 98637e697..e11d69286 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -23,6 +23,7 @@ use std::fmt; pub use self::data_type::DataType; pub use self::ddl::{ AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, TableConstraint, + ReferentialAction, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index 9795f2af3..b49fb240a 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -51,6 +51,7 @@ macro_rules! define_keywords { define_keywords!( ABS, + ACTION, ADD, ASC, ALL, diff --git a/src/parser.rs b/src/parser.rs index 7c6a401d5..f59d7c9ce 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1017,10 +1017,36 @@ impl Parser { ColumnOption::Unique { is_primary: false } } else if self.parse_keyword("REFERENCES") { let foreign_table = self.parse_object_name()?; - let referred_columns = self.parse_parenthesized_column_list(Mandatory)?; + let referred_columns = + self.parse_parenthesized_column_list(Optional)?; + let mut on_delete = None; + let mut on_update = None; + while self.parse_keyword("ON") { + if self.parse_keyword("DELETE") { + if on_delete == None { + on_delete = Some(self.parse_reference_change_action()?); + } else { + return self.expected( + "ON DELETE option not more than once", + self.peek_token() + ); + } + } else if self.parse_keyword("UPDATE") { + if on_update == None { + on_update = Some(self.parse_reference_change_action()?); + } else { + return self.expected( + "ON UPDATE option not more than once", + self.peek_token() + ); + } + } + } ColumnOption::ForeignKey { foreign_table, referred_columns, + on_delete, + on_update, } } else if self.parse_keyword("CHECK") { self.expect_token(&Token::LParen)?; @@ -1034,6 +1060,17 @@ impl Parser { Ok(ColumnOptionDef { name, option }) } + pub fn parse_reference_change_action(&mut self) -> Result { + if self.parse_keyword("RESTRICT") { Ok(ReferentialAction::Restrict) } + else if self.parse_keyword("CASCADE") { Ok(ReferentialAction::Cascade) } + else if self.parse_keywords(vec!["SET", "NULL"]) { Ok(ReferentialAction::SetNull) } + else if self.parse_keywords(vec!["NO", "ACTION"]) { Ok(ReferentialAction::NoAction) } + else if self.parse_keywords(vec!["SET", "DEFAULT"]) { Ok(ReferentialAction::SetDefault) } + else { + self.expected("one of RESTRICT, CASCADE, SET NULL, NO ACTION or SET DEFAULT", self.peek_token()) + } + } + pub fn parse_optional_table_constraint( &mut self, ) -> Result, ParserError> { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 41ceeae54..c68fb6385 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -893,7 +893,9 @@ fn parse_create_table() { lat DOUBLE NULL,\ lng DOUBLE, constrained INT NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0), - ref INT REFERENCES othertable (a, b))"; + ref INT REFERENCES othertable (a, b),\ + ref2 INT references othertable2 on delete cascade on update no action\ + )"; let ast = one_statement_parses_to( sql, "CREATE TABLE uk_cities (\ @@ -901,7 +903,8 @@ fn parse_create_table() { lat double NULL, \ lng double, \ constrained int NULL CONSTRAINT pkey PRIMARY KEY NOT NULL UNIQUE CHECK (constrained > 0), \ - ref int REFERENCES othertable (a, b))", + ref int REFERENCES othertable (a, b), \ + ref2 int REFERENCES othertable2 ON DELETE CASCADE ON UPDATE NO ACTION)", ); match ast { Statement::CreateTable { @@ -978,8 +981,26 @@ fn parse_create_table() { option: ColumnOption::ForeignKey { foreign_table: ObjectName(vec!["othertable".into()]), referred_columns: vec!["a".into(), "b".into(),], + on_delete: None, + on_update: None, } }] + }, + ColumnDef { + name: "ref2".into(), + data_type: DataType::Int, + collation: None, + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::ForeignKey { + foreign_table: ObjectName(vec!["othertable2".into()]), + referred_columns: vec![], + on_delete: Some(ReferentialAction::Cascade), + on_update: Some(ReferentialAction::NoAction), + } + }, + ] } ] ); From 66ed8b4f53064ce9e450e97cdf230c803742f108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20M=C3=BCller?= Date: Mon, 25 May 2020 00:00:00 +0000 Subject: [PATCH 2/7] following clippy --- src/ast/ddl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6c7bab984..8577c9a25 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -187,7 +187,7 @@ impl fmt::Display for ColumnOption { on_update, } => { write!(f, "REFERENCES {}", foreign_table)?; - if referred_columns.len() > 0 { + if ! referred_columns.is_empty() { write!(f, " ({})", display_comma_separated(referred_columns))?; } if let Some(action) = on_delete { From 79514b79ccbfed6d5a45158f69beebd3d7949b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20M=C3=BCller?= Date: Mon, 25 May 2020 00:00:00 +0000 Subject: [PATCH 3/7] run `cargo +nightly fmt` --- src/ast/ddl.rs | 2 +- src/ast/mod.rs | 4 ++-- src/parser.rs | 37 ++++++++++++++++++++----------------- tests/sqlparser_common.rs | 20 +++++++++----------- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 8577c9a25..ac3fd1efc 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -187,7 +187,7 @@ impl fmt::Display for ColumnOption { on_update, } => { write!(f, "REFERENCES {}", foreign_table)?; - if ! referred_columns.is_empty() { + if !referred_columns.is_empty() { write!(f, " ({})", display_comma_separated(referred_columns))?; } if let Some(action) = on_delete { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e11d69286..bf48cd052 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -22,8 +22,8 @@ use std::fmt; pub use self::data_type::DataType; pub use self::ddl::{ - AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, TableConstraint, - ReferentialAction, + AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ReferentialAction, + TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ diff --git a/src/parser.rs b/src/parser.rs index f59d7c9ce..a40fea385 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1017,8 +1017,7 @@ impl Parser { ColumnOption::Unique { is_primary: false } } else if self.parse_keyword("REFERENCES") { let foreign_table = self.parse_object_name()?; - let referred_columns = - self.parse_parenthesized_column_list(Optional)?; + let referred_columns = self.parse_parenthesized_column_list(Optional)?; let mut on_delete = None; let mut on_update = None; while self.parse_keyword("ON") { @@ -1026,19 +1025,15 @@ impl Parser { if on_delete == None { on_delete = Some(self.parse_reference_change_action()?); } else { - return self.expected( - "ON DELETE option not more than once", - self.peek_token() - ); + return self + .expected("ON DELETE option not more than once", self.peek_token()); } } else if self.parse_keyword("UPDATE") { if on_update == None { on_update = Some(self.parse_reference_change_action()?); } else { - return self.expected( - "ON UPDATE option not more than once", - self.peek_token() - ); + return self + .expected("ON UPDATE option not more than once", self.peek_token()); } } } @@ -1061,13 +1056,21 @@ impl Parser { } pub fn parse_reference_change_action(&mut self) -> Result { - if self.parse_keyword("RESTRICT") { Ok(ReferentialAction::Restrict) } - else if self.parse_keyword("CASCADE") { Ok(ReferentialAction::Cascade) } - else if self.parse_keywords(vec!["SET", "NULL"]) { Ok(ReferentialAction::SetNull) } - else if self.parse_keywords(vec!["NO", "ACTION"]) { Ok(ReferentialAction::NoAction) } - else if self.parse_keywords(vec!["SET", "DEFAULT"]) { Ok(ReferentialAction::SetDefault) } - else { - self.expected("one of RESTRICT, CASCADE, SET NULL, NO ACTION or SET DEFAULT", self.peek_token()) + if self.parse_keyword("RESTRICT") { + Ok(ReferentialAction::Restrict) + } else if self.parse_keyword("CASCADE") { + Ok(ReferentialAction::Cascade) + } else if self.parse_keywords(vec!["SET", "NULL"]) { + Ok(ReferentialAction::SetNull) + } else if self.parse_keywords(vec!["NO", "ACTION"]) { + Ok(ReferentialAction::NoAction) + } else if self.parse_keywords(vec!["SET", "DEFAULT"]) { + Ok(ReferentialAction::SetDefault) + } else { + self.expected( + "one of RESTRICT, CASCADE, SET NULL, NO ACTION or SET DEFAULT", + self.peek_token(), + ) } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c68fb6385..0e89416db 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -990,17 +990,15 @@ fn parse_create_table() { name: "ref2".into(), data_type: DataType::Int, collation: None, - options: vec![ - ColumnOptionDef { - name: None, - option: ColumnOption::ForeignKey { - foreign_table: ObjectName(vec!["othertable2".into()]), - referred_columns: vec![], - on_delete: Some(ReferentialAction::Cascade), - on_update: Some(ReferentialAction::NoAction), - } - }, - ] + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::ForeignKey { + foreign_table: ObjectName(vec!["othertable2".into()]), + referred_columns: vec![], + on_delete: Some(ReferentialAction::Cascade), + on_update: Some(ReferentialAction::NoAction), + } + },] } ] ); From 02b021770ba5c181bfe48e241afea84e708ab6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20M=C3=BCller?= Date: Wed, 27 May 2020 00:00:00 +0000 Subject: [PATCH 4/7] =?UTF-8?q?replace=20reference=5Fchange=5Faction?= =?UTF-8?q?=E2=86=92referential=5Faction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ast/ddl.rs | 6 +++--- src/parser.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index ac3fd1efc..88ece6044 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -156,8 +156,8 @@ pub enum ColumnOption { }, /// A referential integrity constraint (`[FOREIGN KEY REFERENCES /// () - /// [ON DELETE ] - /// [ON UPDATE ]`). + /// [ON DELETE ] + /// [ON UPDATE ]`). /// /// The `ON DELETE` option may be placed after the `ON UPDATE` option. ForeignKey { @@ -216,7 +216,7 @@ fn display_constraint_name<'a>(name: &'a Option) -> impl fmt::Display + ' ConstraintName(name) } -/// ` = +/// ` = /// { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` /// /// Used in foreign key constraints in `ON UPDATE` and `ON DELETE` options. diff --git a/src/parser.rs b/src/parser.rs index a40fea385..f475adfb7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1023,14 +1023,14 @@ impl Parser { while self.parse_keyword("ON") { if self.parse_keyword("DELETE") { if on_delete == None { - on_delete = Some(self.parse_reference_change_action()?); + on_delete = Some(self.parse_referential_action()?); } else { return self .expected("ON DELETE option not more than once", self.peek_token()); } } else if self.parse_keyword("UPDATE") { if on_update == None { - on_update = Some(self.parse_reference_change_action()?); + on_update = Some(self.parse_referential_action()?); } else { return self .expected("ON UPDATE option not more than once", self.peek_token()); @@ -1055,7 +1055,7 @@ impl Parser { Ok(ColumnOptionDef { name, option }) } - pub fn parse_reference_change_action(&mut self) -> Result { + pub fn parse_referential_action(&mut self) -> Result { if self.parse_keyword("RESTRICT") { Ok(ReferentialAction::Restrict) } else if self.parse_keyword("CASCADE") { From 8ed4160b7f195c1c1f40cae90403cf41c46a73bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20M=C3=BCller?= Date: Wed, 27 May 2020 00:00:00 +0000 Subject: [PATCH 5/7] document ability to reorder formally --- src/ast/ddl.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 88ece6044..776927669 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -156,10 +156,9 @@ pub enum ColumnOption { }, /// A referential integrity constraint (`[FOREIGN KEY REFERENCES /// () - /// [ON DELETE ] - /// [ON UPDATE ]`). - /// - /// The `ON DELETE` option may be placed after the `ON UPDATE` option. + /// { [ON DELETE ] [ON UPDATE ] | + /// [ON UPDATE ] [ON DELETE ] + /// }`). ForeignKey { foreign_table: ObjectName, referred_columns: Vec, From dae78297b2c086d822fb8ef9dee918e84f533ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20M=C3=BCller?= Date: Wed, 27 May 2020 00:00:00 +0000 Subject: [PATCH 6/7] document special behavior of PostgreSQL --- src/parser.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/parser.rs b/src/parser.rs index f475adfb7..c25b061ee 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1017,6 +1017,8 @@ impl Parser { ColumnOption::Unique { is_primary: false } } else if self.parse_keyword("REFERENCES") { let foreign_table = self.parse_object_name()?; + // PostgreSQL allows omitting the column list and + // uses the primary key column of the foreign table by default let referred_columns = self.parse_parenthesized_column_list(Optional)?; let mut on_delete = None; let mut on_update = None; From 664e44b0bcc4bcf9d400fc15997dd98d624d9821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20M=C3=BCller?= Date: Wed, 27 May 2020 00:00:00 +0000 Subject: [PATCH 7/7] =?UTF-8?q?create=20an=20error=20implicitly=20later=20?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit by consuming the keywords at most once --- src/parser.rs | 22 +++++++--------------- tests/sqlparser_common.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index c25b061ee..7b70a73c0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1022,21 +1022,13 @@ impl Parser { let referred_columns = self.parse_parenthesized_column_list(Optional)?; let mut on_delete = None; let mut on_update = None; - while self.parse_keyword("ON") { - if self.parse_keyword("DELETE") { - if on_delete == None { - on_delete = Some(self.parse_referential_action()?); - } else { - return self - .expected("ON DELETE option not more than once", self.peek_token()); - } - } else if self.parse_keyword("UPDATE") { - if on_update == None { - on_update = Some(self.parse_referential_action()?); - } else { - return self - .expected("ON UPDATE option not more than once", self.peek_token()); - } + loop { + if on_delete.is_none() && self.parse_keywords(vec!["ON", "DELETE"]) { + on_delete = Some(self.parse_referential_action()?); + } else if on_update.is_none() && self.parse_keywords(vec!["ON", "UPDATE"]) { + on_update = Some(self.parse_referential_action()?); + } else { + break; } } ColumnOption::ForeignKey { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0e89416db..d9caf93a2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1015,6 +1015,32 @@ fn parse_create_table() { .contains("Expected column option, found: GARBAGE")); } +#[test] +fn parse_create_table_with_multiple_on_delete_fails() { + parse_sql_statements( + "\ + create table X (\ + y_id int references Y (id) \ + on delete cascade on update cascade on delete no action\ + )", + ) + .expect_err("should have failed"); +} + +#[test] +fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), ParserError> { + let sql = |options: &str| -> String { + format!("create table X (y_id int references Y (id) {})", options) + }; + + parse_sql_statements(&sql("on update cascade on delete no action"))?; + parse_sql_statements(&sql("on delete cascade on update cascade"))?; + parse_sql_statements(&sql("on update no action"))?; + parse_sql_statements(&sql("on delete restrict"))?; + + Ok(()) +} + #[test] fn parse_create_table_with_options() { let sql = "CREATE TABLE t (c int) WITH (foo = 'bar', a = 123)";