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..05dde8aa8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1162,7 +1162,11 @@ 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,\ + 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( sql, @@ -1172,7 +1176,11 @@ 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, \ + 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 { Statement::CreateTable { @@ -1271,7 +1279,43 @@ 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: 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()], + 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!(), @@ -1290,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(