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]