Skip to content

Commit 353b07d

Browse files
Added support for MATCH syntax and unified column option ForeignKey
1 parent 0fb3b6b commit 353b07d

File tree

8 files changed

+272
-54
lines changed

8 files changed

+272
-54
lines changed

src/ast/ddl.rs

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ use sqlparser_derive::{Visit, VisitMut};
3030

3131
use crate::ast::value::escape_single_quote_string;
3232
use crate::ast::{
33-
display_comma_separated, display_separated, table_constraints::TableConstraint, ArgMode,
34-
CommentDef, ConditionalStatements, CreateFunctionBody, CreateFunctionUsing,
33+
display_comma_separated, display_separated,
34+
table_constraints::{ForeignKeyConstraint, TableConstraint},
35+
ArgMode, CommentDef, ConditionalStatements, CreateFunctionBody, CreateFunctionUsing,
3536
CreateTableLikeKind, CreateTableOptions, DataType, Expr, FileFormat, FunctionBehavior,
3637
FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, HiveDistributionStyle,
3738
HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InitializeKind, MySQLColumnPosition,
@@ -1558,20 +1559,14 @@ pub enum ColumnOption {
15581559
is_primary: bool,
15591560
characteristics: Option<ConstraintCharacteristics>,
15601561
},
1561-
/// A referential integrity constraint (`[FOREIGN KEY REFERENCES
1562-
/// <foreign_table> (<referred_columns>)
1562+
/// A referential integrity constraint (`REFERENCES <foreign_table> (<referred_columns>)
1563+
/// [ MATCH { FULL | PARTIAL | SIMPLE } ]
15631564
/// { [ON DELETE <referential_action>] [ON UPDATE <referential_action>] |
15641565
/// [ON UPDATE <referential_action>] [ON DELETE <referential_action>]
1565-
/// }
1566+
/// }
15661567
/// [<constraint_characteristics>]
15671568
/// `).
1568-
ForeignKey {
1569-
foreign_table: ObjectName,
1570-
referred_columns: Vec<Ident>,
1571-
on_delete: Option<ReferentialAction>,
1572-
on_update: Option<ReferentialAction>,
1573-
characteristics: Option<ConstraintCharacteristics>,
1574-
},
1569+
ForeignKey(ForeignKeyConstraint),
15751570
/// `CHECK (<expr>)`
15761571
Check(Expr),
15771572
/// Dialect-specific options, such as:
@@ -1642,6 +1637,12 @@ pub enum ColumnOption {
16421637
Invisible,
16431638
}
16441639

1640+
impl From<ForeignKeyConstraint> for ColumnOption {
1641+
fn from(fk: ForeignKeyConstraint) -> Self {
1642+
ColumnOption::ForeignKey(fk)
1643+
}
1644+
}
1645+
16451646
impl fmt::Display for ColumnOption {
16461647
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
16471648
use ColumnOption::*;
@@ -1668,24 +1669,25 @@ impl fmt::Display for ColumnOption {
16681669
}
16691670
Ok(())
16701671
}
1671-
ForeignKey {
1672-
foreign_table,
1673-
referred_columns,
1674-
on_delete,
1675-
on_update,
1676-
characteristics,
1677-
} => {
1678-
write!(f, "REFERENCES {foreign_table}")?;
1679-
if !referred_columns.is_empty() {
1680-
write!(f, " ({})", display_comma_separated(referred_columns))?;
1672+
ForeignKey(constraint) => {
1673+
write!(f, "REFERENCES {}", constraint.foreign_table)?;
1674+
if !constraint.referred_columns.is_empty() {
1675+
write!(
1676+
f,
1677+
" ({})",
1678+
display_comma_separated(&constraint.referred_columns)
1679+
)?;
16811680
}
1682-
if let Some(action) = on_delete {
1681+
if let Some(match_kind) = &constraint.match_kind {
1682+
write!(f, " {match_kind}")?;
1683+
}
1684+
if let Some(action) = &constraint.on_delete {
16831685
write!(f, " ON DELETE {action}")?;
16841686
}
1685-
if let Some(action) = on_update {
1687+
if let Some(action) = &constraint.on_update {
16861688
write!(f, " ON UPDATE {action}")?;
16871689
}
1688-
if let Some(characteristics) = characteristics {
1690+
if let Some(characteristics) = &constraint.characteristics {
16891691
write!(f, " {characteristics}")?;
16901692
}
16911693
Ok(())

src/ast/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,40 @@ pub enum CastKind {
655655
DoubleColon,
656656
}
657657

658+
/// The MATCH option for foreign key constraints.
659+
///
660+
/// Specifies how to match composite foreign keys against the referenced table.
661+
/// A value inserted into the referencing column(s) is matched against the values
662+
/// of the referenced table and referenced columns using the given match type.
663+
///
664+
/// See: <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-REFERENCES>
665+
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
666+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
667+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
668+
pub enum MatchKind {
669+
/// `MATCH FULL` - Will not allow one column of a multicolumn foreign key to be null
670+
/// unless all foreign key columns are null; if they are all null, the row is not
671+
/// required to have a match in the referenced table.
672+
Full,
673+
/// `MATCH PARTIAL` - Not yet implemented by most databases (part of SQL standard).
674+
/// Would allow partial matches in multicolumn foreign keys.
675+
Partial,
676+
/// `MATCH SIMPLE` - The default behavior. Allows any of the foreign key columns
677+
/// to be null; if any of them are null, the row is not required to have a match
678+
/// in the referenced table.
679+
Simple,
680+
}
681+
682+
impl fmt::Display for MatchKind {
683+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
684+
match self {
685+
MatchKind::Full => write!(f, "MATCH FULL"),
686+
MatchKind::Partial => write!(f, "MATCH PARTIAL"),
687+
MatchKind::Simple => write!(f, "MATCH SIMPLE"),
688+
}
689+
}
690+
}
691+
658692
/// `EXTRACT` syntax variants.
659693
///
660694
/// In Snowflake dialect, the `EXTRACT` expression can support either the `from` syntax

src/ast/spans.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -822,19 +822,7 @@ impl Spanned for ColumnOption {
822822
ColumnOption::Ephemeral(expr) => expr.as_ref().map_or(Span::empty(), |e| e.span()),
823823
ColumnOption::Alias(expr) => expr.span(),
824824
ColumnOption::Unique { .. } => Span::empty(),
825-
ColumnOption::ForeignKey {
826-
foreign_table,
827-
referred_columns,
828-
on_delete,
829-
on_update,
830-
characteristics,
831-
} => union_spans(
832-
core::iter::once(foreign_table.span())
833-
.chain(referred_columns.iter().map(|i| i.span))
834-
.chain(on_delete.iter().map(|i| i.span()))
835-
.chain(on_update.iter().map(|i| i.span()))
836-
.chain(characteristics.iter().map(|i| i.span())),
837-
),
825+
ColumnOption::ForeignKey(constraint) => constraint.span(),
838826
ColumnOption::Check(expr) => expr.span(),
839827
ColumnOption::DialectSpecific(_) => Span::empty(),
840828
ColumnOption::CharacterSet(object_name) => object_name.span(),

src/ast/table_constraints.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
2020
use crate::ast::{
2121
display_comma_separated, display_separated, ConstraintCharacteristics, Expr, Ident,
22-
IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, ObjectName,
23-
ReferentialAction,
22+
IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, MatchKind, NullsDistinctOption,
23+
ObjectName, ReferentialAction,
2424
};
2525
use crate::tokenizer::Span;
2626
use core::fmt;
@@ -189,7 +189,7 @@ impl crate::ast::Spanned for CheckConstraint {
189189
}
190190

191191
/// A referential integrity constraint (`[ CONSTRAINT <name> ] FOREIGN KEY (<columns>)
192-
/// REFERENCES <foreign_table> (<referred_columns>)
192+
/// REFERENCES <foreign_table> (<referred_columns>) [ MATCH { FULL | PARTIAL | SIMPLE } ]
193193
/// { [ON DELETE <referential_action>] [ON UPDATE <referential_action>] |
194194
/// [ON UPDATE <referential_action>] [ON DELETE <referential_action>]
195195
/// }`).
@@ -206,6 +206,7 @@ pub struct ForeignKeyConstraint {
206206
pub referred_columns: Vec<Ident>,
207207
pub on_delete: Option<ReferentialAction>,
208208
pub on_update: Option<ReferentialAction>,
209+
pub match_kind: Option<MatchKind>,
209210
pub characteristics: Option<ConstraintCharacteristics>,
210211
}
211212

@@ -223,6 +224,9 @@ impl fmt::Display for ForeignKeyConstraint {
223224
if !self.referred_columns.is_empty() {
224225
write!(f, "({})", display_comma_separated(&self.referred_columns))?;
225226
}
227+
if let Some(match_kind) = &self.match_kind {
228+
write!(f, " {match_kind}")?;
229+
}
226230
if let Some(action) = &self.on_delete {
227231
write!(f, " ON DELETE {action}")?;
228232
}

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ define_keywords!(
713713
PARAMETER,
714714
PARQUET,
715715
PART,
716+
PARTIAL,
716717
PARTITION,
717718
PARTITIONED,
718719
PARTITIONS,
@@ -885,6 +886,7 @@ define_keywords!(
885886
SHOW,
886887
SIGNED,
887888
SIMILAR,
889+
SIMPLE,
888890
SKIP,
889891
SLOW,
890892
SMALLINT,

src/parser/mod.rs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8044,10 +8044,15 @@ impl<'a> Parser<'a> {
80448044
// PostgreSQL allows omitting the column list and
80458045
// uses the primary key column of the foreign table by default
80468046
let referred_columns = self.parse_parenthesized_column_list(Optional, false)?;
8047+
let mut match_kind = None;
80478048
let mut on_delete = None;
80488049
let mut on_update = None;
80498050
loop {
8050-
if on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) {
8051+
if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) {
8052+
match_kind = Some(self.parse_match_kind()?);
8053+
} else if on_delete.is_none()
8054+
&& self.parse_keywords(&[Keyword::ON, Keyword::DELETE])
8055+
{
80518056
on_delete = Some(self.parse_referential_action()?);
80528057
} else if on_update.is_none()
80538058
&& self.parse_keywords(&[Keyword::ON, Keyword::UPDATE])
@@ -8059,13 +8064,20 @@ impl<'a> Parser<'a> {
80598064
}
80608065
let characteristics = self.parse_constraint_characteristics()?;
80618066

8062-
Ok(Some(ColumnOption::ForeignKey {
8063-
foreign_table,
8064-
referred_columns,
8065-
on_delete,
8066-
on_update,
8067-
characteristics,
8068-
}))
8067+
Ok(Some(
8068+
ForeignKeyConstraint {
8069+
name: None, // Column-level constraints don't have names
8070+
index_name: None, // Not applicable for column-level constraints
8071+
columns: vec![], // Column is implicit for column-level constraints
8072+
foreign_table,
8073+
referred_columns,
8074+
on_delete,
8075+
on_update,
8076+
match_kind,
8077+
characteristics,
8078+
}
8079+
.into(),
8080+
))
80698081
} else if self.parse_keyword(Keyword::CHECK) {
80708082
self.expect_token(&Token::LParen)?;
80718083
// since `CHECK` requires parentheses, we can parse the inner expression in ParserState::Normal
@@ -8339,6 +8351,18 @@ impl<'a> Parser<'a> {
83398351
}
83408352
}
83418353

8354+
pub fn parse_match_kind(&mut self) -> Result<MatchKind, ParserError> {
8355+
if self.parse_keyword(Keyword::FULL) {
8356+
Ok(MatchKind::Full)
8357+
} else if self.parse_keyword(Keyword::PARTIAL) {
8358+
Ok(MatchKind::Partial)
8359+
} else if self.parse_keyword(Keyword::SIMPLE) {
8360+
Ok(MatchKind::Simple)
8361+
} else {
8362+
self.expected("one of FULL, PARTIAL or SIMPLE", self.peek_token())
8363+
}
8364+
}
8365+
83428366
pub fn parse_constraint_characteristics(
83438367
&mut self,
83448368
) -> Result<Option<ConstraintCharacteristics>, ParserError> {
@@ -8449,10 +8473,15 @@ impl<'a> Parser<'a> {
84498473
self.expect_keyword_is(Keyword::REFERENCES)?;
84508474
let foreign_table = self.parse_object_name(false)?;
84518475
let referred_columns = self.parse_parenthesized_column_list(Optional, false)?;
8476+
let mut match_kind = None;
84528477
let mut on_delete = None;
84538478
let mut on_update = None;
84548479
loop {
8455-
if on_delete.is_none() && self.parse_keywords(&[Keyword::ON, Keyword::DELETE]) {
8480+
if match_kind.is_none() && self.parse_keyword(Keyword::MATCH) {
8481+
match_kind = Some(self.parse_match_kind()?);
8482+
} else if on_delete.is_none()
8483+
&& self.parse_keywords(&[Keyword::ON, Keyword::DELETE])
8484+
{
84568485
on_delete = Some(self.parse_referential_action()?);
84578486
} else if on_update.is_none()
84588487
&& self.parse_keywords(&[Keyword::ON, Keyword::UPDATE])
@@ -8474,6 +8503,7 @@ impl<'a> Parser<'a> {
84748503
referred_columns,
84758504
on_delete,
84768505
on_update,
8506+
match_kind,
84778507
characteristics,
84788508
}
84798509
.into(),

tests/sqlparser_common.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3790,27 +3790,35 @@ fn parse_create_table() {
37903790
data_type: DataType::Int(None),
37913791
options: vec![ColumnOptionDef {
37923792
name: None,
3793-
option: ColumnOption::ForeignKey {
3793+
option: ColumnOption::ForeignKey(ForeignKeyConstraint {
3794+
name: None,
3795+
index_name: None,
3796+
columns: vec![],
37943797
foreign_table: ObjectName::from(vec!["othertable".into()]),
37953798
referred_columns: vec!["a".into(), "b".into()],
37963799
on_delete: None,
37973800
on_update: None,
3801+
match_kind: None,
37983802
characteristics: None,
3799-
},
3803+
}),
38003804
}],
38013805
},
38023806
ColumnDef {
38033807
name: "ref2".into(),
38043808
data_type: DataType::Int(None),
38053809
options: vec![ColumnOptionDef {
38063810
name: None,
3807-
option: ColumnOption::ForeignKey {
3811+
option: ColumnOption::ForeignKey(ForeignKeyConstraint {
3812+
name: None,
3813+
index_name: None,
3814+
columns: vec![],
38083815
foreign_table: ObjectName::from(vec!["othertable2".into()]),
38093816
referred_columns: vec![],
38103817
on_delete: Some(ReferentialAction::Cascade),
38113818
on_update: Some(ReferentialAction::NoAction),
3819+
match_kind: None,
38123820
characteristics: None,
3813-
},
3821+
}),
38143822
},],
38153823
},
38163824
]
@@ -3826,6 +3834,7 @@ fn parse_create_table() {
38263834
referred_columns: vec!["lat".into()],
38273835
on_delete: Some(ReferentialAction::Restrict),
38283836
on_update: None,
3837+
match_kind: None,
38293838
characteristics: None,
38303839
}
38313840
.into(),
@@ -3837,6 +3846,7 @@ fn parse_create_table() {
38373846
referred_columns: vec!["lat".into()],
38383847
on_delete: Some(ReferentialAction::NoAction),
38393848
on_update: Some(ReferentialAction::Restrict),
3849+
match_kind: None,
38403850
characteristics: None,
38413851
}
38423852
.into(),
@@ -3848,6 +3858,7 @@ fn parse_create_table() {
38483858
referred_columns: vec!["lat".into()],
38493859
on_delete: Some(ReferentialAction::Cascade),
38503860
on_update: Some(ReferentialAction::SetDefault),
3861+
match_kind: None,
38513862
characteristics: None,
38523863
}
38533864
.into(),
@@ -3859,6 +3870,7 @@ fn parse_create_table() {
38593870
referred_columns: vec!["longitude".into()],
38603871
on_delete: None,
38613872
on_update: Some(ReferentialAction::SetNull),
3873+
match_kind: None,
38623874
characteristics: None,
38633875
}
38643876
.into(),
@@ -3957,6 +3969,7 @@ fn parse_create_table_with_constraint_characteristics() {
39573969
referred_columns: vec!["lat".into()],
39583970
on_delete: Some(ReferentialAction::Restrict),
39593971
on_update: None,
3972+
match_kind: None,
39603973
characteristics: Some(ConstraintCharacteristics {
39613974
deferrable: Some(true),
39623975
initially: Some(DeferrableInitial::Deferred),
@@ -3972,6 +3985,7 @@ fn parse_create_table_with_constraint_characteristics() {
39723985
referred_columns: vec!["lat".into()],
39733986
on_delete: Some(ReferentialAction::NoAction),
39743987
on_update: Some(ReferentialAction::Restrict),
3988+
match_kind: None,
39753989
characteristics: Some(ConstraintCharacteristics {
39763990
deferrable: Some(true),
39773991
initially: Some(DeferrableInitial::Immediate),
@@ -3987,6 +4001,7 @@ fn parse_create_table_with_constraint_characteristics() {
39874001
referred_columns: vec!["lat".into()],
39884002
on_delete: Some(ReferentialAction::Cascade),
39894003
on_update: Some(ReferentialAction::SetDefault),
4004+
match_kind: None,
39904005
characteristics: Some(ConstraintCharacteristics {
39914006
deferrable: Some(false),
39924007
initially: Some(DeferrableInitial::Deferred),
@@ -4002,6 +4017,7 @@ fn parse_create_table_with_constraint_characteristics() {
40024017
referred_columns: vec!["longitude".into()],
40034018
on_delete: None,
40044019
on_update: Some(ReferentialAction::SetNull),
4020+
match_kind: None,
40054021
characteristics: Some(ConstraintCharacteristics {
40064022
deferrable: Some(false),
40074023
initially: Some(DeferrableInitial::Immediate),

0 commit comments

Comments
 (0)