diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f1ce1522b4..a14b902e5a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -204,6 +204,12 @@ const EOF_TOKEN: TokenWithSpan = TokenWithSpan { }, }; +struct ReferenceClauses { + match_kind: Option, + on_delete: Option, + on_update: Option, +} + /// Composite types declarations using angle brackets syntax can be arbitrary /// nested such that the following declaration is possible: /// `ARRAY>` @@ -10182,24 +10188,11 @@ impl<'a> Parser<'a> { // 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, false)?; - let mut match_kind = None; - let mut on_delete = None; - let mut on_update = None; - loop { - if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) { - match_kind = Some(self.parse_match_kind()?); - } else 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; - } - } + let ReferenceClauses { + match_kind, + on_delete, + on_update, + } = self.parse_reference_clauses()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some( @@ -10462,6 +10455,29 @@ impl<'a> Parser<'a> { Ok(clustered_by) } + /// Parse optional `MATCH`, `ON DELETE`, and `ON UPDATE` reference clauses. + fn parse_reference_clauses(&mut self) -> Result { + let mut match_kind = None; + let mut on_delete = None; + let mut on_update = None; + loop { + if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) { + match_kind = Some(self.parse_match_kind()?); + } else 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(ReferenceClauses { + match_kind, + on_delete, + on_update, + }) + } + /// Parse a referential action used in foreign key clauses. /// /// Recognized forms: `RESTRICT`, `CASCADE`, `SET NULL`, `NO ACTION`, `SET DEFAULT`. @@ -10659,24 +10675,11 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::REFERENCES)?; let foreign_table = self.parse_object_name(false)?; let referred_columns = self.parse_parenthesized_column_list(Optional, false)?; - let mut match_kind = None; - let mut on_delete = None; - let mut on_update = None; - loop { - if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) { - match_kind = Some(self.parse_match_kind()?); - } else 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; - } - } + let ReferenceClauses { + match_kind, + on_delete, + on_update, + } = self.parse_reference_clauses()?; let characteristics = self.parse_constraint_characteristics()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index bcbc53b8e2..a95105eba5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22124,3 +22124,89 @@ fn parse_cache_table_options_without_flag() { ); } } + +#[test] +fn parse_reference_clauses_in_any_order() { + let dialects = all_dialects(); + for clauses in [ + "MATCH FULL ON DELETE CASCADE ON UPDATE SET NULL", + "MATCH FULL ON UPDATE SET NULL ON DELETE CASCADE", + "ON DELETE CASCADE MATCH FULL ON UPDATE SET NULL", + "ON DELETE CASCADE ON UPDATE SET NULL MATCH FULL", + "ON UPDATE SET NULL MATCH FULL ON DELETE CASCADE", + "ON UPDATE SET NULL ON DELETE CASCADE MATCH FULL", + ] { + for prefix in [ + "a INT REFERENCES r (id)", + "a INT, FOREIGN KEY (a) REFERENCES r(id)", + ] { + let sql = format!("CREATE TABLE t ({prefix} {clauses} DEFERRABLE INITIALLY DEFERRED)"); + let canonical = format!( + "CREATE TABLE t ({prefix} MATCH FULL ON DELETE CASCADE ON UPDATE SET NULL DEFERRABLE INITIALLY DEFERRED)" + ); + dialects.one_statement_parses_to(&sql, &canonical); + } + } +} + +#[test] +fn parse_reference_clauses_leave_duplicates_unconsumed() { + for (clauses, keyword, match_kind, on_delete, on_update) in [ + ( + "MATCH FULL MATCH SIMPLE", + Keyword::MATCH, + Some(ConstraintReferenceMatchKind::Full), + None, + None, + ), + ( + "ON DELETE CASCADE ON DELETE SET NULL", + Keyword::ON, + None, + Some(ReferentialAction::Cascade), + None, + ), + ( + "ON UPDATE RESTRICT ON UPDATE NO ACTION", + Keyword::ON, + None, + None, + Some(ReferentialAction::Restrict), + ), + ] { + for table_level in [false, true] { + let prefix = if table_level { + "FOREIGN KEY (a) REFERENCES r(id)" + } else { + "REFERENCES r(id)" + }; + let sql = format!("{prefix} {clauses}"); + let mut parser = Parser::new(&GenericDialect {}).try_with_sql(&sql).unwrap(); + let constraint = if table_level { + let Some(TableConstraint::ForeignKey(constraint)) = + parser.parse_optional_table_constraint().unwrap() + else { + panic!("{sql}"); + }; + constraint + } else { + let Some(ColumnOption::ForeignKey(constraint)) = + parser.parse_optional_column_option().unwrap() + else { + panic!("{sql}"); + }; + constraint + }; + assert_eq!( + ( + constraint.match_kind, + constraint.on_delete, + constraint.on_update + ), + (match_kind, on_delete, on_update), + "{sql}" + ); + assert!(parser.peek_keyword(keyword), "{sql}"); + } + } +}