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