diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 9d535a7f22..4a7aa8dae0 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -384,6 +384,7 @@ pub enum TableConstraint { columns: Vec, /// Whether this is a `PRIMARY KEY` or just a `UNIQUE` constraint is_primary: bool, + characteristics: Option, }, /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () /// REFERENCES () @@ -397,6 +398,7 @@ pub enum TableConstraint { referred_columns: Vec, on_delete: Option, on_update: Option, + characteristics: Option, }, /// `[ CONSTRAINT ] CHECK ()` Check { @@ -453,13 +455,22 @@ impl fmt::Display for TableConstraint { name, columns, is_primary, - } => write!( - f, - "{}{} ({})", - display_constraint_name(name), - if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }, - display_comma_separated(columns) - ), + characteristics, + } => { + write!( + f, + "{}{} ({})", + display_constraint_name(name), + if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }, + display_comma_separated(columns) + )?; + + if let Some(characteristics) = characteristics { + write!(f, " {}", characteristics)?; + } + + Ok(()) + } TableConstraint::ForeignKey { name, columns, @@ -467,6 +478,7 @@ impl fmt::Display for TableConstraint { referred_columns, on_delete, on_update, + characteristics, } => { write!( f, @@ -482,6 +494,9 @@ impl fmt::Display for TableConstraint { if let Some(action) = on_update { write!(f, " ON UPDATE {action}")?; } + if let Some(characteristics) = characteristics { + write!(f, " {}", characteristics)?; + } Ok(()) } TableConstraint::Check { name, expr } => { @@ -676,20 +691,24 @@ pub enum ColumnOption { NotNull, /// `DEFAULT ` Default(Expr), - /// `{ PRIMARY KEY | UNIQUE }` + /// `{ PRIMARY KEY | UNIQUE } []` Unique { is_primary: bool, + characteristics: Option, }, /// 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, + characteristics: Option, }, /// `CHECK ()` Check(Expr), @@ -719,14 +738,22 @@ impl fmt::Display for ColumnOption { Null => write!(f, "NULL"), NotNull => write!(f, "NOT NULL"), Default(expr) => write!(f, "DEFAULT {expr}"), - Unique { is_primary } => { - write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }) + Unique { + is_primary, + characteristics, + } => { + write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" })?; + if let Some(characteristics) = characteristics { + write!(f, " {}", characteristics)?; + } + Ok(()) } ForeignKey { foreign_table, referred_columns, on_delete, on_update, + characteristics, } => { write!(f, "REFERENCES {foreign_table}")?; if !referred_columns.is_empty() { @@ -738,6 +765,9 @@ impl fmt::Display for ColumnOption { if let Some(action) = on_update { write!(f, " ON UPDATE {action}")?; } + if let Some(characteristics) = characteristics { + write!(f, " {}", characteristics)?; + } Ok(()) } Check(expr) => write!(f, "CHECK ({expr})"), @@ -826,6 +856,79 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { ConstraintName(name) } +/// ` = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]` +/// +/// Used in UNIQUE and foreign key constraints. The individual settings may occur in any order. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ConstraintCharacteristics { + pub deferrable: Option, + pub initially: Option, + pub enforced: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DeferrableInitial { + Immediate, + Deferred, +} + +impl ConstraintCharacteristics { + fn deferrable_text(&self) -> Option<&'static str> { + self.deferrable.map(|deferrable| { + if deferrable { + "DEFERRABLE" + } else { + "NOT DEFERRABLE" + } + }) + } + + fn initially_immediate_text(&self) -> Option<&'static str> { + self.initially + .map(|initially_immediate| match initially_immediate { + DeferrableInitial::Immediate => "INITIALLY IMMEDIATE", + DeferrableInitial::Deferred => "INITIALLY DEFERRED", + }) + } + + fn enforced_text(&self) -> Option<&'static str> { + self.enforced.map( + |enforced| { + if enforced { + "ENFORCED" + } else { + "NOT ENFORCED" + } + }, + ) + } +} + +impl fmt::Display for ConstraintCharacteristics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let deferrable = self.deferrable_text(); + let initially_immediate = self.initially_immediate_text(); + let enforced = self.enforced_text(); + + match (deferrable, initially_immediate, enforced) { + (None, None, None) => Ok(()), + (None, None, Some(enforced)) => write!(f, "{enforced}"), + (None, Some(initial), None) => write!(f, "{initial}"), + (None, Some(initial), Some(enforced)) => write!(f, "{initial} {enforced}"), + (Some(deferrable), None, None) => write!(f, "{deferrable}"), + (Some(deferrable), None, Some(enforced)) => write!(f, "{deferrable} {enforced}"), + (Some(deferrable), Some(initial), None) => write!(f, "{deferrable} {initial}"), + (Some(deferrable), Some(initial), Some(enforced)) => { + write!(f, "{deferrable} {initial} {enforced}") + } + } + } +} + /// ` = /// { RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT }` /// diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d32cb93efe..ba2e5898dd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -32,8 +32,9 @@ pub use self::data_type::{ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue}; pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, - ColumnOptionDef, GeneratedAs, GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition, - ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, + ColumnOptionDef, ConstraintCharacteristics, DeferrableInitial, GeneratedAs, + GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, + ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, }; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/keywords.rs b/src/keywords.rs index 5054fbf2c1..f14b92b76b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -210,6 +210,7 @@ define_keywords!( DECIMAL, DECLARE, DEFAULT, + DEFERRABLE, DEFERRED, DELAYED, DELETE, @@ -250,6 +251,7 @@ define_keywords!( ENDPOINT, END_FRAME, END_PARTITION, + ENFORCED, ENGINE, ENUM, EPOCH, @@ -343,6 +345,7 @@ define_keywords!( INDEX, INDICATOR, INHERIT, + INITIALLY, INNER, INOUT, INPUTFORMAT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6e08a1b17c..3cffc20f42 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4392,9 +4392,17 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::DEFAULT) { Ok(Some(ColumnOption::Default(self.parse_expr()?))) } else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) { - Ok(Some(ColumnOption::Unique { is_primary: true })) + let characteristics = self.parse_constraint_characteristics()?; + Ok(Some(ColumnOption::Unique { + is_primary: true, + characteristics, + })) } else if self.parse_keyword(Keyword::UNIQUE) { - Ok(Some(ColumnOption::Unique { is_primary: false })) + let characteristics = self.parse_constraint_characteristics()?; + Ok(Some(ColumnOption::Unique { + is_primary: false, + characteristics, + })) } else if self.parse_keyword(Keyword::REFERENCES) { let foreign_table = self.parse_object_name()?; // PostgreSQL allows omitting the column list and @@ -4413,11 +4421,14 @@ impl<'a> Parser<'a> { break; } } + let characteristics = self.parse_constraint_characteristics()?; + Ok(Some(ColumnOption::ForeignKey { foreign_table, referred_columns, on_delete, on_update, + characteristics, })) } else if self.parse_keyword(Keyword::CHECK) { self.expect_token(&Token::LParen)?; @@ -4565,6 +4576,47 @@ impl<'a> Parser<'a> { } } + pub fn parse_constraint_characteristics( + &mut self, + ) -> Result, ParserError> { + let mut cc = ConstraintCharacteristics { + deferrable: None, + initially: None, + enforced: None, + }; + + loop { + if cc.deferrable.is_none() && self.parse_keywords(&[Keyword::NOT, Keyword::DEFERRABLE]) + { + cc.deferrable = Some(false); + } else if cc.deferrable.is_none() && self.parse_keyword(Keyword::DEFERRABLE) { + cc.deferrable = Some(true); + } else if cc.initially.is_none() && self.parse_keyword(Keyword::INITIALLY) { + if self.parse_keyword(Keyword::DEFERRED) { + cc.initially = Some(DeferrableInitial::Deferred); + } else if self.parse_keyword(Keyword::IMMEDIATE) { + cc.initially = Some(DeferrableInitial::Immediate); + } else { + self.expected("one of DEFERRED or IMMEDIATE", self.peek_token())?; + } + } else if cc.enforced.is_none() && self.parse_keyword(Keyword::ENFORCED) { + cc.enforced = Some(true); + } else if cc.enforced.is_none() + && self.parse_keywords(&[Keyword::NOT, Keyword::ENFORCED]) + { + cc.enforced = Some(false); + } else { + break; + } + } + + if cc.deferrable.is_some() || cc.initially.is_some() || cc.enforced.is_some() { + Ok(Some(cc)) + } else { + Ok(None) + } + } + pub fn parse_optional_table_constraint( &mut self, ) -> Result, ParserError> { @@ -4588,10 +4640,12 @@ impl<'a> Parser<'a> { .or(name); let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::Unique { name, columns, is_primary, + characteristics, })) } Token::Word(w) if w.keyword == Keyword::FOREIGN => { @@ -4613,6 +4667,9 @@ impl<'a> Parser<'a> { break; } } + + let characteristics = self.parse_constraint_characteristics()?; + Ok(Some(TableConstraint::ForeignKey { name, columns, @@ -4620,6 +4677,7 @@ impl<'a> Parser<'a> { referred_columns, on_delete, on_update, + characteristics, })) } Token::Word(w) if w.keyword == Keyword::CHECK => { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ec34015a6d..52443ab21a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2474,10 +2474,10 @@ fn parse_create_table() { 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,\ - 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 + constraint fkey foreign key (lat) references othertable3 (lat) on delete restrict deferrable initially deferred,\ + constraint fkey2 foreign key (lat) references othertable4(lat) on delete no action on update restrict deferrable initially immediate, \ + foreign key (lat) references othertable4(lat) on update set default on delete cascade not deferrable initially deferred not enforced, \ + FOREIGN KEY (lng) REFERENCES othertable4 (longitude) ON UPDATE SET NULL enforced not deferrable initially immediate )"; let ast = one_statement_parses_to( sql, @@ -2488,10 +2488,10 @@ fn parse_create_table() { 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, \ - 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)", + CONSTRAINT fkey FOREIGN KEY (lat) REFERENCES othertable3(lat) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED, \ + CONSTRAINT fkey2 FOREIGN KEY (lat) REFERENCES othertable4(lat) ON DELETE NO ACTION ON UPDATE RESTRICT DEFERRABLE INITIALLY IMMEDIATE, \ + FOREIGN KEY (lat) REFERENCES othertable4(lat) ON DELETE CASCADE ON UPDATE SET DEFAULT NOT DEFERRABLE INITIALLY DEFERRED NOT ENFORCED, \ + FOREIGN KEY (lng) REFERENCES othertable4(longitude) ON UPDATE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE ENFORCED)", ); match ast { Statement::CreateTable { @@ -2547,7 +2547,10 @@ fn parse_create_table() { }, ColumnOptionDef { name: Some("pkey".into()), - option: ColumnOption::Unique { is_primary: true }, + option: ColumnOption::Unique { + is_primary: true, + characteristics: None + }, }, ColumnOptionDef { name: None, @@ -2555,7 +2558,10 @@ fn parse_create_table() { }, ColumnOptionDef { name: None, - option: ColumnOption::Unique { is_primary: false }, + option: ColumnOption::Unique { + is_primary: false, + characteristics: None + }, }, ColumnOptionDef { name: None, @@ -2574,6 +2580,7 @@ fn parse_create_table() { referred_columns: vec!["a".into(), "b".into()], on_delete: None, on_update: None, + characteristics: None, }, }], }, @@ -2588,6 +2595,7 @@ fn parse_create_table() { referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::NoAction), + characteristics: None, }, },], }, @@ -2603,6 +2611,11 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, + characteristics: Some(ConstraintCharacteristics { + deferrable: Some(true), + initially: Some(DeferrableInitial::Deferred), + enforced: None + }), }, TableConstraint::ForeignKey { name: Some("fkey2".into()), @@ -2611,6 +2624,11 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), + characteristics: Some(ConstraintCharacteristics { + deferrable: Some(true), + initially: Some(DeferrableInitial::Immediate), + enforced: None, + }), }, TableConstraint::ForeignKey { name: None, @@ -2619,6 +2637,11 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), + characteristics: Some(ConstraintCharacteristics { + deferrable: Some(false), + initially: Some(DeferrableInitial::Deferred), + enforced: Some(false), + }), }, TableConstraint::ForeignKey { name: None, @@ -2627,6 +2650,11 @@ fn parse_create_table() { referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), + characteristics: Some(ConstraintCharacteristics { + deferrable: Some(false), + initially: Some(DeferrableInitial::Immediate), + enforced: Some(true), + }), }, ] ); @@ -2648,6 +2676,112 @@ fn parse_create_table() { .contains("Expected constraint details after CONSTRAINT ")); } +#[test] +fn parse_create_table_column_constraint_characteristics() { + fn test_combo( + syntax: &str, + deferrable: Option, + initially: Option, + enforced: Option, + ) { + let message = if syntax.is_empty() { + "No clause" + } else { + syntax + }; + + let sql = format!("CREATE TABLE t (a int UNIQUE {})", syntax); + let expected_clause = if syntax.is_empty() { + String::new() + } else { + format!(" {syntax}") + }; + let expected = format!("CREATE TABLE t (a INT UNIQUE{})", expected_clause); + let ast = one_statement_parses_to(&sql, &expected); + + let expected_value = if deferrable.is_some() || initially.is_some() || enforced.is_some() { + Some(ConstraintCharacteristics { + deferrable, + initially, + enforced, + }) + } else { + None + }; + + match ast { + Statement::CreateTable { columns, .. } => { + assert_eq!( + columns, + vec![ColumnDef { + name: "a".into(), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { + name: None, + option: ColumnOption::Unique { + is_primary: false, + characteristics: expected_value + } + }] + }], + "{message}" + ) + } + _ => unreachable!(), + } + } + + for deferrable in [None, Some(true), Some(false)] { + for initially in [ + None, + Some(DeferrableInitial::Immediate), + Some(DeferrableInitial::Deferred), + ] { + for enforced in [None, Some(true), Some(false)] { + let deferrable_text = + deferrable.map(|d| if d { "DEFERRABLE" } else { "NOT DEFERRABLE" }); + let initially_text = initially.map(|i| match i { + DeferrableInitial::Immediate => "INITIALLY IMMEDIATE", + DeferrableInitial::Deferred => "INITIALLY DEFERRED", + }); + let enforced_text = enforced.map(|e| if e { "ENFORCED" } else { "NOT ENFORCED" }); + + let syntax = [deferrable_text, initially_text, enforced_text] + .into_iter() + .flatten() + .collect::>() + .join(" "); + + test_combo(&syntax, deferrable, initially, enforced); + } + } + } + + let res = parse_sql_statements( + "CREATE TABLE t (a int NOT NULL UNIQUE DEFERRABLE INITIALLY BADVALUE)", + ); + assert!(res + .unwrap_err() + .to_string() + .contains("Expected one of DEFERRED or IMMEDIATE, found: BADVALUE")); + + let res = parse_sql_statements( + "CREATE TABLE t (a int NOT NULL UNIQUE INITIALLY IMMEDIATE DEFERRABLE INITIALLY DEFERRED)", + ); + res.expect_err("INITIALLY {IMMEDIATE|DEFERRED} setting should only be allowed once"); + + let res = parse_sql_statements( + "CREATE TABLE t (a int NOT NULL UNIQUE DEFERRABLE INITIALLY DEFERRED NOT DEFERRABLE)", + ); + res.expect_err("[NOT] DEFERRABLE setting should only be allowed once"); + + let res = parse_sql_statements( + "CREATE TABLE t (a int NOT NULL UNIQUE DEFERRABLE INITIALLY DEFERRED ENFORCED NOT ENFORCED)", + ); + res.expect_err("[NOT] ENFORCED setting should only be allowed once"); +} + #[test] fn parse_create_table_hive_array() { // Parsing [] type arrays does not work in MsSql since [ is used in is_delimited_identifier_start diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f3ef426775..ff4dd4bbf4 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -451,7 +451,10 @@ fn parse_create_table_auto_increment() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { is_primary: true }, + option: ColumnOption::Unique { + is_primary: true, + characteristics: None + }, }, ColumnOptionDef { name: None, @@ -484,7 +487,8 @@ fn parse_create_table_unique_key() { vec![TableConstraint::Unique { name: Some(Ident::new("bar_key")), columns: vec![Ident::new("bar")], - is_primary: false + is_primary: false, + characteristics: None, }], constraints ); @@ -497,7 +501,10 @@ fn parse_create_table_unique_key() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { is_primary: true }, + option: ColumnOption::Unique { + is_primary: true, + characteristics: None + }, }, ColumnOptionDef { name: None, @@ -707,7 +714,10 @@ fn parse_quote_identifiers() { collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Unique { is_primary: true }, + option: ColumnOption::Unique { + is_primary: true, + characteristics: None + }, }], }], columns diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 8de862fc2a..3e5428d102 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -160,7 +160,10 @@ fn parse_create_table_auto_increment() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Unique { is_primary: true }, + option: ColumnOption::Unique { + is_primary: true, + characteristics: None + }, }, ColumnOptionDef { name: None,