From 0ce736c9d44f65a037309a13d425e5a26d9031fb Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 26 Sep 2026 14:13:17 +0200 Subject: [PATCH 01/13] PostgreSQL: Support CREATE FOREIGN DATA WRAPPER --- src/ast/ddl.rs | 80 +++++++++++++++++++++++++++++++ src/ast/mod.rs | 10 +++- src/ast/spans.rs | 1 + src/keywords.rs | 2 + src/parser/mod.rs | 49 +++++++++++++++++++ tests/sqlparser_postgres.rs | 96 +++++++++++++++++++++++++++++++++++++ 6 files changed, 236 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 86278b4088..0168985bd8 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6307,6 +6307,60 @@ impl fmt::Display for CreateForeignTable { } } +/// The handler/validator clause of a `CREATE FOREIGN DATA WRAPPER` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ForeignDataWrapperRoutineClause { + /// A named function, e.g. `HANDLER myhandler` or `VALIDATOR myvalidator`. + Function(ObjectName), + /// The `NO HANDLER` / `NO VALIDATOR` form. + Absent, +} + +impl ForeignDataWrapperRoutineClause { + fn fmt_with_label(&self, f: &mut fmt::Formatter<'_>, label: &str) -> fmt::Result { + match self { + ForeignDataWrapperRoutineClause::Function(name) => write!(f, " {label} {name}"), + ForeignDataWrapperRoutineClause::Absent => write!(f, " NO {label}"), + } + } +} + +/// A `CREATE FOREIGN DATA WRAPPER` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigndatawrapper.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateForeignDataWrapper { + /// The name of the foreign-data wrapper. Per PostgreSQL this is a bare + /// identifier and cannot be schema-qualified. + pub name: Ident, + /// Optional `HANDLER handler_function` or `NO HANDLER` clause. + pub handler: Option, + /// Optional `VALIDATOR validator_function` or `NO VALIDATOR` clause. + pub validator: Option, + /// Optional `OPTIONS (key 'value', ...)` clause. + pub options: Option>, +} + +impl fmt::Display for CreateForeignDataWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "CREATE FOREIGN DATA WRAPPER {}", self.name)?; + if let Some(handler) = &self.handler { + handler.fmt_with_label(f, "HANDLER")?; + } + if let Some(validator) = &self.validator { + validator.fmt_with_label(f, "VALIDATOR")?; + } + if let Some(options) = &self.options { + write!(f, " OPTIONS ({})", display_comma_separated(options))?; + } + Ok(()) + } +} + impl From for crate::ast::Statement { fn from(v: CreateForeignTable) -> Self { crate::ast::Statement::CreateForeignTable(v) @@ -6508,3 +6562,29 @@ impl fmt::Display for AggregateModifyKind { } } } + +impl From for crate::ast::Statement { + fn from(v: CreateForeignDataWrapper) -> Self { + crate::ast::Statement::CreateForeignDataWrapper(v) + } +} + +impl Spanned for CreateForeignDataWrapper { + fn span(&self) -> Span { + let routine_span = |clause: &Option| match clause { + Some(ForeignDataWrapperRoutineClause::Function(name)) => Some(name.span()), + _ => None, + }; + Span::union_iter( + core::iter::once(self.name.span) + .chain(routine_span(&self.handler)) + .chain(routine_span(&self.validator)) + .chain( + self.options + .iter() + .flatten() + .flat_map(|option| [option.key.span, option.value.span]), + ), + ) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c253e6d10b..1948383036 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -74,12 +74,12 @@ pub use self::ddl::{ ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, CreateAggregateArgs, CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, - CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, + CreateForeignDataWrapper, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateRule, CreateRuleAction, CreateRuleDoKind, CreateRuleEvent, CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, - DropOperatorSignature, DropPolicy, DropTrigger, ForValues, FunctionReturnType, GeneratedAs, + DropOperatorSignature, DropPolicy, DropTrigger, ForValues, ForeignDataWrapperRoutineClause, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, @@ -3838,6 +3838,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.html) CreateForeignTable(CreateForeignTable), /// ```sql + /// CREATE FOREIGN DATA WRAPPER + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigndatawrapper.html) + CreateForeignDataWrapper(CreateForeignDataWrapper), + /// ```sql /// CREATE POLICY /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) @@ -5713,6 +5718,7 @@ impl fmt::Display for Statement { write!(f, "{stmt}") } Statement::CreateForeignTable(stmt) => write!(f, "{stmt}"), + Statement::CreateForeignDataWrapper(stmt) => write!(f, "{stmt}"), Statement::CreatePolicy(policy) => write!(f, "{policy}"), Statement::CreateRule(rule) => write!(f, "{rule}"), Statement::CreateConnector(create_connector) => create_connector.fmt(f), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 69e31efc13..74e0844d56 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -396,6 +396,7 @@ impl Spanned for Statement { Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), Statement::CreateForeignTable(stmt) => stmt.span(), + Statement::CreateForeignDataWrapper(stmt) => stmt.span(), Statement::CreateConnector { .. } => Span::empty(), Statement::CreateOperator(create_operator) => create_operator.span(), Statement::CreateOperatorFamily(create_operator_family) => { diff --git a/src/keywords.rs b/src/keywords.rs index fa810e11b8..b35644befb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -492,6 +492,7 @@ define_keywords!( GROUPING, GROUPS, GZIP, + HANDLER, HASH, HASHES, HAVING, @@ -1171,6 +1172,7 @@ define_keywords!( VALID, VALIDATE, VALIDATION_MODE, + VALIDATOR, VALUE, VALUES, VALUE_OF, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 32fa6f41ce..af98d94260 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5569,6 +5569,8 @@ impl<'a> Parser<'a> { ); } self.parse_create_foreign_table().map(Into::into) + } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER]) { + self.parse_create_foreign_data_wrapper().map(Into::into) } else { self.expected_ref("an object type after CREATE", self.peek_token_ref()) } @@ -21269,6 +21271,53 @@ impl<'a> Parser<'a> { }) } + /// Parse a `CREATE FOREIGN DATA WRAPPER` statement. + /// + /// See + pub fn parse_create_foreign_data_wrapper( + &mut self, + ) -> Result { + let name = self.parse_identifier()?; + + // PostgreSQL accepts HANDLER and VALIDATOR in either order, so they are + // consumed in a loop rather than positionally. + let mut handler = None; + let mut validator = None; + loop { + let loc = self.peek_token_ref().span.start; + let (slot, clause) = if self.parse_keyword(Keyword::HANDLER) { + ( + &mut handler, + ForeignDataWrapperRoutineClause::Function(self.parse_object_name(false)?), + ) + } else if self.parse_keywords(&[Keyword::NO, Keyword::HANDLER]) { + (&mut handler, ForeignDataWrapperRoutineClause::Absent) + } else if self.parse_keyword(Keyword::VALIDATOR) { + ( + &mut validator, + ForeignDataWrapperRoutineClause::Function(self.parse_object_name(false)?), + ) + } else if self.parse_keywords(&[Keyword::NO, Keyword::VALIDATOR]) { + (&mut validator, ForeignDataWrapperRoutineClause::Absent) + } else { + break; + }; + if slot.is_some() { + return parser_err!("conflicting or redundant options", loc); + } + *slot = Some(clause); + } + + let options = self.parse_pg_options_clause()?; + + Ok(CreateForeignDataWrapper { + name, + handler, + validator, + options, + }) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6c523939ca..df48f3719a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10886,3 +10886,99 @@ fn parse_create_aggregate_rejects_output_arguments_and_defaults() { ); } } + +#[test] +fn parse_create_foreign_data_wrapper() { + let sql = "CREATE FOREIGN DATA WRAPPER myfdw"; + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.to_string(), "myfdw"); + assert!(stmt.handler.is_none()); + assert!(stmt.validator.is_none()); + assert!(stmt.options.is_none()); + + let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler"; + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + stmt.handler, + Some(ForeignDataWrapperRoutineClause::Function(ObjectName::from( + vec!["myhandler".into()] + ))) + ); + + let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER"; + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.handler, Some(ForeignDataWrapperRoutineClause::Absent)); + + let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO VALIDATOR"; + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + stmt.validator, + Some(ForeignDataWrapperRoutineClause::Absent) + ); + + let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator OPTIONS (debug 'true')"; + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + stmt.options, + Some(vec![CreateServerOption { + key: "debug".into(), + value: Ident { + value: "true".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }]) + ); + + let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER NO VALIDATOR"; + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.handler, Some(ForeignDataWrapperRoutineClause::Absent)); + assert_eq!( + stmt.validator, + Some(ForeignDataWrapperRoutineClause::Absent) + ); + + // A schema-qualified name is not valid: FDW names are bare identifiers. + assert!(matches!( + pg_and_generic().parse_sql_statements("CREATE FOREIGN DATA WRAPPER myschema.myfdw"), + Err(ParserError::ParserError(_)) + )); +} + +#[test] +fn parse_create_foreign_data_wrapper_clause_order() { + // PostgreSQL takes these in either order; Display normalizes to HANDLER first. + let canonical = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator"; + pg_and_generic().one_statement_parses_to( + "CREATE FOREIGN DATA WRAPPER myfdw VALIDATOR myvalidator HANDLER myhandler", + canonical, + ); + pg_and_generic().one_statement_parses_to( + "CREATE FOREIGN DATA WRAPPER myfdw NO VALIDATOR NO HANDLER", + "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER NO VALIDATOR", + ); + + for sql in [ + "CREATE FOREIGN DATA WRAPPER myfdw HANDLER h1 HANDLER h2", + "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER HANDLER h", + "CREATE FOREIGN DATA WRAPPER myfdw VALIDATOR v NO VALIDATOR", + ] { + let err = pg_and_generic().parse_sql_statements(sql).unwrap_err(); + assert!( + err.to_string().contains("conflicting or redundant options"), + "unexpected error for {sql}: {err}" + ); + } +} From d93cc20f991529a93b2da795128a368e719ab702 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sun, 27 Sep 2026 15:41:16 +0900 Subject: [PATCH 02/13] PostgreSQL: reject CREATE modifiers on FOREIGN DATA WRAPPER Share one has_modifier check between the FOREIGN TABLE and FOREIGN DATA WRAPPER arms so temporary/or-alter/global no longer parse and render as a bare CREATE FOREIGN DATA WRAPPER. Add fuzz seeds. --- .../create_foreign_data_wrapper.sql | 1 + ...create_foreign_data_wrapper_no_handler.sql | 1 + .../create_foreign_data_wrapper_options.sql | 1 + src/parser/mod.rs | 29 +++++++++++-------- tests/sqlparser_postgres.rs | 13 +++++++++ 5 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 fuzz/fuzz_seeds/create_foreign_data_wrapper.sql create mode 100644 fuzz/fuzz_seeds/create_foreign_data_wrapper_no_handler.sql create mode 100644 fuzz/fuzz_seeds/create_foreign_data_wrapper_options.sql diff --git a/fuzz/fuzz_seeds/create_foreign_data_wrapper.sql b/fuzz/fuzz_seeds/create_foreign_data_wrapper.sql new file mode 100644 index 0000000000..c576d746d4 --- /dev/null +++ b/fuzz/fuzz_seeds/create_foreign_data_wrapper.sql @@ -0,0 +1 @@ +CREATE FOREIGN DATA WRAPPER postgres_fdw HANDLER postgres_fdw_handler VALIDATOR postgres_fdw_validator diff --git a/fuzz/fuzz_seeds/create_foreign_data_wrapper_no_handler.sql b/fuzz/fuzz_seeds/create_foreign_data_wrapper_no_handler.sql new file mode 100644 index 0000000000..08bd8f72fc --- /dev/null +++ b/fuzz/fuzz_seeds/create_foreign_data_wrapper_no_handler.sql @@ -0,0 +1 @@ +CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER NO VALIDATOR diff --git a/fuzz/fuzz_seeds/create_foreign_data_wrapper_options.sql b/fuzz/fuzz_seeds/create_foreign_data_wrapper_options.sql new file mode 100644 index 0000000000..9dcc5226d3 --- /dev/null +++ b/fuzz/fuzz_seeds/create_foreign_data_wrapper_options.sql @@ -0,0 +1 @@ +CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator OPTIONS (debug 'true') diff --git a/src/parser/mod.rs b/src/parser/mod.rs index af98d94260..9fab365897 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5466,6 +5466,16 @@ impl<'a> Parser<'a> { let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); let create_view_params = self.parse_create_view_params()?; + // `or_replace` is caught by an earlier arm. It stays so that reordering cannot bypass it. + let has_modifier = or_replace + || or_alter + || temporary + || global.is_some() + || transient + || volatile + || multiset.is_some() + || persistent + || create_view_params.is_some(); if self.peek_keywords(&[Keyword::SNAPSHOT, Keyword::TABLE]) { self.parse_create_snapshot_table().map(Into::into) } else if self.peek_keywords(&[Keyword::TEXT, Keyword::SEARCH]) { @@ -5551,18 +5561,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::SERVER) { self.parse_pg_create_server() } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::TABLE]) { - // `or_replace` cannot reach here today, since the arm above catches it. - // It stays so that reordering the arms cannot make it fall through. - if or_replace - || or_alter - || temporary - || global.is_some() - || transient - || volatile - || multiset.is_some() - || persistent - || create_view_params.is_some() - { + if has_modifier { return parser_err!( "CREATE FOREIGN TABLE does not accept this modifier", modifier_loc @@ -5570,6 +5569,12 @@ impl<'a> Parser<'a> { } self.parse_create_foreign_table().map(Into::into) } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER]) { + if has_modifier { + return parser_err!( + "CREATE FOREIGN DATA WRAPPER does not accept this modifier", + modifier_loc + ); + } self.parse_create_foreign_data_wrapper().map(Into::into) } else { self.expected_ref("an object type after CREATE", self.peek_token_ref()) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index df48f3719a..5a2dc6614d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10955,6 +10955,19 @@ fn parse_create_foreign_data_wrapper() { pg_and_generic().parse_sql_statements("CREATE FOREIGN DATA WRAPPER myschema.myfdw"), Err(ParserError::ParserError(_)) )); + + for sql in [ + "CREATE TEMPORARY FOREIGN DATA WRAPPER myfdw", + "CREATE OR ALTER FOREIGN DATA WRAPPER myfdw", + "CREATE GLOBAL TEMPORARY FOREIGN DATA WRAPPER myfdw", + ] { + let err = pg_and_generic().parse_sql_statements(sql).unwrap_err(); + assert!( + err.to_string() + .contains("CREATE FOREIGN DATA WRAPPER does not accept this modifier"), + "unexpected error for {sql}: {err}" + ); + } } #[test] From d6955e44781982eda999bb12435cb60c3c629564 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:58:12 +0900 Subject: [PATCH 03/13] Update src/ast/ddl.rs Co-authored-by: Luca Cappelletti --- src/ast/ddl.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0168985bd8..4ed1b7366b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6337,10 +6337,9 @@ pub struct CreateForeignDataWrapper { /// The name of the foreign-data wrapper. Per PostgreSQL this is a bare /// identifier and cannot be schema-qualified. pub name: Ident, - /// Optional `HANDLER handler_function` or `NO HANDLER` clause. - pub handler: Option, - /// Optional `VALIDATOR validator_function` or `NO VALIDATOR` clause. - pub validator: Option, + /// `HANDLER` and `VALIDATOR` clauses in source order. PostgreSQL's grammar + /// accepts repeated clauses and rejects them only when executing the statement. + pub routines: Vec, /// Optional `OPTIONS (key 'value', ...)` clause. pub options: Option>, } From 76f0f48485712082e407c4befbe3c792b9b2c7d1 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:58:26 +0900 Subject: [PATCH 04/13] Update tests/sqlparser_postgres.rs Co-authored-by: Luca Cappelletti --- tests/sqlparser_postgres.rs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5a2dc6614d..df462650d3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10972,26 +10972,13 @@ fn parse_create_foreign_data_wrapper() { #[test] fn parse_create_foreign_data_wrapper_clause_order() { - // PostgreSQL takes these in either order; Display normalizes to HANDLER first. - let canonical = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator"; - pg_and_generic().one_statement_parses_to( + for sql in [ "CREATE FOREIGN DATA WRAPPER myfdw VALIDATOR myvalidator HANDLER myhandler", - canonical, - ); - pg_and_generic().one_statement_parses_to( "CREATE FOREIGN DATA WRAPPER myfdw NO VALIDATOR NO HANDLER", - "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER NO VALIDATOR", - ); - - for sql in [ "CREATE FOREIGN DATA WRAPPER myfdw HANDLER h1 HANDLER h2", "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER HANDLER h", "CREATE FOREIGN DATA WRAPPER myfdw VALIDATOR v NO VALIDATOR", ] { - let err = pg_and_generic().parse_sql_statements(sql).unwrap_err(); - assert!( - err.to_string().contains("conflicting or redundant options"), - "unexpected error for {sql}: {err}" - ); + pg_and_generic().verified_stmt(sql); } } From d2cee5f59a25a4a7d60cb5018e53baad133a2814 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:58:40 +0900 Subject: [PATCH 05/13] Update tests/sqlparser_postgres.rs Co-authored-by: Luca Cappelletti --- tests/sqlparser_postgres.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index df462650d3..d81beadeaf 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10951,10 +10951,12 @@ fn parse_create_foreign_data_wrapper() { ); // A schema-qualified name is not valid: FDW names are bare identifiers. - assert!(matches!( + assert_eq!( pg_and_generic().parse_sql_statements("CREATE FOREIGN DATA WRAPPER myschema.myfdw"), - Err(ParserError::ParserError(_)) - )); + Err(ParserError::ParserError( + "Expected: end of statement, found: .".to_string() + )) + ); for sql in [ "CREATE TEMPORARY FOREIGN DATA WRAPPER myfdw", From 3627658a7d845390c35e2005a55f3e58d2e1b409 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:58:54 +0900 Subject: [PATCH 06/13] Update tests/sqlparser_postgres.rs Co-authored-by: Luca Cappelletti --- tests/sqlparser_postgres.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d81beadeaf..42202912ed 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10894,8 +10894,7 @@ fn parse_create_foreign_data_wrapper() { unreachable!() }; assert_eq!(stmt.name.to_string(), "myfdw"); - assert!(stmt.handler.is_none()); - assert!(stmt.validator.is_none()); + assert!(stmt.routines.is_empty()); assert!(stmt.options.is_none()); let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler"; @@ -10903,25 +10902,28 @@ fn parse_create_foreign_data_wrapper() { unreachable!() }; assert_eq!( - stmt.handler, - Some(ForeignDataWrapperRoutineClause::Function(ObjectName::from( + stmt.routines, + vec![ForeignDataWrapperRoutineClause::Handler(ObjectName::from( vec!["myhandler".into()] - ))) + ))] ); let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER"; let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; - assert_eq!(stmt.handler, Some(ForeignDataWrapperRoutineClause::Absent)); + assert_eq!( + stmt.routines, + vec![ForeignDataWrapperRoutineClause::NoHandler] + ); let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO VALIDATOR"; let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!( - stmt.validator, - Some(ForeignDataWrapperRoutineClause::Absent) + stmt.routines, + vec![ForeignDataWrapperRoutineClause::NoValidator] ); let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator OPTIONS (debug 'true')"; @@ -10944,10 +10946,12 @@ fn parse_create_foreign_data_wrapper() { let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; - assert_eq!(stmt.handler, Some(ForeignDataWrapperRoutineClause::Absent)); assert_eq!( - stmt.validator, - Some(ForeignDataWrapperRoutineClause::Absent) + stmt.routines, + vec![ + ForeignDataWrapperRoutineClause::NoHandler, + ForeignDataWrapperRoutineClause::NoValidator + ] ); // A schema-qualified name is not valid: FDW names are bare identifiers. From b42faad2473d84be00ff9162cb2420e7eaaf547f Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:59:06 +0900 Subject: [PATCH 07/13] Update src/ast/ddl.rs Co-authored-by: Luca Cappelletti --- src/ast/ddl.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 4ed1b7366b..14bd5061d0 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6570,14 +6570,13 @@ impl From for crate::ast::Statement { impl Spanned for CreateForeignDataWrapper { fn span(&self) -> Span { - let routine_span = |clause: &Option| match clause { - Some(ForeignDataWrapperRoutineClause::Function(name)) => Some(name.span()), - _ => None, - }; Span::union_iter( core::iter::once(self.name.span) - .chain(routine_span(&self.handler)) - .chain(routine_span(&self.validator)) + .chain(self.routines.iter().filter_map(|routine| match routine { + ForeignDataWrapperRoutineClause::Handler(name) + | ForeignDataWrapperRoutineClause::Validator(name) => Some(name.span()), + _ => None, + })) .chain( self.options .iter() From 5ba5db73dca56b808e67ed0dbaf96d7d7a93923a Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:59:22 +0900 Subject: [PATCH 08/13] Update src/ast/ddl.rs Co-authored-by: Luca Cappelletti --- src/ast/ddl.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 14bd5061d0..a4c02614ec 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6347,11 +6347,8 @@ pub struct CreateForeignDataWrapper { impl fmt::Display for CreateForeignDataWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "CREATE FOREIGN DATA WRAPPER {}", self.name)?; - if let Some(handler) = &self.handler { - handler.fmt_with_label(f, "HANDLER")?; - } - if let Some(validator) = &self.validator { - validator.fmt_with_label(f, "VALIDATOR")?; + for routine in &self.routines { + write!(f, " {routine}")?; } if let Some(options) = &self.options { write!(f, " OPTIONS ({})", display_comma_separated(options))?; From d373d7d012247c63faa0b8fc4d224beb73adff41 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:59:33 +0900 Subject: [PATCH 09/13] Update src/ast/ddl.rs Co-authored-by: Luca Cappelletti --- src/ast/ddl.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a4c02614ec..2fd20dc836 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6307,22 +6307,28 @@ impl fmt::Display for CreateForeignTable { } } -/// The handler/validator clause of a `CREATE FOREIGN DATA WRAPPER` statement. +/// A handler or validator clause of a `CREATE FOREIGN DATA WRAPPER` statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ForeignDataWrapperRoutineClause { - /// A named function, e.g. `HANDLER myhandler` or `VALIDATOR myvalidator`. - Function(ObjectName), - /// The `NO HANDLER` / `NO VALIDATOR` form. - Absent, + /// `HANDLER handler_function` + Handler(ObjectName), + /// `NO HANDLER` + NoHandler, + /// `VALIDATOR validator_function` + Validator(ObjectName), + /// `NO VALIDATOR` + NoValidator, } -impl ForeignDataWrapperRoutineClause { - fn fmt_with_label(&self, f: &mut fmt::Formatter<'_>, label: &str) -> fmt::Result { +impl fmt::Display for ForeignDataWrapperRoutineClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ForeignDataWrapperRoutineClause::Function(name) => write!(f, " {label} {name}"), - ForeignDataWrapperRoutineClause::Absent => write!(f, " NO {label}"), + ForeignDataWrapperRoutineClause::Handler(name) => write!(f, "HANDLER {name}"), + ForeignDataWrapperRoutineClause::NoHandler => f.write_str("NO HANDLER"), + ForeignDataWrapperRoutineClause::Validator(name) => write!(f, "VALIDATOR {name}"), + ForeignDataWrapperRoutineClause::NoValidator => f.write_str("NO VALIDATOR"), } } } From 4c6ab806f023289c5c1aa19367468f5bc637186f Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 19 Oct 2026 15:59:44 +0900 Subject: [PATCH 10/13] Update src/parser/mod.rs Co-authored-by: Luca Cappelletti --- src/parser/mod.rs | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9fab365897..067d6cd3ee 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -21284,41 +21284,27 @@ impl<'a> Parser<'a> { ) -> Result { let name = self.parse_identifier()?; - // PostgreSQL accepts HANDLER and VALIDATOR in either order, so they are - // consumed in a loop rather than positionally. - let mut handler = None; - let mut validator = None; + let mut routines = Vec::new(); loop { - let loc = self.peek_token_ref().span.start; - let (slot, clause) = if self.parse_keyword(Keyword::HANDLER) { - ( - &mut handler, - ForeignDataWrapperRoutineClause::Function(self.parse_object_name(false)?), - ) + let routine = if self.parse_keyword(Keyword::HANDLER) { + ForeignDataWrapperRoutineClause::Handler(self.parse_object_name(false)?) } else if self.parse_keywords(&[Keyword::NO, Keyword::HANDLER]) { - (&mut handler, ForeignDataWrapperRoutineClause::Absent) + ForeignDataWrapperRoutineClause::NoHandler } else if self.parse_keyword(Keyword::VALIDATOR) { - ( - &mut validator, - ForeignDataWrapperRoutineClause::Function(self.parse_object_name(false)?), - ) + ForeignDataWrapperRoutineClause::Validator(self.parse_object_name(false)?) } else if self.parse_keywords(&[Keyword::NO, Keyword::VALIDATOR]) { - (&mut validator, ForeignDataWrapperRoutineClause::Absent) + ForeignDataWrapperRoutineClause::NoValidator } else { break; }; - if slot.is_some() { - return parser_err!("conflicting or redundant options", loc); - } - *slot = Some(clause); + routines.push(routine); } let options = self.parse_pg_options_clause()?; Ok(CreateForeignDataWrapper { name, - handler, - validator, + routines, options, }) } From 2e40fc1a0ee771e6a5c240a29248753c29d9ef9e Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Tue, 20 Oct 2026 10:10:11 +0900 Subject: [PATCH 11/13] test: cover foreign data wrapper AST spans --- tests/sqlparser_postgres.rs | 40 ++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 42202912ed..da83d4f71a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10954,7 +10954,6 @@ fn parse_create_foreign_data_wrapper() { ] ); - // A schema-qualified name is not valid: FDW names are bare identifiers. assert_eq!( pg_and_generic().parse_sql_statements("CREATE FOREIGN DATA WRAPPER myschema.myfdw"), Err(ParserError::ParserError( @@ -10976,6 +10975,45 @@ fn parse_create_foreign_data_wrapper() { } } +#[test] +fn foreign_data_wrapper_display_and_span() { + let ident = |value: &str, start: u64, end: u64| Ident { + value: value.to_string(), + quote_style: None, + span: Span::new(Location::new(1, start), Location::new(1, end)), + }; + let stmt = Statement::CreateForeignDataWrapper(CreateForeignDataWrapper { + name: ident("myfdw", 29, 34), + routines: vec![ + ForeignDataWrapperRoutineClause::Handler(ObjectName::from(ident("myhandler", 43, 52))), + ForeignDataWrapperRoutineClause::NoHandler, + ForeignDataWrapperRoutineClause::Validator(ObjectName::from(ident( + "myvalidator", + 74, + 85, + ))), + ForeignDataWrapperRoutineClause::NoValidator, + ], + options: Some(vec![CreateServerOption { + key: ident("debug", 108, 113), + value: Ident { + value: "true".to_string(), + quote_style: Some('\''), + span: Span::new(Location::new(1, 114), Location::new(1, 120)), + }, + }]), + }); + + assert_eq!( + stmt.to_string(), + "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler NO HANDLER VALIDATOR myvalidator NO VALIDATOR OPTIONS (debug 'true')" + ); + assert_eq!( + stmt.span(), + Span::new(Location::new(1, 29), Location::new(1, 120)) + ); +} + #[test] fn parse_create_foreign_data_wrapper_clause_order() { for sql in [ From 3cff4e032f543436cea56e50446b0eae2bf4e58b Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sun, 25 Oct 2026 14:15:53 +0900 Subject: [PATCH 12/13] fix(parser): preserve keyword table names in FLUSH --- src/parser/mod.rs | 25 ++++++++++--------------- tests/sqlparser_mysql.rs | 10 ++++++++++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 067d6cd3ee..eafe4d621e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1074,22 +1074,17 @@ impl<'a> Parser<'a> { loop { let next_token = self.next_token(); match &next_token.token { - Token::Word(w) => match w.keyword { - Keyword::WITH => { - read_lock = self.parse_keywords(&[Keyword::READ, Keyword::LOCK]); - } - Keyword::FOR => { - export = self.parse_keyword(Keyword::EXPORT); - } - Keyword::NoKeyword => { - self.prev_token(); - tables = self.parse_comma_separated(|p| p.parse_object_name(false))?; - } - _ => {} - }, - _ => { - break; + Token::Word(w) if w.keyword == Keyword::WITH => { + read_lock = self.parse_keywords(&[Keyword::READ, Keyword::LOCK]); + } + Token::Word(w) if w.keyword == Keyword::FOR => { + export = self.parse_keyword(Keyword::EXPORT); } + Token::Word(_) => { + self.prev_token(); + tables = self.parse_comma_separated(|p| p.parse_object_name(false))?; + } + _ => break, } } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 233a5403a5..55f2dc775c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -240,6 +240,16 @@ fn parse_flush() { ); } +#[test] +fn parse_flush_preserves_keyword_table_names() { + for sql in [ + "FLUSH TABLES handler WITH READ LOCK", + "FLUSH TABLES validator WITH READ LOCK", + ] { + mysql_and_generic().verified_stmt(sql); + } +} + #[test] fn parse_show_columns() { assert_eq!( From 73e73fe07d45494ce1f0ef9a51432a14571dd107 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sun, 25 Oct 2026 17:04:30 +0100 Subject: [PATCH 13/13] Format rebased AST exports --- src/ast/mod.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1948383036..87618bbc5f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -74,18 +74,18 @@ pub use self::ddl::{ ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, CreateAggregateArgs, CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, - CreateForeignDataWrapper, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, - CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateRule, - CreateRuleAction, CreateRuleDoKind, CreateRuleEvent, CreateTable, CreateTextSearch, + CreateForeignDataWrapper, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, + CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, + CreateRule, CreateRuleAction, CreateRuleDoKind, CreateRuleEvent, CreateTable, CreateTextSearch, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, - DropOperatorSignature, DropPolicy, DropTrigger, ForValues, ForeignDataWrapperRoutineClause, FunctionReturnType, GeneratedAs, - GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, - IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, - KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, - OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, - PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, - SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, + DropOperatorSignature, DropPolicy, DropTrigger, ForValues, ForeignDataWrapperRoutineClause, + FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, + IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, + IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, + OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, + Owner, Partition, PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, + ReplicaIdentity, SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData,