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..52284a8ba 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, PUBLICATION, SUBSCRIPTION, ALTER DOMAIN/TRIGGER/EXTENSION, CAST, CONVERSION, LANGUAGE, RULE, STATISTICS, ACCESS METHOD, EVENT TRIGGER, TRANSFORM, SECURITY LABEL, USER MAPPING, TABLESPACE)" +version = "0.60.11" +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..922464a1c 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, @@ -534,6 +535,14 @@ pub enum AlterTableOperation { /// Parenthesized options supplied to `SET (...)`. options: Vec, }, + /// `SET TABLESPACE tablespace_name` + /// + /// Note: this is a PostgreSQL-specific operation. + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertable.html) + SetTablespace { + /// The target tablespace name. + tablespace_name: Ident, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -699,6 +708,14 @@ pub enum AlterIndexOperation { /// The new name for the index. index_name: ObjectName, }, + /// `SET TABLESPACE tablespace_name` + /// + /// Note: this is a PostgreSQL-specific operation. + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterindex.html) + SetTablespace { + /// The target tablespace name. + tablespace_name: Ident, + }, } impl fmt::Display for AlterTableOperation { @@ -1044,6 +1061,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::SetOptionsParens { options } => { write!(f, "SET ({})", display_comma_separated(options)) } + AlterTableOperation::SetTablespace { tablespace_name } => { + write!(f, "SET TABLESPACE {tablespace_name}") + } } } } @@ -1054,6 +1074,9 @@ impl fmt::Display for AlterIndexOperation { AlterIndexOperation::RenameIndex { index_name } => { write!(f, "RENAME TO {index_name}") } + AlterIndexOperation::SetTablespace { tablespace_name } => { + write!(f, "SET TABLESPACE {tablespace_name}") + } } } } @@ -3490,6 +3513,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))] @@ -4340,6 +4364,11 @@ pub struct CreateView { pub to: Option, /// MySQL: Optional parameters for the view algorithm, definer, and security context pub params: Option, + /// PostgreSQL: `WITH [NO] DATA` clause on materialized views. + /// `None` means the clause was absent; `Some(true)` means `WITH DATA`; + /// `Some(false)` means `WITH NO DATA`. + /// + pub with_data: Option, } impl fmt::Display for CreateView { @@ -4406,6 +4435,11 @@ impl fmt::Display for CreateView { if self.with_no_schema_binding { write!(f, " WITH NO SCHEMA BINDING")?; } + match self.with_data { + Some(true) => write!(f, " WITH DATA")?, + Some(false) => write!(f, " WITH NO DATA")?, + None => {} + } Ok(()) } } @@ -5388,6 +5422,8 @@ pub enum AlterFunctionKind { Function, /// `AGGREGATE` Aggregate, + /// `PROCEDURE` + Procedure, } impl fmt::Display for AlterFunctionKind { @@ -5395,6 +5431,7 @@ impl fmt::Display for AlterFunctionKind { match self { Self::Function => write!(f, "FUNCTION"), Self::Aggregate => write!(f, "AGGREGATE"), + Self::Procedure => write!(f, "PROCEDURE"), } } } @@ -5469,7 +5506,7 @@ impl fmt::Display for AlterFunction { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "ALTER {} ", self.kind)?; match self.kind { - AlterFunctionKind::Function => { + AlterFunctionKind::Function | AlterFunctionKind::Procedure => { write!(f, "{} ", self.function)?; } AlterFunctionKind::Aggregate => { @@ -5756,3 +5793,1511 @@ 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) + } +} + +/// `ALTER DOMAIN` statement. +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterdomain.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 AlterDomain { + /// Name of the domain being altered. + pub name: ObjectName, + /// The operation to perform. + pub operation: AlterDomainOperation, +} + +/// An [AlterDomain] operation. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterDomainOperation { + /// `ADD CONSTRAINT constraint_name CHECK (expr) [NOT VALID]` + AddConstraint { + /// The constraint to add. + constraint: TableConstraint, + /// Whether `NOT VALID` was specified. + not_valid: bool, + }, + /// `DROP CONSTRAINT [IF EXISTS] constraint_name [CASCADE | RESTRICT]` + DropConstraint { + /// Whether `IF EXISTS` was specified. + if_exists: bool, + /// Name of the constraint to drop. + name: Ident, + /// Optional drop behavior. + drop_behavior: Option, + }, + /// `RENAME CONSTRAINT old_name TO new_name` + RenameConstraint { + /// Existing constraint name. + old_name: Ident, + /// New constraint name. + new_name: Ident, + }, + /// `OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER }` + OwnerTo(Owner), + /// `RENAME TO new_name` + RenameTo { + /// New name for the domain. + new_name: Ident, + }, + /// `SET SCHEMA schema_name` + SetSchema { + /// The target schema name. + schema_name: ObjectName, + }, + /// `SET DEFAULT expr` + SetDefault { + /// Default value expression. + default: Expr, + }, + /// `DROP DEFAULT` + DropDefault, + /// `VALIDATE CONSTRAINT constraint_name` + ValidateConstraint { + /// Name of the constraint to validate. + name: Ident, + }, +} + +impl fmt::Display for AlterDomain { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ALTER DOMAIN {} {}", self.name, self.operation) + } +} + +impl fmt::Display for AlterDomainOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterDomainOperation::AddConstraint { + constraint, + not_valid, + } => { + write!(f, "ADD {constraint}")?; + if *not_valid { + write!(f, " NOT VALID")?; + } + Ok(()) + } + AlterDomainOperation::DropConstraint { + if_exists, + name, + drop_behavior, + } => { + write!(f, "DROP CONSTRAINT")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {name}")?; + if let Some(behavior) = drop_behavior { + write!(f, " {behavior}")?; + } + Ok(()) + } + AlterDomainOperation::RenameConstraint { old_name, new_name } => { + write!(f, "RENAME CONSTRAINT {old_name} TO {new_name}") + } + AlterDomainOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterDomainOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterDomainOperation::SetSchema { schema_name } => { + write!(f, "SET SCHEMA {schema_name}") + } + AlterDomainOperation::SetDefault { default } => write!(f, "SET DEFAULT {default}"), + AlterDomainOperation::DropDefault => write!(f, "DROP DEFAULT"), + AlterDomainOperation::ValidateConstraint { name } => { + write!(f, "VALIDATE CONSTRAINT {name}") + } + } + } +} + +/// 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) + ) + } + } + } +} + +impl From for crate::ast::Statement { + fn from(a: AlterDomain) -> Self { + crate::ast::Statement::AlterDomain(a) + } +} + +/// `ALTER TRIGGER name ON table_name RENAME TO new_name` statement. +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertrigger.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 AlterTrigger { + /// Name of the trigger being altered. + pub name: Ident, + /// Name of the table the trigger is defined on. + pub table_name: ObjectName, + /// The operation to perform. + pub operation: AlterTriggerOperation, +} + +/// An [AlterTrigger] operation. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTriggerOperation { + /// `RENAME TO new_name` + RenameTo { + /// New name for the trigger. + new_name: Ident, + }, +} + +impl fmt::Display for AlterTrigger { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ALTER TRIGGER {} ON {} {}", + self.name, self.table_name, self.operation + ) + } +} + +impl fmt::Display for AlterTriggerOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterTriggerOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + } + } +} + +impl From for crate::ast::Statement { + fn from(a: AlterTrigger) -> Self { + crate::ast::Statement::AlterTrigger(a) + } +} + +/// `ALTER EXTENSION` statement. +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterextension.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 AlterExtension { + /// Name of the extension being altered. + pub name: Ident, + /// The operation to perform. + pub operation: AlterExtensionOperation, +} + +/// An [AlterExtension] operation. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterExtensionOperation { + /// `UPDATE [ TO new_version ]` + UpdateTo { + /// Optional target version string or identifier. + version: Option, + }, + /// `SET SCHEMA schema_name` + SetSchema { + /// The target schema name. + schema_name: ObjectName, + }, + /// `OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER }` + OwnerTo(Owner), + /// `RENAME TO new_name` + RenameTo { + /// New name for the extension. + new_name: Ident, + }, +} + +impl fmt::Display for AlterExtension { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ALTER EXTENSION {} {}", self.name, self.operation) + } +} + +impl fmt::Display for AlterExtensionOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterExtensionOperation::UpdateTo { version } => { + write!(f, "UPDATE")?; + if let Some(v) = version { + write!(f, " TO {v}")?; + } + Ok(()) + } + AlterExtensionOperation::SetSchema { schema_name } => { + write!(f, "SET SCHEMA {schema_name}") + } + AlterExtensionOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterExtensionOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + } + } +} + +impl From for crate::ast::Statement { + fn from(a: AlterExtension) -> Self { + crate::ast::Statement::AlterExtension(a) + } +} + +/// 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) + } +} + +/// The function binding kind for a `CREATE CAST` statement. +/// +/// Note: this is a PostgreSQL-specific construct. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CastFunctionKind { + /// `WITH FUNCTION function_name(arg_types)` + WithFunction { + /// The name of the cast implementation function. + function_name: ObjectName, + /// Optional argument type list. Empty if the function has no arguments + /// declared in the `CREATE CAST` clause. + argument_types: Vec, + }, + /// `WITHOUT FUNCTION` + WithoutFunction, + /// `WITH INOUT` + WithInout, +} + +impl fmt::Display for CastFunctionKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CastFunctionKind::WithFunction { + function_name, + argument_types, + } => { + write!(f, "WITH FUNCTION {function_name}")?; + if !argument_types.is_empty() { + write!(f, "({})", display_comma_separated(argument_types))?; + } + Ok(()) + } + CastFunctionKind::WithoutFunction => write!(f, "WITHOUT FUNCTION"), + CastFunctionKind::WithInout => write!(f, "WITH INOUT"), + } + } +} + +/// A kind of extended statistics collected by `CREATE STATISTICS`. +/// +/// Note: this is a PostgreSQL-specific concept. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum StatisticsKind { + /// `ndistinct` — n-distinct statistics + NDistinct, + /// `dependencies` — functional dependency statistics + Dependencies, + /// `mcv` — most-common-values statistics + Mcv, +} + +impl fmt::Display for StatisticsKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + StatisticsKind::NDistinct => write!(f, "ndistinct"), + StatisticsKind::Dependencies => write!(f, "dependencies"), + StatisticsKind::Mcv => write!(f, "mcv"), + } + } +} + +/// The object kind targeted by a `SECURITY LABEL` 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 enum SecurityLabelObjectKind { + /// `TABLE name` + Table, + /// `COLUMN name.colname` + Column, + /// `DATABASE name` + Database, + /// `DOMAIN name` + Domain, + /// `FUNCTION name` + Function, + /// `ROLE name` + Role, + /// `SCHEMA name` + Schema, + /// `SEQUENCE name` + Sequence, + /// `TYPE name` + Type, + /// `VIEW name` + View, + /// `MATERIALIZED VIEW name` + MaterializedView, +} + +impl fmt::Display for SecurityLabelObjectKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SecurityLabelObjectKind::Table => write!(f, "TABLE"), + SecurityLabelObjectKind::Column => write!(f, "COLUMN"), + SecurityLabelObjectKind::Database => write!(f, "DATABASE"), + SecurityLabelObjectKind::Domain => write!(f, "DOMAIN"), + SecurityLabelObjectKind::Function => write!(f, "FUNCTION"), + SecurityLabelObjectKind::Role => write!(f, "ROLE"), + SecurityLabelObjectKind::Schema => write!(f, "SCHEMA"), + SecurityLabelObjectKind::Sequence => write!(f, "SEQUENCE"), + SecurityLabelObjectKind::Type => write!(f, "TYPE"), + SecurityLabelObjectKind::View => write!(f, "VIEW"), + SecurityLabelObjectKind::MaterializedView => write!(f, "MATERIALIZED VIEW"), + } + } +} + +/// The context in which a cast may be invoked automatically. +/// +/// Note: this is a PostgreSQL-specific construct. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CastContext { + /// No `AS` clause — explicit cast only (default). + Explicit, + /// `AS ASSIGNMENT` + Assignment, + /// `AS IMPLICIT` + Implicit, +} + +impl fmt::Display for CastContext { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CastContext::Explicit => Ok(()), + CastContext::Assignment => write!(f, " AS ASSIGNMENT"), + CastContext::Implicit => write!(f, " AS IMPLICIT"), + } + } +} + +/// A `CREATE CAST` 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 CreateCast { + /// The source type. + pub source_type: DataType, + /// The target type. + pub target_type: DataType, + /// How the cast is implemented. + pub function_kind: CastFunctionKind, + /// The cast context (explicit, assignment, or implicit). + pub cast_context: CastContext, +} + +impl fmt::Display for CreateCast { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE CAST ({source} AS {target}) {function_kind}{context}", + source = self.source_type, + target = self.target_type, + function_kind = self.function_kind, + context = self.cast_context, + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateCast) -> Self { + crate::ast::Statement::CreateCast(v) + } +} + +/// A `CREATE CONVERSION` 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 CreateConversion { + /// The conversion name. + pub name: ObjectName, + /// Whether this is a `DEFAULT` conversion. + pub is_default: bool, + /// The source encoding name (a string literal like `'LATIN1'`). + pub source_encoding: String, + /// The destination encoding name (a string literal like `'UTF8'`). + pub destination_encoding: String, + /// The conversion function name. + pub function_name: ObjectName, +} + +impl fmt::Display for CreateConversion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE")?; + if self.is_default { + write!(f, " DEFAULT")?; + } + write!( + f, + " CONVERSION {name} FOR '{source}' TO '{destination}' FROM {function}", + name = self.name, + source = self.source_encoding, + destination = self.destination_encoding, + function = self.function_name, + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateConversion) -> Self { + crate::ast::Statement::CreateConversion(v) + } +} + +/// A `CREATE LANGUAGE` 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 CreateLanguage { + /// The language name. + pub name: Ident, + /// Whether `OR REPLACE` was specified. + pub or_replace: bool, + /// Whether `TRUSTED` was specified. + pub trusted: bool, + /// Whether `PROCEDURAL` was specified. + pub procedural: bool, + /// Optional `HANDLER handler_function` clause. + pub handler: Option, + /// Optional `INLINE inline_function` clause. + pub inline_handler: Option, + /// Optional `VALIDATOR validator_function` clause. + pub validator: Option, +} + +impl fmt::Display for CreateLanguage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE")?; + if self.or_replace { + write!(f, " OR REPLACE")?; + } + if self.trusted { + write!(f, " TRUSTED")?; + } + if self.procedural { + write!(f, " PROCEDURAL")?; + } + write!(f, " LANGUAGE {}", self.name)?; + if let Some(handler) = &self.handler { + write!(f, " HANDLER {handler}")?; + } + if let Some(inline) = &self.inline_handler { + write!(f, " INLINE {inline}")?; + } + if let Some(validator) = &self.validator { + write!(f, " VALIDATOR {validator}")?; + } + Ok(()) + } +} + +/// A `CREATE STATISTICS` 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 CreateStatistics { + /// Optional `IF NOT EXISTS` clause. + pub if_not_exists: bool, + /// The statistics object name, e.g. `public.s`. + pub name: ObjectName, + /// Optional `(ndistinct, dependencies, mcv)` kind list. + pub kinds: Vec, + /// The expressions (columns or arbitrary expressions) to collect statistics on. + pub on: Vec, + /// The table to collect statistics from. + pub from: ObjectName, +} + +impl fmt::Display for CreateStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE STATISTICS")?; + if self.if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " {}", self.name)?; + if !self.kinds.is_empty() { + write!(f, " ({})", display_comma_separated(&self.kinds))?; + } + write!(f, " ON {}", display_comma_separated(&self.on))?; + write!(f, " FROM {}", self.from)?; + Ok(()) + } +} + +/// A `SECURITY LABEL` 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 SecurityLabel { + /// Optional `FOR provider_name` clause. + pub provider: Option, + /// The kind of object the label is applied to. + pub object_kind: SecurityLabelObjectKind, + /// The name of the object the label is applied to. + pub object_name: ObjectName, + /// The label string, or `None` for `IS NULL`. + pub label: Option, +} + +impl fmt::Display for SecurityLabel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SECURITY LABEL")?; + if let Some(provider) = &self.provider { + write!(f, " FOR {provider}")?; + } + write!(f, " ON {} {}", self.object_kind, self.object_name)?; + write!(f, " IS ")?; + match &self.label { + Some(label) => write!(f, "{label}"), + None => write!(f, "NULL"), + } + } +} + +impl From for crate::ast::Statement { + fn from(v: SecurityLabel) -> Self { + crate::ast::Statement::SecurityLabel(v) + } +} + +/// The role specification in a `CREATE USER MAPPING` 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 enum UserMappingUser { + /// A specific role name. + Ident(Ident), + /// `USER` (current user) + User, + /// `CURRENT_ROLE` + CurrentRole, + /// `CURRENT_USER` + CurrentUser, + /// `PUBLIC` + Public, +} + +impl fmt::Display for UserMappingUser { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + UserMappingUser::Ident(ident) => write!(f, "{ident}"), + UserMappingUser::User => write!(f, "USER"), + UserMappingUser::CurrentRole => write!(f, "CURRENT_ROLE"), + UserMappingUser::CurrentUser => write!(f, "CURRENT_USER"), + UserMappingUser::Public => write!(f, "PUBLIC"), + } + } +} + +/// A `CREATE USER MAPPING` 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 CreateUserMapping { + /// `IF NOT EXISTS` + pub if_not_exists: bool, + /// The user/role for the mapping. + pub user: UserMappingUser, + /// The foreign server name. + pub server_name: Ident, + /// Optional `OPTIONS (key 'value', ...)` clause. + pub options: Option>, +} + +impl fmt::Display for CreateUserMapping { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE USER MAPPING")?; + if self.if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " FOR {} SERVER {}", self.user, 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: CreateLanguage) -> Self { + crate::ast::Statement::CreateLanguage(v) + } +} + +/// The event that triggers a rule. +/// +/// Note: this is a PostgreSQL-specific construct. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RuleEvent { + /// `ON SELECT` rule. + Select, + /// `ON INSERT` rule. + Insert, + /// `ON UPDATE` rule. + Update, + /// `ON DELETE` rule. + Delete, +} + +impl fmt::Display for RuleEvent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RuleEvent::Select => write!(f, "SELECT"), + RuleEvent::Insert => write!(f, "INSERT"), + RuleEvent::Update => write!(f, "UPDATE"), + RuleEvent::Delete => write!(f, "DELETE"), + } + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateStatistics) -> Self { + crate::ast::Statement::CreateStatistics(v) + } +} + +/// The type of access method in `CREATE ACCESS METHOD`. +/// +/// Note: this is a PostgreSQL-specific concept. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AccessMethodType { + /// `INDEX` — an index access method + Index, + /// `TABLE` — a table access method + Table, +} + +impl fmt::Display for AccessMethodType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AccessMethodType::Index => write!(f, "INDEX"), + AccessMethodType::Table => write!(f, "TABLE"), + } + } +} + +/// The action performed by a rule. +/// +/// Note: this is a PostgreSQL-specific construct. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum RuleAction { + /// `NOTHING` + Nothing, + /// One or more statements (parenthesized when more than one). + Statements(Vec), +} + +impl fmt::Display for RuleAction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RuleAction::Nothing => write!(f, "NOTHING"), + RuleAction::Statements(stmts) => { + if stmts.len() == 1 { + write!(f, "{}", stmts[0]) + } else { + write!(f, "(")?; + for (i, stmt) in stmts.iter().enumerate() { + if i > 0 { + write!(f, "; ")?; + } + write!(f, "{stmt}")?; + } + write!(f, ")") + } + } + } + } +} + +/// A `CREATE RULE` 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 CreateRule { + /// The rule name. + pub name: Ident, + /// The event that triggers the rule. + pub event: RuleEvent, + /// The table the rule applies to. + pub table: ObjectName, + /// Optional `WHERE condition` clause. + pub condition: Option, + /// Whether the rule is `INSTEAD` (true) or `ALSO` (false). + pub instead: bool, + /// The action(s) taken by the rule. + pub action: RuleAction, +} + +impl fmt::Display for CreateRule { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE RULE {name} AS ON {event} TO {table}", + name = self.name, + event = self.event, + table = self.table, + )?; + if let Some(condition) = &self.condition { + write!(f, " WHERE {condition}")?; + } + write!(f, " DO")?; + if self.instead { + write!(f, " INSTEAD")?; + } else { + write!(f, " ALSO")?; + } + write!(f, " {}", self.action) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateRule) -> Self { + crate::ast::Statement::CreateRule(v) + } +} + +/// A `CREATE ACCESS METHOD` 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 CreateAccessMethod { + /// The access method name. + pub name: Ident, + /// `TYPE INDEX | TABLE` + pub method_type: AccessMethodType, + /// `HANDLER handler_function` + pub handler: ObjectName, +} + +impl fmt::Display for CreateAccessMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE ACCESS METHOD {name} TYPE {method_type} HANDLER {handler}", + name = self.name, + method_type = self.method_type, + handler = self.handler, + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateAccessMethod) -> Self { + crate::ast::Statement::CreateAccessMethod(v) + } +} + +/// An event name for `CREATE EVENT TRIGGER`. +/// +/// Note: this is a PostgreSQL-specific concept. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum EventTriggerEvent { + /// `ddl_command_start` + DdlCommandStart, + /// `ddl_command_end` + DdlCommandEnd, + /// `table_rewrite` + TableRewrite, + /// `sql_drop` + SqlDrop, +} + +impl fmt::Display for EventTriggerEvent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + EventTriggerEvent::DdlCommandStart => write!(f, "ddl_command_start"), + EventTriggerEvent::DdlCommandEnd => write!(f, "ddl_command_end"), + EventTriggerEvent::TableRewrite => write!(f, "table_rewrite"), + EventTriggerEvent::SqlDrop => write!(f, "sql_drop"), + } + } +} + +/// A `CREATE EVENT TRIGGER` 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 CreateEventTrigger { + /// The trigger name. + pub name: Ident, + /// The event that fires the trigger. + pub event: EventTriggerEvent, + /// Optional `WHEN TAG IN ('tag', ...)` filter. + pub when_tags: Option>, + /// The handler function name (from `EXECUTE FUNCTION name()`). + pub execute: ObjectName, + /// Whether `PROCEDURE` was used instead of `FUNCTION` (older alias). + pub is_procedure: bool, +} + +impl fmt::Display for CreateEventTrigger { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE EVENT TRIGGER {} ON {}", self.name, self.event)?; + if let Some(tags) = &self.when_tags { + write!(f, " WHEN TAG IN ({})", display_comma_separated(tags))?; + } + let func_kw = if self.is_procedure { + "PROCEDURE" + } else { + "FUNCTION" + }; + write!(f, " EXECUTE {func_kw} {}()", self.execute)?; + Ok(()) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateUserMapping) -> Self { + crate::ast::Statement::CreateUserMapping(v) + } +} + +/// A `CREATE TABLESPACE` 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 CreateTablespace { + /// The tablespace name. + pub name: Ident, + /// Optional `OWNER role` clause. + pub owner: Option, + /// The `LOCATION 'directory'` string. + pub location: Value, + /// Optional `WITH (option = value, ...)` clause. + pub with_options: Vec, +} + +impl fmt::Display for CreateTablespace { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE TABLESPACE {}", self.name)?; + if let Some(owner) = &self.owner { + write!(f, " OWNER {owner}")?; + } + write!(f, " LOCATION {}", self.location)?; + 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: CreateEventTrigger) -> Self { + crate::ast::Statement::CreateEventTrigger(v) + } +} + +/// A single element in a `CREATE TRANSFORM` transform list. +/// +/// Either `FROM SQL WITH FUNCTION name(arg_types)` or `TO SQL WITH FUNCTION name(arg_types)`. +/// +/// Note: this is a PostgreSQL-specific concept. +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct TransformElement { + /// `true` = FROM SQL, `false` = TO SQL + pub is_from: bool, + /// The function name. + pub function: ObjectName, + /// The argument type list (may be empty). + pub arg_types: Vec, +} + +impl fmt::Display for TransformElement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let direction = if self.is_from { "FROM" } else { "TO" }; + write!( + f, + "{direction} SQL WITH FUNCTION {}({})", + self.function, + display_comma_separated(&self.arg_types), + ) + } +} + +/// A `CREATE TRANSFORM` 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 CreateTransform { + /// Whether `OR REPLACE` was specified. + pub or_replace: bool, + /// The data type being transformed. + pub type_name: DataType, + /// The procedural language name. + pub language: Ident, + /// The list of transform elements (FROM SQL and/or TO SQL). + pub elements: Vec, +} + +impl fmt::Display for CreateTransform { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CREATE")?; + if self.or_replace { + write!(f, " OR REPLACE")?; + } + write!( + f, + " TRANSFORM FOR {} LANGUAGE {} ({})", + self.type_name, + self.language, + display_comma_separated(&self.elements), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTransform) -> Self { + crate::ast::Statement::CreateTransform(v) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateTablespace) -> Self { + crate::ast::Statement::CreateTablespace(v) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 63b3db644..baf7203d0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -60,28 +60,37 @@ pub use self::dcl::{ SetConfigValue, Use, }; pub use self::ddl::{ - Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner, - AlterFunction, AlterFunctionAction, AlterFunctionKind, AlterFunctionOperation, - AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, - AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, - AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, - AlterTableLock, AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, - AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, + AccessMethodType, AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, + AlterColumnOperation, AlterConnectorOwner, AlterDomain, AlterDomainOperation, AlterExtension, + AlterExtensionOperation, AlterFunction, AlterFunctionAction, AlterFunctionKind, + AlterFunctionOperation, AlterIndexOperation, AlterOperator, AlterOperatorClass, + AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, + AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, + AlterTable, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, + AlterTrigger, AlterTriggerOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, CastContext, CastFunctionKind, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, - ColumnPolicyProperty, ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, - CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, + ColumnPolicyProperty, ConstraintCharacteristics, CreateAccessMethod, CreateAggregate, + CreateAggregateOption, CreateCast, CreateCollation, CreateCollationDefinition, CreateConnector, + CreateConversion, CreateDomain, CreateEventTrigger, CreateExtension, CreateForeignDataWrapper, + CreateForeignTable, CreateFunction, CreateIndex, CreateLanguage, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, - CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, - DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, - DropOperatorSignature, DropPolicy, DropTrigger, ForValues, FunctionReturnType, GeneratedAs, - GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, - IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, - KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, - OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, Owner, Partition, - PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, - TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, - UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, + CreatePublication, CreateRule, CreateStatistics, CreateSubscription, CreateTable, + CreateTablespace, CreateTextSearchConfiguration, CreateTextSearchDictionary, + CreateTextSearchParser, CreateTextSearchTemplate, CreateTransform, CreateTrigger, + CreateUserMapping, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, + DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, + DropOperatorSignature, DropPolicy, DropTrigger, EventTriggerEvent, FdwRoutineClause, ForValues, + FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, + IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, + IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, + OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose, + Owner, Partition, PartitionBoundValue, ProcedureParam, PublicationTarget, ReferentialAction, + RenameTableNameKind, ReplicaIdentity, RuleAction, RuleEvent, SecurityLabel, + SecurityLabelObjectKind, StatisticsKind, TagsColumnOption, TransformElement, TriggerObjectKind, + Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, + UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption, + UserDefinedTypeStorage, UserMappingUser, ViewColumnDef }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, @@ -138,8 +147,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 +3708,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 +3743,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), @@ -3755,11 +3780,23 @@ pub enum Statement { with_options: Vec, }, /// ```sql + /// ALTER DOMAIN + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterdomain.html) + AlterDomain(AlterDomain), + /// ```sql + /// ALTER EXTENSION + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterextension.html) + AlterExtension(AlterExtension), + /// ```sql /// ALTER FUNCTION /// ALTER AGGREGATE + /// ALTER PROCEDURE /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterfunction.html) /// and [PostgreSQL](https://www.postgresql.org/docs/current/sql-alteraggregate.html) + /// and [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterprocedure.html) AlterFunction(AlterFunction), /// ```sql /// ALTER TYPE @@ -3767,6 +3804,11 @@ pub enum Statement { /// ``` AlterType(AlterType), /// ```sql + /// ALTER TRIGGER + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertrigger.html) + AlterTrigger(AlterTrigger), + /// ```sql /// ALTER COLLATION /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altercollation.html) @@ -3974,6 +4016,109 @@ 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 + /// CREATE CAST (source_type AS target_type) WITH FUNCTION func_name [(arg_types)] [AS ASSIGNMENT | AS IMPLICIT] + /// CREATE CAST (source_type AS target_type) WITHOUT FUNCTION [AS ASSIGNMENT | AS IMPLICIT] + /// CREATE CAST (source_type AS target_type) WITH INOUT [AS ASSIGNMENT | AS IMPLICIT] + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateCast(CreateCast), + /// ```sql + /// CREATE [DEFAULT] CONVERSION name FOR 'source_encoding' TO 'dest_encoding' FROM function_name + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateConversion(CreateConversion), + /// ```sql + /// CREATE [OR REPLACE] [TRUSTED] [PROCEDURAL] LANGUAGE name [HANDLER handler_func] [INLINE inline_func] [VALIDATOR validator_func | NO VALIDATOR] + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateLanguage(CreateLanguage), + /// ```sql + /// CREATE RULE name AS ON event TO table [WHERE condition] DO [ALSO | INSTEAD] { NOTHING | command | (command ; ...) } + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateRule(CreateRule), + /// CREATE STATISTICS [ IF NOT EXISTS ] name [ ( kind [, ...] ) ] ON expr [, ...] FROM table_name + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateStatistics(CreateStatistics), + /// ```sql + /// CREATE ACCESS METHOD name TYPE INDEX | TABLE HANDLER handler_function + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateAccessMethod(CreateAccessMethod), + /// ```sql + /// CREATE EVENT TRIGGER name ON event [ WHEN TAG IN ( 'tag' [, ...] ) ] EXECUTE FUNCTION | PROCEDURE function_name() + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateEventTrigger(CreateEventTrigger), + /// ```sql + /// CREATE [ OR REPLACE ] TRANSFORM FOR type_name LANGUAGE lang_name ( transform_element_list ) + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTransform(CreateTransform), + /// ```sql + /// SECURITY LABEL [ FOR provider_name ] ON object_type object_name IS { 'label' | NULL } + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + SecurityLabel(SecurityLabel), + /// ```sql + /// CREATE USER MAPPING [ IF NOT EXISTS ] FOR { role | USER | CURRENT_ROLE | CURRENT_USER | PUBLIC } SERVER server_name [ OPTIONS (...) ] + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateUserMapping(CreateUserMapping), + /// ```sql + /// CREATE TABLESPACE name [ OWNER role ] LOCATION 'directory' [ WITH (options) ] + /// ``` + /// Note: this is a PostgreSQL-specific statement. + /// + CreateTablespace(CreateTablespace), + /// ```sql /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] /// ``` /// Note: this is a PostgreSQL-specific statement. @@ -5456,6 +5601,23 @@ 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::CreateCast(v) => write!(f, "{v}"), + Statement::CreateConversion(v) => write!(f, "{v}"), + Statement::CreateLanguage(v) => write!(f, "{v}"), + Statement::CreateRule(v) => write!(f, "{v}"), + Statement::CreateStatistics(v) => write!(f, "{v}"), + Statement::CreateAccessMethod(v) => write!(f, "{v}"), + Statement::CreateEventTrigger(v) => write!(f, "{v}"), + Statement::CreateTransform(v) => write!(f, "{v}"), + Statement::SecurityLabel(v) => write!(f, "{v}"), + Statement::CreateUserMapping(v) => write!(f, "{v}"), + Statement::CreateTablespace(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 +5665,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 +5674,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}") @@ -5529,7 +5694,10 @@ impl fmt::Display for Statement { } write!(f, " AS {query}") } + Statement::AlterDomain(alter_domain) => write!(f, "{alter_domain}"), + Statement::AlterExtension(alter_extension) => write!(f, "{alter_extension}"), Statement::AlterFunction(alter_function) => write!(f, "{alter_function}"), + Statement::AlterTrigger(alter_trigger) => write!(f, "{alter_trigger}"), Statement::AlterType(AlterType { name, operation }) => { write!(f, "ALTER TYPE {name} {operation}") } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e7a8f94f2..9f29a7721 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,31 @@ 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::CreateCast(_) => Span::empty(), + Statement::CreateConversion(_) => Span::empty(), + Statement::CreateLanguage(_) => Span::empty(), + Statement::CreateRule(_) => Span::empty(), + Statement::CreateStatistics(_) => Span::empty(), + Statement::CreateAccessMethod(_) => Span::empty(), + Statement::CreateEventTrigger(_) => Span::empty(), + Statement::CreateTransform(_) => Span::empty(), + Statement::SecurityLabel(_) => Span::empty(), + Statement::CreateUserMapping(_) => Span::empty(), + Statement::CreateTablespace(_) => 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) => { @@ -405,6 +428,9 @@ impl Spanned for Statement { .chain(core::iter::once(query.span())) .chain(with_options.iter().map(|i| i.span())), ), + Statement::AlterDomain(_) => Span::empty(), + Statement::AlterExtension(_) => Span::empty(), + Statement::AlterTrigger(_) => Span::empty(), // These statements need to be implemented Statement::AlterFunction { .. } => Span::empty(), Statement::AlterType { .. } => Span::empty(), @@ -511,6 +537,7 @@ impl Spanned for Statement { Statement::Vacuum(..) => Span::empty(), Statement::AlterUser(..) => Span::empty(), Statement::Reset(..) => Span::empty(), + Statement::CreateAggregate(_) => Span::empty(), } } } @@ -612,6 +639,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 +693,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 +1141,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 { @@ -1222,6 +1250,7 @@ impl Spanned for AlterTableOperation { AlterTableOperation::SetOptionsParens { options } => { union_spans(options.iter().map(|i| i.span())) } + AlterTableOperation::SetTablespace { .. } => Span::empty(), } } } @@ -1308,6 +1337,7 @@ impl Spanned for AlterIndexOperation { fn span(&self) -> Span { match self { AlterIndexOperation::RenameIndex { index_name } => index_name.span(), + AlterIndexOperation::SetTablespace { .. } => Span::empty(), } } } 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..dc64983d9 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -112,6 +112,7 @@ define_keywords!( ALL, ALLOCATE, ALLOWOVERWRITE, + ALSO, ALTER, ALWAYS, ANALYZE, @@ -130,6 +131,7 @@ define_keywords!( ASENSITIVE, ASOF, ASSERT, + ASSIGNMENT, ASYMMETRIC, AT, ATOMIC, @@ -244,6 +246,7 @@ define_keywords!( COMPRESSION, COMPUPDATE, COMPUTE, + CONFIGURATION, CONCURRENTLY, CONDITION, CONFLICT, @@ -255,6 +258,7 @@ define_keywords!( CONTACT, CONTAINS, CONTINUE, + CONVERSION, CONVERT, COPY, COPY_OPTIONS, @@ -333,6 +337,7 @@ define_keywords!( DETACH, DETAIL, DETERMINISTIC, + DICTIONARY, DIMENSIONS, DIRECTORY, DISABLE, @@ -476,6 +481,7 @@ define_keywords!( GROUPING, GROUPS, GZIP, + HANDLER, HASH, HASHES, HAVING, @@ -501,11 +507,13 @@ define_keywords!( ILIKE, IMMEDIATE, IMMUTABLE, + IMPLICIT, IMPORT, IMPORTED, IN, INCLUDE, INCLUDE_NULL_VALUES, + INLINE, INCLUDING, INCREMENT, INCREMENTAL, @@ -564,6 +572,7 @@ define_keywords!( KEYS, KEY_BLOCK_SIZE, KILL, + LABEL, LAG, LAMBDA, LANGUAGE, @@ -613,6 +622,7 @@ define_keywords!( MANAGEDLOCATION, MANIFEST, MAP, + MAPPING, MASKING, MATCH, MATCHED, @@ -765,6 +775,7 @@ define_keywords!( PARALLEL, PARAMETER, PARQUET, + PARSER, PART, PARTIAL, PARTITION, @@ -812,12 +823,14 @@ define_keywords!( PRINT, PRIOR, PRIVILEGES, + PROCEDURAL, PROCEDURE, PROCESSLIST, PROFILE, PROGRAM, PROJECTION, PUBLIC, + PUBLICATION, PURCHASE, PURGE, QUALIFY, @@ -1006,6 +1019,7 @@ define_keywords!( STRUCT, SUBMULTISET, SUBSCRIPT, + SUBSCRIPTION, SUBSTR, SUBSTRING, SUBSTRING_REGEX, @@ -1035,6 +1049,7 @@ define_keywords!( TASK, TBLPROPERTIES, TEMP, + TEMPLATE, TEMPORARY, TEMPTABLE, TERMINATED, @@ -1067,6 +1082,7 @@ define_keywords!( TRAN, TRANSACTION, TRANSIENT, + TRANSFORM, TRANSLATE, TRANSLATE_REGEX, TRANSLATION, @@ -1078,6 +1094,7 @@ define_keywords!( TRUE, TRUNCATE, TRUNCATECOLUMNS, + TRUSTED, TRY, TRY_CAST, TRY_CONVERT, @@ -1130,6 +1147,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..a253b56ca 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -719,6 +719,7 @@ impl<'a> Parser<'a> { self.parse_vacuum() } Keyword::RESET => self.parse_reset().map(Into::into), + Keyword::SECURITY => self.parse_security_label().map(Into::into), _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -5172,12 +5173,43 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::SECRET) { self.parse_create_secret(or_replace, temporary, persistent) } else if self.parse_keyword(Keyword::USER) { - self.parse_create_user(or_replace).map(Into::into) + if self.parse_keyword(Keyword::MAPPING) { + self.parse_create_user_mapping().map(Into::into) + } else { + 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 self.peek_keyword(Keyword::TRUSTED) + || self.peek_keyword(Keyword::PROCEDURAL) + || self.peek_keyword(Keyword::LANGUAGE) + { + let trusted = self.parse_keyword(Keyword::TRUSTED); + let procedural = self.parse_keyword(Keyword::PROCEDURAL); + if self.parse_keyword(Keyword::LANGUAGE) { + self.parse_create_language(or_replace, trusted, procedural) + .map(Into::into) + } else { + self.expected_ref( + "LANGUAGE after TRUSTED or PROCEDURAL", + self.peek_token_ref(), + ) + } + } else if self.parse_keyword(Keyword::TRANSFORM) { + self.parse_create_transform(or_replace).map(Into::into) } else if or_replace { self.expected_ref( "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION after CREATE OR REPLACE", self.peek_token_ref(), ) + } else if self.parse_keyword(Keyword::CAST) { + self.parse_create_cast().map(Into::into) + } else if self.parse_keyword(Keyword::CONVERSION) { + self.parse_create_conversion(false).map(Into::into) + } else if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CONVERSION]) { + self.parse_create_conversion(true).map(Into::into) + } else if self.parse_keyword(Keyword::RULE) { + self.parse_create_rule().map(Into::into) } else if self.parse_keyword(Keyword::EXTENSION) { self.parse_create_extension().map(Into::into) } else if self.parse_keyword(Keyword::INDEX) { @@ -5213,6 +5245,31 @@ 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 if self.parse_keyword(Keyword::STATISTICS) { + self.parse_create_statistics().map(Into::into) + } else if self.parse_keywords(&[Keyword::ACCESS, Keyword::METHOD]) { + self.parse_create_access_method().map(Into::into) + } else if self.parse_keywords(&[Keyword::EVENT, Keyword::TRIGGER]) { + self.parse_create_event_trigger().map(Into::into) + } else if self.parse_keyword(Keyword::TABLESPACE) { + self.parse_create_tablespace().map(Into::into) } else { self.expected_ref("an object type after CREATE", self.peek_token_ref()) } @@ -6588,6 +6645,20 @@ impl<'a> Parser<'a> { Keyword::BINDING, ]); + // PostgreSQL: optional WITH [NO] DATA clause on materialized views. + // pg_dump emits this clause; parse it so corpus schemas round-trip cleanly. + let with_data = if materialized && self.parse_keyword(Keyword::WITH) { + if self.parse_keyword(Keyword::NO) { + self.expect_keyword_is(Keyword::DATA)?; + Some(false) + } else { + self.expect_keyword_is(Keyword::DATA)?; + Some(true) + } + } else { + None + }; + Ok(CreateView { or_alter, name, @@ -6606,6 +6677,7 @@ impl<'a> Parser<'a> { to, params: create_view_params, name_before_not_exists, + with_data, }) } @@ -7210,6 +7282,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 +8437,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 +8778,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 +10217,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 +10272,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); @@ -10653,6 +11007,9 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) { let name = self.parse_identifier()?; AlterTableOperation::ValidateConstraint { name } + } else if self.parse_keywords(&[Keyword::SET, Keyword::TABLESPACE]) { + let tablespace_name = self.parse_identifier()?; + AlterTableOperation::SetTablespace { tablespace_name } } else { let mut options = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; @@ -10720,6 +11077,10 @@ impl<'a> Parser<'a> { Keyword::SCHEMA, Keyword::USER, Keyword::OPERATOR, + Keyword::DOMAIN, + Keyword::TRIGGER, + Keyword::EXTENSION, + Keyword::PROCEDURE, ])?; match object_type { Keyword::SCHEMA => { @@ -10744,8 +11105,14 @@ impl<'a> Parser<'a> { } else { return self.expected_ref("TO after RENAME", self.peek_token_ref()); } + } else if self.parse_keywords(&[Keyword::SET, Keyword::TABLESPACE]) { + let tablespace_name = self.parse_identifier()?; + AlterIndexOperation::SetTablespace { tablespace_name } } else { - return self.expected_ref("RENAME after ALTER INDEX", self.peek_token_ref()); + return self.expected_ref( + "RENAME or SET TABLESPACE after ALTER INDEX", + self.peek_token_ref(), + ); }; Ok(Statement::AlterIndex { @@ -10755,6 +11122,7 @@ impl<'a> Parser<'a> { } Keyword::FUNCTION => self.parse_alter_function(AlterFunctionKind::Function), Keyword::AGGREGATE => self.parse_alter_function(AlterFunctionKind::Aggregate), + Keyword::PROCEDURE => self.parse_alter_function(AlterFunctionKind::Procedure), Keyword::OPERATOR => { if self.parse_keyword(Keyword::FAMILY) { self.parse_alter_operator_family().map(Into::into) @@ -10768,9 +11136,12 @@ impl<'a> Parser<'a> { Keyword::POLICY => self.parse_alter_policy().map(Into::into), Keyword::CONNECTOR => self.parse_alter_connector(), Keyword::USER => self.parse_alter_user().map(Into::into), + Keyword::DOMAIN => self.parse_alter_domain(), + Keyword::TRIGGER => self.parse_alter_trigger(), + Keyword::EXTENSION => self.parse_alter_extension(), // unreachable because expect_one_of_keywords used above unexpected_keyword => Err(ParserError::ParserError( - format!("Internal parser error: expected any of {{VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), + format!("Internal parser error: expected any of {{VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR, DOMAIN, TRIGGER, EXTENSION, PROCEDURE}}, got {unexpected_keyword:?}"), )), } } @@ -10947,7 +11318,9 @@ impl<'a> Parser<'a> { kind: AlterFunctionKind, ) -> Result { let (function, aggregate_star, aggregate_order_by) = match kind { - AlterFunctionKind::Function => (self.parse_function_desc()?, false, None), + AlterFunctionKind::Function | AlterFunctionKind::Procedure => { + (self.parse_function_desc()?, false, None) + } AlterFunctionKind::Aggregate => self.parse_alter_aggregate_signature()?, }; @@ -10960,7 +11333,9 @@ impl<'a> Parser<'a> { AlterFunctionOperation::SetSchema { schema_name: self.parse_object_name(false)?, } - } else if matches!(kind, AlterFunctionKind::Function) && self.parse_keyword(Keyword::NO) { + } else if matches!(kind, AlterFunctionKind::Function | AlterFunctionKind::Procedure) + && self.parse_keyword(Keyword::NO) + { if !self.parse_keyword(Keyword::DEPENDS) { return self.expected_ref("DEPENDS after NO", self.peek_token_ref()); } @@ -10969,7 +11344,7 @@ impl<'a> Parser<'a> { no: true, extension_name: self.parse_object_name(false)?, } - } else if matches!(kind, AlterFunctionKind::Function) + } else if matches!(kind, AlterFunctionKind::Function | AlterFunctionKind::Procedure) && self.parse_keyword(Keyword::DEPENDS) { self.expect_keywords(&[Keyword::ON, Keyword::EXTENSION])?; @@ -10977,7 +11352,7 @@ impl<'a> Parser<'a> { no: false, extension_name: self.parse_object_name(false)?, } - } else if matches!(kind, AlterFunctionKind::Function) { + } else if matches!(kind, AlterFunctionKind::Function | AlterFunctionKind::Procedure) { let (actions, restrict) = self.parse_alter_function_actions()?; AlterFunctionOperation::Actions { actions, restrict } } else { @@ -10996,35 +11371,142 @@ impl<'a> Parser<'a> { })) } - /// Parse a [Statement::AlterTable] - pub fn parse_alter_table(&mut self, iceberg: bool) -> Result { - let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] - let table_name = self.parse_object_name(false)?; - let on_cluster = self.parse_optional_on_cluster()?; - let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; + /// Parse an `ALTER DOMAIN` statement. + pub fn parse_alter_domain(&mut self) -> Result { + let name = self.parse_object_name(false)?; - let mut location = None; - if self.parse_keyword(Keyword::LOCATION) { - location = Some(HiveSetLocation { - has_set: false, - location: self.parse_identifier()?, - }); - } else if self.parse_keywords(&[Keyword::SET, Keyword::LOCATION]) { - location = Some(HiveSetLocation { - has_set: true, - location: self.parse_identifier()?, - }); - } + let operation = if self.parse_keyword(Keyword::ADD) { + if let Some(constraint) = self.parse_optional_table_constraint()? { + let not_valid = self.parse_keywords(&[Keyword::NOT, Keyword::VALID]); + AlterDomainOperation::AddConstraint { + constraint, + not_valid, + } + } else { + return self.expected_ref("constraint after ADD", self.peek_token_ref()); + } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::CONSTRAINT]) { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_identifier()?; + let drop_behavior = self.parse_optional_drop_behavior(); + AlterDomainOperation::DropConstraint { + if_exists, + name, + drop_behavior, + } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::DEFAULT]) { + AlterDomainOperation::DropDefault + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::CONSTRAINT]) { + let old_name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::TO)?; + let new_name = self.parse_identifier()?; + AlterDomainOperation::RenameConstraint { old_name, new_name } + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + let new_name = self.parse_identifier()?; + AlterDomainOperation::RenameTo { new_name } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterDomainOperation::OwnerTo(self.parse_owner()?) + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterDomainOperation::SetSchema { + schema_name: self.parse_object_name(false)?, + } + } else if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT]) { + AlterDomainOperation::SetDefault { + default: self.parse_expr()?, + } + } else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) { + let name = self.parse_identifier()?; + AlterDomainOperation::ValidateConstraint { name } + } else { + return self.expected_ref( + "ADD, DROP, RENAME, OWNER TO, SET, VALIDATE after ALTER DOMAIN", + self.peek_token_ref(), + ); + }; - let end_token = if self.peek_token_ref().token == Token::SemiColon { - self.peek_token_ref().clone() + Ok(AlterDomain { name, operation }.into()) + } + + /// Parse an `ALTER TRIGGER` statement. + pub fn parse_alter_trigger(&mut self) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::ON)?; + let table_name = self.parse_object_name(false)?; + + let operation = if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + let new_name = self.parse_identifier()?; + AlterTriggerOperation::RenameTo { new_name } } else { - self.get_current_token().clone() + return self.expected_ref("RENAME TO after ALTER TRIGGER ... ON ...", self.peek_token_ref()); }; - Ok(AlterTable { - name: table_name, + Ok(AlterTrigger { + name, + table_name, + operation, + } + .into()) + } + + /// Parse an `ALTER EXTENSION` statement. + pub fn parse_alter_extension(&mut self) -> Result { + let name = self.parse_identifier()?; + + let operation = if self.parse_keyword(Keyword::UPDATE) { + let version = if self.parse_keyword(Keyword::TO) { + Some(self.parse_identifier()?) + } else { + None + }; + AlterExtensionOperation::UpdateTo { version } + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterExtensionOperation::SetSchema { + schema_name: self.parse_object_name(false)?, + } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterExtensionOperation::OwnerTo(self.parse_owner()?) + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + let new_name = self.parse_identifier()?; + AlterExtensionOperation::RenameTo { new_name } + } else { + return self.expected_ref( + "UPDATE, SET SCHEMA, OWNER TO, or RENAME TO after ALTER EXTENSION", + self.peek_token_ref(), + ); + }; + + Ok(AlterExtension { name, operation }.into()) + } + + /// Parse a [Statement::AlterTable] + pub fn parse_alter_table(&mut self, iceberg: bool) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let only = self.parse_keyword(Keyword::ONLY); // [ ONLY ] + let table_name = self.parse_object_name(false)?; + let on_cluster = self.parse_optional_on_cluster()?; + let operations = self.parse_comma_separated(Parser::parse_alter_table_operation)?; + + let mut location = None; + if self.parse_keyword(Keyword::LOCATION) { + location = Some(HiveSetLocation { + has_set: false, + location: self.parse_identifier()?, + }); + } else if self.parse_keywords(&[Keyword::SET, Keyword::LOCATION]) { + location = Some(HiveSetLocation { + has_set: true, + location: self.parse_identifier()?, + }); + } + + let end_token = if self.peek_token_ref().token == Token::SemiColon { + self.peek_token_ref().clone() + } else { + self.get_current_token().clone() + }; + + Ok(AlterTable { + name: table_name, if_exists, only, operations, @@ -12651,6 +13133,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 +20188,624 @@ 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, + }) + } + + /// Parse a `CREATE CAST` statement. + /// + /// See + pub fn parse_create_cast(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let source_type = self.parse_data_type()?; + self.expect_keyword_is(Keyword::AS)?; + let target_type = self.parse_data_type()?; + self.expect_token(&Token::RParen)?; + + let function_kind = if self.parse_keywords(&[Keyword::WITHOUT, Keyword::FUNCTION]) { + CastFunctionKind::WithoutFunction + } else if self.parse_keywords(&[Keyword::WITH, Keyword::INOUT]) { + CastFunctionKind::WithInout + } else if self.parse_keywords(&[Keyword::WITH, Keyword::FUNCTION]) { + let function_name = self.parse_object_name(false)?; + let argument_types = if self.peek_token_ref().token == Token::LParen { + self.expect_token(&Token::LParen)?; + let types = if self.peek_token_ref().token == Token::RParen { + vec![] + } else { + self.parse_comma_separated(|p| p.parse_data_type())? + }; + self.expect_token(&Token::RParen)?; + types + } else { + vec![] + }; + CastFunctionKind::WithFunction { + function_name, + argument_types, + } + } else { + return self.expected_ref( + "WITH FUNCTION, WITHOUT FUNCTION, or WITH INOUT", + self.peek_token_ref(), + ); + }; + + let cast_context = if self.parse_keyword(Keyword::AS) { + if self.parse_keyword(Keyword::ASSIGNMENT) { + CastContext::Assignment + } else if self.parse_keyword(Keyword::IMPLICIT) { + CastContext::Implicit + } else { + return self.expected_ref("ASSIGNMENT or IMPLICIT after AS", self.peek_token_ref()); + } + } else { + CastContext::Explicit + }; + + Ok(CreateCast { + source_type, + target_type, + function_kind, + cast_context, + }) + } + + /// Parse a `CREATE [DEFAULT] CONVERSION` statement. + /// + /// See + pub fn parse_create_conversion( + &mut self, + is_default: bool, + ) -> Result { + let name = self.parse_object_name(false)?; + self.expect_keyword_is(Keyword::FOR)?; + let source_encoding = self.parse_literal_string()?; + self.expect_keyword_is(Keyword::TO)?; + let destination_encoding = self.parse_literal_string()?; + self.expect_keyword_is(Keyword::FROM)?; + let function_name = self.parse_object_name(false)?; + + Ok(CreateConversion { + name, + is_default, + source_encoding, + destination_encoding, + function_name, + }) + } + + /// Parse a `CREATE [OR REPLACE] [TRUSTED] [PROCEDURAL] LANGUAGE` statement. + /// + /// See + pub fn parse_create_language( + &mut self, + or_replace: bool, + trusted: bool, + procedural: bool, + ) -> Result { + let name = self.parse_identifier()?; + + let handler = if self.parse_keyword(Keyword::HANDLER) { + Some(self.parse_object_name(false)?) + } else { + None + }; + + let inline_handler = if self.parse_keyword(Keyword::INLINE) { + Some(self.parse_object_name(false)?) + } else { + None + }; + + let validator = if self.parse_keywords(&[Keyword::NO, Keyword::VALIDATOR]) { + None + } else if self.parse_keyword(Keyword::VALIDATOR) { + Some(self.parse_object_name(false)?) + } else { + None + }; + + Ok(CreateLanguage { + name, + or_replace, + trusted, + procedural, + handler, + inline_handler, + validator, + }) + } + + /// Parse a `CREATE RULE` statement. + /// + /// See + pub fn parse_create_rule(&mut self) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::AS)?; + self.expect_keyword_is(Keyword::ON)?; + + let event = if self.parse_keyword(Keyword::SELECT) { + RuleEvent::Select + } else if self.parse_keyword(Keyword::INSERT) { + RuleEvent::Insert + } else if self.parse_keyword(Keyword::UPDATE) { + RuleEvent::Update + } else if self.parse_keyword(Keyword::DELETE) { + RuleEvent::Delete + } else { + return self.expected_ref( + "SELECT, INSERT, UPDATE, or DELETE after ON", + self.peek_token_ref(), + ); + }; + + self.expect_keyword_is(Keyword::TO)?; + let table = self.parse_object_name(false)?; + + let condition = if self.parse_keyword(Keyword::WHERE) { + Some(self.parse_expr()?) + } else { + None + }; + + self.expect_keyword_is(Keyword::DO)?; + + let instead = if self.parse_keyword(Keyword::INSTEAD) { + true + } else if self.parse_keyword(Keyword::ALSO) { + false + } else { + false + }; + + let action = if self.parse_keyword(Keyword::NOTHING) { + RuleAction::Nothing + } else if self.peek_token_ref().token == Token::LParen { + self.expect_token(&Token::LParen)?; + let mut stmts = Vec::new(); + loop { + stmts.push(self.parse_statement()?); + if !self.consume_token(&Token::SemiColon) { + break; + } + if self.peek_token_ref().token == Token::RParen { + break; + } + } + self.expect_token(&Token::RParen)?; + RuleAction::Statements(stmts) + } else { + let stmt = self.parse_statement()?; + RuleAction::Statements(vec![stmt]) + }; + + Ok(CreateRule { + name, + event, + table, + condition, + instead, + action, + }) + } + + /// Parse a `CREATE STATISTICS` statement. + /// + /// See + pub fn parse_create_statistics(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + + let kinds = if self.consume_token(&Token::LParen) { + let kinds = self.parse_comma_separated(|p| { + let ident = p.parse_identifier()?; + match ident.value.to_lowercase().as_str() { + "ndistinct" => Ok(StatisticsKind::NDistinct), + "dependencies" => Ok(StatisticsKind::Dependencies), + "mcv" => Ok(StatisticsKind::Mcv), + other => Err(ParserError::ParserError(format!( + "Unknown statistics kind: {other}" + ))), + } + })?; + self.expect_token(&Token::RParen)?; + kinds + } else { + vec![] + }; + + self.expect_keyword_is(Keyword::ON)?; + let on = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_keyword_is(Keyword::FROM)?; + let from = self.parse_object_name(false)?; + + Ok(CreateStatistics { + if_not_exists, + name, + kinds, + on, + from, + }) + } + + /// Parse a `CREATE ACCESS METHOD` statement. + /// + /// See + pub fn parse_create_access_method(&mut self) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::TYPE)?; + let method_type = if self.parse_keyword(Keyword::INDEX) { + AccessMethodType::Index + } else if self.parse_keyword(Keyword::TABLE) { + AccessMethodType::Table + } else { + return self.expected_ref("INDEX or TABLE after TYPE", self.peek_token_ref()); + }; + self.expect_keyword_is(Keyword::HANDLER)?; + let handler = self.parse_object_name(false)?; + + Ok(CreateAccessMethod { + name, + method_type, + handler, + }) + } + + /// Parse a `CREATE EVENT TRIGGER` statement. + /// + /// See + pub fn parse_create_event_trigger(&mut self) -> Result { + let name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::ON)?; + let event_ident = self.parse_identifier()?; + let event = match event_ident.value.to_lowercase().as_str() { + "ddl_command_start" => EventTriggerEvent::DdlCommandStart, + "ddl_command_end" => EventTriggerEvent::DdlCommandEnd, + "table_rewrite" => EventTriggerEvent::TableRewrite, + "sql_drop" => EventTriggerEvent::SqlDrop, + other => { + return Err(ParserError::ParserError(format!( + "Unknown event trigger event: {other}" + ))) + } + }; + + let when_tags = if self.parse_keyword(Keyword::WHEN) { + self.expect_keyword_is(Keyword::TAG)?; + self.expect_keyword_is(Keyword::IN)?; + self.expect_token(&Token::LParen)?; + let tags = self.parse_comma_separated(|p| p.parse_value().map(|v| v.value))?; + self.expect_token(&Token::RParen)?; + Some(tags) + } else { + None + }; + + self.expect_keyword_is(Keyword::EXECUTE)?; + let is_procedure = if self.parse_keyword(Keyword::FUNCTION) { + false + } else if self.parse_keyword(Keyword::PROCEDURE) { + true + } else { + return self.expected_ref("FUNCTION or PROCEDURE after EXECUTE", self.peek_token_ref()); + }; + let execute = self.parse_object_name(false)?; + self.expect_token(&Token::LParen)?; + self.expect_token(&Token::RParen)?; + + Ok(CreateEventTrigger { + name, + event, + when_tags, + execute, + is_procedure, + }) + } + + /// Parse a `CREATE [OR REPLACE] TRANSFORM` statement. + /// + /// See + pub fn parse_create_transform(&mut self, or_replace: bool) -> Result { + self.expect_keyword_is(Keyword::FOR)?; + let type_name = self.parse_data_type()?; + self.expect_keyword_is(Keyword::LANGUAGE)?; + let language = self.parse_identifier()?; + self.expect_token(&Token::LParen)?; + let elements = self.parse_comma_separated(|p| { + let is_from = if p.parse_keyword(Keyword::FROM) { + true + } else { + p.expect_keyword_is(Keyword::TO)?; + false + }; + p.expect_keyword_is(Keyword::SQL)?; + p.expect_keyword_is(Keyword::WITH)?; + p.expect_keyword_is(Keyword::FUNCTION)?; + let function = p.parse_object_name(false)?; + p.expect_token(&Token::LParen)?; + let arg_types = if p.peek_token().token == Token::RParen { + vec![] + } else { + p.parse_comma_separated(|p| p.parse_data_type())? + }; + p.expect_token(&Token::RParen)?; + Ok(TransformElement { + is_from, + function, + arg_types, + }) + })?; + self.expect_token(&Token::RParen)?; + + Ok(CreateTransform { + or_replace, + type_name, + language, + elements, + }) + } + + + /// Parse a `SECURITY LABEL` statement. + /// + /// See + pub fn parse_security_label(&mut self) -> Result { + self.expect_keyword_is(Keyword::LABEL)?; + + let provider = if self.parse_keyword(Keyword::FOR) { + Some(self.parse_identifier()?) + } else { + None + }; + + self.expect_keyword_is(Keyword::ON)?; + + let object_kind = if self.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEW]) { + SecurityLabelObjectKind::MaterializedView + } else if self.parse_keyword(Keyword::TABLE) { + SecurityLabelObjectKind::Table + } else if self.parse_keyword(Keyword::COLUMN) { + SecurityLabelObjectKind::Column + } else if self.parse_keyword(Keyword::DATABASE) { + SecurityLabelObjectKind::Database + } else if self.parse_keyword(Keyword::DOMAIN) { + SecurityLabelObjectKind::Domain + } else if self.parse_keyword(Keyword::FUNCTION) { + SecurityLabelObjectKind::Function + } else if self.parse_keyword(Keyword::ROLE) { + SecurityLabelObjectKind::Role + } else if self.parse_keyword(Keyword::SCHEMA) { + SecurityLabelObjectKind::Schema + } else if self.parse_keyword(Keyword::SEQUENCE) { + SecurityLabelObjectKind::Sequence + } else if self.parse_keyword(Keyword::TYPE) { + SecurityLabelObjectKind::Type + } else if self.parse_keyword(Keyword::VIEW) { + SecurityLabelObjectKind::View + } else { + return self.expected_ref( + "TABLE, COLUMN, DATABASE, DOMAIN, FUNCTION, MATERIALIZED VIEW, ROLE, SCHEMA, SEQUENCE, TYPE, or VIEW after ON", + self.peek_token_ref(), + ); + }; + + let object_name = self.parse_object_name(false)?; + + self.expect_keyword_is(Keyword::IS)?; + + let label = if self.parse_keyword(Keyword::NULL) { + None + } else { + Some(self.parse_value()?.value) + }; + + Ok(SecurityLabel { + provider, + object_kind, + object_name, + label, + }) + } + + /// Parse a `CREATE USER MAPPING` statement. + /// + /// See + pub fn parse_create_user_mapping(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + + self.expect_keyword_is(Keyword::FOR)?; + + let user = if self.parse_keyword(Keyword::CURRENT_ROLE) { + UserMappingUser::CurrentRole + } else if self.parse_keyword(Keyword::CURRENT_USER) { + UserMappingUser::CurrentUser + } else if self.parse_keyword(Keyword::PUBLIC) { + UserMappingUser::Public + } else if self.parse_keyword(Keyword::USER) { + UserMappingUser::User + } else { + UserMappingUser::Ident(self.parse_identifier()?) + }; + + 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(CreateUserMapping { + if_not_exists, + user, + server_name, + options, + }) + } + + /// Parse a `CREATE TABLESPACE` statement. + /// + /// See + pub fn parse_create_tablespace(&mut self) -> Result { + let name = self.parse_identifier()?; + + let owner = if self.parse_keyword(Keyword::OWNER) { + Some(self.parse_identifier()?) + } else { + None + }; + + self.expect_keyword_is(Keyword::LOCATION)?; + let location = self.parse_value()?.value; + + let with_options = self.parse_options(Keyword::WITH)?; + + Ok(CreateTablespace { + name, + owner, + location, + 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..d416b4d7c 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,785 @@ 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 alter_table_set_tablespace() { + let sql = "ALTER TABLE t SET TABLESPACE ts"; + let Statement::AlterTable(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.to_string(), "t"); + assert_eq!(stmt.operations.len(), 1); + assert_eq!( + stmt.operations[0], + AlterTableOperation::SetTablespace { + tablespace_name: "ts".into() + } + ); +} + +#[test] +fn alter_index_set_tablespace() { + let sql = "ALTER INDEX idx SET TABLESPACE ts"; + let Statement::AlterIndex { name, operation } = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(name.to_string(), "idx"); + assert_eq!( + operation, + AlterIndexOperation::SetTablespace { + tablespace_name: "ts".into() + } + ); +} + +#[test] +fn alter_domain_add_constraint() { + let sql = "ALTER DOMAIN positive_int ADD CONSTRAINT positive CHECK (VALUE > 0)"; + let Statement::AlterDomain(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.to_string(), "positive_int"); + assert!(matches!( + stmt.operation, + AlterDomainOperation::AddConstraint { not_valid: false, .. } + )); +} + +#[test] +fn alter_domain_drop_constraint() { + let sql = "ALTER DOMAIN email DROP CONSTRAINT valid_email"; + let Statement::AlterDomain(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.to_string(), "email"); + assert!(matches!( + stmt.operation, + AlterDomainOperation::DropConstraint { + if_exists: false, + .. + } + )); +} + +#[test] +fn alter_domain_drop_constraint_if_exists() { + let sql = "ALTER DOMAIN email DROP CONSTRAINT IF EXISTS valid_email"; + let Statement::AlterDomain(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(matches!( + stmt.operation, + AlterDomainOperation::DropConstraint { + if_exists: true, + .. + } + )); +} + +#[test] +fn alter_trigger_rename() { + let sql = "ALTER TRIGGER old_trigger ON orders RENAME TO new_trigger"; + let Statement::AlterTrigger(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "old_trigger"); + assert_eq!(stmt.table_name.to_string(), "orders"); + assert_eq!( + stmt.operation, + AlterTriggerOperation::RenameTo { + new_name: "new_trigger".into() + } + ); +} + +#[test] +fn alter_extension_update() { + let sql = "ALTER EXTENSION pgcrypto UPDATE"; + let Statement::AlterExtension(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "pgcrypto"); + assert_eq!( + stmt.operation, + AlterExtensionOperation::UpdateTo { version: None } + ); +} + +#[test] +fn alter_extension_update_to_version() { + let sql = "ALTER EXTENSION pgcrypto UPDATE TO '3.4'"; + let Statement::AlterExtension(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "pgcrypto"); + assert!(matches!( + stmt.operation, + AlterExtensionOperation::UpdateTo { version: Some(_) } + )); +} + +#[test] +fn alter_procedure_set_search_path() { + let sql = "ALTER PROCEDURE myproc(integer) SET search_path = public"; + let Statement::AlterFunction(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.kind, AlterFunctionKind::Procedure); + assert_eq!(stmt.function.name.to_string(), "myproc"); + assert!(matches!( + stmt.operation, + AlterFunctionOperation::Actions { .. } + )); +} + +#[test] +fn alter_procedure_rename() { + let sql = "ALTER PROCEDURE myproc(integer, text) RENAME TO renamed_proc"; + let Statement::AlterFunction(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.kind, AlterFunctionKind::Procedure); + assert_eq!( + stmt.operation, + AlterFunctionOperation::RenameTo { + new_name: "renamed_proc".into() + } + ); +} + +#[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:?}"), + } +} + +#[test] +fn parse_create_cast_with_function() { + let sql = "CREATE CAST (TEXT AS INTEGER) WITH FUNCTION public.to_int(TEXT) AS ASSIGNMENT"; + let Statement::CreateCast(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.source_type.to_string(), "TEXT"); + assert_eq!(stmt.target_type.to_string(), "INTEGER"); + assert!(matches!(stmt.cast_context, CastContext::Assignment)); + match &stmt.function_kind { + CastFunctionKind::WithFunction { + function_name, + argument_types, + } => { + assert_eq!(function_name.to_string(), "public.to_int"); + assert_eq!(argument_types.len(), 1); + assert_eq!(argument_types[0].to_string(), "TEXT"); + } + other => panic!("unexpected function kind: {other:?}"), + } +} + +#[test] +fn parse_create_cast_without_function() { + let sql = "CREATE CAST (TEXT AS INTEGER) WITHOUT FUNCTION"; + let Statement::CreateCast(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(matches!(stmt.function_kind, CastFunctionKind::WithoutFunction)); + assert!(matches!(stmt.cast_context, CastContext::Explicit)); +} + +#[test] +fn parse_create_cast_with_inout() { + let sql = "CREATE CAST (TEXT AS INTEGER) WITH INOUT AS IMPLICIT"; + let Statement::CreateCast(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(matches!(stmt.function_kind, CastFunctionKind::WithInout)); + assert!(matches!(stmt.cast_context, CastContext::Implicit)); +} + +#[test] +fn parse_create_conversion_basic() { + let sql = "CREATE CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8"; + let Statement::CreateConversion(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.to_string(), "myconv"); + assert!(!stmt.is_default); + assert_eq!(stmt.source_encoding, "LATIN1"); + assert_eq!(stmt.destination_encoding, "UTF8"); + assert_eq!(stmt.function_name.to_string(), "iso8859_1_to_utf8"); +} + +#[test] +fn parse_create_default_conversion() { + let sql = "CREATE DEFAULT CONVERSION myconv FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8"; + let Statement::CreateConversion(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(stmt.is_default); +} + +#[test] +fn parse_create_language_simple() { + let sql = "CREATE LANGUAGE plperl"; + let Statement::CreateLanguage(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "plperl"); + assert!(!stmt.or_replace); + assert!(!stmt.trusted); + assert!(!stmt.procedural); + assert!(stmt.handler.is_none()); + assert!(stmt.inline_handler.is_none()); + assert!(stmt.validator.is_none()); +} + +#[test] +fn parse_create_language_full() { + let sql = "CREATE OR REPLACE TRUSTED PROCEDURAL LANGUAGE plpgsql HANDLER plpgsql_call_handler INLINE plpgsql_inline_handler VALIDATOR plpgsql_validator"; + let Statement::CreateLanguage(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "plpgsql"); + assert!(stmt.or_replace); + assert!(stmt.trusted); + assert!(stmt.procedural); + assert_eq!(stmt.handler.as_ref().unwrap().to_string(), "plpgsql_call_handler"); + assert_eq!(stmt.inline_handler.as_ref().unwrap().to_string(), "plpgsql_inline_handler"); + assert_eq!(stmt.validator.as_ref().unwrap().to_string(), "plpgsql_validator"); +} + +#[test] +fn parse_create_rule_instead_nothing() { + let sql = "CREATE RULE t_del AS ON DELETE TO public.t DO INSTEAD NOTHING"; + let Statement::CreateRule(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "t_del"); + assert!(matches!(stmt.event, RuleEvent::Delete)); + assert_eq!(stmt.table.to_string(), "public.t"); + assert!(stmt.condition.is_none()); + assert!(stmt.instead); + assert!(matches!(stmt.action, RuleAction::Nothing)); +} + +#[test] +fn parse_create_rule_also_nothing() { + let sql = "CREATE RULE t_ins AS ON INSERT TO mytable DO ALSO NOTHING"; + let Statement::CreateRule(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(matches!(stmt.event, RuleEvent::Insert)); + assert!(!stmt.instead); + assert!(matches!(stmt.action, RuleAction::Nothing)); +} + +#[test] +fn parse_create_rule_on_select() { + let sql = "CREATE RULE myview AS ON SELECT TO mytable DO INSTEAD NOTHING"; + let Statement::CreateRule(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(matches!(stmt.event, RuleEvent::Select)); + assert!(stmt.instead); +} + +#[test] +fn parse_create_statistics_basic() { + let sql = "CREATE STATISTICS public.s ON a, b FROM public.t"; + let Statement::CreateStatistics(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(!stmt.if_not_exists); + assert_eq!(stmt.name.to_string(), "public.s"); + assert!(stmt.kinds.is_empty()); + assert_eq!(stmt.on.len(), 2); + assert_eq!(stmt.from.to_string(), "public.t"); +} + +#[test] +fn parse_create_statistics_if_not_exists_with_kinds() { + let sql = "CREATE STATISTICS IF NOT EXISTS mystat (ndistinct, dependencies, mcv) ON col1, col2 FROM mytable"; + let Statement::CreateStatistics(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(stmt.if_not_exists); + assert_eq!(stmt.name.to_string(), "mystat"); + assert_eq!( + stmt.kinds, + vec![ + StatisticsKind::NDistinct, + StatisticsKind::Dependencies, + StatisticsKind::Mcv, + ] + ); + assert_eq!(stmt.on.len(), 2); +} + +#[test] +fn parse_create_access_method_index() { + let sql = "CREATE ACCESS METHOD my_am TYPE INDEX HANDLER bthandler"; + let Statement::CreateAccessMethod(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "my_am"); + assert_eq!(stmt.method_type, AccessMethodType::Index); + assert_eq!(stmt.handler.to_string(), "bthandler"); +} + +#[test] +fn parse_create_access_method_table() { + let sql = "CREATE ACCESS METHOD my_tam TYPE TABLE HANDLER heap_tableam_handler"; + let Statement::CreateAccessMethod(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "my_tam"); + assert_eq!(stmt.method_type, AccessMethodType::Table); + assert_eq!(stmt.handler.to_string(), "heap_tableam_handler"); +} + +#[test] +fn parse_create_event_trigger_basic() { + let sql = "CREATE EVENT TRIGGER myet ON ddl_command_start EXECUTE FUNCTION public.handler()"; + let Statement::CreateEventTrigger(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "myet"); + assert_eq!(stmt.event, EventTriggerEvent::DdlCommandStart); + assert!(stmt.when_tags.is_none()); + assert_eq!(stmt.execute.to_string(), "public.handler"); + assert!(!stmt.is_procedure); +} + +#[test] +fn parse_create_event_trigger_with_when_tags() { + let sql = "CREATE EVENT TRIGGER myet ON ddl_command_end WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE') EXECUTE FUNCTION abort_any_command()"; + let Statement::CreateEventTrigger(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.event, EventTriggerEvent::DdlCommandEnd); + let tags = stmt.when_tags.unwrap(); + assert_eq!(tags.len(), 2); + assert_eq!(tags[0].to_string(), "'CREATE TABLE'"); + assert_eq!(tags[1].to_string(), "'ALTER TABLE'"); +} + +#[test] +fn parse_create_transform_basic() { + let sql = "CREATE TRANSFORM FOR INT LANGUAGE sql (FROM SQL WITH FUNCTION f1(internal), TO SQL WITH FUNCTION f2(INT))"; + let Statement::CreateTransform(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(!stmt.or_replace); + assert_eq!(stmt.type_name.to_string(), "INT"); + assert_eq!(stmt.language.value, "sql"); + assert_eq!(stmt.elements.len(), 2); + assert!(stmt.elements[0].is_from); + assert_eq!(stmt.elements[0].function.to_string(), "f1"); + assert!(!stmt.elements[1].is_from); + assert_eq!(stmt.elements[1].function.to_string(), "f2"); +} + +#[test] +fn parse_create_or_replace_transform() { + let sql = "CREATE OR REPLACE TRANSFORM FOR BIGINT LANGUAGE plpgsql (FROM SQL WITH FUNCTION int8recv(internal))"; + let Statement::CreateTransform(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(stmt.or_replace); + assert_eq!(stmt.type_name.to_string(), "BIGINT"); + assert_eq!(stmt.language.value, "plpgsql"); + assert_eq!(stmt.elements.len(), 1); + assert!(stmt.elements[0].is_from); +} + +#[test] +fn parse_security_label_on_table() { + let sql = "SECURITY LABEL FOR selinux ON TABLE public.t IS 'system_u:object_r:sepgsql_table_t:s0'"; + let Statement::SecurityLabel(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.provider.as_ref().unwrap().value, "selinux"); + assert!(matches!(stmt.object_kind, SecurityLabelObjectKind::Table)); + assert_eq!(stmt.object_name.to_string(), "public.t"); + assert_eq!( + stmt.label.as_ref().unwrap().to_string(), + "'system_u:object_r:sepgsql_table_t:s0'" + ); +} + +#[test] +fn parse_security_label_no_provider_null() { + let sql = "SECURITY LABEL ON TABLE public.t IS NULL"; + let Statement::SecurityLabel(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(stmt.provider.is_none()); + assert!(matches!(stmt.object_kind, SecurityLabelObjectKind::Table)); + assert!(stmt.label.is_none()); +} + +#[test] +fn parse_security_label_on_role() { + let sql = "SECURITY LABEL FOR selinux ON ROLE admin IS 'system_u:object_r:sepgsql_role_t:s0'"; + let Statement::SecurityLabel(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.provider.as_ref().unwrap().value, "selinux"); + assert!(matches!(stmt.object_kind, SecurityLabelObjectKind::Role)); + assert_eq!(stmt.object_name.to_string(), "admin"); +} + +#[test] +fn parse_security_label_on_schema() { + let sql = "SECURITY LABEL ON SCHEMA myschema IS 'system_u:object_r:sepgsql_schema_t:s0'"; + let Statement::SecurityLabel(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(stmt.provider.is_none()); + assert!(matches!(stmt.object_kind, SecurityLabelObjectKind::Schema)); + assert_eq!(stmt.object_name.to_string(), "myschema"); +} + +#[test] +fn parse_create_user_mapping_basic() { + let sql = "CREATE USER MAPPING FOR postgres SERVER my_server"; + let Statement::CreateUserMapping(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(!stmt.if_not_exists); + assert!(matches!(stmt.user, UserMappingUser::Ident(_))); + if let UserMappingUser::Ident(ident) = &stmt.user { + assert_eq!(ident.value, "postgres"); + } + assert_eq!(stmt.server_name.value, "my_server"); + assert!(stmt.options.is_none()); +} + +#[test] +fn parse_create_user_mapping_if_not_exists_with_options() { + let sql = r#"CREATE USER MAPPING IF NOT EXISTS FOR postgres SERVER my_server OPTIONS ("user" 'bob')"#; + let Statement::CreateUserMapping(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(stmt.if_not_exists); + assert!(matches!(stmt.user, UserMappingUser::Ident(_))); + assert_eq!(stmt.server_name.value, "my_server"); + let opts = stmt.options.as_ref().unwrap(); + assert_eq!(opts.len(), 1); + assert_eq!(opts[0].key.value, "user"); + assert_eq!(opts[0].value.value, "bob"); +} + +#[test] +fn parse_create_user_mapping_current_user() { + let sql = "CREATE USER MAPPING FOR CURRENT_USER SERVER my_server"; + let Statement::CreateUserMapping(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(matches!(stmt.user, UserMappingUser::CurrentUser)); +} + +#[test] +fn parse_create_user_mapping_public() { + let sql = "CREATE USER MAPPING FOR PUBLIC SERVER my_server"; + let Statement::CreateUserMapping(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert!(matches!(stmt.user, UserMappingUser::Public)); +} + +#[test] +fn parse_create_tablespace_basic() { + let sql = "CREATE TABLESPACE my_ts LOCATION '/mnt/data'"; + let Statement::CreateTablespace(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "my_ts"); + assert!(stmt.owner.is_none()); + assert_eq!(stmt.location.to_string(), "'/mnt/data'"); + assert!(stmt.with_options.is_empty()); +} + +#[test] +fn parse_create_tablespace_with_owner() { + let sql = "CREATE TABLESPACE my_ts OWNER admin LOCATION '/mnt/data'"; + let Statement::CreateTablespace(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "my_ts"); + assert_eq!(stmt.owner.as_ref().unwrap().value, "admin"); + assert_eq!(stmt.location.to_string(), "'/mnt/data'"); +} + +#[test] +fn parse_create_tablespace_with_options() { + let sql = "CREATE TABLESPACE my_ts LOCATION '/mnt/data' WITH (seq_page_cost = 1.0)"; + let Statement::CreateTablespace(stmt) = pg().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.name.value, "my_ts"); + assert_eq!(stmt.with_options.len(), 1); + match &stmt.with_options[0] { + SqlOption::KeyValue { key, value } => { + assert_eq!(key.value, "seq_page_cost"); + assert_eq!(value.to_string(), "1.0"); + } + other => panic!("unexpected option: {other:?}"), + } +}