From 353b07d20c80e995011bb8937a311d6ee13f36e2 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 8 Oct 2025 16:01:34 +0200 Subject: [PATCH 01/17] Added support for MATCH syntax and unified column option ForeignKey --- src/ast/ddl.rs | 52 +++++++------ src/ast/mod.rs | 34 +++++++++ src/ast/spans.rs | 14 +--- src/ast/table_constraints.rs | 10 ++- src/keywords.rs | 2 + src/parser/mod.rs | 48 +++++++++--- tests/sqlparser_common.rs | 24 +++++- tests/sqlparser_postgres.rs | 142 +++++++++++++++++++++++++++++++++++ 8 files changed, 272 insertions(+), 54 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c6ab7ad10..4fb9d8054 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,8 +30,9 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, table_constraints::TableConstraint, ArgMode, - CommentDef, ConditionalStatements, CreateFunctionBody, CreateFunctionUsing, + display_comma_separated, display_separated, + table_constraints::{ForeignKeyConstraint, TableConstraint}, + ArgMode, CommentDef, ConditionalStatements, CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, DataType, Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, InitializeKind, MySQLColumnPosition, @@ -1558,20 +1559,14 @@ pub enum ColumnOption { is_primary: bool, characteristics: Option, }, - /// A referential integrity constraint (`[FOREIGN KEY REFERENCES - /// () + /// A referential integrity constraint (`REFERENCES () + /// [ MATCH { FULL | PARTIAL | SIMPLE } ] /// { [ON DELETE ] [ON UPDATE ] | /// [ON UPDATE ] [ON DELETE ] - /// } + /// } /// [] /// `). - ForeignKey { - foreign_table: ObjectName, - referred_columns: Vec, - on_delete: Option, - on_update: Option, - characteristics: Option, - }, + ForeignKey(ForeignKeyConstraint), /// `CHECK ()` Check(Expr), /// Dialect-specific options, such as: @@ -1642,6 +1637,12 @@ pub enum ColumnOption { Invisible, } +impl From 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::*; @@ -1668,24 +1669,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 52968d7db..22bf12a90 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -655,6 +655,40 @@ pub enum CastKind { DoubleColon, } +/// The MATCH option for foreign key constraints. +/// +/// Specifies how to match composite foreign keys against the referenced table. +/// A value inserted into the referencing column(s) is matched against the values +/// of the referenced table and referenced columns using the given match type. +/// +/// See: +#[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 MatchKind { + /// `MATCH FULL` - Will not allow one column of a multicolumn foreign key to be null + /// unless all foreign key columns are null; if they are all null, the row is not + /// required to have a match in the referenced table. + Full, + /// `MATCH PARTIAL` - Not yet implemented by most databases (part of SQL standard). + /// Would allow partial matches in multicolumn foreign keys. + Partial, + /// `MATCH SIMPLE` - The default behavior. Allows any of the foreign key columns + /// to be null; if any of them are null, the row is not required to have a match + /// in the referenced table. + Simple, +} + +impl fmt::Display for MatchKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MatchKind::Full => write!(f, "MATCH FULL"), + MatchKind::Partial => write!(f, "MATCH PARTIAL"), + MatchKind::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 0a303fcfd..dab5b55a2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -822,19 +822,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 afcf62959..5daa022ef 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -19,8 +19,8 @@ use crate::ast::{ display_comma_separated, display_separated, ConstraintCharacteristics, Expr, Ident, - IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, ObjectName, - ReferentialAction, + IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, MatchKind, 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 ] FOREIGN KEY () -/// REFERENCES () +/// REFERENCES () [ MATCH { FULL | PARTIAL | SIMPLE } ] /// { [ON DELETE ] [ON UPDATE ] | /// [ON UPDATE ] [ON DELETE ] /// }`). @@ -206,6 +206,7 @@ pub struct ForeignKeyConstraint { pub referred_columns: Vec, pub on_delete: Option, pub on_update: Option, + pub match_kind: Option, pub characteristics: Option, } @@ -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 3c9855222..35bf616d9 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 d1e9b1e78..18b5c115e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8044,10 +8044,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]) @@ -8059,13 +8064,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![], // Column is implicit 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 @@ -8339,6 +8351,18 @@ impl<'a> Parser<'a> { } } + pub fn parse_match_kind(&mut self) -> Result { + if self.parse_keyword(Keyword::FULL) { + Ok(MatchKind::Full) + } else if self.parse_keyword(Keyword::PARTIAL) { + Ok(MatchKind::Partial) + } else if self.parse_keyword(Keyword::SIMPLE) { + Ok(MatchKind::Simple) + } else { + self.expected("one of FULL, PARTIAL or SIMPLE", self.peek_token()) + } + } + pub fn parse_constraint_characteristics( &mut self, ) -> Result, ParserError> { @@ -8449,10 +8473,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]) @@ -8474,6 +8503,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 a1fd48d3e..39a9619a7 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 45ae32dac..5b9417d06 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6481,6 +6481,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(), @@ -6646,3 +6647,144 @@ fn parse_alter_schema() { _ => unreachable!(), } } + +#[test] +/// Test to verify whether `MATCH FULL` syntax for foreign keys is parsed correctly. +fn parse_foreign_key_match_full() { + let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH FULL, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH FULL)"; + let statement = pg_and_generic().verified_stmt(sql); + match statement { + Statement::CreateTable(CreateTable { + columns, + constraints, + .. + }) => { + // Check column-level foreign key with MATCH FULL + assert_eq!(columns[0].name.value, "order_id"); + match &columns[0].options[1].option { + ColumnOption::ForeignKey(constraint) => { + assert_eq!(constraint.foreign_table.to_string(), "another_table"); + assert_eq!(constraint.match_kind, Some(MatchKind::Full)); + } + _ => panic!("Expected ColumnOption::ForeignKey"), + } + + // Check table-level foreign key constraint with MATCH FULL + match &constraints[0] { + TableConstraint::ForeignKey(constraint) => { + assert_eq!(constraint.foreign_table.to_string(), "customers"); + assert_eq!(constraint.match_kind, Some(MatchKind::Full)); + } + _ => panic!("Expected TableConstraint::ForeignKey"), + } + } + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } +} + +#[test] +/// Test to verify whether `MATCH SIMPLE` syntax for foreign keys is parsed correctly. +fn parse_foreign_key_match_simple() { + let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH SIMPLE, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH SIMPLE)"; + let statement = pg_and_generic().verified_stmt(sql); + match statement { + Statement::CreateTable(CreateTable { + columns, + constraints, + .. + }) => { + // Check column-level foreign key with MATCH SIMPLE + assert_eq!(columns[0].name.value, "order_id"); + match &columns[0].options[1].option { + ColumnOption::ForeignKey(constraint) => { + assert_eq!(constraint.foreign_table.to_string(), "another_table"); + assert_eq!(constraint.match_kind, Some(MatchKind::Simple)); + } + _ => panic!("Expected ColumnOption::ForeignKey"), + } + + // Check table-level foreign key constraint with MATCH SIMPLE + match &constraints[0] { + TableConstraint::ForeignKey(constraint) => { + assert_eq!(constraint.foreign_table.to_string(), "customers"); + assert_eq!(constraint.match_kind, Some(MatchKind::Simple)); + } + _ => panic!("Expected TableConstraint::ForeignKey"), + } + } + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } +} + +#[test] +/// Test to verify whether `MATCH PARTIAL` syntax for foreign keys is parsed correctly. +fn parse_foreign_key_match_partial() { + let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH PARTIAL, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH PARTIAL)"; + let statement = pg_and_generic().verified_stmt(sql); + match statement { + Statement::CreateTable(CreateTable { + columns, + constraints, + .. + }) => { + // Check column-level foreign key with MATCH PARTIAL + assert_eq!(columns[0].name.value, "order_id"); + match &columns[0].options[1].option { + ColumnOption::ForeignKey(constraint) => { + assert_eq!(constraint.foreign_table.to_string(), "another_table"); + assert_eq!(constraint.match_kind, Some(MatchKind::Partial)); + } + _ => panic!("Expected ColumnOption::ForeignKey"), + } + + // Check table-level foreign key constraint with MATCH PARTIAL + match &constraints[0] { + TableConstraint::ForeignKey(constraint) => { + assert_eq!(constraint.foreign_table.to_string(), "customers"); + assert_eq!(constraint.match_kind, Some(MatchKind::Partial)); + } + _ => panic!("Expected TableConstraint::ForeignKey"), + } + } + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } +} + +#[test] +/// Test to verify foreign key MATCH syntax combined with ON DELETE/ON UPDATE actions +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)"; + + let statement = pg_and_generic().verified_stmt(sql); + match statement { + Statement::CreateTable(CreateTable { + columns, + constraints, + .. + }) => { + // Check column-level foreign key with MATCH FULL and actions + match &columns[0].options[0].option { + ColumnOption::ForeignKey(constraint) => { + assert_eq!(constraint.foreign_table.to_string(), "another_table"); + assert_eq!(constraint.match_kind, Some(MatchKind::Full)); + assert_eq!(constraint.on_delete, Some(ReferentialAction::Cascade)); + assert_eq!(constraint.on_update, Some(ReferentialAction::Restrict)); + } + _ => panic!("Expected ColumnOption::ForeignKey"), + } + + // Check table-level foreign key constraint with MATCH SIMPLE and actions + match &constraints[0] { + TableConstraint::ForeignKey(constraint) => { + assert_eq!(constraint.name.as_ref().unwrap().value, "fk_customer"); + assert_eq!(constraint.foreign_table.to_string(), "customers"); + assert_eq!(constraint.match_kind, Some(MatchKind::Simple)); + assert_eq!(constraint.on_delete, Some(ReferentialAction::SetNull)); + assert_eq!(constraint.on_update, Some(ReferentialAction::Cascade)); + } + _ => panic!("Expected TableConstraint::ForeignKey"), + } + } + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), + } +} From ebe6a9e011264a9b31e2e7bf32e8dbaa27f5c3f7 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 8 Oct 2025 16:46:17 +0200 Subject: [PATCH 02/17] Now `ForeignKeyConstraint` include the ident of the column --- src/parser/mod.rs | 35 ++++++++++++++++++++++------------- tests/sqlparser_common.rs | 4 ++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 18b5c115e..dff0c6052 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7919,7 +7919,7 @@ impl<'a> Parser<'a> { } pub fn parse_column_def(&mut self) -> Result { - let name = self.parse_identifier()?; + let col_name = self.parse_identifier()?; let data_type = if self.is_column_type_sqlite_unspecified() { DataType::Unspecified } else { @@ -7929,7 +7929,7 @@ impl<'a> Parser<'a> { loop { if self.parse_keyword(Keyword::CONSTRAINT) { let name = Some(self.parse_identifier()?); - if let Some(option) = self.parse_optional_column_option()? { + if let Some(option) = self.parse_optional_column_option(&col_name)? { options.push(ColumnOptionDef { name, option }); } else { return self.expected( @@ -7937,14 +7937,14 @@ impl<'a> Parser<'a> { self.peek_token(), ); } - } else if let Some(option) = self.parse_optional_column_option()? { + } else if let Some(option) = self.parse_optional_column_option(&col_name)? { options.push(ColumnOptionDef { name: None, option }); } else { break; }; } Ok(ColumnDef { - name, + name: col_name, data_type, options, }) @@ -7973,7 +7973,10 @@ impl<'a> Parser<'a> { } } - pub fn parse_optional_column_option(&mut self) -> Result, ParserError> { + pub fn parse_optional_column_option( + &mut self, + column_ident: &Ident, + ) -> Result, ParserError> { if let Some(option) = self.dialect.parse_column_option(self)? { return option; } @@ -7981,12 +7984,15 @@ impl<'a> Parser<'a> { self.with_state( ColumnDefinition, |parser| -> Result, ParserError> { - parser.parse_optional_column_option_inner() + parser.parse_optional_column_option_inner(column_ident) }, ) } - fn parse_optional_column_option_inner(&mut self) -> Result, ParserError> { + fn parse_optional_column_option_inner( + &mut self, + column_ident: &Ident, + ) -> Result, ParserError> { if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) { Ok(Some(ColumnOption::CharacterSet( self.parse_object_name(false)?, @@ -8068,7 +8074,7 @@ impl<'a> Parser<'a> { ForeignKeyConstraint { name: None, // Column-level constraints don't have names index_name: None, // Not applicable for column-level constraints - columns: vec![], // Column is implicit for column-level constraints + columns: vec![column_ident.clone()], foreign_table, referred_columns, on_delete, @@ -9076,7 +9082,7 @@ impl<'a> Parser<'a> { let new_name = self.parse_identifier()?; let data_type = self.parse_data_type()?; let mut options = vec![]; - while let Some(option) = self.parse_optional_column_option()? { + while let Some(option) = self.parse_optional_column_option(&new_name)? { options.push(option); } @@ -9094,7 +9100,7 @@ impl<'a> Parser<'a> { let col_name = self.parse_identifier()?; let data_type = self.parse_data_type()?; let mut options = vec![]; - while let Some(option) = self.parse_optional_column_option()? { + while let Some(option) = self.parse_optional_column_option(&col_name)? { options.push(option); } @@ -11355,7 +11361,7 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier()?; - let options = self.parse_view_column_options()?; + let options = self.parse_view_column_options(&name)?; let data_type = if dialect_of!(self is ClickHouseDialect) { Some(self.parse_data_type()?) } else { @@ -11368,10 +11374,13 @@ impl<'a> Parser<'a> { }) } - fn parse_view_column_options(&mut self) -> Result, ParserError> { + fn parse_view_column_options( + &mut self, + column_ident: &Ident, + ) -> Result, ParserError> { let mut options = Vec::new(); loop { - let option = self.parse_optional_column_option()?; + let option = self.parse_optional_column_option(column_ident)?; if let Some(option) = option { options.push(option); } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 39a9619a7..65304519e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3793,7 +3793,7 @@ fn parse_create_table() { option: ColumnOption::ForeignKey(ForeignKeyConstraint { name: None, index_name: None, - columns: vec![], + columns: vec!["ref".into()], foreign_table: ObjectName::from(vec!["othertable".into()]), referred_columns: vec!["a".into(), "b".into()], on_delete: None, @@ -3811,7 +3811,7 @@ fn parse_create_table() { option: ColumnOption::ForeignKey(ForeignKeyConstraint { name: None, index_name: None, - columns: vec![], + columns: vec!["ref2".into()], foreign_table: ObjectName::from(vec!["othertable2".into()]), referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), From 04e03f7febb5198cb172cba2520fa41a5ef0ec90 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:39:50 +0200 Subject: [PATCH 03/17] Update src/ast/mod.rs Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 22bf12a90..7d3bccc7e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -665,7 +665,7 @@ pub enum CastKind { #[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 MatchKind { +pub enum ConstraintReferenceMatchKind { /// `MATCH FULL` - Will not allow one column of a multicolumn foreign key to be null /// unless all foreign key columns are null; if they are all null, the row is not /// required to have a match in the referenced table. From 0601237d666de32d35b7972fddb584cc140b56dd Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:40:12 +0200 Subject: [PATCH 04/17] Update src/ast/mod.rs Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7d3bccc7e..5004b26af 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -655,11 +655,7 @@ pub enum CastKind { DoubleColon, } -/// The MATCH option for foreign key constraints. -/// -/// Specifies how to match composite foreign keys against the referenced table. -/// A value inserted into the referencing column(s) is matched against the values -/// of the referenced table and referenced columns using the given match type. +/// `MATCH` type for constraint references /// /// See: #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] From 0e75acbd158c083276895a8f7868908dddef249c Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:40:21 +0200 Subject: [PATCH 05/17] Update src/ast/mod.rs Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5004b26af..dc7536faa 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -662,16 +662,11 @@ pub enum CastKind { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ConstraintReferenceMatchKind { - /// `MATCH FULL` - Will not allow one column of a multicolumn foreign key to be null - /// unless all foreign key columns are null; if they are all null, the row is not - /// required to have a match in the referenced table. + /// `MATCH FULL` Full, - /// `MATCH PARTIAL` - Not yet implemented by most databases (part of SQL standard). - /// Would allow partial matches in multicolumn foreign keys. + /// `MATCH PARTIAL` Partial, - /// `MATCH SIMPLE` - The default behavior. Allows any of the foreign key columns - /// to be null; if any of them are null, the row is not required to have a match - /// in the referenced table. + /// `MATCH SIMPLE` Simple, } From 8098137df159f7e70452509cd53f84534a4ef49e Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:42:50 +0200 Subject: [PATCH 06/17] Update tests/sqlparser_postgres.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_postgres.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5b9417d06..e52d882da 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6649,7 +6649,6 @@ fn parse_alter_schema() { } #[test] -/// Test to verify whether `MATCH FULL` syntax for foreign keys is parsed correctly. fn parse_foreign_key_match_full() { let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH FULL, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH FULL)"; let statement = pg_and_generic().verified_stmt(sql); From 2664ac9dd1ae7a8d574663cd5d91a6a836cf4f87 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:43:05 +0200 Subject: [PATCH 07/17] Update tests/sqlparser_postgres.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_postgres.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e52d882da..8055eaed3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6716,7 +6716,6 @@ fn parse_foreign_key_match_simple() { } #[test] -/// Test to verify whether `MATCH PARTIAL` syntax for foreign keys is parsed correctly. fn parse_foreign_key_match_partial() { let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH PARTIAL, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH PARTIAL)"; let statement = pg_and_generic().verified_stmt(sql); From b97d5f767777859f87265df7b0e367f01e514c88 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:43:13 +0200 Subject: [PATCH 08/17] Update tests/sqlparser_postgres.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_postgres.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 8055eaed3..e9f810d23 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6725,7 +6725,6 @@ fn parse_foreign_key_match_partial() { constraints, .. }) => { - // Check column-level foreign key with MATCH PARTIAL assert_eq!(columns[0].name.value, "order_id"); match &columns[0].options[1].option { ColumnOption::ForeignKey(constraint) => { From d8d78b62fc96412b8ce6f2b5cfea2af444110334 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:43:21 +0200 Subject: [PATCH 09/17] Update tests/sqlparser_postgres.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_postgres.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e9f810d23..894b64999 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6734,7 +6734,6 @@ fn parse_foreign_key_match_partial() { _ => panic!("Expected ColumnOption::ForeignKey"), } - // Check table-level foreign key constraint with MATCH PARTIAL match &constraints[0] { TableConstraint::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "customers"); From e317c9b2ac25e4db88a319164ee9be8b2be15fcd Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:43:30 +0200 Subject: [PATCH 10/17] Update tests/sqlparser_postgres.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_postgres.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 894b64999..7880af221 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6682,7 +6682,6 @@ fn parse_foreign_key_match_full() { } #[test] -/// Test to verify whether `MATCH SIMPLE` syntax for foreign keys is parsed correctly. fn parse_foreign_key_match_simple() { let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH SIMPLE, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH SIMPLE)"; let statement = pg_and_generic().verified_stmt(sql); From 330e62643a32af4eb13cb179da9286661c19076e Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:43:38 +0200 Subject: [PATCH 11/17] Update tests/sqlparser_postgres.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_postgres.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7880af221..480e78251 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6746,7 +6746,6 @@ fn parse_foreign_key_match_partial() { } #[test] -/// Test to verify foreign key MATCH syntax combined with ON DELETE/ON UPDATE actions 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)"; From 402ef42c679a695a1fb3ae585ba94e6834094124 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Fri, 10 Oct 2025 13:47:34 +0200 Subject: [PATCH 12/17] Update tests/sqlparser_postgres.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_postgres.rs | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 480e78251..7e45f6cab 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6749,36 +6749,5 @@ fn parse_foreign_key_match_partial() { 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)"; - let statement = pg_and_generic().verified_stmt(sql); - match statement { - Statement::CreateTable(CreateTable { - columns, - constraints, - .. - }) => { - // Check column-level foreign key with MATCH FULL and actions - match &columns[0].options[0].option { - ColumnOption::ForeignKey(constraint) => { - assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!(constraint.match_kind, Some(MatchKind::Full)); - assert_eq!(constraint.on_delete, Some(ReferentialAction::Cascade)); - assert_eq!(constraint.on_update, Some(ReferentialAction::Restrict)); - } - _ => panic!("Expected ColumnOption::ForeignKey"), - } - - // Check table-level foreign key constraint with MATCH SIMPLE and actions - match &constraints[0] { - TableConstraint::ForeignKey(constraint) => { - assert_eq!(constraint.name.as_ref().unwrap().value, "fk_customer"); - assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!(constraint.match_kind, Some(MatchKind::Simple)); - assert_eq!(constraint.on_delete, Some(ReferentialAction::SetNull)); - assert_eq!(constraint.on_update, Some(ReferentialAction::Cascade)); - } - _ => panic!("Expected TableConstraint::ForeignKey"), - } - } - _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), - } + pg_and_generic().verified_stmt(sql); } From 67be33dc1605a9ac7f9483ac0db5440ace0b3f99 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 Oct 2025 13:48:18 +0200 Subject: [PATCH 13/17] Finished renaming to ConstraintReferenceMatchKind --- src/ast/mod.rs | 8 ++++---- src/ast/table_constraints.rs | 4 ++-- src/parser/mod.rs | 8 ++++---- tests/sqlparser_postgres.rs | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dc7536faa..79de329d4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -670,12 +670,12 @@ pub enum ConstraintReferenceMatchKind { Simple, } -impl fmt::Display for MatchKind { +impl fmt::Display for ConstraintReferenceMatchKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - MatchKind::Full => write!(f, "MATCH FULL"), - MatchKind::Partial => write!(f, "MATCH PARTIAL"), - MatchKind::Simple => write!(f, "MATCH SIMPLE"), + Self::Full => write!(f, "MATCH FULL"), + Self::Partial => write!(f, "MATCH PARTIAL"), + Self::Simple => write!(f, "MATCH SIMPLE"), } } } diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index 5daa022ef..a22955d99 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -19,7 +19,7 @@ use crate::ast::{ display_comma_separated, display_separated, ConstraintCharacteristics, Expr, Ident, - IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, MatchKind, NullsDistinctOption, + IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, ConstraintReferenceMatchKind, NullsDistinctOption, ObjectName, ReferentialAction, }; use crate::tokenizer::Span; @@ -206,7 +206,7 @@ pub struct ForeignKeyConstraint { pub referred_columns: Vec, pub on_delete: Option, pub on_update: Option, - pub match_kind: Option, + pub match_kind: Option, pub characteristics: Option, } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dff0c6052..1bb1507aa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8357,13 +8357,13 @@ impl<'a> Parser<'a> { } } - pub fn parse_match_kind(&mut self) -> Result { + pub fn parse_match_kind(&mut self) -> Result { if self.parse_keyword(Keyword::FULL) { - Ok(MatchKind::Full) + Ok(ConstraintReferenceMatchKind::Full) } else if self.parse_keyword(Keyword::PARTIAL) { - Ok(MatchKind::Partial) + Ok(ConstraintReferenceMatchKind::Partial) } else if self.parse_keyword(Keyword::SIMPLE) { - Ok(MatchKind::Simple) + Ok(ConstraintReferenceMatchKind::Simple) } else { self.expected("one of FULL, PARTIAL or SIMPLE", self.peek_token()) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7e45f6cab..622c9a0c0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6663,7 +6663,7 @@ fn parse_foreign_key_match_full() { match &columns[0].options[1].option { ColumnOption::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!(constraint.match_kind, Some(MatchKind::Full)); + assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Full)); } _ => panic!("Expected ColumnOption::ForeignKey"), } @@ -6672,7 +6672,7 @@ fn parse_foreign_key_match_full() { match &constraints[0] { TableConstraint::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!(constraint.match_kind, Some(MatchKind::Full)); + assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Full)); } _ => panic!("Expected TableConstraint::ForeignKey"), } @@ -6696,7 +6696,7 @@ fn parse_foreign_key_match_simple() { match &columns[0].options[1].option { ColumnOption::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!(constraint.match_kind, Some(MatchKind::Simple)); + assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Simple)); } _ => panic!("Expected ColumnOption::ForeignKey"), } @@ -6705,7 +6705,7 @@ fn parse_foreign_key_match_simple() { match &constraints[0] { TableConstraint::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!(constraint.match_kind, Some(MatchKind::Simple)); + assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Simple)); } _ => panic!("Expected TableConstraint::ForeignKey"), } @@ -6728,7 +6728,7 @@ fn parse_foreign_key_match_partial() { match &columns[0].options[1].option { ColumnOption::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!(constraint.match_kind, Some(MatchKind::Partial)); + assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Partial)); } _ => panic!("Expected ColumnOption::ForeignKey"), } @@ -6736,7 +6736,7 @@ fn parse_foreign_key_match_partial() { match &constraints[0] { TableConstraint::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!(constraint.match_kind, Some(MatchKind::Partial)); + assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Partial)); } _ => panic!("Expected TableConstraint::ForeignKey"), } From dd61527b72837ffb4ce38816417eb451913cb1b5 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 Oct 2025 13:48:33 +0200 Subject: [PATCH 14/17] Merged conflict --- src/ast/table_constraints.rs | 6 +++--- tests/sqlparser_postgres.rs | 30 ++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index a22955d99..ddf0c1253 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, ConstraintReferenceMatchKind, 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; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 622c9a0c0..1e6723f44 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6663,7 +6663,10 @@ fn parse_foreign_key_match_full() { match &columns[0].options[1].option { ColumnOption::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Full)); + assert_eq!( + constraint.match_kind, + Some(ConstraintReferenceMatchKind::Full) + ); } _ => panic!("Expected ColumnOption::ForeignKey"), } @@ -6672,7 +6675,10 @@ fn parse_foreign_key_match_full() { match &constraints[0] { TableConstraint::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Full)); + assert_eq!( + constraint.match_kind, + Some(ConstraintReferenceMatchKind::Full) + ); } _ => panic!("Expected TableConstraint::ForeignKey"), } @@ -6696,7 +6702,10 @@ fn parse_foreign_key_match_simple() { match &columns[0].options[1].option { ColumnOption::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Simple)); + assert_eq!( + constraint.match_kind, + Some(ConstraintReferenceMatchKind::Simple) + ); } _ => panic!("Expected ColumnOption::ForeignKey"), } @@ -6705,7 +6714,10 @@ fn parse_foreign_key_match_simple() { match &constraints[0] { TableConstraint::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Simple)); + assert_eq!( + constraint.match_kind, + Some(ConstraintReferenceMatchKind::Simple) + ); } _ => panic!("Expected TableConstraint::ForeignKey"), } @@ -6728,7 +6740,10 @@ fn parse_foreign_key_match_partial() { match &columns[0].options[1].option { ColumnOption::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Partial)); + assert_eq!( + constraint.match_kind, + Some(ConstraintReferenceMatchKind::Partial) + ); } _ => panic!("Expected ColumnOption::ForeignKey"), } @@ -6736,7 +6751,10 @@ fn parse_foreign_key_match_partial() { match &constraints[0] { TableConstraint::ForeignKey(constraint) => { assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!(constraint.match_kind, Some(ConstraintReferenceMatchKind::Partial)); + assert_eq!( + constraint.match_kind, + Some(ConstraintReferenceMatchKind::Partial) + ); } _ => panic!("Expected TableConstraint::ForeignKey"), } From 1c364cb89e71f3ad30ec94b38a3457c384ec8892 Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 10 Oct 2025 13:53:09 +0200 Subject: [PATCH 15/17] Merged and simplified match test --- tests/sqlparser_postgres.rs | 130 ++++++++---------------------------- 1 file changed, 28 insertions(+), 102 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1e6723f44..12401eb01 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6649,117 +6649,43 @@ fn parse_alter_schema() { } #[test] -fn parse_foreign_key_match_full() { - let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH FULL, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH FULL)"; - let statement = pg_and_generic().verified_stmt(sql); - match statement { - Statement::CreateTable(CreateTable { - columns, - constraints, - .. - }) => { - // Check column-level foreign key with MATCH FULL - assert_eq!(columns[0].name.value, "order_id"); - match &columns[0].options[1].option { - ColumnOption::ForeignKey(constraint) => { - assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!( - constraint.match_kind, - Some(ConstraintReferenceMatchKind::Full) - ); - } - _ => panic!("Expected ColumnOption::ForeignKey"), - } - - // Check table-level foreign key constraint with MATCH FULL - match &constraints[0] { - TableConstraint::ForeignKey(constraint) => { - assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!( - constraint.match_kind, - Some(ConstraintReferenceMatchKind::Full) - ); - } - _ => panic!("Expected TableConstraint::ForeignKey"), - } - } - _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), - } -} - -#[test] -fn parse_foreign_key_match_simple() { - let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH SIMPLE, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH SIMPLE)"; - let statement = pg_and_generic().verified_stmt(sql); - match statement { - Statement::CreateTable(CreateTable { - columns, - constraints, - .. - }) => { - // Check column-level foreign key with MATCH SIMPLE - assert_eq!(columns[0].name.value, "order_id"); - match &columns[0].options[1].option { - ColumnOption::ForeignKey(constraint) => { - assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!( - constraint.match_kind, - Some(ConstraintReferenceMatchKind::Simple) - ); - } - _ => panic!("Expected ColumnOption::ForeignKey"), - } +fn parse_foreign_key_match() { + let test_cases = [ + ("MATCH FULL", ConstraintReferenceMatchKind::Full), + ("MATCH SIMPLE", ConstraintReferenceMatchKind::Simple), + ("MATCH PARTIAL", ConstraintReferenceMatchKind::Partial), + ]; - // Check table-level foreign key constraint with MATCH SIMPLE - match &constraints[0] { - TableConstraint::ForeignKey(constraint) => { - assert_eq!(constraint.foreign_table.to_string(), "customers"); - assert_eq!( - constraint.match_kind, - Some(ConstraintReferenceMatchKind::Simple) - ); + 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"), } - _ => panic!("Expected TableConstraint::ForeignKey"), } + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), } - _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), - } -} - -#[test] -fn parse_foreign_key_match_partial() { - let sql = "CREATE TABLE orders (order_id INT PRIMARY KEY REFERENCES another_table (id) MATCH PARTIAL, customer_id INT, FOREIGN KEY (customer_id) REFERENCES customers(customer_id) MATCH PARTIAL)"; - let statement = pg_and_generic().verified_stmt(sql); - match statement { - Statement::CreateTable(CreateTable { - columns, - constraints, - .. - }) => { - assert_eq!(columns[0].name.value, "order_id"); - match &columns[0].options[1].option { - ColumnOption::ForeignKey(constraint) => { - assert_eq!(constraint.foreign_table.to_string(), "another_table"); - assert_eq!( - constraint.match_kind, - Some(ConstraintReferenceMatchKind::Partial) - ); - } - _ => panic!("Expected ColumnOption::ForeignKey"), - } - match &constraints[0] { + // 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.foreign_table.to_string(), "customers"); - assert_eq!( - constraint.match_kind, - Some(ConstraintReferenceMatchKind::Partial) - ); + assert_eq!(constraint.match_kind, Some(expected_kind)); } _ => panic!("Expected TableConstraint::ForeignKey"), - } + }, + _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), } - _ => unreachable!("{:?} should parse to Statement::CreateTable", sql), } } From a0aa870ebbc710431dadec2ffb361b07bcf601d6 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 11 Oct 2025 16:31:44 +0200 Subject: [PATCH 16/17] Removed clone of column ident --- src/parser/mod.rs | 27 ++++++++++----------------- tests/sqlparser_common.rs | 4 ++-- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6a3a1bc99..176be80d8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7950,7 +7950,7 @@ impl<'a> Parser<'a> { loop { if self.parse_keyword(Keyword::CONSTRAINT) { let name = Some(self.parse_identifier()?); - if let Some(option) = self.parse_optional_column_option(&col_name)? { + if let Some(option) = self.parse_optional_column_option()? { options.push(ColumnOptionDef { name, option }); } else { return self.expected( @@ -7958,7 +7958,7 @@ impl<'a> Parser<'a> { self.peek_token(), ); } - } else if let Some(option) = self.parse_optional_column_option(&col_name)? { + } else if let Some(option) = self.parse_optional_column_option()? { options.push(ColumnOptionDef { name: None, option }); } else { break; @@ -7994,10 +7994,7 @@ impl<'a> Parser<'a> { } } - pub fn parse_optional_column_option( - &mut self, - column_ident: &Ident, - ) -> Result, ParserError> { + pub fn parse_optional_column_option(&mut self) -> Result, ParserError> { if let Some(option) = self.dialect.parse_column_option(self)? { return option; } @@ -8005,15 +8002,12 @@ impl<'a> Parser<'a> { self.with_state( ColumnDefinition, |parser| -> Result, ParserError> { - parser.parse_optional_column_option_inner(column_ident) + parser.parse_optional_column_option_inner() }, ) } - fn parse_optional_column_option_inner( - &mut self, - column_ident: &Ident, - ) -> Result, ParserError> { + fn parse_optional_column_option_inner(&mut self) -> Result, ParserError> { if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) { Ok(Some(ColumnOption::CharacterSet( self.parse_object_name(false)?, @@ -8095,7 +8089,7 @@ impl<'a> Parser<'a> { ForeignKeyConstraint { name: None, // Column-level constraints don't have names index_name: None, // Not applicable for column-level constraints - columns: vec![column_ident.clone()], + columns: vec![], // Not applicable for column-level constraints foreign_table, referred_columns, on_delete, @@ -9103,7 +9097,7 @@ impl<'a> Parser<'a> { let new_name = self.parse_identifier()?; let data_type = self.parse_data_type()?; let mut options = vec![]; - while let Some(option) = self.parse_optional_column_option(&new_name)? { + while let Some(option) = self.parse_optional_column_option()? { options.push(option); } @@ -9121,7 +9115,7 @@ impl<'a> Parser<'a> { let col_name = self.parse_identifier()?; let data_type = self.parse_data_type()?; let mut options = vec![]; - while let Some(option) = self.parse_optional_column_option(&col_name)? { + while let Some(option) = self.parse_optional_column_option()? { options.push(option); } @@ -11383,7 +11377,7 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier()?; - let options = self.parse_view_column_options(&name)?; + let options = self.parse_view_column_options()?; let data_type = if dialect_of!(self is ClickHouseDialect) { Some(self.parse_data_type()?) } else { @@ -11398,11 +11392,10 @@ impl<'a> Parser<'a> { fn parse_view_column_options( &mut self, - column_ident: &Ident, ) -> Result, ParserError> { let mut options = Vec::new(); loop { - let option = self.parse_optional_column_option(column_ident)?; + let option = self.parse_optional_column_option()?; if let Some(option) = option { options.push(option); } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ca1205186..52f38b10e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3793,7 +3793,7 @@ fn parse_create_table() { option: ColumnOption::ForeignKey(ForeignKeyConstraint { name: None, index_name: None, - columns: vec!["ref".into()], + columns: vec![], foreign_table: ObjectName::from(vec!["othertable".into()]), referred_columns: vec!["a".into(), "b".into()], on_delete: None, @@ -3811,7 +3811,7 @@ fn parse_create_table() { option: ColumnOption::ForeignKey(ForeignKeyConstraint { name: None, index_name: None, - columns: vec!["ref2".into()], + columns: vec![], foreign_table: ObjectName::from(vec!["othertable2".into()]), referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), From c6cb4860fb54d290ef8f989b09a0e2f0faceb437 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 11 Oct 2025 16:33:47 +0200 Subject: [PATCH 17/17] Formatted code --- src/parser/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 176be80d8..ef583dd37 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11390,9 +11390,7 @@ impl<'a> Parser<'a> { }) } - fn parse_view_column_options( - &mut self, - ) -> Result, ParserError> { + fn parse_view_column_options(&mut self) -> Result, ParserError> { let mut options = Vec::new(); loop { let option = self.parse_optional_column_option()?;