Added support for MATCH syntax and unified column option ForeignKey (#2062)
Some checks are pending
license / Release Audit Tool (RAT) (push) Waiting to run
Rust / codestyle (push) Waiting to run
Rust / lint (push) Waiting to run
Rust / benchmark-lint (push) Waiting to run
Rust / compile (push) Waiting to run
Rust / docs (push) Waiting to run
Rust / compile-no-std (push) Waiting to run
Rust / test (beta) (push) Waiting to run
Rust / test (nightly) (push) Waiting to run
Rust / test (stable) (push) Waiting to run

Co-authored-by: Ifeanyi Ubah <ify1992@yahoo.com>
This commit is contained in:
Luca Cappelletti 2025-10-15 13:15:55 +02:00 committed by GitHub
parent 4490c8c55c
commit c8531d41a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 179 additions and 63 deletions

View file

@ -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(())

View file

@ -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

View file

@ -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(),

View file

@ -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}")?;
}

View file

@ -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,

View file

@ -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(),

View file

@ -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),

View file

@ -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);
}