diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 7333ad287..776927669 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -155,10 +155,15 @@ pub enum ColumnOption { is_primary: bool, }, /// A referential integrity constraint (`[FOREIGN KEY REFERENCES - /// ()`). + /// () + /// { [ON DELETE ] [ON UPDATE ] | + /// [ON UPDATE ] [ON DELETE ] + /// }`). ForeignKey { foreign_table: ObjectName, referred_columns: Vec, + on_delete: Option, + on_update: Option, }, // `CHECK ()` Check(Expr), @@ -177,12 +182,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.is_empty() { + 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 +214,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..bf48cd052 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -22,7 +22,8 @@ use std::fmt; pub use self::data_type::DataType; pub use self::ddl::{ - AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, TableConstraint, + AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ReferentialAction, + TableConstraint, }; 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..7b70a73c0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1017,10 +1017,25 @@ 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)?; + // 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; + 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 { foreign_table, referred_columns, + on_delete, + on_update, } } else if self.parse_keyword("CHECK") { self.expect_token(&Token::LParen)?; @@ -1034,6 +1049,25 @@ impl Parser { Ok(ColumnOptionDef { name, option }) } + pub fn parse_referential_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..d9caf93a2 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,24 @@ 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), + } + },] } ] ); @@ -996,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)";