From 1459358de1f86c535ebdd71480588f403fcf6221 Mon Sep 17 00:00:00 2001 From: Aleksandar Maksimovic Date: Thu, 15 Oct 2026 18:46:31 +0000 Subject: [PATCH] feat(postgres): parse statistics, view, and sequence statements Parse CREATE, ALTER, and DROP STATISTICS, RECURSIVE views, the WITH [CASCADED | LOCAL] CHECK OPTION clause, value-less view options, the ALTER VIEW property forms, and ALTER SEQUENCE. --- Cargo.toml | 2 +- src/ast/ddl.rs | 280 ++++++++++++++++++++++++++- src/ast/mod.rs | 91 +++++++-- src/ast/spans.rs | 4 + src/parser/mod.rs | 372 ++++++++++++++++++++++++++++++++++-- tests/sqlparser_common.rs | 12 ++ tests/sqlparser_postgres.rs | 330 ++++++++++++++++++++++++++++++++ 7 files changed, 1054 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45de42bc4c..be36b276e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ [package] name = "sqlparser-dsql" description = "SQL parser fork with Aurora DSQL extensions and DSQL-supported PostgreSQL syntax. Based on sqlparser 0.62.0." -version = "0.62.11" +version = "0.62.12" authors = [ "Apache DataFusion ", "Amazon Web Services", diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index bc32836b1d..8eb3d4d389 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -4483,6 +4483,35 @@ pub struct CreateView { pub to: Option, /// MySQL: Optional parameters for the view algorithm, definer, and security context pub params: Option, + /// PostgreSQL: `RECURSIVE` view modifier + /// + pub recursive: bool, + /// PostgreSQL: `WITH [ CASCADED | LOCAL ] CHECK OPTION` clause + /// + pub check_option: Option, +} + +/// `WITH [ CASCADED | LOCAL ] CHECK OPTION` clause of `CREATE VIEW`. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ViewCheckOption { + /// `WITH CHECK OPTION` + Unspecified, + /// `WITH CASCADED CHECK OPTION` + Cascaded, + /// `WITH LOCAL CHECK OPTION` + Local, +} + +impl fmt::Display for ViewCheckOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ViewCheckOption::Unspecified => f.write_str("WITH CHECK OPTION"), + ViewCheckOption::Cascaded => f.write_str("WITH CASCADED CHECK OPTION"), + ViewCheckOption::Local => f.write_str("WITH LOCAL CHECK OPTION"), + } + } } impl fmt::Display for CreateView { @@ -4498,7 +4527,7 @@ impl fmt::Display for CreateView { } write!( f, - "{secure}{materialized}{temporary}VIEW {if_not_and_name}{to}", + "{secure}{materialized}{temporary}{recursive}VIEW {if_not_and_name}{to}", if_not_and_name = if self.if_not_exists { if self.name_before_not_exists { format!("{} IF NOT EXISTS", self.name) @@ -4515,6 +4544,7 @@ impl fmt::Display for CreateView { "" }, temporary = if self.temporary { "TEMPORARY " } else { "" }, + recursive = if self.recursive { "RECURSIVE " } else { "" }, to = self .to .as_ref() @@ -4546,6 +4576,9 @@ impl fmt::Display for CreateView { f.write_str(" AS")?; SpaceOrNewline.fmt(f)?; self.query.fmt(f)?; + if let Some(check_option) = self.check_option { + write!(f, " {check_option}")?; + } if self.with_no_schema_binding { write!(f, " WITH NO SCHEMA BINDING")?; } @@ -5917,3 +5950,248 @@ impl From for crate::ast::Statement { crate::ast::Statement::AlterPolicy(v) } } + +/// PostgreSQL `CREATE STATISTICS` 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 { + /// `IF NOT EXISTS` was present (requires a name). + pub if_not_exists: bool, + /// Optional statistics object name. + pub name: Option, + /// Statistics kinds, e.g. `ndistinct`, `dependencies`, `mcv`. + pub kinds: Vec, + /// Columns or parenthesized expressions the statistics cover. + pub expressions: Vec, + /// Table the statistics are built on. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table: ObjectName, +} + +impl fmt::Display for CreateStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("CREATE STATISTICS")?; + if self.if_not_exists { + f.write_str(" IF NOT EXISTS")?; + } + if let Some(name) = &self.name { + write!(f, " {name}")?; + } + if !self.kinds.is_empty() { + write!(f, " ({})", display_comma_separated(&self.kinds))?; + } + write!( + f, + " ON {} FROM {}", + display_comma_separated(&self.expressions), + self.table + ) + } +} + +/// PostgreSQL `ALTER STATISTICS` 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 AlterStatistics { + /// `IF EXISTS` was present (only with `SET STATISTICS`). + pub if_exists: bool, + /// Statistics object name. + pub name: ObjectName, + /// Change applied to the statistics object. + pub operation: AlterStatisticsOperation, +} + +/// Change applied by an `ALTER STATISTICS` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterStatisticsOperation { + /// `OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER }` + OwnerTo(Owner), + /// `RENAME TO new_name` + RenameTo(Ident), + /// `SET SCHEMA new_schema` + SetSchema(ObjectName), + /// `SET STATISTICS { new_target | DEFAULT }`; `None` is `DEFAULT`. + SetStatistics(Option), +} + +impl fmt::Display for AlterStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("ALTER STATISTICS ")?; + if self.if_exists { + f.write_str("IF EXISTS ")?; + } + write!(f, "{} ", self.name)?; + match &self.operation { + AlterStatisticsOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterStatisticsOperation::RenameTo(name) => write!(f, "RENAME TO {name}"), + AlterStatisticsOperation::SetSchema(schema) => write!(f, "SET SCHEMA {schema}"), + AlterStatisticsOperation::SetStatistics(Some(target)) => { + write!(f, "SET STATISTICS {target}") + } + AlterStatisticsOperation::SetStatistics(None) => f.write_str("SET STATISTICS DEFAULT"), + } + } +} + +/// Change applied by a PostgreSQL `ALTER VIEW` statement. +/// +/// +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterViewOperation { + /// `ALTER [ COLUMN ] column_name SET DEFAULT expression` + AlterColumnSetDefault { + /// `true` when the `COLUMN` keyword was present. + column_keyword: bool, + /// Column name. + column: Ident, + /// Default expression. + default: Expr, + }, + /// `ALTER [ COLUMN ] column_name DROP DEFAULT` + AlterColumnDropDefault { + /// `true` when the `COLUMN` keyword was present. + column_keyword: bool, + /// Column name. + column: Ident, + }, + /// `OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER }` + OwnerTo(Owner), + /// `RENAME [ COLUMN ] column_name TO new_column_name` + RenameColumn { + /// `true` when the `COLUMN` keyword was present. + column_keyword: bool, + /// Current column name. + old_name: Ident, + /// New column name. + new_name: Ident, + }, + /// `RENAME TO new_name` + RenameTo(Ident), + /// `SET SCHEMA new_schema` + SetSchema(ObjectName), + /// `SET ( view_option_name [= view_option_value] [, ... ] )` + SetOptions(Vec), + /// `RESET ( view_option_name [, ... ] )` + ResetOptions(Vec), +} + +impl fmt::Display for AlterViewOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let column_kw = |present: bool| if present { "COLUMN " } else { "" }; + match self { + AlterViewOperation::AlterColumnSetDefault { + column_keyword, + column, + default, + } => write!( + f, + "ALTER {}{column} SET DEFAULT {default}", + column_kw(*column_keyword) + ), + AlterViewOperation::AlterColumnDropDefault { + column_keyword, + column, + } => write!( + f, + "ALTER {}{column} DROP DEFAULT", + column_kw(*column_keyword) + ), + AlterViewOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterViewOperation::RenameColumn { + column_keyword, + old_name, + new_name, + } => write!( + f, + "RENAME {}{old_name} TO {new_name}", + column_kw(*column_keyword) + ), + AlterViewOperation::RenameTo(name) => write!(f, "RENAME TO {name}"), + AlterViewOperation::SetSchema(schema) => write!(f, "SET SCHEMA {schema}"), + AlterViewOperation::SetOptions(options) => { + write!(f, "SET ({})", display_comma_separated(options)) + } + AlterViewOperation::ResetOptions(options) => { + write!(f, "RESET ({})", display_comma_separated(options)) + } + } + } +} + +/// PostgreSQL `ALTER SEQUENCE` 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 AlterSequence { + /// `IF EXISTS` was present. + pub if_exists: bool, + /// Sequence name. + pub name: ObjectName, + /// Change applied to the sequence. + pub operation: AlterSequenceOperation, +} + +/// Change applied by an `ALTER SEQUENCE` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterSequenceOperation { + /// `[ AS data_type ]`, sequence options, and `[ OWNED BY ... ]`. + Options { + /// Optional `AS data_type`. + data_type: Option, + /// Sequence options, including `RESTART`. + sequence_options: Vec, + /// Optional `OWNED BY { table_name.column_name | NONE }`. + owned_by: Option, + }, + /// `OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER }` + OwnerTo(Owner), + /// `RENAME TO new_name` + RenameTo(Ident), + /// `SET SCHEMA new_schema` + SetSchema(ObjectName), +} + +impl fmt::Display for AlterSequence { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("ALTER SEQUENCE ")?; + if self.if_exists { + f.write_str("IF EXISTS ")?; + } + write!(f, "{}", self.name)?; + match &self.operation { + AlterSequenceOperation::Options { + data_type, + sequence_options, + owned_by, + } => { + if let Some(data_type) = data_type { + write!(f, " AS {data_type}")?; + } + for option in sequence_options { + write!(f, "{option}")?; + } + if let Some(owned_by) = owned_by { + write!(f, " OWNED BY {owned_by}")?; + } + Ok(()) + } + AlterSequenceOperation::OwnerTo(owner) => write!(f, " OWNER TO {owner}"), + AlterSequenceOperation::RenameTo(name) => write!(f, " RENAME TO {name}"), + AlterSequenceOperation::SetSchema(schema) => write!(f, " SET SCHEMA {schema}"), + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 430c061919..e4921b9f4b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -65,24 +65,26 @@ pub use self::ddl::{ AlterFunctionOperation, AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, - AlterTable, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, - AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, - AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, - ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateCollation, - CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, CreateFunction, - CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, - CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTrigger, CreateView, Deduplicate, - DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, - DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, - ForValues, FunctionReturnType, GeneratedAs, 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, WithData, + AlterSequence, AlterSequenceOperation, AlterStatistics, AlterStatisticsOperation, AlterTable, + AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, AlterType, + AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, + AlterTypeRenameValue, AlterViewOperation, ClusteredBy, ColumnDef, ColumnOption, + ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, + CreateCollation, CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, + CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, + CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateStatistics, 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, ViewCheckOption, ViewColumnDef, + WithData, }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, @@ -3822,6 +3824,34 @@ pub enum Statement { /// ``` AlterType(AlterType), /// ```sql + /// ALTER VIEW [ IF EXISTS ] name { ALTER [ COLUMN ] ... | OWNER TO ... | RENAME ... | SET ... | RESET ... } + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterview.html) + AlterViewOperation { + /// `true` when `IF EXISTS` was present. + if_exists: bool, + /// View name being altered. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + name: ObjectName, + /// Change applied to the view. + operation: AlterViewOperation, + }, + /// ```sql + /// ALTER SEQUENCE + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altersequence.html) + AlterSequence(AlterSequence), + /// ```sql + /// CREATE STATISTICS + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createstatistics.html) + CreateStatistics(CreateStatistics), + /// ```sql + /// ALTER STATISTICS + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterstatistics.html) + AlterStatistics(AlterStatistics), + /// ```sql /// ALTER COLLATION /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altercollation.html) @@ -5637,6 +5667,18 @@ impl fmt::Display for Statement { Statement::AlterType(AlterType { name, operation }) => { write!(f, "ALTER TYPE {name} {operation}") } + Statement::AlterViewOperation { + if_exists, + name, + operation, + } => write!( + f, + "ALTER VIEW {if_exists}{name} {operation}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ), + Statement::AlterSequence(alter_sequence) => write!(f, "{alter_sequence}"), + Statement::CreateStatistics(create_statistics) => write!(f, "{create_statistics}"), + Statement::AlterStatistics(alter_statistics) => write!(f, "{alter_statistics}"), Statement::AlterCollation(alter_collation) => write!(f, "{alter_collation}"), Statement::AlterOperator(alter_operator) => write!(f, "{alter_operator}"), Statement::AlterOperatorFamily(alter_operator_family) => { @@ -6539,6 +6581,8 @@ pub enum SequenceOptions { Cache(Expr), /// `CYCLE` or `NO CYCLE` option. Cycle(bool), + /// `ALTER SEQUENCE ... RESTART [ [ WITH ] ]`; second value indicates presence of `WITH`. + Restart(Option, bool), } impl fmt::Display for SequenceOptions { @@ -6578,6 +6622,14 @@ impl fmt::Display for SequenceOptions { SequenceOptions::Cycle(no) => { write!(f, " {}CYCLE", if *no { "NO " } else { "" }) } + SequenceOptions::Restart(restart, with) => match restart { + Some(restart) => write!( + f, + " RESTART{with} {restart}", + with = if *with { " WITH" } else { "" } + ), + None => f.write_str(" RESTART"), + }, } } } @@ -8607,6 +8659,8 @@ pub enum ObjectType { Role, /// A sequence. Sequence, + /// An extended statistics object. + Statistics, /// A stage. Stage, /// A type definition. @@ -8629,6 +8683,7 @@ impl fmt::Display for ObjectType { ObjectType::Database => "DATABASE", ObjectType::Role => "ROLE", ObjectType::Sequence => "SEQUENCE", + ObjectType::Statistics => "STATISTICS", ObjectType::Stage => "STAGE", ObjectType::Type => "TYPE", ObjectType::User => "USER", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 49b6fa0045..8976d26cb3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -415,6 +415,10 @@ impl Spanned for Statement { // These statements need to be implemented Statement::AlterFunction { .. } => Span::empty(), Statement::AlterType { .. } => Span::empty(), + Statement::AlterViewOperation { .. } => Span::empty(), + Statement::AlterSequence { .. } => Span::empty(), + Statement::CreateStatistics { .. } => Span::empty(), + Statement::AlterStatistics { .. } => Span::empty(), Statement::AlterCollation { .. } => Span::empty(), Statement::AlterOperator { .. } => Span::empty(), Statement::AlterOperatorFamily { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ed08e4caec..fa104b40e4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5175,6 +5175,7 @@ impl<'a> Parser<'a> { .map(Into::into) } else if self.peek_keyword(Keyword::MATERIALIZED) || self.peek_keyword(Keyword::VIEW) + || self.peek_keywords(&[Keyword::RECURSIVE, Keyword::VIEW]) || self.peek_keywords(&[Keyword::SECURE, Keyword::MATERIALIZED, Keyword::VIEW]) || self.peek_keywords(&[Keyword::SECURE, Keyword::VIEW]) { @@ -5207,6 +5208,22 @@ impl<'a> Parser<'a> { ) } else if self.parse_keyword(Keyword::EXTENSION) { self.parse_create_extension().map(Into::into) + } else if self.parse_keyword(Keyword::STATISTICS) { + if temporary + || or_alter + || volatile + || persistent + || transient + || global.is_some() + || multiset.is_some() + || create_view_params.is_some() + { + return parser_err!( + "CREATE STATISTICS does not take table or view modifiers", + self.peek_token_ref().span.start + ); + } + self.parse_create_statistics() } else if self.parse_keyword(Keyword::INDEX) { self.parse_create_index(false).map(Into::into) } else if self.parse_keywords(&[Keyword::UNIQUE, Keyword::INDEX]) { @@ -6573,6 +6590,7 @@ impl<'a> Parser<'a> { ) -> Result { let secure = self.parse_keyword(Keyword::SECURE); let materialized = self.parse_keyword(Keyword::MATERIALIZED); + let recursive = !materialized && self.parse_keyword(Keyword::RECURSIVE); self.expect_keyword_is(Keyword::VIEW)?; let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); // Tries to parse IF NOT EXISTS either before name or after name @@ -6585,8 +6603,10 @@ impl<'a> Parser<'a> { let if_not_exists = if_not_exists_first || name_before_not_exists; let mut copy_grants = self.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]); // Many dialects support `OR ALTER` right after `CREATE`, but we don't (yet). - // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. let columns = self.parse_view_columns()?; + if recursive && columns.is_empty() { + return self.expected_ref("column list for RECURSIVE VIEW", self.peek_token_ref()); + } // Snowflake also documents `COPY GRANTS` *after* the column list; accept // either position, but not both. // @@ -6633,7 +6653,27 @@ impl<'a> Parser<'a> { self.expect_keyword_is(Keyword::AS)?; let query = self.parse_query()?; - // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. + let check_option = if materialized { + None + } else if self.parse_keywords(&[Keyword::WITH, Keyword::CHECK, Keyword::OPTION]) { + Some(ViewCheckOption::Unspecified) + } else if self.parse_keywords(&[ + Keyword::WITH, + Keyword::CASCADED, + Keyword::CHECK, + Keyword::OPTION, + ]) { + Some(ViewCheckOption::Cascaded) + } else if self.parse_keywords(&[ + Keyword::WITH, + Keyword::LOCAL, + Keyword::CHECK, + Keyword::OPTION, + ]) { + Some(ViewCheckOption::Local) + } else { + None + }; let with_no_schema_binding = dialect_of!(self is RedshiftSqlDialect | GenericDialect) && self.parse_keywords(&[ @@ -6661,6 +6701,8 @@ impl<'a> Parser<'a> { to, params: create_view_params, name_before_not_exists, + recursive, + check_option, }) } @@ -7431,6 +7473,8 @@ impl<'a> Parser<'a> { ObjectType::Database } else if self.parse_keyword(Keyword::SEQUENCE) { ObjectType::Sequence + } else if self.parse_keyword(Keyword::STATISTICS) { + ObjectType::Statistics } else if self.parse_keyword(Keyword::STAGE) { ObjectType::Stage } else if self.parse_keyword(Keyword::TYPE) { @@ -10269,6 +10313,13 @@ impl<'a> Parser<'a> { } _ => { let name = self.parse_identifier()?; + // PostgreSQL storage and view options take an optional value: + // `WITH (security_barrier)` means `security_barrier = true`. + if dialect_of!(self is PostgreSqlDialect | GenericDialect) + && matches!(self.peek_token_ref().token, Token::Comma | Token::RParen) + { + return Ok(SqlOption::Ident(name)); + } self.expect_token(&Token::Eq)?; let value = self.parse_expr()?; @@ -10967,6 +11018,8 @@ impl<'a> Parser<'a> { Keyword::SCHEMA, Keyword::USER, Keyword::OPERATOR, + Keyword::SEQUENCE, + Keyword::STATISTICS, ])?; match object_type { Keyword::SCHEMA => { @@ -10975,6 +11028,8 @@ impl<'a> Parser<'a> { self.parse_alter_schema() } Keyword::VIEW => self.parse_alter_view(), + Keyword::SEQUENCE => self.parse_alter_sequence(), + Keyword::STATISTICS => self.parse_alter_statistics(), Keyword::TYPE => self.parse_alter_type(), Keyword::COLLATION => self.parse_alter_collation().map(Into::into), Keyword::TABLE => self.parse_alter_table(false), @@ -11358,7 +11413,21 @@ impl<'a> Parser<'a> { /// Parse an `ALTER VIEW` statement. pub fn parse_alter_view(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let name = self.parse_object_name(false)?; + if let Some(operation) = self.parse_alter_view_operation()? { + return Ok(Statement::AlterViewOperation { + if_exists, + name, + operation, + }); + } + if if_exists { + return self.expected_ref( + "ALTER, OWNER TO, RENAME, SET, or RESET after ALTER VIEW IF EXISTS", + self.peek_token_ref(), + ); + } let columns = self.parse_parenthesized_column_list(Optional, false)?; let with_options = self.parse_options(Keyword::WITH)?; @@ -11374,6 +11443,233 @@ impl<'a> Parser<'a> { }) } + /// Parse the PostgreSQL `ALTER VIEW` operation that follows the view name, + /// or return `None` for the `[ (columns) ] [ WITH (...) ] AS query` form. + fn parse_alter_view_operation(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::ALTER) { + let column_keyword = self.parse_keyword(Keyword::COLUMN); + let column = self.parse_identifier()?; + let operation = if self.parse_keywords(&[Keyword::SET, Keyword::DEFAULT]) { + AlterViewOperation::AlterColumnSetDefault { + column_keyword, + column, + default: self.parse_expr()?, + } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::DEFAULT]) { + AlterViewOperation::AlterColumnDropDefault { + column_keyword, + column, + } + } else { + return self.expected_ref("SET DEFAULT or DROP DEFAULT", self.peek_token_ref()); + }; + Ok(Some(operation)) + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + Ok(Some(AlterViewOperation::OwnerTo(self.parse_owner()?))) + } else if self.parse_keyword(Keyword::RENAME) { + if self.parse_keyword(Keyword::TO) { + return Ok(Some(AlterViewOperation::RenameTo(self.parse_identifier()?))); + } + let column_keyword = self.parse_keyword(Keyword::COLUMN); + let old_name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::TO)?; + let new_name = self.parse_identifier()?; + Ok(Some(AlterViewOperation::RenameColumn { + column_keyword, + old_name, + new_name, + })) + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + Ok(Some(AlterViewOperation::SetSchema( + self.parse_object_name(false)?, + ))) + } else if self.parse_keyword(Keyword::SET) { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; + self.expect_token(&Token::RParen)?; + Ok(Some(AlterViewOperation::SetOptions(options))) + } else if self.parse_keyword(Keyword::RESET) { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + Ok(Some(AlterViewOperation::ResetOptions(options))) + } else { + Ok(None) + } + } + + /// Parse a PostgreSQL `ALTER SEQUENCE` statement. + pub fn parse_alter_sequence(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let operation = if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterSequenceOperation::OwnerTo(self.parse_owner()?) + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterSequenceOperation::RenameTo(self.parse_identifier()?) + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterSequenceOperation::SetSchema(self.parse_object_name(false)?) + } else { + let mut data_type = self.parse_sequence_data_type(None)?; + let mut sequence_options = vec![]; + let mut owned_by = None; + loop { + let mut options = self.parse_create_sequence_options()?; + let progressed = !options.is_empty(); + sequence_options.append(&mut options); + if self.peek_keyword(Keyword::AS) { + data_type = self.parse_sequence_data_type(data_type)?; + } else if self.parse_keyword(Keyword::RESTART) { + let with = self.parse_keyword(Keyword::WITH); + let restart = if with + || matches!( + self.peek_token_ref().token, + Token::Number(_, _) | Token::Minus | Token::Plus + ) { + Some(self.parse_number()?) + } else { + None + }; + sequence_options.push(SequenceOptions::Restart(restart, with)); + } else if self.peek_keywords(&[Keyword::OWNED, Keyword::BY]) { + owned_by = self.parse_sequence_owned_by(owned_by)?; + } else if !progressed { + break; + } + } + if data_type.is_none() && sequence_options.is_empty() && owned_by.is_none() { + return self.expected_ref("an ALTER SEQUENCE action", self.peek_token_ref()); + } + AlterSequenceOperation::Options { + data_type, + sequence_options, + owned_by, + } + }; + Ok(Statement::AlterSequence(AlterSequence { + if_exists, + name, + operation, + })) + } + + /// Parse a PostgreSQL `CREATE STATISTICS` statement. + pub fn parse_create_statistics(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = if if_not_exists + || !(self.peek_keyword(Keyword::ON) || self.peek_token_ref().token == Token::LParen) + { + Some(self.parse_object_name(false)?) + } else { + None + }; + let kinds = if self.consume_token(&Token::LParen) { + let kinds = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + kinds + } else { + vec![] + }; + self.expect_keyword_is(Keyword::ON)?; + let expressions = self.parse_comma_separated(|parser| { + // PostgreSQL `stats_param`: an unqualified column name, a windowless + // function expression, or a parenthesized expression. + let expr = parser.parse_expr()?; + let allowed = match &expr { + Expr::Identifier(_) | Expr::Nested(_) => true, + Expr::Function(function) => { + function.over.is_none() + && function.filter.is_none() + && function.within_group.is_empty() + && function.null_treatment.is_none() + } + Expr::Cast { + kind: CastKind::Cast, + .. + } + | Expr::Extract { .. } + | Expr::Ceil { .. } + | Expr::Floor { .. } + | Expr::Position { .. } + | Expr::Substring { .. } + | Expr::Trim { .. } + | Expr::Overlay { .. } => true, + _ => false, + }; + if allowed { + Ok(expr) + } else { + parser_err!( + format!( + "CREATE STATISTICS expects a column name, a function call, or a parenthesized expression, found {expr}" + ), + parser.peek_token_ref().span.start + ) + } + })?; + self.expect_keyword_is(Keyword::FROM)?; + let table = self.parse_object_name(false)?; + Ok(Statement::CreateStatistics(CreateStatistics { + if_not_exists, + name, + kinds, + expressions, + table, + })) + } + + /// Parse a PostgreSQL `SignedIconst`: an optional sign followed by an + /// integer literal that fits in 32 bits, such as `100`, `-1`, or `+5`. + fn parse_signed_iconst(&mut self) -> Result { + let sign = match self.peek_token_ref().token { + Token::Minus | Token::Plus => 1, + _ => 0, + }; + let is_iconst = match &self.peek_nth_token_ref(sign).token { + Token::Number(n, false) => { + n.bytes().all(|b| b.is_ascii_digit()) && n.parse::().is_ok() + } + _ => false, + }; + if !is_iconst { + return self.expected_ref("an integer", self.peek_nth_token_ref(sign)); + } + self.parse_number() + } + + /// Parse a PostgreSQL `ALTER STATISTICS` statement. + pub fn parse_alter_statistics(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let operation = if self.parse_keywords(&[Keyword::SET, Keyword::STATISTICS]) { + if self.parse_keyword(Keyword::DEFAULT) { + AlterStatisticsOperation::SetStatistics(None) + } else { + AlterStatisticsOperation::SetStatistics(Some(self.parse_signed_iconst()?)) + } + } else if if_exists { + return self.expected_ref( + "SET STATISTICS after ALTER STATISTICS IF EXISTS", + self.peek_token_ref(), + ); + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterStatisticsOperation::OwnerTo(self.parse_owner()?) + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterStatisticsOperation::RenameTo(self.parse_identifier()?) + } else if self.parse_keywords(&[Keyword::SET, Keyword::SCHEMA]) { + AlterStatisticsOperation::SetSchema(self.parse_object_name(false)?) + } else { + return self.expected_ref( + "OWNER TO, RENAME TO, SET SCHEMA, or SET STATISTICS", + self.peek_token_ref(), + ); + }; + Ok(Statement::AlterStatistics(AlterStatistics { + if_exists, + name, + operation, + })) + } + /// Parse a [Statement::AlterType] pub fn parse_alter_type(&mut self) -> Result { let name = self.parse_object_name(false)?; @@ -20055,22 +20351,23 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); //name let name = self.parse_object_name(false)?; - //[ AS data_type ] - let mut data_type: Option = None; - if self.parse_keywords(&[Keyword::AS]) { - data_type = Some(self.parse_data_type()?) - } - let sequence_options = self.parse_create_sequence_options()?; - // [ OWNED BY { table_name.column_name | NONE } ] - let owned_by = if self.parse_keywords(&[Keyword::OWNED, Keyword::BY]) { - if self.parse_keywords(&[Keyword::NONE]) { - Some(ObjectName::from(vec![Ident::new("NONE")])) - } else { - Some(self.parse_object_name(false)?) + // `AS data_type` and `OWNED BY` are sequence options, so like the other + // options they may appear in any order. + let mut data_type = None; + let mut sequence_options = vec![]; + let mut owned_by = None; + loop { + let mut options = self.parse_create_sequence_options()?; + let progressed = !options.is_empty(); + sequence_options.append(&mut options); + if self.peek_keyword(Keyword::AS) { + data_type = self.parse_sequence_data_type(data_type)?; + } else if self.peek_keywords(&[Keyword::OWNED, Keyword::BY]) { + owned_by = self.parse_sequence_owned_by(owned_by)?; + } else if !progressed { + break; } - } else { - None - }; + } Ok(Statement::CreateSequence { temporary, if_not_exists, @@ -20081,6 +20378,47 @@ impl<'a> Parser<'a> { }) } + /// Parse an optional `OWNED BY { table_name.column_name | NONE }` sequence + /// option. `current` is the value already parsed; a second `OWNED BY` is an + /// error. + fn parse_sequence_owned_by( + &mut self, + current: Option, + ) -> Result, ParserError> { + if !self.parse_keywords(&[Keyword::OWNED, Keyword::BY]) { + return Ok(current); + } + if current.is_some() { + return parser_err!( + "conflicting or redundant sequence OWNED BY options", + self.peek_token_ref().span.start + ); + } + Ok(Some(if self.parse_keyword(Keyword::NONE) { + ObjectName::from(vec![Ident::new("NONE")]) + } else { + self.parse_object_name(false)? + })) + } + + /// Parse an optional `AS data_type` sequence option. `current` is the type + /// already parsed for this statement; a second `AS` is an error. + fn parse_sequence_data_type( + &mut self, + current: Option, + ) -> Result, ParserError> { + if !self.parse_keyword(Keyword::AS) { + return Ok(current); + } + if current.is_some() { + return parser_err!( + "conflicting or redundant sequence AS data_type options", + self.peek_token_ref().span.start + ); + } + Ok(Some(self.parse_data_type()?)) + } + fn parse_create_sequence_options(&mut self) -> Result, ParserError> { let mut sequence_options = vec![]; // PostgreSQL allows these clauses in any order (e.g. pg_dump emits diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4456f9ee22..12589919fc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8531,6 +8531,8 @@ fn parse_create_view() { name_before_not_exists: _, secure: _, copy_grants: _, + recursive: false, + check_option: None, }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8650,6 +8652,8 @@ fn parse_create_view_temporary() { name_before_not_exists: _, secure: _, copy_grants: _, + recursive: false, + check_option: None, }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8692,6 +8696,8 @@ fn parse_create_or_replace_view() { name_before_not_exists: _, secure: _, copy_grants: _, + recursive: false, + check_option: None, }) => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); @@ -8738,6 +8744,8 @@ fn parse_create_or_replace_materialized_view() { name_before_not_exists: _, secure: _, copy_grants: _, + recursive: false, + check_option: None, }) => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); @@ -8780,6 +8788,8 @@ fn parse_create_materialized_view() { name_before_not_exists: _, secure: _, copy_grants: _, + recursive: false, + check_option: None, }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); @@ -8822,6 +8832,8 @@ fn parse_create_materialized_view_with_cluster_by() { name_before_not_exists: _, secure: _, copy_grants: _, + recursive: false, + check_option: None, }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index f7db11a504..5c6d37c78d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9731,3 +9731,333 @@ fn set_session_characteristics_requires_transaction_mode() { .parse_sql_statements("SET SESSION CHARACTERISTICS AS TRANSACTION") .is_err()); } + +#[test] +fn dsql_supported_create_statistics() { + for sql in [ + "CREATE STATISTICS s1 ON a, b FROM t", + "CREATE STATISTICS s2 (ndistinct, dependencies, mcv) ON a, b FROM t", + "CREATE STATISTICS IF NOT EXISTS s2 (dependencies) ON a, b FROM t", + "CREATE STATISTICS s3 ON (lower(c)) FROM t", + "CREATE STATISTICS s4 ON a, (b + 1) FROM t", + "CREATE STATISTICS ON a, c FROM t", + "CREATE STATISTICS (mcv) ON a, b FROM t", + "CREATE STATISTICS myschema.s5 ON b, c FROM myschema.t2", + "CREATE STATISTICS s6 ON a, CEIL(b) FROM t", + "CREATE STATISTICS s7 ON a, CAST(b AS TEXT) FROM t", + "CREATE STATISTICS s8 ON a, EXTRACT(YEAR FROM d) FROM t", + "CREATE STATISTICS s9 ON a, SUBSTRING(c FROM 1 FOR 2) FROM t", + "CREATE STATISTICS s10 ON a, COALESCE(a, 0) FROM t", + "CREATE STATISTICS s11 ON lower(c), a FROM t", + "CREATE STATISTICS s12 ON a, FLOOR(b), POSITION('x' IN c) FROM t", + "CREATE STATISTICS s13 ON TRIM(c), OVERLAY(c PLACING 'x' FROM 1 FOR 1) FROM t", + ] { + pg().verified_stmt(sql); + } + + match pg() + .verified_stmt("CREATE STATISTICS IF NOT EXISTS s (ndistinct, mcv) ON a, (b + 1) FROM s.t") + { + Statement::CreateStatistics(CreateStatistics { + if_not_exists, + name, + kinds, + expressions, + table, + }) => { + assert!(if_not_exists); + assert_eq!(name, Some(ObjectName::from(vec![Ident::new("s")]))); + assert_eq!(kinds, vec![Ident::new("ndistinct"), Ident::new("mcv")]); + assert_eq!(expressions.len(), 2); + assert_eq!( + table, + ObjectName::from(vec![Ident::new("s"), Ident::new("t")]) + ); + } + _ => unreachable!(), + } + + for sql in [ + "CREATE STATISTICS IF NOT EXISTS ON a, b FROM t", + "CREATE STATISTICS s ON a, b", + "CREATE STATISTICS s () ON a, b FROM t", + "CREATE STATISTICS s ON a + b FROM t", + "CREATE STATISTICS s ON a, b + 1 FROM t", + "CREATE TEMPORARY STATISTICS s ON a, b FROM t", + "CREATE OR ALTER STATISTICS s ON a, b FROM t", + "CREATE GLOBAL STATISTICS s ON a, b FROM t", + "CREATE VOLATILE STATISTICS s ON a, b FROM t", + "CREATE TRANSIENT STATISTICS s ON a, b FROM t", + "CREATE STATISTICS s ON t.a, b FROM t", + "CREATE STATISTICS s ON a, sum(a) OVER () FROM t", + "CREATE STATISTICS s ON a, b::TEXT FROM t", + "CREATE STATISTICS s ON a, count(*) FILTER (WHERE a > 0) FROM t", + "CREATE STATISTICS s ON a, percentile_cont(0.5) WITHIN GROUP (ORDER BY b) FROM t", + "CREATE MULTISET STATISTICS s ON a, b FROM t", + ] { + assert!(pg().parse_sql_statements(sql).is_err(), "{sql}"); + } +} + +#[test] +fn dsql_supported_alter_statistics() { + for sql in [ + "ALTER STATISTICS s SET STATISTICS 100", + "ALTER STATISTICS s SET STATISTICS -1", + "ALTER STATISTICS s SET STATISTICS DEFAULT", + "ALTER STATISTICS IF EXISTS s SET STATISTICS 0", + "ALTER STATISTICS s RENAME TO s2", + "ALTER STATISTICS s OWNER TO CURRENT_USER", + "ALTER STATISTICS s SET SCHEMA other", + "ALTER STATISTICS s SET STATISTICS +50", + "ALTER STATISTICS s SET STATISTICS 2147483647", + ] { + pg().verified_stmt(sql); + } + + match pg().verified_stmt("ALTER STATISTICS IF EXISTS myschema.s SET STATISTICS DEFAULT") { + Statement::AlterStatistics(AlterStatistics { + if_exists, + name, + operation, + }) => { + assert!(if_exists); + assert_eq!(name.to_string(), "myschema.s"); + assert_eq!(operation, AlterStatisticsOperation::SetStatistics(None)); + } + _ => unreachable!(), + } + + // IF EXISTS belongs to the SET STATISTICS form only. + for sql in [ + "ALTER STATISTICS IF EXISTS s RENAME TO s2", + "ALTER STATISTICS IF EXISTS s OWNER TO CURRENT_USER", + "ALTER STATISTICS IF EXISTS s SET SCHEMA other", + "ALTER STATISTICS s", + "ALTER STATISTICS s SET STATISTICS 50.5", + "ALTER STATISTICS s SET STATISTICS 1e2", + "ALTER STATISTICS s SET STATISTICS 99999999999999999999", + "ALTER STATISTICS s SET STATISTICS 2147483648", + ] { + assert!(pg().parse_sql_statements(sql).is_err(), "{sql}"); + } +} + +#[test] +fn dsql_supported_drop_statistics() { + for sql in [ + "DROP STATISTICS s", + "DROP STATISTICS IF EXISTS s, other.s2 CASCADE", + "DROP STATISTICS s RESTRICT", + ] { + match pg().verified_stmt(sql) { + Statement::Drop { object_type, .. } => assert_eq!(object_type, ObjectType::Statistics), + _ => unreachable!(), + } + } +} + +#[test] +fn dsql_supported_create_view_forms() { + for sql in [ + "CREATE RECURSIVE VIEW nums (n) AS VALUES (1) UNION ALL SELECT n + 1 FROM nums WHERE n < 10", + "CREATE OR REPLACE RECURSIVE VIEW public.nums (n) AS VALUES (1) UNION ALL SELECT n + 1 FROM nums WHERE n < 20", + "CREATE VIEW v1 AS SELECT id, a FROM t WHERE a > 0 WITH CHECK OPTION", + "CREATE VIEW v2 AS SELECT id, a FROM v1 WHERE a < 100 WITH CASCADED CHECK OPTION", + "CREATE OR REPLACE VIEW v2 AS SELECT id, a FROM v1 WHERE a < 50 WITH LOCAL CHECK OPTION", + "CREATE VIEW v3 WITH (security_invoker = true, security_barrier) AS SELECT id FROM t", + "CREATE VIEW v4 WITH (check_option = local) AS SELECT id, a FROM t WHERE a > 0", + ] { + pg().verified_stmt(sql); + } + + match pg().verified_stmt( + "CREATE OR REPLACE VIEW v WITH (security_barrier) AS SELECT 1 WITH CASCADED CHECK OPTION", + ) { + Statement::CreateView(CreateView { + or_replace, + recursive, + check_option, + options, + .. + }) => { + assert!(or_replace); + assert!(!recursive); + assert_eq!(check_option, Some(ViewCheckOption::Cascaded)); + assert_eq!( + options, + CreateTableOptions::With(vec![SqlOption::Ident(Ident::new("security_barrier"))]) + ); + } + _ => unreachable!(), + } + + match pg().verified_stmt("CREATE RECURSIVE VIEW nums (n) AS VALUES (1)") { + Statement::CreateView(CreateView { + recursive, + check_option, + columns, + .. + }) => { + assert!(recursive); + assert_eq!(check_option, None); + assert_eq!(columns.len(), 1); + } + _ => unreachable!(), + } + + // A recursive view names its output columns; RECURSIVE and CHECK OPTION + // belong to plain views. + for sql in [ + "CREATE RECURSIVE VIEW r AS VALUES (1)", + "CREATE MATERIALIZED RECURSIVE VIEW r (n) AS SELECT 1", + "CREATE MATERIALIZED VIEW mv AS SELECT 1 WITH CHECK OPTION", + ] { + assert!(pg().parse_sql_statements(sql).is_err(), "{sql}"); + } +} + +#[test] +fn dsql_supported_alter_view_operations() { + for sql in [ + "ALTER VIEW v ALTER COLUMN a SET DEFAULT 1", + "ALTER VIEW IF EXISTS v ALTER a DROP DEFAULT", + "ALTER VIEW v OWNER TO CURRENT_USER", + "ALTER VIEW v RENAME COLUMN id TO new_id", + "ALTER VIEW v RENAME new_id TO id", + "ALTER VIEW v RENAME TO v2", + "ALTER VIEW v SET SCHEMA other", + "ALTER VIEW v SET (check_option = cascaded, security_barrier = true)", + "ALTER VIEW v SET (security_barrier)", + "ALTER VIEW v RESET (check_option, security_barrier)", + "ALTER VIEW IF EXISTS missing RENAME TO missing2", + ] { + pg().verified_stmt(sql); + } + + match pg().verified_stmt("ALTER VIEW IF EXISTS s.v ALTER COLUMN ts SET DEFAULT now()") { + Statement::AlterViewOperation { + if_exists, + name, + operation, + } => { + assert!(if_exists); + assert_eq!(name.to_string(), "s.v"); + assert!(matches!( + operation, + AlterViewOperation::AlterColumnSetDefault { + column_keyword: true, + .. + } + )); + } + _ => unreachable!(), + } + + // The query form keeps its own statement variant. + assert!(matches!( + pg().verified_stmt("ALTER VIEW v AS SELECT 1"), + Statement::AlterView { .. } + )); + + for sql in [ + "ALTER VIEW v", + "ALTER VIEW v RESET ()", + "ALTER VIEW v RENAME COLUMN a", + "ALTER VIEW IF EXISTS v AS SELECT 1", + ] { + assert!(pg().parse_sql_statements(sql).is_err(), "{sql}"); + } +} + +#[test] +fn dsql_supported_alter_sequence() { + for sql in [ + "ALTER SEQUENCE seq INCREMENT BY 2 MINVALUE 1 MAXVALUE 1000000 NO CYCLE", + "ALTER SEQUENCE seq RESTART WITH 105", + "ALTER SEQUENCE IF EXISTS seq RESTART", + "ALTER SEQUENCE seq START WITH 10", + "ALTER SEQUENCE seq CACHE 65536", + "ALTER SEQUENCE seq NO MINVALUE NO MAXVALUE CYCLE", + "ALTER SEQUENCE seq INCREMENT 3 RESTART 1 CACHE 1", + "ALTER SEQUENCE seq AS BIGINT", + "ALTER SEQUENCE seq OWNED BY t.a", + "ALTER SEQUENCE seq OWNED BY NONE", + "ALTER SEQUENCE seq OWNER TO CURRENT_USER", + "ALTER SEQUENCE seq RENAME TO seq2", + "ALTER SEQUENCE seq2 SET SCHEMA other", + "ALTER SEQUENCE IF EXISTS missing RESTART", + ] { + pg().verified_stmt(sql); + } + + match pg().verified_stmt( + "ALTER SEQUENCE IF EXISTS s.seq AS BIGINT RESTART WITH 5 CACHE 1 OWNED BY t.a", + ) { + Statement::AlterSequence(AlterSequence { + if_exists, + name, + operation: + AlterSequenceOperation::Options { + data_type, + sequence_options, + owned_by, + }, + }) => { + assert!(if_exists); + assert_eq!(name.to_string(), "s.seq"); + assert_eq!(data_type, Some(DataType::BigInt(None))); + assert_eq!( + sequence_options, + vec![ + SequenceOptions::Restart(Some(Expr::value(number("5"))), true), + SequenceOptions::Cache(Expr::value(number("1"))), + ] + ); + assert_eq!(owned_by.map(|o| o.to_string()), Some("t.a".to_string())); + } + _ => unreachable!(), + } + + // `AS data_type` is a sequence option and may appear anywhere in the list. + for (sql, canonical) in [ + ( + "ALTER SEQUENCE seq RESTART AS BIGINT", + "ALTER SEQUENCE seq AS BIGINT RESTART", + ), + ( + "ALTER SEQUENCE seq INCREMENT BY 1 AS BIGINT CACHE 1", + "ALTER SEQUENCE seq AS BIGINT INCREMENT BY 1 CACHE 1", + ), + ( + "CREATE SEQUENCE seq CACHE 1 AS BIGINT", + "CREATE SEQUENCE seq AS BIGINT CACHE 1", + ), + ( + "CREATE SEQUENCE seq CACHE 1 OWNED BY NONE AS BIGINT", + "CREATE SEQUENCE seq AS BIGINT CACHE 1 OWNED BY NONE", + ), + ( + "CREATE SEQUENCE seq OWNED BY t.a CACHE 1", + "CREATE SEQUENCE seq CACHE 1 OWNED BY t.a", + ), + ( + "ALTER SEQUENCE seq OWNED BY NONE AS BIGINT CACHE 1", + "ALTER SEQUENCE seq AS BIGINT CACHE 1 OWNED BY NONE", + ), + ] { + pg().one_statement_parses_to(sql, canonical); + } + + for sql in [ + "ALTER SEQUENCE seq", + "ALTER SEQUENCE IF EXISTS seq", + "ALTER SEQUENCE seq RENAME seq2", + "ALTER SEQUENCE seq AS BIGINT CACHE 1 AS BIGINT", + "CREATE SEQUENCE seq AS BIGINT CACHE 1 AS BIGINT", + "CREATE SEQUENCE seq OWNED BY NONE CACHE 1 OWNED BY NONE", + "ALTER SEQUENCE seq OWNED BY NONE CACHE 1 OWNED BY NONE", + ] { + assert!(pg().parse_sql_statements(sql).is_err(), "{sql}"); + } +}