diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 3294a7a81..4a8678e44 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,14 +30,15 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, ArgMode, AttachedToken, CommentDef, - ConditionalStatements, CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, - CreateTableOptions, CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, - FunctionCalledOnNull, FunctionDesc, FunctionDeterminismSpecifier, FunctionParallel, - HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, - InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, - OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, - SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableConstraint, TableVersion, + display_comma_separated, display_separated, + table_constraints::{ForeignKeyConstraint, TableConstraint}, + ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody, + CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, CreateViewParams, DataType, Expr, + FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDesc, FunctionDeterminismSpecifier, + FunctionParallel, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, + HiveSetLocation, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, + OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, + RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, WrappedCollection, }; @@ -1559,20 +1560,14 @@ pub enum ColumnOption { is_primary: bool, characteristics: Option, }, - /// A referential integrity constraint (`[FOREIGN KEY REFERENCES - /// () + /// A referential integrity constraint (`REFERENCES () + /// [ MATCH { FULL | PARTIAL | SIMPLE } ] /// { [ON DELETE ] [ON UPDATE ] | /// [ON UPDATE ] [ON DELETE ] - /// } + /// } /// [] /// `). - ForeignKey { - foreign_table: ObjectName, - referred_columns: Vec, - on_delete: Option, - on_update: Option, - characteristics: Option, - }, + ForeignKey(ForeignKeyConstraint), /// `CHECK ()` Check(Expr), /// Dialect-specific options, such as: @@ -1643,6 +1638,12 @@ pub enum ColumnOption { Invisible, } +impl From for ColumnOption { + fn from(fk: ForeignKeyConstraint) -> Self { + ColumnOption::ForeignKey(fk) + } +} + impl fmt::Display for ColumnOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use ColumnOption::*; @@ -1669,24 +1670,25 @@ impl fmt::Display for ColumnOption { } Ok(()) } - ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => { - write!(f, "REFERENCES {foreign_table}")?; - if !referred_columns.is_empty() { - write!(f, " ({})", display_comma_separated(referred_columns))?; + ForeignKey(constraint) => { + write!(f, "REFERENCES {}", constraint.foreign_table)?; + if !constraint.referred_columns.is_empty() { + write!( + f, + " ({})", + display_comma_separated(&constraint.referred_columns) + )?; } - if let Some(action) = on_delete { + if let Some(match_kind) = &constraint.match_kind { + write!(f, " {match_kind}")?; + } + if let Some(action) = &constraint.on_delete { write!(f, " ON DELETE {action}")?; } - if let Some(action) = on_update { + if let Some(action) = &constraint.on_update { write!(f, " ON UPDATE {action}")?; } - if let Some(characteristics) = characteristics { + if let Some(characteristics) = &constraint.characteristics { write!(f, " {characteristics}")?; } Ok(()) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index fef8943ef..f4e2825dd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -657,6 +657,31 @@ pub enum CastKind { DoubleColon, } +/// `MATCH` type for constraint references +/// +/// See: +#[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 ConstraintReferenceMatchKind { + /// `MATCH FULL` + Full, + /// `MATCH PARTIAL` + Partial, + /// `MATCH SIMPLE` + Simple, +} + +impl fmt::Display for ConstraintReferenceMatchKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Full => write!(f, "MATCH FULL"), + Self::Partial => write!(f, "MATCH PARTIAL"), + Self::Simple => write!(f, "MATCH SIMPLE"), + } + } +} + /// `EXTRACT` syntax variants. /// /// In Snowflake dialect, the `EXTRACT` expression can support either the `from` syntax diff --git a/src/ast/spans.rs b/src/ast/spans.rs index de3439cfc..1e5a96bc4 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -741,19 +741,7 @@ impl Spanned for ColumnOption { ColumnOption::Ephemeral(expr) => expr.as_ref().map_or(Span::empty(), |e| e.span()), ColumnOption::Alias(expr) => expr.span(), ColumnOption::Unique { .. } => Span::empty(), - ColumnOption::ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - } => union_spans( - core::iter::once(foreign_table.span()) - .chain(referred_columns.iter().map(|i| i.span)) - .chain(on_delete.iter().map(|i| i.span())) - .chain(on_update.iter().map(|i| i.span())) - .chain(characteristics.iter().map(|i| i.span())), - ), + ColumnOption::ForeignKey(constraint) => constraint.span(), ColumnOption::Check(expr) => expr.span(), ColumnOption::DialectSpecific(_) => Span::empty(), ColumnOption::CharacterSet(object_name) => object_name.span(), diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index afcf62959..ddf0c1253 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -18,9 +18,9 @@ //! SQL Abstract Syntax Tree (AST) types for table constraints use crate::ast::{ - display_comma_separated, display_separated, ConstraintCharacteristics, Expr, Ident, - IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, ObjectName, - ReferentialAction, + display_comma_separated, display_separated, ConstraintCharacteristics, + ConstraintReferenceMatchKind, Expr, Ident, IndexColumn, IndexOption, IndexType, + KeyOrIndexDisplay, NullsDistinctOption, ObjectName, ReferentialAction, }; use crate::tokenizer::Span; use core::fmt; @@ -189,7 +189,7 @@ impl crate::ast::Spanned for CheckConstraint { } /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () -/// REFERENCES () +/// REFERENCES () [ MATCH { FULL | PARTIAL | SIMPLE } ] /// { [ON DELETE ] [ON UPDATE ] | /// [ON UPDATE ] [ON DELETE ] /// }`). @@ -206,6 +206,7 @@ pub struct ForeignKeyConstraint { pub referred_columns: Vec, pub on_delete: Option, pub on_update: Option, + pub match_kind: Option, pub characteristics: Option, } @@ -223,6 +224,9 @@ impl fmt::Display for ForeignKeyConstraint { if !self.referred_columns.is_empty() { write!(f, "({})", display_comma_separated(&self.referred_columns))?; } + if let Some(match_kind) = &self.match_kind { + write!(f, " {match_kind}")?; + } if let Some(action) = &self.on_delete { write!(f, " ON DELETE {action}")?; } diff --git a/src/keywords.rs b/src/keywords.rs index 3c9855222..35bf616d9 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -713,6 +713,7 @@ define_keywords!( PARAMETER, PARQUET, PART, + PARTIAL, PARTITION, PARTITIONED, PARTITIONS, @@ -885,6 +886,7 @@ define_keywords!( SHOW, SIGNED, SIMILAR, + SIMPLE, SKIP, SLOW, SMALLINT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 70f4d8568..ef583dd37 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7940,7 +7940,7 @@ impl<'a> Parser<'a> { } pub fn parse_column_def(&mut self) -> Result { - let name = self.parse_identifier()?; + let col_name = self.parse_identifier()?; let data_type = if self.is_column_type_sqlite_unspecified() { DataType::Unspecified } else { @@ -7965,7 +7965,7 @@ impl<'a> Parser<'a> { }; } Ok(ColumnDef { - name, + name: col_name, data_type, options, }) @@ -8065,10 +8065,15 @@ 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 on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) { + 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]) @@ -8080,13 +8085,20 @@ impl<'a> Parser<'a> { } let characteristics = self.parse_constraint_characteristics()?; - Ok(Some(ColumnOption::ForeignKey { - foreign_table, - referred_columns, - on_delete, - on_update, - characteristics, - })) + Ok(Some( + ForeignKeyConstraint { + name: None, // Column-level constraints don't have names + index_name: None, // Not applicable for column-level constraints + columns: vec![], // Not applicable for column-level constraints + foreign_table, + referred_columns, + on_delete, + on_update, + match_kind, + characteristics, + } + .into(), + )) } else if self.parse_keyword(Keyword::CHECK) { self.expect_token(&Token::LParen)?; // since `CHECK` requires parentheses, we can parse the inner expression in ParserState::Normal @@ -8360,6 +8372,18 @@ impl<'a> Parser<'a> { } } + pub fn parse_match_kind(&mut self) -> Result { + if self.parse_keyword(Keyword::FULL) { + Ok(ConstraintReferenceMatchKind::Full) + } else if self.parse_keyword(Keyword::PARTIAL) { + Ok(ConstraintReferenceMatchKind::Partial) + } else if self.parse_keyword(Keyword::SIMPLE) { + Ok(ConstraintReferenceMatchKind::Simple) + } else { + self.expected("one of FULL, PARTIAL or SIMPLE", self.peek_token()) + } + } + pub fn parse_constraint_characteristics( &mut self, ) -> Result, ParserError> { @@ -8470,10 +8494,15 @@ 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 on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) { + 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]) @@ -8495,6 +8524,7 @@ impl<'a> Parser<'a> { referred_columns, on_delete, on_update, + match_kind, characteristics, } .into(), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 773937c5a..52f38b10e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3790,13 +3790,17 @@ fn parse_create_table() { data_type: DataType::Int(None), options: vec![ColumnOptionDef { name: None, - option: ColumnOption::ForeignKey { + option: ColumnOption::ForeignKey(ForeignKeyConstraint { + name: None, + index_name: None, + columns: vec![], foreign_table: ObjectName::from(vec!["othertable".into()]), referred_columns: vec!["a".into(), "b".into()], on_delete: None, on_update: None, + match_kind: None, characteristics: None, - }, + }), }], }, ColumnDef { @@ -3804,13 +3808,17 @@ fn parse_create_table() { data_type: DataType::Int(None), options: vec![ColumnOptionDef { name: None, - option: ColumnOption::ForeignKey { + option: ColumnOption::ForeignKey(ForeignKeyConstraint { + name: None, + index_name: None, + columns: vec![], foreign_table: ObjectName::from(vec!["othertable2".into()]), referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::NoAction), + match_kind: None, characteristics: None, - }, + }), },], }, ] @@ -3826,6 +3834,7 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, + match_kind: None, characteristics: None, } .into(), @@ -3837,6 +3846,7 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), + match_kind: None, characteristics: None, } .into(), @@ -3848,6 +3858,7 @@ fn parse_create_table() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), + match_kind: None, characteristics: None, } .into(), @@ -3859,6 +3870,7 @@ fn parse_create_table() { referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), + match_kind: None, characteristics: None, } .into(), @@ -3957,6 +3969,7 @@ fn parse_create_table_with_constraint_characteristics() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), on_update: None, + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Deferred), @@ -3972,6 +3985,7 @@ fn parse_create_table_with_constraint_characteristics() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), on_update: Some(ReferentialAction::Restrict), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(true), initially: Some(DeferrableInitial::Immediate), @@ -3987,6 +4001,7 @@ fn parse_create_table_with_constraint_characteristics() { referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::SetDefault), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(false), initially: Some(DeferrableInitial::Deferred), @@ -4002,6 +4017,7 @@ fn parse_create_table_with_constraint_characteristics() { referred_columns: vec!["longitude".into()], on_delete: None, on_update: Some(ReferentialAction::SetNull), + match_kind: None, characteristics: Some(ConstraintCharacteristics { deferrable: Some(false), initially: Some(DeferrableInitial::Immediate), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e18bf662a..9d08540ad 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6438,6 +6438,7 @@ fn parse_alter_table_constraint_not_valid() { referred_columns: vec!["ref".into()], on_delete: None, on_update: None, + match_kind: None, characteristics: None, } .into(), @@ -6603,3 +6604,51 @@ fn parse_alter_schema() { _ => unreachable!(), } } + +#[test] +fn parse_foreign_key_match() { + let test_cases = [ + ("MATCH FULL", ConstraintReferenceMatchKind::Full), + ("MATCH SIMPLE", ConstraintReferenceMatchKind::Simple), + ("MATCH PARTIAL", ConstraintReferenceMatchKind::Partial), + ]; + + for (match_clause, expected_kind) in test_cases { + // Test column-level foreign key + let sql = format!("CREATE TABLE t (id INT REFERENCES other_table (id) {match_clause})"); + let statement = pg_and_generic().verified_stmt(&sql); + match statement { + Statement::CreateTable(CreateTable { columns, .. }) => { + match &columns[0].options[0].option { + ColumnOption::ForeignKey(constraint) => { + assert_eq!(constraint.match_kind, Some(expected_kind)); + } + _ => panic!("Expected ColumnOption::ForeignKey"), + } + } + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } + + // Test table-level foreign key constraint + let sql = format!( + "CREATE TABLE t (id INT, FOREIGN KEY (id) REFERENCES other_table(id) {match_clause})" + ); + let statement = pg_and_generic().verified_stmt(&sql); + match statement { + Statement::CreateTable(CreateTable { constraints, .. }) => match &constraints[0] { + TableConstraint::ForeignKey(constraint) => { + assert_eq!(constraint.match_kind, Some(expected_kind)); + } + _ => panic!("Expected TableConstraint::ForeignKey"), + }, + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } + } +} + +#[test] +fn parse_foreign_key_match_with_actions() { + let sql = "CREATE TABLE orders (order_id INT REFERENCES another_table (id) MATCH FULL ON DELETE CASCADE ON UPDATE RESTRICT, customer_id INT, CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH SIMPLE ON DELETE SET NULL ON UPDATE CASCADE)"; + + pg_and_generic().verified_stmt(sql); +}