From 894abf3cf16a2c16aae735158b2c27c25aaf4e5b Mon Sep 17 00:00:00 2001 From: Filipe Guerreiro Date: Fri, 17 Apr 2026 11:21:57 +0900 Subject: [PATCH 1/7] 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 67aefb392..e2dc6ebeb 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 886bea26d..17ac34242 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 adc1443fc..d5e1c2cdb 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 808e5f03d..1ae28f0aa 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 2/7] 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 7501919a0..398bd25c6 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 3/7] 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 86315b1ef..93c32033b 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 4/7] 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 e2dc6ebeb..04ba196ef 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 398bd25c6..16d731c00 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 5/7] 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 04ba196ef..1cdeb91bc 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 16d731c00..5fb42ff40 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 93c32033b..4fb255913 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 6/7] 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 d5e1c2cdb..a465f8f87 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 7/7] 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 5fb42ff40..e8b70c9b5 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); }