From 894abf3cf16a2c16aae735158b2c27c25aaf4e5b Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 17 Apr 2026 11:21:57 +0900 Subject: [PATCH 01/13] feat(ast): add CreateForeignDataWrapper and CreateForeignTable types - Add HANDLER and VALIDATOR keywords to the keyword list - Add FdwRoutineClause enum for HANDLER/NO HANDLER and VALIDATOR/NO VALIDATOR clauses - Add CreateForeignDataWrapper struct for CREATE FOREIGN DATA WRAPPER - Add CreateForeignTable struct for CREATE FOREIGN TABLE - Export new types from ast::mod and add Statement variants - Add spans.rs coverage returning Span::empty() for the new variants --- src/ast/ddl.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++- src/ast/mod.rs | 23 ++++++++--- src/ast/spans.rs | 2 + src/keywords.rs | 2 + 4 files changed, 125 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 67aefb3928..e2dc6ebeb2 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -42,7 +42,8 @@ use crate::ast::{ UniqueConstraint, }, ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody, - CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, CreateViewParams, DataType, Expr, + CreateFunctionUsing, CreateServerOption, CreateTableLikeKind, CreateTableOptions, + CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDefinitionSetParam, FunctionDesc, FunctionDeterminismSpecifier, FunctionParallel, FunctionSecurity, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, InitializeKind, @@ -5757,3 +5758,104 @@ impl From for crate::ast::Statement { crate::ast::Statement::AlterPolicy(v) } } + +/// The handler/validator clause of a `CREATE FOREIGN DATA WRAPPER` statement. +/// +/// Specifies either a named function or the absence of a function. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum FdwRoutineClause { + /// A named function, e.g. `HANDLER myhandler` or `VALIDATOR myvalidator`. + Function(ObjectName), + /// The `NO HANDLER` or `NO VALIDATOR` form. + NoFunction, +} + +/// 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. + 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 { + match handler { + FdwRoutineClause::Function(name) => write!(f, " HANDLER {name}")?, + FdwRoutineClause::NoFunction => write!(f, " NO HANDLER")?, + } + } + if let Some(validator) = &self.validator { + match validator { + FdwRoutineClause::Function(name) => write!(f, " VALIDATOR {name}")?, + FdwRoutineClause::NoFunction => write!(f, " NO VALIDATOR")?, + } + } + if let Some(options) = &self.options { + write!(f, " OPTIONS ({})", display_comma_separated(options))?; + } + Ok(()) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateForeignDataWrapper) -> Self { + crate::ast::Statement::CreateForeignDataWrapper(v) + } +} + +/// A `CREATE FOREIGN TABLE` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.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 CreateForeignTable { + /// The foreign table name. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, + /// Whether `IF NOT EXISTS` was specified. + pub if_not_exists: bool, + /// Column definitions. + pub columns: Vec, + /// The `SERVER server_name` clause. + pub server_name: Ident, + /// Optional `OPTIONS (key 'value', ...)` clause at the table level. + pub options: Option>, +} + +impl fmt::Display for CreateForeignTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "CREATE FOREIGN TABLE {if_not_exists}{name} ({columns}) SERVER {server_name}", + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + name = self.name, + columns = display_comma_separated(&self.columns), + server_name = self.server_name, + )?; + 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) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 886bea26d5..17ac342421 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -69,11 +69,12 @@ pub use self::ddl::{ AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, - CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, - CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, - CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, - DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, - DropOperatorSignature, DropPolicy, DropTrigger, ForValues, FunctionReturnType, GeneratedAs, + CreateConnector, CreateDomain, CreateExtension, CreateForeignDataWrapper, CreateForeignTable, + CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, + CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTrigger, CreateView, + Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, + DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, + DropTrigger, FdwRoutineClause, ForValues, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, @@ -3737,6 +3738,16 @@ pub enum Statement { /// A `CREATE SERVER` statement. CreateServer(CreateServerStatement), /// ```sql + /// CREATE FOREIGN DATA WRAPPER + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigndatawrapper.html) + CreateForeignDataWrapper(CreateForeignDataWrapper), + /// ```sql + /// CREATE FOREIGN TABLE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.html) + CreateForeignTable(CreateForeignTable), + /// ```sql /// CREATE POLICY /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) @@ -5542,6 +5553,8 @@ impl fmt::Display for Statement { Statement::CreateServer(stmt) => { write!(f, "{stmt}") } + Statement::CreateForeignDataWrapper(stmt) => write!(f, "{stmt}"), + Statement::CreateForeignTable(stmt) => write!(f, "{stmt}"), Statement::CreatePolicy(policy) => write!(f, "{policy}"), Statement::CreateConnector(create_connector) => create_connector.fmt(f), Statement::CreateOperator(create_operator) => create_operator.fmt(f), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index adc1443fc7..d5e1c2cdb1 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -393,6 +393,8 @@ impl Spanned for Statement { Statement::DropOperatorClass(drop_operator_class) => drop_operator_class.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), + Statement::CreateForeignDataWrapper { .. } => Span::empty(), + Statement::CreateForeignTable { .. } => Span::empty(), 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 808e5f03d8..1ae28f0aae 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -476,6 +476,7 @@ define_keywords!( GROUPING, GROUPS, GZIP, + HANDLER, HASH, HASHES, HAVING, @@ -1130,6 +1131,7 @@ define_keywords!( VALID, VALIDATE, VALIDATION_MODE, + VALIDATOR, VALUE, VALUES, VALUE_OF, From 6f28faaf51bcd7a5238a9b9e73cc88b3bf354b46 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 17 Apr 2026 11:22:02 +0900 Subject: [PATCH 02/13] feat(parser): parse CREATE FOREIGN DATA WRAPPER and CREATE FOREIGN TABLE - Dispatch on FOREIGN keyword in parse_create, branching on DATA WRAPPER or TABLE - parse_create_foreign_data_wrapper: parses optional HANDLER/NO HANDLER, VALIDATOR/NO VALIDATOR, and OPTIONS clauses - parse_create_foreign_table: parses IF NOT EXISTS, column list via parse_columns, required SERVER name, and optional OPTIONS clause --- src/parser/mod.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7501919a0c..398bd25c63 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5212,6 +5212,17 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::SERVER) { self.parse_pg_create_server() + } else if self.parse_keyword(Keyword::FOREIGN) { + if self.parse_keywords(&[Keyword::DATA, Keyword::WRAPPER]) { + self.parse_create_foreign_data_wrapper().map(Into::into) + } else if self.parse_keyword(Keyword::TABLE) { + self.parse_create_foreign_table().map(Into::into) + } else { + self.expected_ref( + "DATA WRAPPER or TABLE after CREATE FOREIGN", + self.peek_token_ref(), + ) + } } else { self.expected_ref("an object type after CREATE", self.peek_token_ref()) } @@ -19716,6 +19727,86 @@ 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()?; + + let handler = if self.parse_keyword(Keyword::HANDLER) { + Some(FdwRoutineClause::Function(self.parse_object_name(false)?)) + } else if self.parse_keywords(&[Keyword::NO, Keyword::HANDLER]) { + Some(FdwRoutineClause::NoFunction) + } else { + None + }; + + let validator = if self.parse_keyword(Keyword::VALIDATOR) { + Some(FdwRoutineClause::Function(self.parse_object_name(false)?)) + } else if self.parse_keywords(&[Keyword::NO, Keyword::VALIDATOR]) { + Some(FdwRoutineClause::NoFunction) + } else { + None + }; + + let options = if self.parse_keyword(Keyword::OPTIONS) { + self.expect_token(&Token::LParen)?; + let opts = self.parse_comma_separated(|p| { + let key = p.parse_identifier()?; + let value = p.parse_identifier()?; + Ok(CreateServerOption { key, value }) + })?; + self.expect_token(&Token::RParen)?; + Some(opts) + } else { + None + }; + + Ok(CreateForeignDataWrapper { + name, + handler, + validator, + options, + }) + } + + /// Parse a `CREATE FOREIGN TABLE` statement. + /// + /// See + pub fn parse_create_foreign_table( + &mut self, + ) -> Result { + let if_not_exists = + self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let (columns, _constraints) = self.parse_columns()?; + self.expect_keyword_is(Keyword::SERVER)?; + let server_name = self.parse_identifier()?; + + let options = if self.parse_keyword(Keyword::OPTIONS) { + self.expect_token(&Token::LParen)?; + let opts = self.parse_comma_separated(|p| { + let key = p.parse_identifier()?; + let value = p.parse_identifier()?; + Ok(CreateServerOption { key, value }) + })?; + self.expect_token(&Token::RParen)?; + Some(opts) + } else { + None + }; + + Ok(CreateForeignTable { + name, + if_not_exists, + columns, + server_name, + options, + }) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index From 1b97004f3791708f42922cab5164351c38fdcbfb Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 17 Apr 2026 11:22:08 +0900 Subject: [PATCH 03/13] test: add CREATE FOREIGN DATA WRAPPER and CREATE FOREIGN TABLE tests Round-trip tests via pg().verified_stmt for: - FDW: name-only, HANDLER, NO HANDLER, NO VALIDATOR, combined HANDLER+VALIDATOR+OPTIONS - FOREIGN TABLE: basic columns+SERVER, IF NOT EXISTS, table-level OPTIONS --- tests/sqlparser_postgres.rs | 96 +++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 86315b1ef9..93c32033b1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9221,3 +9221,99 @@ fn parse_lock_table() { } } } + +#[test] +fn parse_create_foreign_data_wrapper() { + // Minimal: name only. + let sql = "CREATE FOREIGN DATA WRAPPER myfdw"; + let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "myfdw"); + assert!(stmt.handler.is_none()); + assert!(stmt.validator.is_none()); + assert!(stmt.options.is_none()); + + // With HANDLER. + let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler"; + let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + stmt.handler, + Some(FdwRoutineClause::Function(ObjectName::from(vec![ + "myhandler".into() + ]))) + ); + + // With NO HANDLER. + let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER"; + let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.handler, Some(FdwRoutineClause::NoFunction)); + + // With NO VALIDATOR. + let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO VALIDATOR"; + let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.validator, Some(FdwRoutineClause::NoFunction)); + + // With HANDLER, VALIDATOR, and OPTIONS. + let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator OPTIONS (debug 'true')"; + let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + stmt.handler, + Some(FdwRoutineClause::Function(ObjectName::from(vec![ + "myhandler".into() + ]))) + ); + assert_eq!( + stmt.validator, + Some(FdwRoutineClause::Function(ObjectName::from(vec![ + "myvalidator".into() + ]))) + ); + let options = stmt.options.unwrap(); + assert_eq!(options.len(), 1); + assert_eq!(options[0].key.value, "debug"); + assert_eq!(options[0].value.value, "true"); +} + +#[test] +fn parse_create_foreign_table() { + // Basic: columns and SERVER. + let sql = "CREATE FOREIGN TABLE ft1 (id INTEGER, name TEXT) SERVER myserver"; + let Statement::CreateForeignTable(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.to_string(), "ft1"); + assert!(!stmt.if_not_exists); + assert_eq!(stmt.columns.len(), 2); + assert_eq!(stmt.columns[0].name.value, "id"); + assert_eq!(stmt.columns[1].name.value, "name"); + assert_eq!(stmt.server_name.value, "myserver"); + assert!(stmt.options.is_none()); + + // With IF NOT EXISTS. + let sql = "CREATE FOREIGN TABLE IF NOT EXISTS ft2 (col INTEGER) SERVER remoteserver"; + let Statement::CreateForeignTable(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(stmt.if_not_exists); + assert_eq!(stmt.name.to_string(), "ft2"); + + // With table-level OPTIONS. + let sql = + "CREATE FOREIGN TABLE ft3 (col INTEGER) SERVER remoteserver OPTIONS (schema_name 'public')"; + let Statement::CreateForeignTable(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + let options = stmt.options.unwrap(); + assert_eq!(options.len(), 1); + assert_eq!(options[0].key.value, "schema_name"); + assert_eq!(options[0].value.value, "public"); +} From e585a2f7caca365c702ddcaec1db5dd3d3b80aac Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 18 Apr 2026 18:17:59 +0900 Subject: [PATCH 04/13] chore: apply cargo fmt and sort new keywords --- src/ast/ddl.rs | 23 +++++++++++++---------- src/parser/mod.rs | 7 ++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index e2dc6ebeb2..04ba196ef0 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -43,15 +43,14 @@ use crate::ast::{ }, ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody, CreateFunctionUsing, CreateServerOption, CreateTableLikeKind, CreateTableOptions, - CreateViewParams, DataType, Expr, - FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDefinitionSetParam, FunctionDesc, - FunctionDeterminismSpecifier, FunctionParallel, FunctionSecurity, HiveDistributionStyle, - HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, InitializeKind, - MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, - OrderByExpr, ProjectionSelect, Query, RefreshModeKind, ResetConfig, RowAccessPolicy, - SequenceOptions, Spanned, SqlOption, StorageLifecyclePolicy, StorageSerializationPolicy, - TableVersion, Tag, TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, - TriggerReferencing, Value, ValueWithSpan, WrappedCollection, + CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull, + FunctionDefinitionSetParam, FunctionDesc, FunctionDeterminismSpecifier, FunctionParallel, + FunctionSecurity, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, + HiveSetLocation, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, + OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, + ResetConfig, RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageLifecyclePolicy, + StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, TriggerExecBody, TriggerObject, + TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, WrappedCollection, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; @@ -5842,7 +5841,11 @@ impl fmt::Display for CreateForeignTable { write!( f, "CREATE FOREIGN TABLE {if_not_exists}{name} ({columns}) SERVER {server_name}", - if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, name = self.name, columns = display_comma_separated(&self.columns), server_name = self.server_name, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 398bd25c63..16d731c00d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19775,11 +19775,8 @@ impl<'a> Parser<'a> { /// Parse a `CREATE FOREIGN TABLE` statement. /// /// See - pub fn parse_create_foreign_table( - &mut self, - ) -> Result { - let if_not_exists = - self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + pub fn parse_create_foreign_table(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = self.parse_object_name(false)?; let (columns, _constraints) = self.parse_columns()?; self.expect_keyword_is(Keyword::SERVER)?; From e2fc3b1740715d7584f34657b63c3e4350380b02 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 18 Apr 2026 20:16:28 +0900 Subject: [PATCH 05/13] review: preserve foreign-table constraints, share helpers, name hygiene - CreateForeignTable now carries `constraints: Vec` so CHECK and other table-level constraints round-trip faithfully instead of being silently discarded. - CreateForeignDataWrapper.name is now ObjectName for parity with other CREATE statements and to permit schema-qualified FDW names. - Rename FdwRoutineClause::NoFunction -> Absent so the variant is not misleading when used for VALIDATOR. - Extract parse_fdw_options_clause shared by CREATE SERVER, CREATE FOREIGN DATA WRAPPER, and CREATE FOREIGN TABLE. - Extract parse_fdw_routine_clause parameterised over HANDLER / VALIDATOR so handler and validator share one code path. - FdwRoutineClause gains a Display helper that formats with a label, replacing the duplicated match arms in CreateForeignDataWrapper. - Tests cover schema-qualified FDW name and round-trip of a CHECK constraint on CREATE FOREIGN TABLE. --- src/ast/ddl.rs | 40 +++++++++------ src/parser/mod.rs | 99 +++++++++++++++++-------------------- tests/sqlparser_postgres.rs | 29 +++++++++-- 3 files changed, 96 insertions(+), 72 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 04ba196ef0..1cdeb91bcb 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5760,15 +5760,25 @@ impl From for crate::ast::Statement { /// The handler/validator clause of a `CREATE FOREIGN DATA WRAPPER` statement. /// -/// Specifies either a named function or the absence of a function. +/// The function-or-absence portion of a `HANDLER` or `VALIDATOR` clause on a +/// foreign data wrapper. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum FdwRoutineClause { /// A named function, e.g. `HANDLER myhandler` or `VALIDATOR myvalidator`. Function(ObjectName), - /// The `NO HANDLER` or `NO VALIDATOR` form. - NoFunction, + /// The `NO HANDLER` / `NO VALIDATOR` form. + Absent, +} + +impl FdwRoutineClause { + fn fmt_with_label(&self, f: &mut fmt::Formatter<'_>, label: &str) -> fmt::Result { + match self { + FdwRoutineClause::Function(name) => write!(f, " {label} {name}"), + FdwRoutineClause::Absent => write!(f, " NO {label}"), + } + } } /// A `CREATE FOREIGN DATA WRAPPER` statement. @@ -5778,8 +5788,8 @@ pub enum FdwRoutineClause { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct CreateForeignDataWrapper { - /// The name of the foreign-data wrapper. - pub name: Ident, + /// The name of the foreign-data wrapper. Can be schema-qualified. + pub name: ObjectName, /// Optional `HANDLER handler_function` or `NO HANDLER` clause. pub handler: Option, /// Optional `VALIDATOR validator_function` or `NO VALIDATOR` clause. @@ -5792,16 +5802,10 @@ 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 { - match handler { - FdwRoutineClause::Function(name) => write!(f, " HANDLER {name}")?, - FdwRoutineClause::NoFunction => write!(f, " NO HANDLER")?, - } + handler.fmt_with_label(f, "HANDLER")?; } if let Some(validator) = &self.validator { - match validator { - FdwRoutineClause::Function(name) => write!(f, " VALIDATOR {name}")?, - FdwRoutineClause::NoFunction => write!(f, " NO VALIDATOR")?, - } + validator.fmt_with_label(f, "VALIDATOR")?; } if let Some(options) = &self.options { write!(f, " OPTIONS ({})", display_comma_separated(options))?; @@ -5830,6 +5834,9 @@ pub struct CreateForeignTable { pub if_not_exists: bool, /// Column definitions. pub columns: Vec, + /// Table-level constraints (e.g. `CHECK (...)`, composite `FOREIGN KEY`). + /// PostgreSQL accepts these in `CREATE FOREIGN TABLE` column lists. + pub constraints: Vec, /// The `SERVER server_name` clause. pub server_name: Ident, /// Optional `OPTIONS (key 'value', ...)` clause at the table level. @@ -5840,7 +5847,7 @@ impl fmt::Display for CreateForeignTable { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "CREATE FOREIGN TABLE {if_not_exists}{name} ({columns}) SERVER {server_name}", + "CREATE FOREIGN TABLE {if_not_exists}{name} ({columns}", if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { @@ -5848,8 +5855,11 @@ impl fmt::Display for CreateForeignTable { }, name = self.name, columns = display_comma_separated(&self.columns), - server_name = self.server_name, )?; + if !self.constraints.is_empty() { + write!(f, ", {}", display_comma_separated(&self.constraints))?; + } + write!(f, ") SERVER {}", self.server_name)?; if let Some(options) = &self.options { write!(f, " OPTIONS ({})", display_comma_separated(options))?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 16d731c00d..5fb42ff40d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19706,16 +19706,7 @@ impl<'a> Parser<'a> { self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?; let foreign_data_wrapper = self.parse_object_name(false)?; - let mut options = None; - if self.parse_keyword(Keyword::OPTIONS) { - self.expect_token(&Token::LParen)?; - options = Some(self.parse_comma_separated(|p| { - let key = p.parse_identifier()?; - let value = p.parse_identifier()?; - Ok(CreateServerOption { key, value }) - })?); - self.expect_token(&Token::RParen)?; - } + let options = self.parse_fdw_options_clause()?; Ok(Statement::CreateServer(CreateServerStatement { name, @@ -19727,42 +19718,53 @@ impl<'a> Parser<'a> { })) } + /// Parse an optional `OPTIONS ( key value [, ...] )` clause shared by + /// `CREATE SERVER`, `CREATE FOREIGN DATA WRAPPER`, and `CREATE FOREIGN TABLE`. + fn parse_fdw_options_clause( + &mut self, + ) -> Result>, ParserError> { + if !self.parse_keyword(Keyword::OPTIONS) { + return Ok(None); + } + self.expect_token(&Token::LParen)?; + let opts = self.parse_comma_separated(|p| { + let key = p.parse_identifier()?; + let value = p.parse_identifier()?; + Ok(CreateServerOption { key, value }) + })?; + self.expect_token(&Token::RParen)?; + Ok(Some(opts)) + } + + /// Parse an optional `HANDLER f | NO HANDLER` / `VALIDATOR f | NO VALIDATOR` + /// clause on `CREATE FOREIGN DATA WRAPPER`. The caller passes the positive + /// keyword (`HANDLER` or `VALIDATOR`); the `NO ` form is also + /// recognised. + fn parse_fdw_routine_clause( + &mut self, + keyword: Keyword, + ) -> Result, ParserError> { + if self.parse_keyword(keyword) { + Ok(Some(FdwRoutineClause::Function( + self.parse_object_name(false)?, + ))) + } else if self.parse_keywords(&[Keyword::NO, keyword]) { + Ok(Some(FdwRoutineClause::Absent)) + } else { + Ok(None) + } + } + /// Parse a `CREATE FOREIGN DATA WRAPPER` statement. /// /// See pub fn parse_create_foreign_data_wrapper( &mut self, ) -> Result { - let name = self.parse_identifier()?; - - let handler = if self.parse_keyword(Keyword::HANDLER) { - Some(FdwRoutineClause::Function(self.parse_object_name(false)?)) - } else if self.parse_keywords(&[Keyword::NO, Keyword::HANDLER]) { - Some(FdwRoutineClause::NoFunction) - } else { - None - }; - - let validator = if self.parse_keyword(Keyword::VALIDATOR) { - Some(FdwRoutineClause::Function(self.parse_object_name(false)?)) - } else if self.parse_keywords(&[Keyword::NO, Keyword::VALIDATOR]) { - Some(FdwRoutineClause::NoFunction) - } else { - None - }; - - let options = if self.parse_keyword(Keyword::OPTIONS) { - self.expect_token(&Token::LParen)?; - let opts = self.parse_comma_separated(|p| { - let key = p.parse_identifier()?; - let value = p.parse_identifier()?; - Ok(CreateServerOption { key, value }) - })?; - self.expect_token(&Token::RParen)?; - Some(opts) - } else { - None - }; + let name = self.parse_object_name(false)?; + let handler = self.parse_fdw_routine_clause(Keyword::HANDLER)?; + let validator = self.parse_fdw_routine_clause(Keyword::VALIDATOR)?; + let options = self.parse_fdw_options_clause()?; Ok(CreateForeignDataWrapper { name, @@ -19778,27 +19780,16 @@ impl<'a> Parser<'a> { pub fn parse_create_foreign_table(&mut self) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = self.parse_object_name(false)?; - let (columns, _constraints) = self.parse_columns()?; + let (columns, constraints) = self.parse_columns()?; self.expect_keyword_is(Keyword::SERVER)?; let server_name = self.parse_identifier()?; - - let options = if self.parse_keyword(Keyword::OPTIONS) { - self.expect_token(&Token::LParen)?; - let opts = self.parse_comma_separated(|p| { - let key = p.parse_identifier()?; - let value = p.parse_identifier()?; - Ok(CreateServerOption { key, value }) - })?; - self.expect_token(&Token::RParen)?; - Some(opts) - } else { - None - }; + let options = self.parse_fdw_options_clause()?; Ok(CreateForeignTable { name, if_not_exists, columns, + constraints, server_name, options, }) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 93c32033b1..4fb2559130 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9229,7 +9229,7 @@ fn parse_create_foreign_data_wrapper() { let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { unreachable!() }; - assert_eq!(stmt.name.value, "myfdw"); + assert_eq!(stmt.name.to_string(), "myfdw"); assert!(stmt.handler.is_none()); assert!(stmt.validator.is_none()); assert!(stmt.options.is_none()); @@ -9251,14 +9251,14 @@ fn parse_create_foreign_data_wrapper() { let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { unreachable!() }; - assert_eq!(stmt.handler, Some(FdwRoutineClause::NoFunction)); + assert_eq!(stmt.handler, Some(FdwRoutineClause::Absent)); // With NO VALIDATOR. let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO VALIDATOR"; let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { unreachable!() }; - assert_eq!(stmt.validator, Some(FdwRoutineClause::NoFunction)); + assert_eq!(stmt.validator, Some(FdwRoutineClause::Absent)); // With HANDLER, VALIDATOR, and OPTIONS. let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator OPTIONS (debug 'true')"; @@ -9317,3 +9317,26 @@ fn parse_create_foreign_table() { assert_eq!(options[0].key.value, "schema_name"); assert_eq!(options[0].value.value, "public"); } + +#[test] +fn parse_create_foreign_table_with_check_constraint() { + // PostgreSQL accepts table-level CHECK constraints in CREATE FOREIGN TABLE. + // The constraint must round-trip rather than being silently dropped. + let sql = + "CREATE FOREIGN TABLE ft (id INTEGER, CONSTRAINT id_positive CHECK (id > 0)) SERVER s"; + let Statement::CreateForeignTable(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.columns.len(), 1); + assert_eq!(stmt.constraints.len(), 1); +} + +#[test] +fn parse_create_foreign_data_wrapper_with_schema_qualified_name() { + // Schema-qualified FDW names should parse and round-trip through ObjectName. + let sql = "CREATE FOREIGN DATA WRAPPER myschema.myfdw"; + let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.to_string(), "myschema.myfdw"); +} From 0b6324da04ea1e90d22f81872613824b51aa7fb6 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 18 Apr 2026 20:22:21 +0900 Subject: [PATCH 06/13] review: implement real Spanned for CreateForeignDataWrapper and CreateForeignTable Return each statement's name span instead of Span::empty(), matching the sibling Create* arms. --- src/ast/spans.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index d5e1c2cdb1..a465f8f87c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -393,8 +393,8 @@ impl Spanned for Statement { Statement::DropOperatorClass(drop_operator_class) => drop_operator_class.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), - Statement::CreateForeignDataWrapper { .. } => Span::empty(), - Statement::CreateForeignTable { .. } => Span::empty(), + Statement::CreateForeignDataWrapper(stmt) => stmt.name.span(), + Statement::CreateForeignTable(stmt) => stmt.name.span(), Statement::CreateConnector { .. } => Span::empty(), Statement::CreateOperator(create_operator) => create_operator.span(), Statement::CreateOperatorFamily(create_operator_family) => { From 6c0bd85426ce98d5f862aec1f9e14d2e72ff31f6 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sat, 18 Apr 2026 20:24:12 +0900 Subject: [PATCH 07/13] chore: cargo fmt --- 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 5fb42ff40d..e8b70c9b59 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19720,9 +19720,7 @@ impl<'a> Parser<'a> { /// Parse an optional `OPTIONS ( key value [, ...] )` clause shared by /// `CREATE SERVER`, `CREATE FOREIGN DATA WRAPPER`, and `CREATE FOREIGN TABLE`. - fn parse_fdw_options_clause( - &mut self, - ) -> Result>, ParserError> { + fn parse_fdw_options_clause(&mut self) -> Result>, ParserError> { if !self.parse_keyword(Keyword::OPTIONS) { return Ok(None); } From 4a60804e5646e202b8bc5f492e6fd8b5624d13b2 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 18 May 2026 18:16:13 +0900 Subject: [PATCH 08/13] fix(postgres): correct CREATE FOREIGN TABLE display and add real spans - Fix CreateForeignTable Display emitting `(, CONSTRAINT ...)` when columns is empty but constraints is non-empty; only emit the joining comma when both are non-empty, mirroring CreateTable::fmt - Implement union Spanned for CreateForeignTable and CreateForeignDataWrapper and delegate from spans.rs via stmt.span() - Add Display for FdwRoutineClause defaulting to the HANDLER label - Strengthen CREATE FOREIGN TABLE/FDW OPTIONS assertions to full CreateServerOption structs including quote_style and span - Run the four new foreign tests under pg_and_generic() - Document the deliberate parser relaxation for schema-qualified FDW names - Flatten the nested CREATE FOREIGN dispatch into the parser chain - Trim the redundant FdwRoutineClause doc sentence --- src/ast/ddl.rs | 39 ++++++++++++++++++++--- src/ast/spans.rs | 4 +-- src/parser/mod.rs | 15 +++------ tests/sqlparser_postgres.rs | 63 ++++++++++++++++++++++++++----------- 4 files changed, 84 insertions(+), 37 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index b792f1ce50..4bd837d6b4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5824,9 +5824,6 @@ impl From for crate::ast::Statement { } /// The handler/validator clause of a `CREATE FOREIGN DATA WRAPPER` statement. -/// -/// The function-or-absence portion of a `HANDLER` or `VALIDATOR` clause on a -/// foreign data wrapper. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -5846,6 +5843,12 @@ impl FdwRoutineClause { } } +impl fmt::Display for FdwRoutineClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.fmt_with_label(f, "HANDLER") + } +} + /// A `CREATE FOREIGN DATA WRAPPER` statement. /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigndatawrapper.html) @@ -5885,6 +5888,20 @@ impl From for crate::ast::Statement { } } +impl Spanned for CreateForeignDataWrapper { + fn span(&self) -> Span { + let routine_span = |clause: &Option| match clause { + Some(FdwRoutineClause::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)), + ) + } +} + /// A `CREATE FOREIGN TABLE` statement. /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigntable.html) @@ -5921,9 +5938,10 @@ impl fmt::Display for CreateForeignTable { name = self.name, columns = display_comma_separated(&self.columns), )?; - if !self.constraints.is_empty() { - write!(f, ", {}", display_comma_separated(&self.constraints))?; + if !self.columns.is_empty() && !self.constraints.is_empty() { + write!(f, ", ")?; } + write!(f, "{}", display_comma_separated(&self.constraints))?; write!(f, ") SERVER {}", self.server_name)?; if let Some(options) = &self.options { write!(f, " OPTIONS ({})", display_comma_separated(options))?; @@ -5937,3 +5955,14 @@ impl From for crate::ast::Statement { crate::ast::Statement::CreateForeignTable(v) } } + +impl Spanned for CreateForeignTable { + fn span(&self) -> Span { + Span::union_iter( + core::iter::once(self.name.span()) + .chain(self.columns.iter().map(|column| column.span())) + .chain(self.constraints.iter().map(|constraint| constraint.span())) + .chain(core::iter::once(self.server_name.span)), + ) + } +} diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 71926ed010..b33f50e82f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -393,8 +393,8 @@ impl Spanned for Statement { Statement::DropOperatorClass(drop_operator_class) => drop_operator_class.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), - Statement::CreateForeignDataWrapper(stmt) => stmt.name.span(), - Statement::CreateForeignTable(stmt) => stmt.name.span(), + Statement::CreateForeignDataWrapper(stmt) => stmt.span(), + Statement::CreateForeignTable(stmt) => stmt.span(), Statement::CreateConnector { .. } => Span::empty(), Statement::CreateOperator(create_operator) => create_operator.span(), Statement::CreateOperatorFamily(create_operator_family) => { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ecf87e4068..40b2dc4760 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5214,17 +5214,10 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::SERVER) { self.parse_pg_create_server() - } else if self.parse_keyword(Keyword::FOREIGN) { - if self.parse_keywords(&[Keyword::DATA, Keyword::WRAPPER]) { - self.parse_create_foreign_data_wrapper().map(Into::into) - } else if self.parse_keyword(Keyword::TABLE) { - self.parse_create_foreign_table().map(Into::into) - } else { - self.expected_ref( - "DATA WRAPPER or TABLE after CREATE FOREIGN", - self.peek_token_ref(), - ) - } + } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER]) { + self.parse_create_foreign_data_wrapper().map(Into::into) + } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::TABLE]) { + self.parse_create_foreign_table().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 66b8ff3cbc..a49e2c1617 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9248,7 +9248,7 @@ fn parse_lock_table() { fn parse_create_foreign_data_wrapper() { // Minimal: name only. let sql = "CREATE FOREIGN DATA WRAPPER myfdw"; - let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!(stmt.name.to_string(), "myfdw"); @@ -9258,7 +9258,7 @@ fn parse_create_foreign_data_wrapper() { // With HANDLER. let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler"; - let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!( @@ -9270,21 +9270,21 @@ fn parse_create_foreign_data_wrapper() { // With NO HANDLER. let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO HANDLER"; - let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!(stmt.handler, Some(FdwRoutineClause::Absent)); // With NO VALIDATOR. let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO VALIDATOR"; - let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!(stmt.validator, Some(FdwRoutineClause::Absent)); // With HANDLER, VALIDATOR, and OPTIONS. let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator OPTIONS (debug 'true')"; - let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!( @@ -9299,17 +9299,24 @@ fn parse_create_foreign_data_wrapper() { "myvalidator".into() ]))) ); - let options = stmt.options.unwrap(); - assert_eq!(options.len(), 1); - assert_eq!(options[0].key.value, "debug"); - assert_eq!(options[0].value.value, "true"); + assert_eq!( + stmt.options, + Some(vec![CreateServerOption { + key: "debug".into(), + value: Ident { + value: "true".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }]) + ); } #[test] fn parse_create_foreign_table() { // Basic: columns and SERVER. let sql = "CREATE FOREIGN TABLE ft1 (id INTEGER, name TEXT) SERVER myserver"; - let Statement::CreateForeignTable(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!(stmt.name.to_string(), "ft1"); @@ -9322,7 +9329,7 @@ fn parse_create_foreign_table() { // With IF NOT EXISTS. let sql = "CREATE FOREIGN TABLE IF NOT EXISTS ft2 (col INTEGER) SERVER remoteserver"; - let Statement::CreateForeignTable(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert!(stmt.if_not_exists); @@ -9331,13 +9338,20 @@ fn parse_create_foreign_table() { // With table-level OPTIONS. let sql = "CREATE FOREIGN TABLE ft3 (col INTEGER) SERVER remoteserver OPTIONS (schema_name 'public')"; - let Statement::CreateForeignTable(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; - let options = stmt.options.unwrap(); - assert_eq!(options.len(), 1); - assert_eq!(options[0].key.value, "schema_name"); - assert_eq!(options[0].value.value, "public"); + assert_eq!( + stmt.options, + Some(vec![CreateServerOption { + key: "schema_name".into(), + value: Ident { + value: "public".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }, + }]) + ); } #[test] @@ -9346,18 +9360,29 @@ fn parse_create_foreign_table_with_check_constraint() { // The constraint must round-trip rather than being silently dropped. let sql = "CREATE FOREIGN TABLE ft (id INTEGER, CONSTRAINT id_positive CHECK (id > 0)) SERVER s"; - let Statement::CreateForeignTable(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!(stmt.columns.len(), 1); assert_eq!(stmt.constraints.len(), 1); + + // Zero columns with only a table-level constraint must not emit `(, CONSTRAINT ...)`. + let sql = "CREATE FOREIGN TABLE ft (CONSTRAINT c CHECK (id > 0)) SERVER s"; + let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.columns.len(), 0); + assert_eq!(stmt.constraints.len(), 1); } #[test] fn parse_create_foreign_data_wrapper_with_schema_qualified_name() { - // Schema-qualified FDW names should parse and round-trip through ObjectName. + // NOTE: PostgreSQL FDW names are unqualified per the spec; a schema-qualified + // name like `myschema.myfdw` is non-conformant input. This is a deliberate + // parser relaxation (we accept and round-trip it via ObjectName rather than + // rejecting it), so this test enshrines that relaxation on purpose. let sql = "CREATE FOREIGN DATA WRAPPER myschema.myfdw"; - let Statement::CreateForeignDataWrapper(stmt) = pg().verified_stmt(sql) else { + let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; assert_eq!(stmt.name.to_string(), "myschema.myfdw"); From 72b321450cd1a42682dc37158fa86583d623e965 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Sun, 14 Jun 2026 18:12:05 +0900 Subject: [PATCH 09/13] refactor(postgres): spell out FDW abbreviation and drop misleading Display Rename FdwRoutineClause to ForeignDataWrapperRoutineClause and the parse_fdw_* helpers to parse_foreign_data_wrapper_* to match the codebase convention of spelling out foreign data wrapper. Remove the Display impl that hardcoded HANDLER, since every call site uses fmt_with_label with the correct per-clause label. Rename the opts local to options and correct a doc comment spelling. --- src/ast/ddl.rs | 22 ++++++++-------------- src/ast/mod.rs | 2 +- src/parser/mod.rs | 28 +++++++++++++++------------- tests/sqlparser_postgres.rs | 37 ++++++++++++++++++++++++++----------- 4 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 4bd837d6b4..cda5212c9c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5827,28 +5827,22 @@ impl From for crate::ast::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 FdwRoutineClause { +pub enum ForeignDataWrapperRoutineClause { /// A named function, e.g. `HANDLER myhandler` or `VALIDATOR myvalidator`. Function(ObjectName), /// The `NO HANDLER` / `NO VALIDATOR` form. Absent, } -impl FdwRoutineClause { +impl ForeignDataWrapperRoutineClause { fn fmt_with_label(&self, f: &mut fmt::Formatter<'_>, label: &str) -> fmt::Result { match self { - FdwRoutineClause::Function(name) => write!(f, " {label} {name}"), - FdwRoutineClause::Absent => write!(f, " NO {label}"), + ForeignDataWrapperRoutineClause::Function(name) => write!(f, " {label} {name}"), + ForeignDataWrapperRoutineClause::Absent => write!(f, " NO {label}"), } } } -impl fmt::Display for FdwRoutineClause { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.fmt_with_label(f, "HANDLER") - } -} - /// A `CREATE FOREIGN DATA WRAPPER` statement. /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigndatawrapper.html) @@ -5859,9 +5853,9 @@ pub struct CreateForeignDataWrapper { /// The name of the foreign-data wrapper. Can be schema-qualified. pub name: ObjectName, /// Optional `HANDLER handler_function` or `NO HANDLER` clause. - pub handler: Option, + pub handler: Option, /// Optional `VALIDATOR validator_function` or `NO VALIDATOR` clause. - pub validator: Option, + pub validator: Option, /// Optional `OPTIONS (key 'value', ...)` clause. pub options: Option>, } @@ -5890,8 +5884,8 @@ impl From for crate::ast::Statement { impl Spanned for CreateForeignDataWrapper { fn span(&self) -> Span { - let routine_span = |clause: &Option| match clause { - Some(FdwRoutineClause::Function(name)) => Some(name.span()), + let routine_span = |clause: &Option| match clause { + Some(ForeignDataWrapperRoutineClause::Function(name)) => Some(name.span()), _ => None, }; Span::union_iter( diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 001e6630f2..8b3ef650e4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -74,7 +74,7 @@ pub use self::ddl::{ CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, - DropTrigger, FdwRoutineClause, ForValues, FunctionReturnType, GeneratedAs, + DropTrigger, ForValues, ForeignDataWrapperRoutineClause, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 40b2dc4760..4e2eb46910 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19872,7 +19872,7 @@ impl<'a> Parser<'a> { self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?; let foreign_data_wrapper = self.parse_object_name(false)?; - let options = self.parse_fdw_options_clause()?; + let options = self.parse_foreign_data_wrapper_options_clause()?; Ok(Statement::CreateServer(CreateServerStatement { name, @@ -19886,34 +19886,36 @@ impl<'a> Parser<'a> { /// Parse an optional `OPTIONS ( key value [, ...] )` clause shared by /// `CREATE SERVER`, `CREATE FOREIGN DATA WRAPPER`, and `CREATE FOREIGN TABLE`. - fn parse_fdw_options_clause(&mut self) -> Result>, ParserError> { + fn parse_foreign_data_wrapper_options_clause( + &mut self, + ) -> Result>, ParserError> { if !self.parse_keyword(Keyword::OPTIONS) { return Ok(None); } self.expect_token(&Token::LParen)?; - let opts = self.parse_comma_separated(|p| { + let options = self.parse_comma_separated(|p| { let key = p.parse_identifier()?; let value = p.parse_identifier()?; Ok(CreateServerOption { key, value }) })?; self.expect_token(&Token::RParen)?; - Ok(Some(opts)) + Ok(Some(options)) } /// Parse an optional `HANDLER f | NO HANDLER` / `VALIDATOR f | NO VALIDATOR` /// clause on `CREATE FOREIGN DATA WRAPPER`. The caller passes the positive /// keyword (`HANDLER` or `VALIDATOR`); the `NO ` form is also - /// recognised. - fn parse_fdw_routine_clause( + /// recognized. + fn parse_foreign_data_wrapper_routine_clause( &mut self, keyword: Keyword, - ) -> Result, ParserError> { + ) -> Result, ParserError> { if self.parse_keyword(keyword) { - Ok(Some(FdwRoutineClause::Function( + Ok(Some(ForeignDataWrapperRoutineClause::Function( self.parse_object_name(false)?, ))) } else if self.parse_keywords(&[Keyword::NO, keyword]) { - Ok(Some(FdwRoutineClause::Absent)) + Ok(Some(ForeignDataWrapperRoutineClause::Absent)) } else { Ok(None) } @@ -19926,9 +19928,9 @@ impl<'a> Parser<'a> { &mut self, ) -> Result { let name = self.parse_object_name(false)?; - let handler = self.parse_fdw_routine_clause(Keyword::HANDLER)?; - let validator = self.parse_fdw_routine_clause(Keyword::VALIDATOR)?; - let options = self.parse_fdw_options_clause()?; + let handler = self.parse_foreign_data_wrapper_routine_clause(Keyword::HANDLER)?; + let validator = self.parse_foreign_data_wrapper_routine_clause(Keyword::VALIDATOR)?; + let options = self.parse_foreign_data_wrapper_options_clause()?; Ok(CreateForeignDataWrapper { name, @@ -19947,7 +19949,7 @@ impl<'a> Parser<'a> { let (columns, constraints) = self.parse_columns()?; self.expect_keyword_is(Keyword::SERVER)?; let server_name = self.parse_identifier()?; - let options = self.parse_fdw_options_clause()?; + let options = self.parse_foreign_data_wrapper_options_clause()?; Ok(CreateForeignTable { name, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a49e2c1617..7771149500 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9263,9 +9263,9 @@ fn parse_create_foreign_data_wrapper() { }; assert_eq!( stmt.handler, - Some(FdwRoutineClause::Function(ObjectName::from(vec![ - "myhandler".into() - ]))) + Some(ForeignDataWrapperRoutineClause::Function(ObjectName::from( + vec!["myhandler".into()] + ))) ); // With NO HANDLER. @@ -9273,14 +9273,17 @@ fn parse_create_foreign_data_wrapper() { let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() }; - assert_eq!(stmt.handler, Some(FdwRoutineClause::Absent)); + assert_eq!(stmt.handler, Some(ForeignDataWrapperRoutineClause::Absent)); // With NO VALIDATOR. 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(FdwRoutineClause::Absent)); + assert_eq!( + stmt.validator, + Some(ForeignDataWrapperRoutineClause::Absent) + ); // With HANDLER, VALIDATOR, and OPTIONS. let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler VALIDATOR myvalidator OPTIONS (debug 'true')"; @@ -9289,15 +9292,15 @@ fn parse_create_foreign_data_wrapper() { }; assert_eq!( stmt.handler, - Some(FdwRoutineClause::Function(ObjectName::from(vec![ - "myhandler".into() - ]))) + Some(ForeignDataWrapperRoutineClause::Function(ObjectName::from( + vec!["myhandler".into()] + ))) ); assert_eq!( stmt.validator, - Some(FdwRoutineClause::Function(ObjectName::from(vec![ - "myvalidator".into() - ]))) + Some(ForeignDataWrapperRoutineClause::Function(ObjectName::from( + vec!["myvalidator".into()] + ))) ); assert_eq!( stmt.options, @@ -9310,6 +9313,18 @@ fn parse_create_foreign_data_wrapper() { }, }]) ); + + // Each clause must render its own label: round-tripping must not emit + // `NO HANDLER NO HANDLER`, which a single hardcoded Display label would. + 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) + ); } #[test] From 9a1d29be3cb5b440f0bfce1842274c247dac94e7 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Mon, 13 Jul 2026 08:37:57 +0900 Subject: [PATCH 10/13] Reformat ast re-export list with cargo fmt --- 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 f6a1e60914..168895a0a7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -74,16 +74,16 @@ pub use self::ddl::{ ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, CreateForeignDataWrapper, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, - CreatePolicyCommand, CreatePolicyType, 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, - TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, + CreatePolicyCommand, CreatePolicyType, 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, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData, From 48de67367fa9952f860b31181d82b9d964524db2 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 24 Jul 2026 16:03:14 +0900 Subject: [PATCH 11/13] Parse the FDW name as a bare Ident, rename the shared OPTIONS helper - FDW name is a bare identifier in the PostgreSQL grammar; parse_identifier replaces parse_object_name, which accepted schema-qualified names the server rejects. Drop the test that pinned that relaxation. - parse_options_clause (was parse_foreign_data_wrapper_options_clause) is shared by CREATE SERVER, CREATE FOREIGN DATA WRAPPER, and CREATE FOREIGN TABLE. --- src/ast/ddl.rs | 7 ++++--- src/parser/mod.rs | 12 +++++------- tests/sqlparser_postgres.rs | 13 ------------- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index b574d6fa2f..9db445c950 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6005,8 +6005,9 @@ impl ForeignDataWrapperRoutineClause { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct CreateForeignDataWrapper { - /// The name of the foreign-data wrapper. Can be schema-qualified. - pub name: ObjectName, + /// 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. @@ -6044,7 +6045,7 @@ impl Spanned for CreateForeignDataWrapper { _ => None, }; Span::union_iter( - core::iter::once(self.name.span()) + core::iter::once(self.name.span) .chain(routine_span(&self.handler)) .chain(routine_span(&self.validator)), ) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index df7a3b929a..8d871c41d5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -20204,7 +20204,7 @@ impl<'a> Parser<'a> { self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?; let foreign_data_wrapper = self.parse_object_name(false)?; - let options = self.parse_foreign_data_wrapper_options_clause()?; + let options = self.parse_options_clause()?; Ok(Statement::CreateServer(CreateServerStatement { name, @@ -20218,9 +20218,7 @@ impl<'a> Parser<'a> { /// Parse an optional `OPTIONS ( key value [, ...] )` clause shared by /// `CREATE SERVER`, `CREATE FOREIGN DATA WRAPPER`, and `CREATE FOREIGN TABLE`. - fn parse_foreign_data_wrapper_options_clause( - &mut self, - ) -> Result>, ParserError> { + fn parse_options_clause(&mut self) -> Result>, ParserError> { if !self.parse_keyword(Keyword::OPTIONS) { return Ok(None); } @@ -20259,10 +20257,10 @@ impl<'a> Parser<'a> { pub fn parse_create_foreign_data_wrapper( &mut self, ) -> Result { - let name = self.parse_object_name(false)?; + let name = self.parse_identifier()?; let handler = self.parse_foreign_data_wrapper_routine_clause(Keyword::HANDLER)?; let validator = self.parse_foreign_data_wrapper_routine_clause(Keyword::VALIDATOR)?; - let options = self.parse_foreign_data_wrapper_options_clause()?; + let options = self.parse_options_clause()?; Ok(CreateForeignDataWrapper { name, @@ -20281,7 +20279,7 @@ impl<'a> Parser<'a> { let (columns, constraints) = self.parse_columns()?; self.expect_keyword_is(Keyword::SERVER)?; let server_name = self.parse_identifier()?; - let options = self.parse_foreign_data_wrapper_options_clause()?; + let options = self.parse_options_clause()?; Ok(CreateForeignTable { name, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 306f32d565..b81f146341 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9606,19 +9606,6 @@ fn parse_create_foreign_table_with_check_constraint() { assert_eq!(stmt.constraints.len(), 1); } -#[test] -fn parse_create_foreign_data_wrapper_with_schema_qualified_name() { - // NOTE: PostgreSQL FDW names are unqualified per the spec; a schema-qualified - // name like `myschema.myfdw` is non-conformant input. This is a deliberate - // parser relaxation (we accept and round-trip it via ObjectName rather than - // rejecting it), so this test enshrines that relaxation on purpose. - let sql = "CREATE FOREIGN DATA WRAPPER myschema.myfdw"; - let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { - unreachable!() - }; - assert_eq!(stmt.name.to_string(), "myschema.myfdw"); -} - #[test] fn exclude_as_column_name() { // `EXCLUDE` is a non-reserved keyword, so it stays usable as a column name From 18ff915962404be42da9f9ead266863b7131072d Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 24 Jul 2026 16:08:15 +0900 Subject: [PATCH 12/13] Address review: rename OPTIONS helper, add a negative FDW-name test - parse_generic_options_clause (was parse_options_clause) disambiguates from the neighbouring parse_options, which returns Vec. - Assert CREATE FOREIGN DATA WRAPPER myschema.myfdw errors, locking the bare-identifier rule the previous commit switched to. --- src/parser/mod.rs | 10 ++++++---- tests/sqlparser_postgres.rs | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8d871c41d5..abd71d0e55 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -20204,7 +20204,7 @@ impl<'a> Parser<'a> { self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?; let foreign_data_wrapper = self.parse_object_name(false)?; - let options = self.parse_options_clause()?; + let options = self.parse_generic_options_clause()?; Ok(Statement::CreateServer(CreateServerStatement { name, @@ -20218,7 +20218,9 @@ impl<'a> Parser<'a> { /// Parse an optional `OPTIONS ( key value [, ...] )` clause shared by /// `CREATE SERVER`, `CREATE FOREIGN DATA WRAPPER`, and `CREATE FOREIGN TABLE`. - fn parse_options_clause(&mut self) -> Result>, ParserError> { + fn parse_generic_options_clause( + &mut self, + ) -> Result>, ParserError> { if !self.parse_keyword(Keyword::OPTIONS) { return Ok(None); } @@ -20260,7 +20262,7 @@ impl<'a> Parser<'a> { let name = self.parse_identifier()?; let handler = self.parse_foreign_data_wrapper_routine_clause(Keyword::HANDLER)?; let validator = self.parse_foreign_data_wrapper_routine_clause(Keyword::VALIDATOR)?; - let options = self.parse_options_clause()?; + let options = self.parse_generic_options_clause()?; Ok(CreateForeignDataWrapper { name, @@ -20279,7 +20281,7 @@ impl<'a> Parser<'a> { let (columns, constraints) = self.parse_columns()?; self.expect_keyword_is(Keyword::SERVER)?; let server_name = self.parse_identifier()?; - let options = self.parse_options_clause()?; + let options = self.parse_generic_options_clause()?; Ok(CreateForeignTable { name, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b81f146341..714605a565 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9541,6 +9541,12 @@ fn parse_create_foreign_data_wrapper() { 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] From 538c4189f1e2dcca7c77ad132d913dff11dd8490 Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 24 Jul 2026 17:15:22 +0900 Subject: [PATCH 13/13] Document generic OPTIONS reuse of CreateServerOption doc comment said CREATE SERVER only; the struct now backs the OPTIONS clause for CREATE SERVER, CREATE FOREIGN DATA WRAPPER, and CREATE FOREIGN TABLE. public since 0.62.0, so documented, not renamed. also drops test comments that restated the next-line SQL. --- src/ast/mod.rs | 5 ++++- tests/sqlparser_postgres.rs | 8 -------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 168895a0a7..8ef038c7fe 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9101,7 +9101,10 @@ impl fmt::Display for CreateServerStatement { } } -/// A key/value option for `CREATE SERVER`. +/// A key/value entry in a Postgres `OPTIONS ( ... )` clause. The name is +/// historical (introduced in 0.62.0 for `CREATE SERVER`); it is now the +/// shared OPTIONS element for `CREATE SERVER`, `CREATE FOREIGN DATA WRAPPER`, +/// and `CREATE FOREIGN TABLE`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 714605a565..a3cc959136 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9462,7 +9462,6 @@ fn parse_lock_table() { #[test] fn parse_create_foreign_data_wrapper() { - // Minimal: name only. let sql = "CREATE FOREIGN DATA WRAPPER myfdw"; let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() @@ -9472,7 +9471,6 @@ fn parse_create_foreign_data_wrapper() { assert!(stmt.validator.is_none()); assert!(stmt.options.is_none()); - // With HANDLER. let sql = "CREATE FOREIGN DATA WRAPPER myfdw HANDLER myhandler"; let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() @@ -9484,14 +9482,12 @@ fn parse_create_foreign_data_wrapper() { ))) ); - // With NO HANDLER. 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)); - // With NO VALIDATOR. let sql = "CREATE FOREIGN DATA WRAPPER myfdw NO VALIDATOR"; let Statement::CreateForeignDataWrapper(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() @@ -9501,7 +9497,6 @@ fn parse_create_foreign_data_wrapper() { Some(ForeignDataWrapperRoutineClause::Absent) ); - // With HANDLER, VALIDATOR, and OPTIONS. 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!() @@ -9551,7 +9546,6 @@ fn parse_create_foreign_data_wrapper() { #[test] fn parse_create_foreign_table() { - // Basic: columns and SERVER. let sql = "CREATE FOREIGN TABLE ft1 (id INTEGER, name TEXT) SERVER myserver"; let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() @@ -9564,7 +9558,6 @@ fn parse_create_foreign_table() { assert_eq!(stmt.server_name.value, "myserver"); assert!(stmt.options.is_none()); - // With IF NOT EXISTS. let sql = "CREATE FOREIGN TABLE IF NOT EXISTS ft2 (col INTEGER) SERVER remoteserver"; let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { unreachable!() @@ -9572,7 +9565,6 @@ fn parse_create_foreign_table() { assert!(stmt.if_not_exists); assert_eq!(stmt.name.to_string(), "ft2"); - // With table-level OPTIONS. let sql = "CREATE FOREIGN TABLE ft3 (col INTEGER) SERVER remoteserver OPTIONS (schema_name 'public')"; let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else {