diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 67aefb392..1cdeb91bc 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -42,15 +42,15 @@ use crate::ast::{ UniqueConstraint, }, ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody, - CreateFunctionUsing, 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, + 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, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; @@ -5757,3 +5757,118 @@ impl From for crate::ast::Statement { crate::ast::Statement::AlterPolicy(v) } } + +/// 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))] +pub enum FdwRoutineClause { + /// A named function, e.g. `HANDLER myhandler` or `VALIDATOR myvalidator`. + Function(ObjectName), + /// 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. +/// +/// 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. 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. + pub validator: Option, + /// Optional `OPTIONS (key 'value', ...)` clause. + pub options: Option>, +} + +impl fmt::Display for CreateForeignDataWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "CREATE FOREIGN DATA WRAPPER {}", self.name)?; + if let Some(handler) = &self.handler { + handler.fmt_with_label(f, "HANDLER")?; + } + if let Some(validator) = &self.validator { + validator.fmt_with_label(f, "VALIDATOR")?; + } + if let Some(options) = &self.options { + write!(f, " OPTIONS ({})", display_comma_separated(options))?; + } + Ok(()) + } +} + +impl From for crate::ast::Statement { + fn from(v: 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, + /// 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. + 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}", + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + name = self.name, + columns = display_comma_separated(&self.columns), + )?; + 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))?; + } + 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..a465f8f87 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(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) => { 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, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7501919a0..e8b70c9b5 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()) } @@ -19695,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, @@ -19716,6 +19718,81 @@ 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_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, + 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 = self.parse_fdw_options_clause()?; + + Ok(CreateForeignTable { + name, + if_not_exists, + columns, + constraints, + server_name, + options, + }) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 86315b1ef..4fb255913 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9221,3 +9221,122 @@ 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.to_string(), "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::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::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 { + 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"); +} + +#[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"); +}