This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new c8531d41 Added support for MATCH syntax and unified column option 
ForeignKey (#2062)
c8531d41 is described below

commit c8531d41a1985951538dae3a78d73ea69436d4f2
Author: Luca Cappelletti <[email protected]>
AuthorDate: Wed Oct 15 13:15:55 2025 +0200

    Added support for MATCH syntax and unified column option ForeignKey (#2062)
    
    Co-authored-by: Ifeanyi Ubah <[email protected]>
---
 src/ast/ddl.rs               | 64 +++++++++++++++++++++++---------------------
 src/ast/mod.rs               | 25 +++++++++++++++++
 src/ast/spans.rs             | 14 +---------
 src/ast/table_constraints.rs | 12 ++++++---
 src/keywords.rs              |  2 ++
 src/parser/mod.rs            | 52 +++++++++++++++++++++++++++--------
 tests/sqlparser_common.rs    | 24 ++++++++++++++---
 tests/sqlparser_postgres.rs  | 49 +++++++++++++++++++++++++++++++++
 8 files changed, 179 insertions(+), 63 deletions(-)

diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 3294a7a8..4a8678e4 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<ConstraintCharacteristics>,
     },
-    /// A referential integrity constraint (`[FOREIGN KEY REFERENCES
-    /// <foreign_table> (<referred_columns>)
+    /// A referential integrity constraint (`REFERENCES <foreign_table> 
(<referred_columns>)
+    /// [ MATCH { FULL | PARTIAL | SIMPLE } ]
     /// { [ON DELETE <referential_action>] [ON UPDATE <referential_action>] |
     ///   [ON UPDATE <referential_action>] [ON DELETE <referential_action>]
-    /// }
+    /// }         
     /// [<constraint_characteristics>]
     /// `).
-    ForeignKey {
-        foreign_table: ObjectName,
-        referred_columns: Vec<Ident>,
-        on_delete: Option<ReferentialAction>,
-        on_update: Option<ReferentialAction>,
-        characteristics: Option<ConstraintCharacteristics>,
-    },
+    ForeignKey(ForeignKeyConstraint),
     /// `CHECK (<expr>)`
     Check(Expr),
     /// Dialect-specific options, such as:
@@ -1643,6 +1638,12 @@ pub enum ColumnOption {
     Invisible,
 }
 
+impl From<ForeignKeyConstraint> 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 fef8943e..f4e2825d 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: 
<https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-REFERENCES>
+#[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 de3439cf..1e5a96bc 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 afcf6295..ddf0c125 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 <name> ] FOREIGN KEY 
(<columns>)
-/// REFERENCES <foreign_table> (<referred_columns>)
+/// REFERENCES <foreign_table> (<referred_columns>) [ MATCH { FULL | PARTIAL | 
SIMPLE } ]
 /// { [ON DELETE <referential_action>] [ON UPDATE <referential_action>] |
 ///   [ON UPDATE <referential_action>] [ON DELETE <referential_action>]
 /// }`).
@@ -206,6 +206,7 @@ pub struct ForeignKeyConstraint {
     pub referred_columns: Vec<Ident>,
     pub on_delete: Option<ReferentialAction>,
     pub on_update: Option<ReferentialAction>,
+    pub match_kind: Option<ConstraintReferenceMatchKind>,
     pub characteristics: Option<ConstraintCharacteristics>,
 }
 
@@ -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 3c985522..35bf616d 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 70f4d856..ef583dd3 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<ColumnDef, ParserError> {
-        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<ConstraintReferenceMatchKind, 
ParserError> {
+        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<Option<ConstraintCharacteristics>, 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 773937c5..52f38b10 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 e18bf662..9d08540a 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);
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to