diff --git a/.worktrees/fix-pr-2307-ci b/.worktrees/fix-pr-2307-ci new file mode 160000 index 000000000..90803e045 --- /dev/null +++ b/.worktrees/fix-pr-2307-ci @@ -0,0 +1 @@ +Subproject commit 90803e045da78472949bda9a1edab5fa5ff37441 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1a35c4881 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Test Commands + +```bash +# Build +cargo build +cargo build --all-features + +# Run all tests +cargo test --all-features + +# Run a single test +cargo test test_name --all-features + +# Run tests for a specific dialect +cargo test sqlparser_postgres --all-features + +# Lint +cargo clippy --all-targets --all-features -- -D warnings + +# Format +cargo fmt --all + +# Check (faster than full build) +cargo check --all-targets --all-features + +# Build docs +cargo doc --document-private-items --no-deps --workspace --all-features + +# Run benchmarks (from sqlparser_bench directory) +cd sqlparser_bench && cargo bench +``` + +## Crate Features + +- `serde`: Adds Serialize/Deserialize for all AST nodes +- `visitor`: Adds a Visitor for recursively walking the AST +- `recursive-protection` (default): Stack overflow protection +- `json_example`: For CLI example only + +## Architecture + +This is an extensible SQL lexer and parser that produces an Abstract Syntax Tree (AST). + +### Core Components + +- **`src/tokenizer.rs`**: Lexer that converts SQL text into tokens. `Tokenizer::new(dialect, sql).tokenize()` returns `Vec`. + +- **`src/parser/mod.rs`**: Recursive descent parser using Pratt parsing for expressions. Entry point is `Parser::parse_sql(&dialect, sql)` returning `Vec`. + +- **`src/ast/mod.rs`**: AST type definitions. `Statement` is the top-level enum. Key types: `Query`, `Select`, `Expr`, `DataType`, `ObjectName`. + +- **`src/dialect/mod.rs`**: SQL dialect trait and implementations. Each dialect (PostgreSQL, MySQL, etc.) customizes parsing behavior. `GenericDialect` is the most permissive. + +### Dialect System + +Dialects customize parsing via the `Dialect` trait. Methods control identifier quoting, keyword handling, and syntax variations. Dialect-specific features should work with both the specific dialect AND `GenericDialect`. + +### Testing Patterns + +Tests use `TestedDialects` from `src/test_utils.rs`: + +```rust +use sqlparser::test_utils::*; + +// Test across all dialects +all_dialects().verified_stmt("SELECT 1"); + +// Test specific dialects +TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]).verified_stmt("..."); + +// Test all dialects except specific ones +all_dialects_except(|d| d.is::()).verified_stmt("..."); +``` + +Key test helpers: +- `verified_stmt(sql)`: Parse and verify round-trip serialization +- `verified_query(sql)`: Same but returns `Query` +- `one_statement_parses_to(sql, canonical)`: Test with different canonical form + +### Round-Trip Invariant + +AST nodes implement `Display` to reproduce the original SQL (minus comments/whitespace). Tests verify `parse(sql).to_string() == sql`. + +### Source Spans + +AST nodes include `Span` information for source locations. When constructing AST nodes manually, use `Span::empty()`. diff --git a/Cargo.toml b/Cargo.toml index 80d8b6903..9c6ffb078 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,14 @@ # under the License. [package] -name = "sqlparser" -description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.61.0" -authors = ["Apache DataFusion "] -homepage = "https://github.com/apache/datafusion-sqlparser-rs" -documentation = "https://docs.rs/sqlparser/" -keywords = ["ansi", "sql", "lexer", "parser"] -repository = "https://github.com/apache/datafusion-sqlparser-rs" +name = "pgmold-sqlparser" +description = "Fork of sqlparser with additional PostgreSQL features (PARTITION OF, SECURITY DEFINER/INVOKER, SET params, EXCLUDE, TEXT SEARCH, AGGREGATE, FOREIGN TABLE/FDW)" +version = "0.60.8" +authors = ["Filipe Guerreiro "] +homepage = "https://github.com/fmguerreiro/datafusion-sqlparser-rs" +documentation = "https://docs.rs/pgmold-sqlparser/" +keywords = ["ansi", "sql", "lexer", "parser", "postgresql"] +repository = "https://github.com/fmguerreiro/datafusion-sqlparser-rs" license = "Apache-2.0" include = [ "src/**/*.rs", diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 285eec505..9ccd6c4f8 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -480,6 +480,11 @@ pub enum DataType { /// /// [PostgreSQL]: https://www.postgresql.org/docs/current/plpgsql-trigger.html Trigger, + /// SETOF type modifier for [PostgreSQL] function return types, + /// e.g. `CREATE FUNCTION ... RETURNS SETOF text`. + /// + /// [PostgreSQL]: https://www.postgresql.org/docs/current/sql-createfunction.html + SetOf(Box), /// Any data type, used in BigQuery UDF definitions for templated parameters, see [BigQuery]. /// /// [BigQuery]: https://cloud.google.com/bigquery/docs/user-defined-functions#templated-sql-udf-parameters @@ -796,6 +801,7 @@ impl fmt::Display for DataType { } DataType::Unspecified => Ok(()), DataType::Trigger => write!(f, "TRIGGER"), + DataType::SetOf(inner) => write!(f, "SETOF {inner}"), DataType::AnyType => write!(f, "ANY TYPE"), DataType::Table(fields) => match fields { Some(fields) => { diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1f0432909..e0f203a8d 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, @@ -3490,6 +3491,7 @@ impl fmt::Display for DistStyle { } } + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -5756,3 +5758,489 @@ 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) + } +} + +/// CREATE AGGREGATE statement. +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateAggregate { + /// True if `OR REPLACE` was specified. + pub or_replace: bool, + /// The aggregate name (can be schema-qualified). + pub name: ObjectName, + /// Input argument types. Empty for zero-argument aggregates. + pub args: Vec, + /// The options listed inside the required parentheses after the argument + /// list (e.g. `SFUNC`, `STYPE`, `FINALFUNC`, `PARALLEL`, …). + pub options: Vec, +} + +impl fmt::Display for CreateAggregate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE")?; + if self.or_replace { + write!(f, " OR REPLACE")?; + } + write!(f, " AGGREGATE {}", self.name)?; + write!(f, " ({})", display_comma_separated(&self.args))?; + write!(f, " (")?; + for (i, option) in self.options.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{option}")?; + } + write!(f, ")") + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateAggregate) -> Self { + crate::ast::Statement::CreateAggregate(v) + } +} + +/// A single option in a `CREATE AGGREGATE` options list. +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateAggregateOption { + /// `SFUNC = state_transition_function` + Sfunc(ObjectName), + /// `STYPE = state_data_type` + Stype(DataType), + /// `SSPACE = state_data_size` (in bytes) + Sspace(u64), + /// `FINALFUNC = final_function` + Finalfunc(ObjectName), + /// `FINALFUNC_EXTRA` — pass extra dummy arguments to the final function. + FinalfuncExtra, + /// `FINALFUNC_MODIFY = { READ_ONLY | SHAREABLE | READ_WRITE }` + FinalfuncModify(AggregateModifyKind), + /// `COMBINEFUNC = combine_function` + Combinefunc(ObjectName), + /// `SERIALFUNC = serial_function` + Serialfunc(ObjectName), + /// `DESERIALFUNC = deserial_function` + Deserialfunc(ObjectName), + /// `INITCOND = initial_condition` (a string literal) + Initcond(Value), + /// `MSFUNC = moving_state_transition_function` + Msfunc(ObjectName), + /// `MINVFUNC = moving_inverse_transition_function` + Minvfunc(ObjectName), + /// `MSTYPE = moving_state_data_type` + Mstype(DataType), + /// `MSSPACE = moving_state_data_size` (in bytes) + Msspace(u64), + /// `MFINALFUNC = moving_final_function` + Mfinalfunc(ObjectName), + /// `MFINALFUNC_EXTRA` + MfinalfuncExtra, + /// `MFINALFUNC_MODIFY = { READ_ONLY | SHAREABLE | READ_WRITE }` + MfinalfuncModify(AggregateModifyKind), + /// `MINITCOND = moving_initial_condition` (a string literal) + Minitcond(Value), + /// `SORTOP = sort_operator` + Sortop(ObjectName), + /// `PARALLEL = { SAFE | RESTRICTED | UNSAFE }` + Parallel(FunctionParallel), + /// `HYPOTHETICAL` — marks the aggregate as hypothetical-set. + Hypothetical, +} + +impl fmt::Display for CreateAggregateOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Sfunc(name) => write!(f, "SFUNC = {name}"), + Self::Stype(data_type) => write!(f, "STYPE = {data_type}"), + Self::Sspace(size) => write!(f, "SSPACE = {size}"), + Self::Finalfunc(name) => write!(f, "FINALFUNC = {name}"), + Self::FinalfuncExtra => write!(f, "FINALFUNC_EXTRA"), + Self::FinalfuncModify(kind) => write!(f, "FINALFUNC_MODIFY = {kind}"), + Self::Combinefunc(name) => write!(f, "COMBINEFUNC = {name}"), + Self::Serialfunc(name) => write!(f, "SERIALFUNC = {name}"), + Self::Deserialfunc(name) => write!(f, "DESERIALFUNC = {name}"), + Self::Initcond(cond) => write!(f, "INITCOND = {cond}"), + Self::Msfunc(name) => write!(f, "MSFUNC = {name}"), + Self::Minvfunc(name) => write!(f, "MINVFUNC = {name}"), + Self::Mstype(data_type) => write!(f, "MSTYPE = {data_type}"), + Self::Msspace(size) => write!(f, "MSSPACE = {size}"), + Self::Mfinalfunc(name) => write!(f, "MFINALFUNC = {name}"), + Self::MfinalfuncExtra => write!(f, "MFINALFUNC_EXTRA"), + Self::MfinalfuncModify(kind) => write!(f, "MFINALFUNC_MODIFY = {kind}"), + Self::Minitcond(cond) => write!(f, "MINITCOND = {cond}"), + Self::Sortop(name) => write!(f, "SORTOP = {name}"), + Self::Parallel(parallel) => { + let kind = match parallel { + FunctionParallel::Safe => "SAFE", + FunctionParallel::Restricted => "RESTRICTED", + FunctionParallel::Unsafe => "UNSAFE", + }; + write!(f, "PARALLEL = {kind}") + } + Self::Hypothetical => write!(f, "HYPOTHETICAL"), + } + } +} + +/// Modifier kind for `FINALFUNC_MODIFY` / `MFINALFUNC_MODIFY` in `CREATE AGGREGATE`. +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AggregateModifyKind { + /// The final function does not modify the transition state. + ReadOnly, + /// The transition state may be shared between aggregate calls. + Shareable, + /// The final function may modify the transition state. + ReadWrite, +} + +impl fmt::Display for AggregateModifyKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ReadOnly => write!(f, "READ_ONLY"), + Self::Shareable => write!(f, "SHAREABLE"), + Self::ReadWrite => write!(f, "READ_WRITE"), + } + } +} + +/// `CREATE TEXT SEARCH CONFIGURATION` statement. +/// +/// Note: this is a PostgreSQL-specific statement. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateTextSearchConfiguration { + /// Name of the text search configuration being created. + pub name: ObjectName, + /// Options list — must include `PARSER = parser_name`. + pub options: Vec, +} + +impl fmt::Display for CreateTextSearchConfiguration { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH CONFIGURATION {name} ({options})", + name = self.name, + options = display_comma_separated(&self.options), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTextSearchConfiguration) -> Self { + crate::ast::Statement::CreateTextSearchConfiguration(v) + } +} + +/// `CREATE TEXT SEARCH DICTIONARY` statement. +/// +/// Note: this is a PostgreSQL-specific statement. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateTextSearchDictionary { + /// Name of the text search dictionary being created. + pub name: ObjectName, + /// Options list — must include `TEMPLATE = template_name`. + pub options: Vec, +} + +impl fmt::Display for CreateTextSearchDictionary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH DICTIONARY {name} ({options})", + name = self.name, + options = display_comma_separated(&self.options), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTextSearchDictionary) -> Self { + crate::ast::Statement::CreateTextSearchDictionary(v) + } +} + +/// `CREATE TEXT SEARCH PARSER` statement. +/// +/// Note: this is a PostgreSQL-specific statement. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateTextSearchParser { + /// Name of the text search parser being created. + pub name: ObjectName, + /// Options list — must include `START`, `GETTOKEN`, `END`, `LEXTYPES` (and optionally `HEADLINE`). + pub options: Vec, +} + +impl fmt::Display for CreateTextSearchParser { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH PARSER {name} ({options})", + name = self.name, + options = display_comma_separated(&self.options), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTextSearchParser) -> Self { + crate::ast::Statement::CreateTextSearchParser(v) + } +} + +/// `CREATE TEXT SEARCH TEMPLATE` statement. +/// +/// Note: this is a PostgreSQL-specific statement. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateTextSearchTemplate { + /// Name of the text search template being created. + pub name: ObjectName, + /// Options list — must include `LEXIZE` (and optionally `INIT`). + pub options: Vec, +} + +impl fmt::Display for CreateTextSearchTemplate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE TEXT SEARCH TEMPLATE {name} ({options})", + name = self.name, + options = display_comma_separated(&self.options), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTextSearchTemplate) -> Self { + crate::ast::Statement::CreateTextSearchTemplate(v) + } +} + +/// The target of a `CREATE PUBLICATION` statement: which rows to publish. +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum PublicationTarget { + /// `FOR ALL TABLES` + AllTables, + /// `FOR TABLE table [, ...]` + Tables(Vec), + /// `FOR TABLES IN SCHEMA schema [, ...]` + TablesInSchema(Vec), +} + +impl fmt::Display for PublicationTarget { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PublicationTarget::AllTables => write!(f, "FOR ALL TABLES"), + PublicationTarget::Tables(tables) => { + write!(f, "FOR TABLE {}", display_comma_separated(tables)) + } + PublicationTarget::TablesInSchema(schemas) => { + write!( + f, + "FOR TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + } + } +} + +/// A `CREATE PUBLICATION` statement. +/// +/// Note: this is a PostgreSQL-specific statement. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreatePublication { + /// The publication name. + pub name: Ident, + /// Optional target specification (`FOR ALL TABLES`, `FOR TABLE ...`, or `FOR TABLES IN SCHEMA ...`). + pub target: Option, + /// Optional `WITH (key = value, ...)` clause. + pub with_options: Vec, +} + +impl fmt::Display for CreatePublication { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE PUBLICATION {}", self.name)?; + if let Some(target) = &self.target { + write!(f, " {target}")?; + } + if !self.with_options.is_empty() { + write!(f, " WITH ({})", display_comma_separated(&self.with_options))?; + } + Ok(()) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreatePublication) -> Self { + crate::ast::Statement::CreatePublication(v) + } +} + +/// A `CREATE SUBSCRIPTION` statement. +/// +/// Note: this is a PostgreSQL-specific statement. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateSubscription { + /// The subscription name. + pub name: Ident, + /// The `CONNECTION 'conninfo'` string. + pub connection: Value, + /// The `PUBLICATION publication_name [, ...]` list. + pub publications: Vec, + /// Optional `WITH (key = value, ...)` clause. + pub with_options: Vec, +} + +impl fmt::Display for CreateSubscription { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE SUBSCRIPTION {name} CONNECTION {connection} PUBLICATION {publications}", + name = self.name, + connection = self.connection, + publications = display_comma_separated(&self.publications), + )?; + if !self.with_options.is_empty() { + write!(f, " WITH ({})", display_comma_separated(&self.with_options))?; + } + Ok(()) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateSubscription) -> Self { + crate::ast::Statement::CreateSubscription(v) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 63b3db644..7ad65c358 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -67,13 +67,17 @@ pub use self::ddl::{ AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, 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, + AggregateModifyKind, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, + ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, + CreateAggregateOption, CreateCollation, CreateCollationDefinition, CreateConnector, + CreateDomain, CreateExtension, CreateForeignDataWrapper, CreateForeignTable, CreateFunction, + CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, + CreatePolicyCommand, CreatePolicyType, CreatePublication, CreateSubscription, CreateTable, + CreateTextSearchConfiguration, CreateTextSearchDictionary, CreateTextSearchParser, + CreateTextSearchTemplate, CreateTrigger, PublicationTarget, + 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, @@ -138,8 +142,9 @@ mod dml; pub mod helpers; pub mod table_constraints; pub use table_constraints::{ - CheckConstraint, ConstraintUsingIndex, ForeignKeyConstraint, FullTextOrSpatialConstraint, - IndexConstraint, PrimaryKeyConstraint, TableConstraint, UniqueConstraint, + CheckConstraint, ConstraintUsingIndex, ExclusionConstraint, ExclusionElement, + ForeignKeyConstraint, FullTextOrSpatialConstraint, IndexConstraint, PrimaryKeyConstraint, + TableConstraint, UniqueConstraint, }; mod operator; mod query; @@ -3698,6 +3703,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) @@ -3723,6 +3738,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createopclass.html) CreateOperatorClass(CreateOperatorClass), /// ```sql + /// CREATE AGGREGATE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createaggregate.html) + CreateAggregate(CreateAggregate), + /// ```sql /// ALTER TABLE /// ``` AlterTable(AlterTable), @@ -3974,6 +3994,42 @@ pub enum Statement { /// CreateCollation(CreateCollation), /// ```sql + /// CREATE TEXT SEARCH CONFIGURATION name ( PARSER = parser_name ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTextSearchConfiguration(CreateTextSearchConfiguration), + /// ```sql + /// CREATE TEXT SEARCH DICTIONARY name ( TEMPLATE = template_name [, option = value, ...] ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTextSearchDictionary(CreateTextSearchDictionary), + /// ```sql + /// CREATE TEXT SEARCH PARSER name ( START = start_fn, GETTOKEN = gettoken_fn, END = end_fn, LEXTYPES = lextypes_fn [, HEADLINE = headline_fn] ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTextSearchParser(CreateTextSearchParser), + /// ```sql + /// CREATE TEXT SEARCH TEMPLATE name ( [INIT = init_fn,] LEXIZE = lexize_fn ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTextSearchTemplate(CreateTextSearchTemplate), + /// ```sql + /// CREATE PUBLICATION name [ FOR ALL TABLES | FOR TABLE table [, ...] | FOR TABLES IN SCHEMA schema [, ...] ] [ WITH ( option = value [, ...] ) ] + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreatePublication(CreatePublication), + /// ```sql + /// CREATE SUBSCRIPTION name CONNECTION 'conninfo' PUBLICATION publication_name [, ...] [ WITH ( option = value [, ...] ) ] + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateSubscription(CreateSubscription), + /// ```sql /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] /// ``` /// Note: this is a PostgreSQL-specific statement. @@ -5456,6 +5512,12 @@ impl fmt::Display for Statement { Statement::CreateIndex(create_index) => create_index.fmt(f), Statement::CreateExtension(create_extension) => write!(f, "{create_extension}"), Statement::CreateCollation(create_collation) => write!(f, "{create_collation}"), + Statement::CreateTextSearchConfiguration(v) => write!(f, "{v}"), + Statement::CreateTextSearchDictionary(v) => write!(f, "{v}"), + Statement::CreateTextSearchParser(v) => write!(f, "{v}"), + Statement::CreateTextSearchTemplate(v) => write!(f, "{v}"), + Statement::CreatePublication(v) => write!(f, "{v}"), + Statement::CreateSubscription(v) => write!(f, "{v}"), Statement::DropExtension(drop_extension) => write!(f, "{drop_extension}"), Statement::DropOperator(drop_operator) => write!(f, "{drop_operator}"), Statement::DropOperatorFamily(drop_operator_family) => { @@ -5503,6 +5565,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), @@ -5510,6 +5574,7 @@ impl fmt::Display for Statement { create_operator_family.fmt(f) } Statement::CreateOperatorClass(create_operator_class) => create_operator_class.fmt(f), + Statement::CreateAggregate(create_aggregate) => create_aggregate.fmt(f), Statement::AlterTable(alter_table) => write!(f, "{alter_table}"), Statement::AlterIndex { name, operation } => { write!(f, "ALTER INDEX {name} {operation}") diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e7a8f94f2..f4e56f3f5 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -265,6 +265,10 @@ impl Spanned for Values { /// - [Statement::Declare] /// - [Statement::CreateExtension] /// - [Statement::CreateCollation] +/// - [Statement::CreateTextSearchConfiguration] +/// - [Statement::CreateTextSearchDictionary] +/// - [Statement::CreateTextSearchParser] +/// - [Statement::CreateTextSearchTemplate] /// - [Statement::AlterCollation] /// - [Statement::Fetch] /// - [Statement::Flush] @@ -380,12 +384,20 @@ impl Spanned for Statement { Statement::CreateRole(create_role) => create_role.span(), Statement::CreateExtension(create_extension) => create_extension.span(), Statement::CreateCollation(create_collation) => create_collation.span(), + Statement::CreateTextSearchConfiguration(_) => Span::empty(), + Statement::CreateTextSearchDictionary(_) => Span::empty(), + Statement::CreateTextSearchParser(_) => Span::empty(), + Statement::CreateTextSearchTemplate(_) => Span::empty(), + Statement::CreatePublication(_) => Span::empty(), + Statement::CreateSubscription(_) => Span::empty(), Statement::DropExtension(drop_extension) => drop_extension.span(), Statement::DropOperator(drop_operator) => drop_operator.span(), Statement::DropOperatorFamily(drop_operator_family) => drop_operator_family.span(), 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) => { @@ -511,6 +523,7 @@ impl Spanned for Statement { Statement::Vacuum(..) => Span::empty(), Statement::AlterUser(..) => Span::empty(), Statement::Reset(..) => Span::empty(), + Statement::CreateAggregate(_) => Span::empty(), } } } @@ -612,6 +625,31 @@ impl Spanned for CreateTable { } } +impl Spanned for PartitionBoundValue { + fn span(&self) -> Span { + match self { + PartitionBoundValue::Expr(expr) => expr.span(), + PartitionBoundValue::MinValue => Span::empty(), + PartitionBoundValue::MaxValue => Span::empty(), + } + } +} + +impl Spanned for ForValues { + fn span(&self) -> Span { + match self { + ForValues::In(exprs) => union_spans(exprs.iter().map(|e| e.span())), + ForValues::From { from, to } => union_spans( + from.iter() + .map(|v| v.span()) + .chain(to.iter().map(|v| v.span())), + ), + ForValues::With { .. } => Span::empty(), + ForValues::Default => Span::empty(), + } + } +} + impl Spanned for ColumnDef { fn span(&self) -> Span { let ColumnDef { @@ -641,39 +679,13 @@ impl Spanned for TableConstraint { TableConstraint::Check(constraint) => constraint.span(), TableConstraint::Index(constraint) => constraint.span(), TableConstraint::FulltextOrSpatial(constraint) => constraint.span(), + TableConstraint::Exclusion(constraint) => constraint.span(), TableConstraint::PrimaryKeyUsingIndex(constraint) | TableConstraint::UniqueUsingIndex(constraint) => constraint.span(), } } } -impl Spanned for PartitionBoundValue { - fn span(&self) -> Span { - match self { - PartitionBoundValue::Expr(expr) => expr.span(), - // MINVALUE and MAXVALUE are keywords without tracked spans - PartitionBoundValue::MinValue => Span::empty(), - PartitionBoundValue::MaxValue => Span::empty(), - } - } -} - -impl Spanned for ForValues { - fn span(&self) -> Span { - match self { - ForValues::In(exprs) => union_spans(exprs.iter().map(|e| e.span())), - ForValues::From { from, to } => union_spans( - from.iter() - .map(|v| v.span()) - .chain(to.iter().map(|v| v.span())), - ), - // WITH (MODULUS n, REMAINDER r) - u64 values have no spans - ForValues::With { .. } => Span::empty(), - ForValues::Default => Span::empty(), - } - } -} - impl Spanned for CreateIndex { fn span(&self) -> Span { let CreateIndex { @@ -1115,6 +1127,8 @@ impl Spanned for AlterTableOperation { partition, } => name.span.union_opt(&partition.as_ref().map(|i| i.span)), AlterTableOperation::DisableRowLevelSecurity => Span::empty(), + AlterTableOperation::ForceRowLevelSecurity => Span::empty(), + AlterTableOperation::NoForceRowLevelSecurity => Span::empty(), AlterTableOperation::DisableRule { name } => name.span, AlterTableOperation::DisableTrigger { name } => name.span, AlterTableOperation::DropConstraint { diff --git a/src/ast/table_constraints.rs b/src/ast/table_constraints.rs index 9ba196a81..3c37fa407 100644 --- a/src/ast/table_constraints.rs +++ b/src/ast/table_constraints.rs @@ -101,6 +101,9 @@ pub enum TableConstraint { /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html /// [2]: https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html FulltextOrSpatial(FullTextOrSpatialConstraint), + /// PostgreSQL `EXCLUDE` constraint: + /// `[ CONSTRAINT ] EXCLUDE [ USING ] ( WITH [, ...] ) [ INCLUDE () ] [ WHERE () ]` + Exclusion(ExclusionConstraint), /// PostgreSQL [definition][1] for promoting an existing unique index to a /// `PRIMARY KEY` constraint: /// @@ -155,6 +158,12 @@ impl From for TableConstraint { } } +impl From for TableConstraint { + fn from(constraint: ExclusionConstraint) -> Self { + TableConstraint::Exclusion(constraint) + } +} + impl fmt::Display for TableConstraint { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -164,6 +173,7 @@ impl fmt::Display for TableConstraint { TableConstraint::Check(constraint) => constraint.fmt(f), TableConstraint::Index(constraint) => constraint.fmt(f), TableConstraint::FulltextOrSpatial(constraint) => constraint.fmt(f), + TableConstraint::Exclusion(constraint) => constraint.fmt(f), TableConstraint::PrimaryKeyUsingIndex(c) => c.fmt_with_keyword(f, "PRIMARY KEY"), TableConstraint::UniqueUsingIndex(c) => c.fmt_with_keyword(f, "UNIQUE"), } @@ -554,6 +564,83 @@ impl crate::ast::Spanned for UniqueConstraint { } } +/// One element in an `EXCLUDE` constraint's element list: +/// ` WITH ` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExclusionElement { + /// The column or expression to exclude on. + pub expr: Expr, + /// The operator to use for the exclusion check (e.g. `=`, `&&`). + pub operator: String, +} + +impl fmt::Display for ExclusionElement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} WITH {}", self.expr, self.operator) + } +} + +/// PostgreSQL `EXCLUDE` constraint: +/// `[ CONSTRAINT ] EXCLUDE [ USING ] ( WITH [, ...] ) [ INCLUDE () ] [ WHERE () ]` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExclusionConstraint { + /// Optional constraint name. + pub name: Option, + /// Index access method (e.g. `gist`, `btree`). Defaults to `gist` if omitted. + pub index_method: Option, + /// The list of `(element WITH operator)` pairs. + pub elements: Vec, + /// Columns to include in the index via `INCLUDE (...)`. + pub include: Vec, + /// Optional `WHERE (predicate)` for a partial exclusion constraint. + pub where_clause: Option>, + /// `DEFERRABLE` / `INITIALLY DEFERRED` characteristics. + pub characteristics: Option, +} + +impl fmt::Display for ExclusionConstraint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use crate::ast::ddl::display_constraint_name; + write!(f, "{}EXCLUDE", display_constraint_name(&self.name))?; + if let Some(method) = &self.index_method { + write!(f, " USING {method}")?; + } + write!(f, " ({})", display_comma_separated(&self.elements))?; + if !self.include.is_empty() { + write!(f, " INCLUDE ({})", display_comma_separated(&self.include))?; + } + if let Some(predicate) = &self.where_clause { + write!(f, " WHERE ({predicate})")?; + } + if let Some(characteristics) = &self.characteristics { + write!(f, " {characteristics}")?; + } + Ok(()) + } +} + +impl crate::ast::Spanned for ExclusionConstraint { + fn span(&self) -> Span { + fn union_spans>(iter: I) -> Span { + Span::union_iter(iter) + } + + union_spans( + self.name + .iter() + .map(|i| i.span) + .chain(self.index_method.iter().map(|i| i.span)) + .chain(self.include.iter().map(|i| i.span)) + .chain(self.where_clause.iter().map(|e| e.span())) + .chain(self.characteristics.iter().map(|c| c.span())), + ) + } +} + /// PostgreSQL constraint that promotes an existing unique index to a table constraint. /// /// `[ CONSTRAINT constraint_name ] { UNIQUE | PRIMARY KEY } USING INDEX index_name diff --git a/src/keywords.rs b/src/keywords.rs index 808e5f03d..d86e04569 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -244,6 +244,7 @@ define_keywords!( COMPRESSION, COMPUPDATE, COMPUTE, + CONFIGURATION, CONCURRENTLY, CONDITION, CONFLICT, @@ -333,6 +334,7 @@ define_keywords!( DETACH, DETAIL, DETERMINISTIC, + DICTIONARY, DIMENSIONS, DIRECTORY, DISABLE, @@ -476,6 +478,7 @@ define_keywords!( GROUPING, GROUPS, GZIP, + HANDLER, HASH, HASHES, HAVING, @@ -765,6 +768,7 @@ define_keywords!( PARALLEL, PARAMETER, PARQUET, + PARSER, PART, PARTIAL, PARTITION, @@ -818,6 +822,7 @@ define_keywords!( PROGRAM, PROJECTION, PUBLIC, + PUBLICATION, PURCHASE, PURGE, QUALIFY, @@ -1006,6 +1011,7 @@ define_keywords!( STRUCT, SUBMULTISET, SUBSCRIPT, + SUBSCRIPTION, SUBSTR, SUBSTRING, SUBSTRING_REGEX, @@ -1035,6 +1041,7 @@ define_keywords!( TASK, TBLPROPERTIES, TEMP, + TEMPLATE, TEMPORARY, TEMPTABLE, TERMINATED, @@ -1130,6 +1137,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 a5526723b..9e193ec57 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5173,6 +5173,8 @@ impl<'a> Parser<'a> { self.parse_create_secret(or_replace, temporary, persistent) } else if self.parse_keyword(Keyword::USER) { self.parse_create_user(or_replace).map(Into::into) + } else if self.parse_keyword(Keyword::AGGREGATE) { + self.parse_create_aggregate(or_replace).map(Into::into) } else if or_replace { self.expected_ref( "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE", @@ -5213,6 +5215,23 @@ 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::TEXT, Keyword::SEARCH]) { + self.parse_create_text_search() + } else if self.parse_keyword(Keyword::PUBLICATION) { + self.parse_create_publication().map(Into::into) + } else if self.parse_keyword(Keyword::SUBSCRIPTION) { + self.parse_create_subscription().map(Into::into) } else { self.expected_ref("an object type after CREATE", self.peek_token_ref()) } @@ -7210,6 +7229,194 @@ impl<'a> Parser<'a> { }) } + /// Parse a [Statement::CreateAggregate] + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createaggregate.html) + pub fn parse_create_aggregate( + &mut self, + or_replace: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + + // Argument type list: `(input_data_type [, ...])` or `(*)` for zero-arg. + self.expect_token(&Token::LParen)?; + let args = if self.consume_token(&Token::Mul) { + // zero-argument aggregate written as `(*)` — treat as empty arg list. + vec![] + } else if self.consume_token(&Token::RParen) { + self.prev_token(); + vec![] + } else { + let parsed = self.parse_comma_separated(|p| p.parse_data_type())?; + parsed + }; + self.expect_token(&Token::RParen)?; + + // Options block: `( SFUNC = ..., STYPE = ..., ... )` + self.expect_token(&Token::LParen)?; + let mut options: Vec = Vec::new(); + loop { + let token = self.next_token(); + match &token.token { + Token::RParen => break, + Token::Comma => continue, + Token::Word(word) => { + let option = self.parse_create_aggregate_option(&word.value.to_uppercase())?; + options.push(option); + } + other => { + return Err(ParserError::ParserError(format!( + "Unexpected token in CREATE AGGREGATE options: {other:?}" + ))); + } + } + } + + Ok(CreateAggregate { + or_replace, + name, + args, + options, + }) + } + + fn parse_create_aggregate_option( + &mut self, + key: &str, + ) -> Result { + match key { + "SFUNC" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Sfunc( + self.parse_object_name(false)?, + )) + } + "STYPE" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Stype(self.parse_data_type()?)) + } + "SSPACE" => { + self.expect_token(&Token::Eq)?; + let size = self.parse_literal_uint()?; + Ok(CreateAggregateOption::Sspace(size)) + } + "FINALFUNC" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Finalfunc( + self.parse_object_name(false)?, + )) + } + "FINALFUNC_EXTRA" => Ok(CreateAggregateOption::FinalfuncExtra), + "FINALFUNC_MODIFY" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::FinalfuncModify( + self.parse_aggregate_modify_kind()?, + )) + } + "COMBINEFUNC" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Combinefunc( + self.parse_object_name(false)?, + )) + } + "SERIALFUNC" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Serialfunc( + self.parse_object_name(false)?, + )) + } + "DESERIALFUNC" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Deserialfunc( + self.parse_object_name(false)?, + )) + } + "INITCOND" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Initcond(self.parse_value()?.value)) + } + "MSFUNC" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Msfunc( + self.parse_object_name(false)?, + )) + } + "MINVFUNC" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Minvfunc( + self.parse_object_name(false)?, + )) + } + "MSTYPE" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Mstype(self.parse_data_type()?)) + } + "MSSPACE" => { + self.expect_token(&Token::Eq)?; + let size = self.parse_literal_uint()?; + Ok(CreateAggregateOption::Msspace(size)) + } + "MFINALFUNC" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Mfinalfunc( + self.parse_object_name(false)?, + )) + } + "MFINALFUNC_EXTRA" => Ok(CreateAggregateOption::MfinalfuncExtra), + "MFINALFUNC_MODIFY" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::MfinalfuncModify( + self.parse_aggregate_modify_kind()?, + )) + } + "MINITCOND" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Minitcond(self.parse_value()?.value)) + } + "SORTOP" => { + self.expect_token(&Token::Eq)?; + Ok(CreateAggregateOption::Sortop( + self.parse_object_name(false)?, + )) + } + "PARALLEL" => { + self.expect_token(&Token::Eq)?; + let parallel = match self.expect_one_of_keywords(&[ + Keyword::SAFE, + Keyword::RESTRICTED, + Keyword::UNSAFE, + ])? { + Keyword::SAFE => FunctionParallel::Safe, + Keyword::RESTRICTED => FunctionParallel::Restricted, + Keyword::UNSAFE => FunctionParallel::Unsafe, + _ => unreachable!(), + }; + Ok(CreateAggregateOption::Parallel(parallel)) + } + "HYPOTHETICAL" => Ok(CreateAggregateOption::Hypothetical), + other => Err(ParserError::ParserError(format!( + "Unknown CREATE AGGREGATE option: {other}" + ))), + } + } + + fn parse_aggregate_modify_kind(&mut self) -> Result { + let token = self.next_token(); + match &token.token { + Token::Word(word) => match word.value.to_uppercase().as_str() { + "READ_ONLY" => Ok(AggregateModifyKind::ReadOnly), + "SHAREABLE" => Ok(AggregateModifyKind::Shareable), + "READ_WRITE" => Ok(AggregateModifyKind::ReadWrite), + other => Err(ParserError::ParserError(format!( + "Expected READ_ONLY, SHAREABLE, or READ_WRITE, got: {other}" + ))), + }, + other => Err(ParserError::ParserError(format!( + "Expected READ_ONLY, SHAREABLE, or READ_WRITE, got: {other:?}" + ))), + } + } + /// Parse a [Statement::CreateOperatorFamily] /// /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createopfamily.html) @@ -8177,6 +8384,49 @@ impl<'a> Parser<'a> { }) } + /// Parse a PostgreSQL-specific `CREATE TEXT SEARCH CONFIGURATION | DICTIONARY | PARSER | TEMPLATE` statement. + pub fn parse_create_text_search(&mut self) -> Result { + if self.parse_keyword(Keyword::CONFIGURATION) { + let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; + self.expect_token(&Token::RParen)?; + Ok(Statement::CreateTextSearchConfiguration( + CreateTextSearchConfiguration { name, options }, + )) + } else if self.parse_keyword(Keyword::DICTIONARY) { + let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; + self.expect_token(&Token::RParen)?; + Ok(Statement::CreateTextSearchDictionary( + CreateTextSearchDictionary { name, options }, + )) + } else if self.parse_keyword(Keyword::PARSER) { + let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; + self.expect_token(&Token::RParen)?; + Ok(Statement::CreateTextSearchParser(CreateTextSearchParser { + name, + options, + })) + } else if self.parse_keyword(Keyword::TEMPLATE) { + let name = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; + self.expect_token(&Token::RParen)?; + Ok(Statement::CreateTextSearchTemplate( + CreateTextSearchTemplate { name, options }, + )) + } else { + self.expected_ref( + "CONFIGURATION, DICTIONARY, PARSER, or TEMPLATE after CREATE TEXT SEARCH", + self.peek_token_ref(), + ) + } + } + /// Parse a PostgreSQL-specific [Statement::DropExtension] statement. pub fn parse_drop_extension(&mut self) -> Result { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); @@ -8475,7 +8725,6 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let table_name = self.parse_object_name(allow_unquoted_hyphen)?; - // PostgreSQL PARTITION OF for child partition tables // Note: This is a PostgreSQL-specific feature, but the dialect check was intentionally // removed to allow GenericDialect and other dialects to parse this syntax. This enables // multi-dialect SQL tools to work with PostgreSQL-specific DDL statements. @@ -9915,6 +10164,50 @@ impl<'a> Parser<'a> { .into(), )) } + Token::Word(w) if w.keyword == Keyword::EXCLUDE => { + let index_method = if self.parse_keyword(Keyword::USING) { + Some(self.parse_identifier()?) + } else { + None + }; + + self.expect_token(&Token::LParen)?; + let elements = + self.parse_comma_separated(|p| p.parse_exclusion_element())?; + self.expect_token(&Token::RParen)?; + + let include = if self.parse_keyword(Keyword::INCLUDE) { + self.expect_token(&Token::LParen)?; + let cols = self.parse_comma_separated(|p| p.parse_identifier())?; + self.expect_token(&Token::RParen)?; + cols + } else { + vec![] + }; + + let where_clause = if self.parse_keyword(Keyword::WHERE) { + self.expect_token(&Token::LParen)?; + let predicate = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + Some(Box::new(predicate)) + } else { + None + }; + + let characteristics = self.parse_constraint_characteristics()?; + + Ok(Some( + ExclusionConstraint { + name, + index_method, + elements, + include, + where_clause, + characteristics, + } + .into(), + )) + } _ => { if name.is_some() { self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", next_token) @@ -9926,6 +10219,14 @@ impl<'a> Parser<'a> { } } + fn parse_exclusion_element(&mut self) -> Result { + let expr = self.parse_expr()?; + self.expect_keyword_is(Keyword::WITH)?; + let operator_token = self.next_token(); + let operator = operator_token.token.to_string(); + Ok(ExclusionElement { expr, operator }) + } + fn parse_optional_nulls_distinct(&mut self) -> Result { Ok(if self.parse_keyword(Keyword::NULLS) { let not = self.parse_keyword(Keyword::NOT); @@ -12651,6 +12952,10 @@ impl<'a> Parser<'a> { Ok(DataType::Tuple(field_defs)) } Keyword::TRIGGER => Ok(DataType::Trigger), + Keyword::SETOF => { + let inner = self.parse_data_type()?; + Ok(DataType::SetOf(Box::new(inner))) + } Keyword::ANY if self.peek_keyword(Keyword::TYPE) => { let _ = self.parse_keyword(Keyword::TYPE); Ok(DataType::AnyType) @@ -19702,6 +20007,139 @@ 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, + }) + } + + /// Parse a `CREATE PUBLICATION` statement. + /// + /// See + pub fn parse_create_publication(&mut self) -> Result { + let name = self.parse_identifier()?; + + let target = if self.parse_keyword(Keyword::FOR) { + if self.parse_keywords(&[Keyword::ALL, Keyword::TABLES]) { + Some(PublicationTarget::AllTables) + } else if self.parse_keyword(Keyword::TABLE) { + let tables = self.parse_comma_separated(|p| p.parse_object_name(false))?; + Some(PublicationTarget::Tables(tables)) + } else if self.parse_keywords(&[Keyword::TABLES, Keyword::IN, Keyword::SCHEMA]) { + let schemas = self.parse_comma_separated(|p| p.parse_identifier())?; + Some(PublicationTarget::TablesInSchema(schemas)) + } else { + return self.expected_ref( + "ALL TABLES, TABLE, or TABLES IN SCHEMA after FOR", + self.peek_token_ref(), + ); + } + } else { + None + }; + + let with_options = self.parse_options(Keyword::WITH)?; + + Ok(CreatePublication { + name, + target, + with_options, + }) + } + + /// Parse a `CREATE SUBSCRIPTION` statement. + /// + /// See + pub fn parse_create_subscription(&mut self) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::CONNECTION)?; + let connection = self.parse_value()?.value; + self.expect_keyword_is(Keyword::PUBLICATION)?; + let publications = self.parse_comma_separated(|p| p.parse_identifier())?; + let with_options = self.parse_options(Keyword::WITH)?; + + Ok(CreateSubscription { + name, + connection, + publications, + with_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 07b62dd93..314c4f51d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -947,6 +947,138 @@ fn parse_alter_collation() { ); } +#[test] +fn parse_create_text_search_configuration() { + assert_eq!( + pg().verified_stmt( + "CREATE TEXT SEARCH CONFIGURATION public.myconfig (PARSER = myparser)" + ), + Statement::CreateTextSearchConfiguration(CreateTextSearchConfiguration { + name: ObjectName::from(vec![Ident::new("public"), Ident::new("myconfig")]), + options: vec![SqlOption::KeyValue { + key: Ident::new("PARSER"), + value: Expr::Identifier(Ident::new("myparser")), + }], + }) + ); + + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH CONFIGURATION myconfig PARSER = pg_catalog.default"), + Err(ParserError::ParserError( + "Expected: (, found: PARSER".to_string() + )) + ); +} + +#[test] +fn parse_create_text_search_dictionary() { + assert_eq!( + pg().verified_stmt( + "CREATE TEXT SEARCH DICTIONARY public.mydict (TEMPLATE = snowball, language = english)" + ), + Statement::CreateTextSearchDictionary(CreateTextSearchDictionary { + name: ObjectName::from(vec![Ident::new("public"), Ident::new("mydict")]), + options: vec![ + SqlOption::KeyValue { + key: Ident::new("TEMPLATE"), + value: Expr::Identifier(Ident::new("snowball")), + }, + SqlOption::KeyValue { + key: Ident::new("language"), + value: Expr::Identifier(Ident::new("english")), + }, + ], + }) + ); + + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH DICTIONARY mydict"), + Err(ParserError::ParserError( + "Expected: (, found: EOF".to_string() + )) + ); +} + +#[test] +fn parse_create_text_search_parser() { + assert_eq!( + pg().verified_stmt( + "CREATE TEXT SEARCH PARSER myparser (START = prsd_start, GETTOKEN = prsd_nexttoken, END = prsd_end, LEXTYPES = prsd_lextype, HEADLINE = prsd_headline)" + ), + Statement::CreateTextSearchParser(CreateTextSearchParser { + name: ObjectName::from(vec![Ident::new("myparser")]), + options: vec![ + SqlOption::KeyValue { + key: Ident::new("START"), + value: Expr::Identifier(Ident::new("prsd_start")), + }, + SqlOption::KeyValue { + key: Ident::new("GETTOKEN"), + value: Expr::Identifier(Ident::new("prsd_nexttoken")), + }, + SqlOption::KeyValue { + key: Ident::new("END"), + value: Expr::Identifier(Ident::new("prsd_end")), + }, + SqlOption::KeyValue { + key: Ident::new("LEXTYPES"), + value: Expr::Identifier(Ident::new("prsd_lextype")), + }, + SqlOption::KeyValue { + key: Ident::new("HEADLINE"), + value: Expr::Identifier(Ident::new("prsd_headline")), + }, + ], + }) + ); + + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH PARSER myparser START = prsd_start"), + Err(ParserError::ParserError( + "Expected: (, found: START".to_string() + )) + ); +} + +#[test] +fn parse_create_text_search_template() { + assert_eq!( + pg().verified_stmt( + "CREATE TEXT SEARCH TEMPLATE mytemplate (INIT = dinit, LEXIZE = dlexize)" + ), + Statement::CreateTextSearchTemplate(CreateTextSearchTemplate { + name: ObjectName::from(vec![Ident::new("mytemplate")]), + options: vec![ + SqlOption::KeyValue { + key: Ident::new("INIT"), + value: Expr::Identifier(Ident::new("dinit")), + }, + SqlOption::KeyValue { + key: Ident::new("LEXIZE"), + value: Expr::Identifier(Ident::new("dlexize")), + }, + ], + }) + ); + + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH TEMPLATE mytemplate LEXIZE = dlexize"), + Err(ParserError::ParserError( + "Expected: (, found: LEXIZE".to_string() + )) + ); +} + +#[test] +fn parse_create_text_search_invalid_subtype() { + assert_eq!( + pg().parse_sql_statements("CREATE TEXT SEARCH UNKNOWN myname (option = value)"), + Err(ParserError::ParserError( + "Expected: CONFIGURATION, DICTIONARY, PARSER, or TEMPLATE after CREATE TEXT SEARCH, found: UNKNOWN".to_string() + )) + ); +} + #[test] fn parse_drop_and_comment_collation_ast() { assert_eq!( @@ -9134,6 +9266,140 @@ fn parse_pg_analyze() { } } +#[test] +fn parse_exclude_constraint_basic() { + let sql = + "CREATE TABLE t (room INT, CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =))"; + match pg().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!(1, create_table.constraints.len()); + match &create_table.constraints[0] { + TableConstraint::Exclusion(c) => { + assert_eq!(c.name, Some(Ident::new("no_overlap"))); + assert_eq!(c.index_method, Some(Ident::new("gist"))); + assert_eq!(c.elements.len(), 1); + assert_eq!(c.elements[0].operator, "="); + assert_eq!(c.include.len(), 0); + assert!(c.where_clause.is_none()); + } + other => panic!("Expected Exclusion, got {other:?}"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_exclude_constraint_multi_element() { + let sql = + "CREATE TABLE t (room INT, during INT, EXCLUDE USING gist (room WITH =, during WITH &&))"; + match pg().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!(1, create_table.constraints.len()); + match &create_table.constraints[0] { + TableConstraint::Exclusion(c) => { + assert!(c.name.is_none()); + assert_eq!(c.index_method, Some(Ident::new("gist"))); + assert_eq!(c.elements.len(), 2); + assert_eq!(c.elements[0].operator, "="); + assert_eq!(c.elements[1].operator, "&&"); + } + other => panic!("Expected Exclusion, got {other:?}"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_exclude_constraint_with_where() { + let sql = + "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) WHERE (col > 0))"; + match pg().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!(1, create_table.constraints.len()); + match &create_table.constraints[0] { + TableConstraint::Exclusion(c) => { + assert!(c.where_clause.is_some()); + } + other => panic!("Expected Exclusion, got {other:?}"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_exclude_constraint_with_include() { + let sql = + "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) INCLUDE (col))"; + match pg().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!(1, create_table.constraints.len()); + match &create_table.constraints[0] { + TableConstraint::Exclusion(c) => { + assert_eq!(c.include, vec![Ident::new("col")]); + } + other => panic!("Expected Exclusion, got {other:?}"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_exclude_constraint_no_using() { + let sql = "CREATE TABLE t (col INT, EXCLUDE (col WITH =))"; + match pg().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!(1, create_table.constraints.len()); + match &create_table.constraints[0] { + TableConstraint::Exclusion(c) => { + assert!(c.index_method.is_none()); + } + other => panic!("Expected Exclusion, got {other:?}"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_exclude_constraint_deferrable() { + let sql = + "CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =) DEFERRABLE INITIALLY DEFERRED)"; + match pg().verified_stmt(sql) { + Statement::CreateTable(create_table) => { + assert_eq!(1, create_table.constraints.len()); + match &create_table.constraints[0] { + TableConstraint::Exclusion(c) => { + let characteristics = c.characteristics.as_ref().unwrap(); + assert_eq!(characteristics.deferrable, Some(true)); + assert_eq!( + characteristics.initially, + Some(DeferrableInitial::Deferred) + ); + } + other => panic!("Expected Exclusion, got {other:?}"), + } + } + _ => panic!("Expected CreateTable"), + } +} + +#[test] +fn parse_exclude_constraint_in_alter_table() { + let sql = + "ALTER TABLE t ADD CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =)"; + pg().verified_stmt(sql); +} + +#[test] +fn roundtrip_exclude_constraint() { + let sql = "CREATE TABLE t (CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =, during WITH &&) INCLUDE (id) WHERE (active = true))"; + pg().verified_stmt(sql); +} + #[test] fn parse_lock_table() { pg_and_generic().one_statement_parses_to( @@ -9193,3 +9459,265 @@ 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"); +} + +#[test] +fn parse_create_aggregate_basic() { + let sql = "CREATE AGGREGATE myavg (NUMERIC) (SFUNC = numeric_avg_accum, STYPE = internal, FINALFUNC = numeric_avg, INITCOND = '0')"; + let stmt = pg().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(!agg.or_replace); + assert_eq!(agg.name.to_string(), "myavg"); + assert_eq!(agg.args.len(), 1); + assert_eq!(agg.args[0].to_string(), "NUMERIC"); + assert_eq!(agg.options.len(), 4); + assert_eq!( + agg.options[0].to_string(), + "SFUNC = numeric_avg_accum" + ); + assert_eq!(agg.options[1].to_string(), "STYPE = internal"); + assert_eq!(agg.options[2].to_string(), "FINALFUNC = numeric_avg"); + assert_eq!(agg.options[3].to_string(), "INITCOND = '0'"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_or_replace_with_parallel() { + let sql = "CREATE OR REPLACE AGGREGATE sum2 (INT4, INT4) (SFUNC = int4pl, STYPE = INT4, PARALLEL = SAFE)"; + let stmt = pg().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(agg.or_replace); + assert_eq!(agg.name.to_string(), "sum2"); + assert_eq!(agg.args.len(), 2); + assert_eq!(agg.options.len(), 3); + assert_eq!(agg.options[2].to_string(), "PARALLEL = SAFE"); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_aggregate_with_moving_aggregate_options() { + let sql = "CREATE AGGREGATE moving_sum (FLOAT8) (SFUNC = float8pl, STYPE = FLOAT8, MSFUNC = float8pl, MINVFUNC = float8mi, MSTYPE = FLOAT8, MFINALFUNC_EXTRA, MFINALFUNC_MODIFY = READ_ONLY)"; + let stmt = pg().verified_stmt(sql); + match stmt { + Statement::CreateAggregate(agg) => { + assert!(!agg.or_replace); + assert_eq!(agg.name.to_string(), "moving_sum"); + assert_eq!(agg.args.len(), 1); + assert_eq!(agg.options.len(), 7); + assert_eq!(agg.options[4].to_string(), "MSTYPE = FLOAT8"); + assert_eq!(agg.options[5].to_string(), "MFINALFUNC_EXTRA"); + assert_eq!( + agg.options[6].to_string(), + "MFINALFUNC_MODIFY = READ_ONLY" + ); + } + _ => panic!("Expected CreateAggregate, got: {stmt:?}"), + } +} + +#[test] +fn parse_create_publication_basic() { + let sql = "CREATE PUBLICATION mypub FOR TABLE public.t"; + let Statement::CreatePublication(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "mypub"); + assert!(stmt.with_options.is_empty()); + match stmt.target.unwrap() { + PublicationTarget::Tables(tables) => { + assert_eq!(tables.len(), 1); + assert_eq!(tables[0].to_string(), "public.t"); + } + other => panic!("unexpected target: {other:?}"), + } +} + +#[test] +fn parse_create_publication_for_all_tables() { + let sql = "CREATE PUBLICATION mypub FOR ALL TABLES"; + let Statement::CreatePublication(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "mypub"); + assert!(matches!(stmt.target, Some(PublicationTarget::AllTables))); + assert!(stmt.with_options.is_empty()); +} + +#[test] +fn parse_create_publication_for_tables_in_schema() { + let sql = "CREATE PUBLICATION mypub FOR TABLES IN SCHEMA myschema"; + let Statement::CreatePublication(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "mypub"); + match stmt.target.unwrap() { + PublicationTarget::TablesInSchema(schemas) => { + assert_eq!(schemas.len(), 1); + assert_eq!(schemas[0].value, "myschema"); + } + other => panic!("unexpected target: {other:?}"), + } +} + +#[test] +fn parse_create_publication_with_options() { + let sql = "CREATE PUBLICATION mypub FOR ALL TABLES WITH (publish = 'insert, update')"; + let Statement::CreatePublication(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "mypub"); + assert!(matches!(stmt.target, Some(PublicationTarget::AllTables))); + assert_eq!(stmt.with_options.len(), 1); + match &stmt.with_options[0] { + SqlOption::KeyValue { key, value } => { + assert_eq!(key.value, "publish"); + assert_eq!(value.to_string(), "'insert, update'"); + } + other => panic!("unexpected option: {other:?}"), + } +} + +#[test] +fn parse_create_subscription_basic() { + let sql = "CREATE SUBSCRIPTION mysub CONNECTION 'host=localhost' PUBLICATION mypub"; + let Statement::CreateSubscription(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "mysub"); + assert_eq!(stmt.connection.to_string(), "'host=localhost'"); + assert_eq!(stmt.publications.len(), 1); + assert_eq!(stmt.publications[0].value, "mypub"); + assert!(stmt.with_options.is_empty()); +} + +#[test] +fn parse_create_subscription_with_options() { + let sql = "CREATE SUBSCRIPTION mysub CONNECTION 'host=localhost dbname=mydb' PUBLICATION mypub, otherpub WITH (copy_data = true, slot_name = 'myslot')"; + let Statement::CreateSubscription(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "mysub"); + assert_eq!( + stmt.connection.to_string(), + "'host=localhost dbname=mydb'" + ); + assert_eq!(stmt.publications.len(), 2); + assert_eq!(stmt.publications[0].value, "mypub"); + assert_eq!(stmt.publications[1].value, "otherpub"); + assert_eq!(stmt.with_options.len(), 2); + match &stmt.with_options[0] { + SqlOption::KeyValue { key, value } => { + assert_eq!(key.value, "copy_data"); + assert_eq!(value.to_string(), "true"); + } + other => panic!("unexpected option: {other:?}"), + } + match &stmt.with_options[1] { + SqlOption::KeyValue { key, value } => { + assert_eq!(key.value, "slot_name"); + assert_eq!(value.to_string(), "'myslot'"); + } + other => panic!("unexpected option: {other:?}"), + } +}