From 67f0bca651a11f4ea906e81b578c555b7bb33806 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 12:48:20 +0200 Subject: [PATCH 01/12] Moved `Analyze` out of `Statement` enum --- src/ast/mod.rs | 110 +++++++++++++++++++++++++--------------------- src/ast/spans.rs | 63 +++++++++++++------------- src/parser/mod.rs | 4 +- 3 files changed, 95 insertions(+), 82 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4c1743feb..4f87cb7ed 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3060,6 +3060,59 @@ impl Display for ExceptionWhen { } } +/// ANALYZE TABLE statement (Hive-specific) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Analyze { + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + pub partitions: Option>, + pub for_columns: bool, + pub columns: Vec, + pub cache_metadata: bool, + pub noscan: bool, + pub compute_statistics: bool, + pub has_table_keyword: bool, +} + +impl fmt::Display for Analyze { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ANALYZE{}{table_name}", + if self.has_table_keyword { + " TABLE " + } else { + " " + }, + table_name = self.table_name + )?; + if let Some(ref parts) = self.partitions { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + + if self.compute_statistics { + write!(f, " COMPUTE STATISTICS")?; + } + if self.noscan { + write!(f, " NOSCAN")?; + } + if self.cache_metadata { + write!(f, " CACHE METADATA")?; + } + if self.for_columns { + write!(f, " FOR COLUMNS")?; + if !self.columns.is_empty() { + write!(f, " {}", display_comma_separated(&self.columns))?; + } + } + Ok(()) + } +} + /// A top-level statement (SELECT, INSERT, CREATE, etc.) #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -3074,17 +3127,7 @@ pub enum Statement { /// ANALYZE /// ``` /// Analyze (Hive) - Analyze { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - partitions: Option>, - for_columns: bool, - columns: Vec, - cache_metadata: bool, - noscan: bool, - compute_statistics: bool, - has_table_keyword: bool, - }, + Analyze(Analyze), Set(Set), /// ```sql /// TRUNCATE @@ -4324,6 +4367,12 @@ pub enum Statement { Vacuum(VacuumStatement), } +impl From for Statement { + fn from(analyze: Analyze) -> Self { + Statement::Analyze(analyze) + } +} + /// ```sql /// {COPY | REVOKE} CURRENT GRANTS /// ``` @@ -4633,44 +4682,7 @@ impl fmt::Display for Statement { )?; Ok(()) } - Statement::Analyze { - table_name, - partitions, - for_columns, - columns, - cache_metadata, - noscan, - compute_statistics, - has_table_keyword, - } => { - write!( - f, - "ANALYZE{}{table_name}", - if *has_table_keyword { " TABLE " } else { " " } - )?; - if let Some(ref parts) = partitions { - if !parts.is_empty() { - write!(f, " PARTITION ({})", display_comma_separated(parts))?; - } - } - - if *compute_statistics { - write!(f, " COMPUTE STATISTICS")?; - } - if *noscan { - write!(f, " NOSCAN")?; - } - if *cache_metadata { - write!(f, " CACHE METADATA")?; - } - if *for_columns { - write!(f, " FOR COLUMNS")?; - if !columns.is_empty() { - write!(f, " {}", display_comma_separated(columns))?; - } - } - Ok(()) - } + Statement::Analyze(analyze) => analyze.fmt(f), Statement::Insert(insert) => insert.fmt(f), Statement::Install { extension_name: name, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 4c53e55ce..7b2cd6d93 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -25,23 +25,23 @@ use crate::tokenizer::Span; use super::{ dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterIndexOperation, AlterTableOperation, Array, Assignment, AssignmentTarget, AttachedToken, - BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, - ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectBy, - ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, - Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, - Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, - FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, - Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, - LateralView, LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, - ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction, - RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, - SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, - TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, - UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement, - WildcardAdditionalOptions, With, WithFill, + AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget, + AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, + ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, + ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, + CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, + ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, + FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, + IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, + JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, + MatchRecognizePattern, Measure, NamedParenthesizedList, NamedWindowDefinition, ObjectName, + ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, + OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, RaiseStatement, + RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, + SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, + TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, + WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -298,20 +298,7 @@ impl Spanned for Values { impl Spanned for Statement { fn span(&self) -> Span { match self { - Statement::Analyze { - table_name, - partitions, - for_columns: _, - columns, - cache_metadata: _, - noscan: _, - compute_statistics: _, - has_table_keyword: _, - } => union_spans( - core::iter::once(table_name.span()) - .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))) - .chain(columns.iter().map(|i| i.span)), - ), + Statement::Analyze(analyze) => analyze.span(), Statement::Truncate { table_names, partitions, @@ -944,6 +931,20 @@ impl Spanned for ConstraintCharacteristics { } } +impl Spanned for Analyze { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.table_name.span()) + .chain( + self.partitions + .iter() + .flat_map(|i| i.iter().map(|k| k.span())), + ) + .chain(self.columns.iter().map(|i| i.span)), + ) + } +} + /// # partial span /// /// Missing spans: diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d97fa1bd0..8e311d5ec 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1167,7 +1167,7 @@ impl<'a> Parser<'a> { } } - Ok(Statement::Analyze { + Ok(Analyze { has_table_keyword, table_name, for_columns, @@ -1176,7 +1176,7 @@ impl<'a> Parser<'a> { cache_metadata, noscan, compute_statistics, - }) + }.into()) } /// Parse a new expression including wildcard & qualified wildcard. From f006125b7b098760d60e8c4103fbb2ef0fc4bf14 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 12:59:47 +0200 Subject: [PATCH 02/12] Moved `Truncate` out of the `Statement` enum --- src/ast/ddl.rs | 71 +++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 68 ++++++----------------------------- src/ast/spans.rs | 14 +------- src/parser/mod.rs | 8 +++-- tests/sqlparser_common.rs | 8 ++--- tests/sqlparser_postgres.rs | 12 +++---- 6 files changed, 97 insertions(+), 84 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c4f769675..79842db22 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3416,3 +3416,74 @@ impl fmt::Display for DropTrigger { Ok(()) } } + +/// A `TRUNCATE` statement. +/// +/// ```sql +/// TRUNCATE TABLE table_names [PARTITION (partitions)] [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE | RESTRICT] [ON CLUSTER cluster_name] +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Truncate { + /// Table names to truncate + pub table_names: Vec, + /// Optional partition specification + pub partitions: Option>, + /// TABLE - optional keyword + pub table: bool, + /// Postgres-specific option: [ RESTART IDENTITY | CONTINUE IDENTITY ] + pub identity: Option, + /// Postgres-specific option: [ CASCADE | RESTRICT ] + pub cascade: Option, + /// ClickHouse-specific option: [ ON CLUSTER cluster_name ] + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) + pub on_cluster: Option, +} + +impl fmt::Display for Truncate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let table = if self.table { "TABLE " } else { "" }; + + write!( + f, + "TRUNCATE {table}{table_names}", + table_names = display_comma_separated(&self.table_names) + )?; + + if let Some(identity) = &self.identity { + match identity { + super::TruncateIdentityOption::Restart => write!(f, " RESTART IDENTITY")?, + super::TruncateIdentityOption::Continue => write!(f, " CONTINUE IDENTITY")?, + } + } + if let Some(cascade) = &self.cascade { + match cascade { + super::CascadeOption::Cascade => write!(f, " CASCADE")?, + super::CascadeOption::Restrict => write!(f, " RESTRICT")?, + } + } + + if let Some(ref parts) = &self.partitions { + if !parts.is_empty() { + write!(f, " PARTITION ({})", display_comma_separated(parts))?; + } + } + if let Some(on_cluster) = &self.on_cluster { + write!(f, " ON CLUSTER {on_cluster}")?; + } + Ok(()) + } +} + +impl Spanned for Truncate { + fn span(&self) -> Span { + Span::union_iter( + self.table_names.iter().map(|i| i.name.span()).chain( + self.partitions + .iter() + .flat_map(|i| i.iter().map(|k| k.span())), + ), + ) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4f87cb7ed..5039c9832 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -69,8 +69,8 @@ pub use self::ddl::{ IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TableConstraint, - TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, - ViewColumnDef, + TagsColumnOption, Truncate, UserDefinedTypeCompositeAttributeDef, + UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{Delete, Insert}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -3133,23 +3133,7 @@ pub enum Statement { /// TRUNCATE /// ``` /// Truncate (Hive) - Truncate { - table_names: Vec, - partitions: Option>, - /// TABLE - optional keyword; - table: bool, - /// Postgres-specific option - /// [ RESTART IDENTITY | CONTINUE IDENTITY ] - identity: Option, - /// Postgres-specific option - /// [ CASCADE | RESTRICT ] - cascade: Option, - /// ClickHouse-specific option - /// [ ON CLUSTER cluster_name ] - /// - /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/truncate/) - on_cluster: Option, - }, + Truncate(Truncate), /// ```sql /// MSCK /// ``` @@ -4373,6 +4357,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(truncate: ddl::Truncate) -> Self { + Statement::Truncate(truncate) + } +} + /// ```sql /// {COPY | REVOKE} CURRENT GRANTS /// ``` @@ -4589,45 +4579,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Truncate { - table_names, - partitions, - table, - identity, - cascade, - on_cluster, - } => { - let table = if *table { "TABLE " } else { "" }; - - write!( - f, - "TRUNCATE {table}{table_names}", - table_names = display_comma_separated(table_names) - )?; - - if let Some(identity) = identity { - match identity { - TruncateIdentityOption::Restart => write!(f, " RESTART IDENTITY")?, - TruncateIdentityOption::Continue => write!(f, " CONTINUE IDENTITY")?, - } - } - if let Some(cascade) = cascade { - match cascade { - CascadeOption::Cascade => write!(f, " CASCADE")?, - CascadeOption::Restrict => write!(f, " RESTRICT")?, - } - } - - if let Some(ref parts) = partitions { - if !parts.is_empty() { - write!(f, " PARTITION ({})", display_comma_separated(parts))?; - } - } - if let Some(on_cluster) = on_cluster { - write!(f, " ON CLUSTER {on_cluster}")?; - } - Ok(()) - } + Statement::Truncate(truncate) => truncate.fmt(f), Statement::Case(stmt) => { write!(f, "{stmt}") } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7b2cd6d93..93b856d62 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -299,19 +299,7 @@ impl Spanned for Statement { fn span(&self) -> Span { match self { Statement::Analyze(analyze) => analyze.span(), - Statement::Truncate { - table_names, - partitions, - table: _, - identity: _, - cascade: _, - on_cluster: _, - } => union_spans( - table_names - .iter() - .map(|i| i.name.span()) - .chain(partitions.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ), + Statement::Truncate(truncate) => truncate.span(), Statement::Msck { table_name, repair: _, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8e311d5ec..cd0ff6fba 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1024,14 +1024,15 @@ impl<'a> Parser<'a> { let on_cluster = self.parse_optional_on_cluster()?; - Ok(Statement::Truncate { + Ok(Truncate { table_names, partitions, table, identity, cascade, on_cluster, - }) + } + .into()) } fn parse_cascade_option(&mut self) -> Option { @@ -1176,7 +1177,8 @@ impl<'a> Parser<'a> { cache_metadata, noscan, compute_statistics, - }.into()) + } + .into()) } /// Parse a new expression including wildcard & qualified wildcard. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 365d5469e..784e1cf37 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -13343,8 +13343,8 @@ fn test_extract_seconds_single_quote_err() { fn test_truncate_table_with_on_cluster() { let sql = "TRUNCATE TABLE t ON CLUSTER cluster_name"; match all_dialects().verified_stmt(sql) { - Statement::Truncate { on_cluster, .. } => { - assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); + Statement::Truncate(truncate) => { + assert_eq!(truncate.on_cluster, Some(Ident::new("cluster_name"))); } _ => panic!("Expected: TRUNCATE TABLE statement"), } @@ -16399,14 +16399,14 @@ fn parse_truncate_only() { ]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: true, identity: None, cascade: None, on_cluster: None, - }, + }), truncate ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 196a82f54..6479273e5 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4956,14 +4956,14 @@ fn parse_truncate() { only: false, }]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: false, identity: None, cascade: None, on_cluster: None, - }, + }), truncate ); } @@ -4980,14 +4980,14 @@ fn parse_truncate_with_options() { }]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: true, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, - }, + }), truncate ); } @@ -5013,14 +5013,14 @@ fn parse_truncate_with_table_list() { ]; assert_eq!( - Statement::Truncate { + Statement::Truncate(Truncate { table_names, partitions: None, table: true, identity: Some(TruncateIdentityOption::Restart), cascade: Some(CascadeOption::Cascade), on_cluster: None, - }, + }), truncate ); } From 4862b28b3f85a3801901844e78d0a445a23b4287 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 13:08:45 +0200 Subject: [PATCH 03/12] Moved the `Msck` struct outside of `Statement` enum --- src/ast/ddl.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 36 +++++++++++------------------------- src/ast/spans.rs | 6 +----- src/parser/mod.rs | 5 +++-- 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 79842db22..d09b20b57 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3487,3 +3487,43 @@ impl Spanned for Truncate { ) } } + +/// An `MSCK` statement. +/// +/// ```sql +/// MSCK [REPAIR] TABLE table_name [ADD|DROP|SYNC PARTITIONS] +/// ``` +/// MSCK (Hive) - MetaStore Check command +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Msck { + /// Table name to check + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + /// Whether to repair the table + pub repair: bool, + /// Partition action (ADD, DROP, or SYNC) + pub partition_action: Option, +} + +impl fmt::Display for Msck { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "MSCK {repair}TABLE {table}", + repair = if self.repair { "REPAIR " } else { "" }, + table = self.table_name + )?; + if let Some(pa) = &self.partition_action { + write!(f, " {pa}")?; + } + Ok(()) + } +} + +impl Spanned for Msck { + fn span(&self) -> Span { + self.table_name.span() + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5039c9832..479f195e4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -67,9 +67,9 @@ pub use self::ddl::{ CreateFunction, CreateIndex, CreateTable, CreateTrigger, Deduplicate, DeferrableInitial, DropBehavior, DropTrigger, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, - ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TableConstraint, - TagsColumnOption, Truncate, UserDefinedTypeCompositeAttributeDef, + IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, Owner, + Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, + TableConstraint, TagsColumnOption, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{Delete, Insert}; @@ -3138,12 +3138,7 @@ pub enum Statement { /// MSCK /// ``` /// Msck (Hive) - Msck { - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - table_name: ObjectName, - repair: bool, - partition_action: Option, - }, + Msck(Msck), /// ```sql /// SELECT /// ``` @@ -4363,6 +4358,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(msck: ddl::Msck) -> Self { + Statement::Msck(msck) + } +} + /// ```sql /// {COPY | REVOKE} CURRENT GRANTS /// ``` @@ -4563,22 +4564,7 @@ impl fmt::Display for Statement { } write!(f, " {source}") } - Statement::Msck { - table_name, - repair, - partition_action, - } => { - write!( - f, - "MSCK {repair}TABLE {table}", - repair = if *repair { "REPAIR " } else { "" }, - table = table_name - )?; - if let Some(pa) = partition_action { - write!(f, " {pa}")?; - } - Ok(()) - } + Statement::Msck(msck) => msck.fmt(f), Statement::Truncate(truncate) => truncate.fmt(f), Statement::Case(stmt) => { write!(f, "{stmt}") diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 93b856d62..eefb77b95 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -300,11 +300,7 @@ impl Spanned for Statement { match self { Statement::Analyze(analyze) => analyze.span(), Statement::Truncate(truncate) => truncate.span(), - Statement::Msck { - table_name, - repair: _, - partition_action: _, - } => table_name.span(), + Statement::Msck(msck) => msck.span(), Statement::Query(query) => query.span(), Statement::Insert(insert) => insert.span(), Statement::Install { extension_name } => extension_name.span, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cd0ff6fba..68b2c90bd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -982,11 +982,12 @@ impl<'a> Parser<'a> { Ok(pa) })? .unwrap_or_default(); - Ok(Statement::Msck { + Ok(Msck { repair, table_name, partition_action, - }) + } + .into()) } pub fn parse_truncate(&mut self) -> Result { From 9a4d23563b6094aac19b7f53bf6c8babc8e49587 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 13:32:55 +0200 Subject: [PATCH 04/12] Moved the `Update` struct out of the `Statement` enum --- src/ast/dml.rs | 65 +++++++++++++++++++++++++++++++- src/ast/mod.rs | 75 +++++-------------------------------- src/ast/spans.rs | 43 ++++++++++++--------- src/parser/mod.rs | 5 ++- tests/sqlparser_common.rs | 14 +++---- tests/sqlparser_mysql.rs | 6 +-- tests/sqlparser_postgres.rs | 2 +- tests/sqlparser_sqlite.rs | 6 +-- 8 files changed, 117 insertions(+), 99 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index e4d99bcfc..c0bfcb19f 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -29,7 +29,7 @@ use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; use super::{ display_comma_separated, query::InputFormatClause, Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, Query, SelectItem, - Setting, SqliteOnConflict, TableObject, TableWithJoins, + Setting, SqliteOnConflict, TableObject, TableWithJoins, UpdateTableFromKind, }; /// INSERT statement. @@ -240,3 +240,66 @@ impl Display for Delete { Ok(()) } } + +/// UPDATE 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 Update { + /// TABLE + pub table: TableWithJoins, + /// Column assignments + pub assignments: Vec, + /// Table which provide value to be set + pub from: Option, + /// WHERE + pub selection: Option, + /// RETURNING + pub returning: Option>, + /// SQLite-specific conflict resolution clause + pub or: Option, + /// LIMIT + pub limit: Option, +} + +impl Display for Update { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("UPDATE ")?; + if let Some(or) = &self.or { + or.fmt(f)?; + f.write_str(" ")?; + } + self.table.fmt(f)?; + if let Some(UpdateTableFromKind::BeforeSet(from)) = &self.from { + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; + } + if !self.assignments.is_empty() { + SpaceOrNewline.fmt(f)?; + f.write_str("SET")?; + indented_list(f, &self.assignments)?; + } + if let Some(UpdateTableFromKind::AfterSet(from)) = &self.from { + SpaceOrNewline.fmt(f)?; + f.write_str("FROM")?; + indented_list(f, from)?; + } + if let Some(selection) = &self.selection { + SpaceOrNewline.fmt(f)?; + f.write_str("WHERE")?; + SpaceOrNewline.fmt(f)?; + Indent(selection).fmt(f)?; + } + if let Some(returning) = &self.returning { + SpaceOrNewline.fmt(f)?; + f.write_str("RETURNING")?; + indented_list(f, returning)?; + } + if let Some(limit) = &self.limit { + SpaceOrNewline.fmt(f)?; + write!(f, "LIMIT {limit}")?; + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 479f195e4..4efeb39af 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -43,7 +43,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::{ - display_utils::{indented_list, SpaceOrNewline}, + display_utils::SpaceOrNewline, tokenizer::{Span, Token}, }; use crate::{ @@ -72,7 +72,7 @@ pub use self::ddl::{ TableConstraint, TagsColumnOption, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; -pub use self::dml::{Delete, Insert}; +pub use self::dml::{Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, @@ -3241,22 +3241,7 @@ pub enum Statement { /// ```sql /// UPDATE /// ``` - Update { - /// TABLE - table: TableWithJoins, - /// Column assignments - assignments: Vec, - /// Table which provide value to be set - from: Option, - /// WHERE - selection: Option, - /// RETURNING - returning: Option>, - /// SQLite-specific conflict resolution clause - or: Option, - /// LIMIT - limit: Option, - }, + Update(Update), /// ```sql /// DELETE /// ``` @@ -4676,53 +4661,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::Update { - table, - assignments, - from, - selection, - returning, - or, - limit, - } => { - f.write_str("UPDATE ")?; - if let Some(or) = or { - or.fmt(f)?; - f.write_str(" ")?; - } - table.fmt(f)?; - if let Some(UpdateTableFromKind::BeforeSet(from)) = from { - SpaceOrNewline.fmt(f)?; - f.write_str("FROM")?; - indented_list(f, from)?; - } - if !assignments.is_empty() { - SpaceOrNewline.fmt(f)?; - f.write_str("SET")?; - indented_list(f, assignments)?; - } - if let Some(UpdateTableFromKind::AfterSet(from)) = from { - SpaceOrNewline.fmt(f)?; - f.write_str("FROM")?; - indented_list(f, from)?; - } - if let Some(selection) = selection { - SpaceOrNewline.fmt(f)?; - f.write_str("WHERE")?; - SpaceOrNewline.fmt(f)?; - Indent(selection).fmt(f)?; - } - if let Some(returning) = returning { - SpaceOrNewline.fmt(f)?; - f.write_str("RETURNING")?; - indented_list(f, returning)?; - } - if let Some(limit) = limit { - SpaceOrNewline.fmt(f)?; - write!(f, "LIMIT {limit}")?; - } - Ok(()) - } + Statement::Update(update) => update.fmt(f), Statement::Delete(delete) => delete.fmt(f), Statement::Open(open) => open.fmt(f), Statement::Close { cursor } => { @@ -10891,6 +10830,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(u: Update) -> Self { + Self::Update(u) + } +} + impl From for Statement { fn from(c: CaseStatement) -> Self { Self::Case(c) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index eefb77b95..c95918c31 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -40,8 +40,8 @@ use super::{ RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, - TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef, - WhileStatement, WildcardAdditionalOptions, With, WithFill, + TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Value, Values, + ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -346,21 +346,7 @@ impl Spanned for Statement { CloseCursor::All => Span::empty(), CloseCursor::Specific { name } => name.span, }, - Statement::Update { - table, - assignments, - from, - selection, - returning, - or: _, - limit: _, - } => union_spans( - core::iter::once(table.span()) - .chain(assignments.iter().map(|i| i.span())) - .chain(from.iter().map(|i| i.span())) - .chain(selection.iter().map(|i| i.span())) - .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))), - ), + Statement::Update(update) => update.span(), Statement::Delete(delete) => delete.span(), Statement::CreateView { or_alter: _, @@ -997,6 +983,29 @@ impl Spanned for Delete { } } +impl Spanned for Update { + fn span(&self) -> Span { + let Update { + table, + assignments, + from, + selection, + returning, + or: _, + limit, + } = self; + + union_spans( + core::iter::once(table.span()) + .chain(assignments.iter().map(|i| i.span())) + .chain(from.iter().map(|i| i.span())) + .chain(selection.iter().map(|i| i.span())) + .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) + .chain(limit.iter().map(|i| i.span())), + ) + } +} + impl Spanned for FromTable { fn span(&self) -> Span { match self { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 68b2c90bd..7f927622e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15640,7 +15640,7 @@ impl<'a> Parser<'a> { } else { None }; - Ok(Statement::Update { + Ok(Update { table, assignments, from, @@ -15648,7 +15648,8 @@ impl<'a> Parser<'a> { returning, or, limit, - }) + } + .into()) } /// Parse a `var = expr` assignment, used in an UPDATE statement diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 784e1cf37..243d42449 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -377,12 +377,12 @@ fn parse_insert_sqlite() { fn parse_update() { let sql = "UPDATE t SET a = 1, b = 2, c = 3 WHERE d"; match verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, selection, .. - } => { + }) => { assert_eq!(table.to_string(), "t".to_string()); assert_eq!( assignments, @@ -439,7 +439,7 @@ fn parse_update_set_from() { let stmt = dialects.verified_stmt(sql); assert_eq!( stmt, - Statement::Update { + Statement::Update(Update { table: TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], @@ -516,7 +516,7 @@ fn parse_update_set_from() { returning: None, or: None, limit: None - } + }) ); let sql = "UPDATE T SET a = b FROM U, (SELECT foo FROM V) AS W WHERE 1 = 1"; @@ -527,7 +527,7 @@ fn parse_update_set_from() { fn parse_update_with_table_alias() { let sql = "UPDATE users AS u SET u.username = 'new_user' WHERE u.username = 'old_user'"; match verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, from: _from, @@ -535,7 +535,7 @@ fn parse_update_with_table_alias() { returning, or: None, limit: None, - } => { + }) => { assert_eq!( TableWithJoins { relation: TableFactor::Table { @@ -591,7 +591,7 @@ fn parse_update_with_table_alias() { #[test] fn parse_update_or() { let expect_or_clause = |sql: &str, expected_action: SqliteOnConflict| match verified_stmt(sql) { - Statement::Update { or, .. } => assert_eq!(or, Some(expected_action)), + Statement::Update(Update { or, .. }) => assert_eq!(or, Some(expected_action)), other => unreachable!("Expected update with or, got {:?}", other), }; expect_or_clause( diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5d75aa508..0066a872d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2601,7 +2601,7 @@ fn parse_insert_with_numeric_prefix_column_name() { fn parse_update_with_joins() { let sql = "UPDATE orders AS o JOIN customers AS c ON o.customer_id = c.id SET o.completed = true WHERE c.firstname = 'Peter'"; match mysql().verified_stmt(sql) { - Statement::Update { + Statement::Update(Update { table, assignments, from: _from, @@ -2609,7 +2609,7 @@ fn parse_update_with_joins() { returning, or: None, limit: None, - } => { + }) => { assert_eq!( TableWithJoins { relation: TableFactor::Table { @@ -4177,7 +4177,7 @@ fn test_variable_assignment_using_colon_equal() { let stmt = mysql().verified_stmt(sql_update); match stmt { - Statement::Update { assignments, .. } => { + Statement::Update(Update { assignments, .. }) => { assert_eq!( assignments, vec![Assignment { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 6479273e5..7b7385544 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2008,7 +2008,7 @@ fn parse_pg_returning() { RETURNING temp_lo AS lo, temp_hi AS hi, prcp", ); match stmt { - Statement::Update { returning, .. } => { + Statement::Update(Update { returning, .. }) => { assert_eq!( Some(vec![ SelectItem::ExprWithAlias { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 114aca03a..62fbc5035 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -467,7 +467,7 @@ fn parse_update_tuple_row_values() { // See https://github.com/sqlparser-rs/sqlparser-rs/issues/1311 assert_eq!( sqlite().verified_stmt("UPDATE x SET (a, b) = (1, 2)"), - Statement::Update { + Statement::Update(Update { or: None, assignments: vec![Assignment { target: AssignmentTarget::Tuple(vec![ @@ -487,7 +487,7 @@ fn parse_update_tuple_row_values() { from: None, returning: None, limit: None - } + }) ); } @@ -596,7 +596,7 @@ fn test_regexp_operator() { #[test] fn test_update_delete_limit() { match sqlite().verified_stmt("UPDATE foo SET bar = 1 LIMIT 99") { - Statement::Update { limit, .. } => { + Statement::Update(Update { limit, .. }) => { assert_eq!(limit, Some(Expr::value(number("99")))); } _ => unreachable!(), From 94f3b1b66d1005e8bd74485436ff0362a34e5257 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 13:59:42 +0200 Subject: [PATCH 05/12] Moved `CreateView` struct out of `Statement` enum --- src/ast/ddl.rs | 150 ++++++++++++++++++++++++++++++++-- src/ast/mod.rs | 138 ++++--------------------------- src/ast/spans.rs | 26 +----- src/parser/mod.rs | 5 +- tests/sqlparser_bigquery.rs | 12 +-- tests/sqlparser_clickhouse.rs | 2 +- tests/sqlparser_common.rs | 63 +++++++------- tests/sqlparser_mysql.rs | 16 ++-- tests/sqlparser_snowflake.rs | 10 +-- tests/sqlparser_sqlite.rs | 4 +- 10 files changed, 214 insertions(+), 212 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index d09b20b57..42df95d1e 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -31,13 +31,13 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, ArgMode, CommentDef, ConditionalStatements, - CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, DataType, - Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, - FunctionParallel, HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, - InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, - OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, - SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, - TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, + CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, + CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull, + FunctionDeterminismSpecifier, FunctionParallel, HiveDistributionStyle, HiveFormat, + HiveIOFormat, HiveRowFormat, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, + OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, + RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, + Tag, TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, WrappedCollection, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; @@ -3527,3 +3527,139 @@ impl Spanned for Msck { self.table_name.span() } } + +/// CREATE 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 struct CreateView { + /// True if this is a `CREATE OR ALTER VIEW` statement + /// + /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql) + pub or_alter: bool, + pub or_replace: bool, + pub materialized: bool, + /// Snowflake: SECURE view modifier + /// + pub secure: bool, + /// View name + pub name: ObjectName, + /// If `if_not_exists` is true, this flag is set to true if the view name comes before the `IF NOT EXISTS` clause. + /// Example: + /// ```sql + /// CREATE VIEW myview IF NOT EXISTS AS SELECT 1` + /// ``` + /// Otherwise, the flag is set to false if the view name comes after the clause + /// Example: + /// ```sql + /// CREATE VIEW IF NOT EXISTS myview AS SELECT 1` + /// ``` + pub name_before_not_exists: bool, + pub columns: Vec, + pub query: Box, + pub options: CreateTableOptions, + pub cluster_by: Vec, + /// Snowflake: Views can have comments in Snowflake. + /// + pub comment: Option, + /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause + pub with_no_schema_binding: bool, + /// if true, has SQLite `IF NOT EXISTS` clause + pub if_not_exists: bool, + /// if true, has SQLite `TEMP` or `TEMPORARY` clause + pub temporary: bool, + /// if not None, has Clickhouse `TO` clause, specify the table into which to insert results + /// + pub to: Option, + /// MySQL: Optional parameters for the view algorithm, definer, and security context + pub params: Option, +} + +impl fmt::Display for CreateView { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {or_alter}{or_replace}", + or_alter = if self.or_alter { "OR ALTER " } else { "" }, + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + )?; + if let Some(ref params) = self.params { + params.fmt(f)?; + } + write!( + f, + "{secure}{materialized}{temporary}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) + } else { + format!("IF NOT EXISTS {}", self.name) + } + } else { + format!("{}", self.name) + }, + secure = if self.secure { "SECURE " } else { "" }, + materialized = if self.materialized { + "MATERIALIZED " + } else { + "" + }, + temporary = if self.temporary { "TEMPORARY " } else { "" }, + to = self + .to + .as_ref() + .map(|to| format!(" TO {to}")) + .unwrap_or_default() + )?; + if !self.columns.is_empty() { + write!(f, " ({})", display_comma_separated(&self.columns))?; + } + if matches!(self.options, CreateTableOptions::With(_)) { + write!(f, " {}", self.options)?; + } + if let Some(ref comment) = self.comment { + write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?; + } + if !self.cluster_by.is_empty() { + write!( + f, + " CLUSTER BY ({})", + display_comma_separated(&self.cluster_by) + )?; + } + if matches!(self.options, CreateTableOptions::Options(_)) { + write!(f, " {}", self.options)?; + } + f.write_str(" AS")?; + SpaceOrNewline.fmt(f)?; + self.query.fmt(f)?; + if self.with_no_schema_binding { + write!(f, " WITH NO SCHEMA BINDING")?; + } + Ok(()) + } +} + +impl Spanned for CreateView { + fn span(&self) -> Span { + let name_span = self.name.span(); + let query_span = self.query.span(); + let options_span = self.options.span(); + + // Union all the relevant spans + let mut spans = vec![name_span, query_span, options_span]; + + // Add column spans + spans.extend(self.columns.iter().map(|col| col.span())); + + // Add cluster_by spans + spans.extend(self.cluster_by.iter().map(|ident| ident.span)); + + // Add to span if present + if let Some(ref to) = self.to { + spans.push(to.span()); + } + + Span::union_iter(spans) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4efeb39af..580b0a288 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -64,13 +64,13 @@ pub use self::ddl::{ AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, - CreateFunction, CreateIndex, CreateTable, CreateTrigger, Deduplicate, DeferrableInitial, - DropBehavior, DropTrigger, GeneratedAs, GeneratedExpressionMode, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, Owner, - Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, - TableConstraint, TagsColumnOption, Truncate, UserDefinedTypeCompositeAttributeDef, - UserDefinedTypeRepresentation, ViewColumnDef, + CreateFunction, CreateIndex, CreateTable, CreateTrigger, CreateView, Deduplicate, + DeferrableInitial, DropBehavior, DropTrigger, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, + NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, + ReplicaIdentity, TableConstraint, TagsColumnOption, Truncate, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -3249,48 +3249,7 @@ pub enum Statement { /// ```sql /// CREATE VIEW /// ``` - CreateView { - /// True if this is a `CREATE OR ALTER VIEW` statement - /// - /// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-view-transact-sql) - or_alter: bool, - or_replace: bool, - materialized: bool, - /// Snowflake: SECURE view modifier - /// - secure: bool, - /// View name - name: ObjectName, - /// If `if_not_exists` is true, this flag is set to true if the view name comes before the `IF NOT EXISTS` clause. - /// Example: - /// ```sql - /// CREATE VIEW myview IF NOT EXISTS AS SELECT 1` - /// ``` - /// Otherwise, the flag is set to false if the view name comes after the clause - /// Example: - /// ```sql - /// CREATE VIEW IF NOT EXISTS myview AS SELECT 1` - /// ``` - name_before_not_exists: bool, - columns: Vec, - query: Box, - options: CreateTableOptions, - cluster_by: Vec, - /// Snowflake: Views can have comments in Snowflake. - /// - comment: Option, - /// if true, has RedShift [`WITH NO SCHEMA BINDING`] clause - with_no_schema_binding: bool, - /// if true, has SQLite `IF NOT EXISTS` clause - if_not_exists: bool, - /// if true, has SQLite `TEMP` or `TEMPORARY` clause - temporary: bool, - /// if not None, has Clickhouse `TO` clause, specify the table into which to insert results - /// - to: Option, - /// MySQL: Optional parameters for the view algorithm, definer, and security context - params: Option, - }, + CreateView(CreateView), /// ```sql /// CREATE TABLE /// ``` @@ -4817,80 +4776,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::CreateView { - or_alter, - name, - or_replace, - columns, - query, - materialized, - secure, - options, - cluster_by, - comment, - with_no_schema_binding, - if_not_exists, - temporary, - to, - params, - name_before_not_exists, - } => { - write!( - f, - "CREATE {or_alter}{or_replace}", - or_alter = if *or_alter { "OR ALTER " } else { "" }, - or_replace = if *or_replace { "OR REPLACE " } else { "" }, - )?; - if let Some(params) = params { - params.fmt(f)?; - } - write!( - f, - "{secure}{materialized}{temporary}VIEW {if_not_and_name}{to}", - if_not_and_name = if *if_not_exists { - if *name_before_not_exists { - format!("{name} IF NOT EXISTS") - } else { - format!("IF NOT EXISTS {name}") - } - } else { - format!("{name}") - }, - secure = if *secure { "SECURE " } else { "" }, - materialized = if *materialized { "MATERIALIZED " } else { "" }, - temporary = if *temporary { "TEMPORARY " } else { "" }, - to = to - .as_ref() - .map(|to| format!(" TO {to}")) - .unwrap_or_default() - )?; - if !columns.is_empty() { - write!(f, " ({})", display_comma_separated(columns))?; - } - if matches!(options, CreateTableOptions::With(_)) { - write!(f, " {options}")?; - } - if let Some(comment) = comment { - write!( - f, - " COMMENT = '{}'", - value::escape_single_quote_string(comment) - )?; - } - if !cluster_by.is_empty() { - write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; - } - if matches!(options, CreateTableOptions::Options(_)) { - write!(f, " {options}")?; - } - f.write_str(" AS")?; - SpaceOrNewline.fmt(f)?; - query.fmt(f)?; - if *with_no_schema_binding { - write!(f, " WITH NO SCHEMA BINDING")?; - } - Ok(()) - } + Statement::CreateView(create_view) => create_view.fmt(f), Statement::CreateTable(create_table) => create_table.fmt(f), Statement::LoadData { local, @@ -10836,6 +10722,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(cv: CreateView) -> Self { + Self::CreateView(cv) + } +} + impl From for Statement { fn from(c: CaseStatement) -> Self { Self::Case(c) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index c95918c31..e9a89ebd3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -348,31 +348,7 @@ impl Spanned for Statement { }, Statement::Update(update) => update.span(), Statement::Delete(delete) => delete.span(), - Statement::CreateView { - or_alter: _, - or_replace: _, - materialized: _, - secure: _, - name, - columns, - query, - options, - cluster_by, - comment: _, - with_no_schema_binding: _, - if_not_exists: _, - temporary: _, - to, - name_before_not_exists: _, - params: _, - } => union_spans( - core::iter::once(name.span()) - .chain(columns.iter().map(|i| i.span())) - .chain(core::iter::once(query.span())) - .chain(core::iter::once(options.span())) - .chain(cluster_by.iter().map(|i| i.span)) - .chain(to.iter().map(|i| i.span())), - ), + Statement::CreateView(create_view) => create_view.span(), Statement::CreateTable(create_table) => create_table.span(), Statement::CreateVirtualTable { name, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7f927622e..98c806393 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5929,7 +5929,7 @@ impl<'a> Parser<'a> { Keyword::BINDING, ]); - Ok(Statement::CreateView { + Ok(CreateView { or_alter, name, columns, @@ -5946,7 +5946,8 @@ impl<'a> Parser<'a> { to, params: create_view_params, name_before_not_exists, - }) + } + .into()) } /// Parse optional parameters for the `CREATE VIEW` statement supported by [MySQL]. diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 4f0cfa3e8..03a0ac813 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -332,13 +332,13 @@ fn parse_create_view_with_options() { "AS SELECT column_1, column_2, column_3 FROM myproject.mydataset.mytable", ); match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, query, options, columns, .. - } => { + }) => { assert_eq!( name, ObjectName::from(vec![ @@ -401,7 +401,7 @@ fn parse_create_view_with_options() { fn parse_create_view_if_not_exists() { let sql = "CREATE VIEW IF NOT EXISTS mydataset.newview AS SELECT foo FROM bar"; match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, query, @@ -414,7 +414,7 @@ fn parse_create_view_if_not_exists() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("mydataset.newview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); @@ -435,12 +435,12 @@ fn parse_create_view_if_not_exists() { fn parse_create_view_with_unquoted_hyphen() { let sql = "CREATE VIEW IF NOT EXISTS my-pro-ject.mydataset.myview AS SELECT 1"; match bigquery().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, query, if_not_exists, .. - } => { + }) => { assert_eq!("my-pro-ject.mydataset.myview", name.to_string()); assert_eq!("SELECT 1", query.to_string()); assert!(if_not_exists); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index bc1431f9c..f8cf3f2ca 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -904,7 +904,7 @@ fn parse_create_table_with_variant_default_expressions() { #[test] fn parse_create_view_with_fields_data_types() { match clickhouse().verified_stmt(r#"CREATE VIEW v (i "int", f "String") AS SELECT * FROM t"#) { - Statement::CreateView { name, columns, .. } => { + Statement::CreateView(CreateView { name, columns, .. }) => { assert_eq!(name, ObjectName::from(vec!["v".into()])); assert_eq!( columns, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 243d42449..c6af59e84 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7607,7 +7607,7 @@ fn parse_ctes() { // CTE in a view let sql = &format!("CREATE VIEW v AS {with}"); match verified_stmt(sql) { - Statement::CreateView { query, .. } => assert_ctes_in_select(&cte_sqls, &query), + Statement::CreateView(create_view) => assert_ctes_in_select(&cte_sqls, &create_view.query), _ => panic!("Expected: CREATE VIEW"), } // CTE in a CTE... @@ -8095,7 +8095,7 @@ fn parse_drop_database_if_exists() { fn parse_create_view() { let sql = "CREATE VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, columns, @@ -8112,7 +8112,7 @@ fn parse_create_view() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -8138,7 +8138,7 @@ fn parse_create_view() { fn parse_create_view_with_options() { let sql = "CREATE VIEW v WITH (foo = 'bar', a = 123) AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { options, .. } => { + Statement::CreateView(create_view) => { assert_eq!( CreateTableOptions::With(vec![ SqlOption::KeyValue { @@ -8152,7 +8152,7 @@ fn parse_create_view_with_options() { value: Expr::value(number("123")), }, ]), - options + create_view.options ); } _ => unreachable!(), @@ -8165,24 +8165,21 @@ fn parse_create_view_with_columns() { // TODO: why does this fail for ClickHouseDialect? (#1449) // match all_dialects().verified_stmt(sql) { match all_dialects_except(|d| d.is::()).verified_stmt(sql) { - Statement::CreateView { - or_alter, - name, - columns, - or_replace, - options, - query, - materialized, - cluster_by, - comment, - with_no_schema_binding: late_binding, - if_not_exists, - temporary, - to, - params, - name_before_not_exists: _, - secure: _, - } => { + Statement::CreateView(create_view) => { + let or_alter = create_view.or_alter; + let name = create_view.name; + let columns = create_view.columns; + let or_replace = create_view.or_replace; + let options = create_view.options; + let query = create_view.query; + let materialized = create_view.materialized; + let cluster_by = create_view.cluster_by; + let comment = create_view.comment; + let late_binding = create_view.with_no_schema_binding; + let if_not_exists = create_view.if_not_exists; + let temporary = create_view.temporary; + let to = create_view.to; + let params = create_view.params; assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!( @@ -8216,7 +8213,7 @@ fn parse_create_view_with_columns() { fn parse_create_view_temporary() { let sql = "CREATE TEMPORARY VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, columns, @@ -8233,7 +8230,7 @@ fn parse_create_view_temporary() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -8257,7 +8254,7 @@ fn parse_create_view_temporary() { fn parse_create_or_replace_view() { let sql = "CREATE OR REPLACE VIEW v AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, columns, @@ -8274,7 +8271,7 @@ fn parse_create_or_replace_view() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -8302,7 +8299,7 @@ fn parse_create_or_replace_materialized_view() { // https://docs.snowflake.com/en/sql-reference/sql/create-materialized-view.html let sql = "CREATE OR REPLACE MATERIALIZED VIEW v AS SELECT 1"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, columns, @@ -8319,7 +8316,7 @@ fn parse_create_or_replace_materialized_view() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); @@ -8343,7 +8340,7 @@ fn parse_create_or_replace_materialized_view() { fn parse_create_materialized_view() { let sql = "CREATE MATERIALIZED VIEW myschema.myview AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, or_replace, @@ -8360,7 +8357,7 @@ fn parse_create_materialized_view() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); @@ -8384,7 +8381,7 @@ fn parse_create_materialized_view() { fn parse_create_materialized_view_with_cluster_by() { let sql = "CREATE MATERIALIZED VIEW myschema.myview CLUSTER BY (foo) AS SELECT foo FROM bar"; match verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { or_alter, name, or_replace, @@ -8401,7 +8398,7 @@ fn parse_create_materialized_view_with_cluster_by() { params, name_before_not_exists: _, secure: _, - } => { + }) => { assert_eq!(or_alter, false); assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 0066a872d..40d4374fe 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3855,7 +3855,7 @@ fn parse_revoke() { fn parse_create_view_algorithm_param() { let sql = "CREATE ALGORITHM = MERGE VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3863,7 +3863,7 @@ fn parse_create_view_algorithm_param() { security, }), .. - } = stmt + }) = stmt { assert_eq!(algorithm, Some(CreateViewAlgorithm::Merge)); assert!(definer.is_none()); @@ -3879,7 +3879,7 @@ fn parse_create_view_algorithm_param() { fn parse_create_view_definer_param() { let sql = "CREATE DEFINER = 'jeffrey'@'localhost' VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3887,7 +3887,7 @@ fn parse_create_view_definer_param() { security, }), .. - } = stmt + }) = stmt { assert!(algorithm.is_none()); if let Some(GranteeName::UserHost { user, host }) = definer { @@ -3908,7 +3908,7 @@ fn parse_create_view_definer_param() { fn parse_create_view_security_param() { let sql = "CREATE SQL SECURITY DEFINER VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3916,7 +3916,7 @@ fn parse_create_view_security_param() { security, }), .. - } = stmt + }) = stmt { assert!(algorithm.is_none()); assert!(definer.is_none()); @@ -3931,7 +3931,7 @@ fn parse_create_view_security_param() { fn parse_create_view_multiple_params() { let sql = "CREATE ALGORITHM = UNDEFINED DEFINER = `root`@`%` SQL SECURITY INVOKER VIEW foo AS SELECT 1"; let stmt = mysql().verified_stmt(sql); - if let Statement::CreateView { + if let Statement::CreateView(CreateView { params: Some(CreateViewParams { algorithm, @@ -3939,7 +3939,7 @@ fn parse_create_view_multiple_params() { security, }), .. - } = stmt + }) = stmt { assert_eq!(algorithm, Some(CreateViewAlgorithm::Undefined)); if let Some(GranteeName::UserHost { user, host }) = definer { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e04bfaf5d..e7a128343 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -53,11 +53,11 @@ fn parse_sf_create_secure_view_and_materialized_view() { "CREATE OR REPLACE SECURE MATERIALIZED VIEW v AS SELECT 1", ] { match snowflake().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { secure, materialized, .. - } => { + }) => { assert!(secure); if sql.contains("MATERIALIZED") { assert!(materialized); @@ -1047,7 +1047,7 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { test_utils::TestedDialects::new(vec![Box::new(SnowflakeDialect {}) as Box]); match dialect.verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, or_replace, @@ -1060,7 +1060,7 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("v", name.to_string()); assert_eq!(columns, vec![]); assert_eq!(options, CreateTableOptions::None); @@ -3281,7 +3281,7 @@ fn parse_view_column_descriptions() { let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; match snowflake().verified_stmt(sql) { - Statement::CreateView { name, columns, .. } => { + Statement::CreateView(CreateView { name, columns, .. }) => { assert_eq!(name.to_string(), "v"); assert_eq!( columns, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 62fbc5035..5083ecd08 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -166,7 +166,7 @@ fn parse_create_virtual_table() { fn parse_create_view_temporary_if_not_exists() { let sql = "CREATE TEMPORARY VIEW IF NOT EXISTS myschema.myview AS SELECT foo FROM bar"; match sqlite_and_generic().verified_stmt(sql) { - Statement::CreateView { + Statement::CreateView(CreateView { name, columns, query, @@ -179,7 +179,7 @@ fn parse_create_view_temporary_if_not_exists() { if_not_exists, temporary, .. - } => { + }) => { assert_eq!("myschema.myview", name.to_string()); assert_eq!(Vec::::new(), columns); assert_eq!("SELECT foo FROM bar", query.to_string()); From ef260ca0317c1621396ca3932761d085f0808248 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 14:12:29 +0200 Subject: [PATCH 06/12] Moved `CreateRole` struct out of the `Statement` enum --- src/ast/dcl.rs | 113 +++++++++++++++++++++++++++++++- src/ast/ddl.rs | 2 +- src/ast/mod.rs | 124 +++--------------------------------- src/ast/spans.rs | 2 +- src/parser/mod.rs | 5 +- tests/sqlparser_common.rs | 14 ++-- tests/sqlparser_mssql.rs | 10 +-- tests/sqlparser_postgres.rs | 109 ++++++++++--------------------- 8 files changed, 167 insertions(+), 212 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 079894075..d04875a73 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -28,8 +28,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use super::{display_comma_separated, Expr, Ident, Password}; +use super::{display_comma_separated, Expr, Ident, Password, Spanned}; use crate::ast::{display_separated, ObjectName}; +use crate::tokenizer::Span; /// An option in `ROLE` statement. /// @@ -252,3 +253,113 @@ impl fmt::Display for SecondaryRoles { } } } + +/// CREATE ROLE statement +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrole.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 CreateRole { + pub names: Vec, + pub if_not_exists: bool, + // Postgres + pub login: Option, + pub inherit: Option, + pub bypassrls: Option, + pub password: Option, + pub superuser: Option, + pub create_db: Option, + pub create_role: Option, + pub replication: Option, + pub connection_limit: Option, + pub valid_until: Option, + pub in_role: Vec, + pub in_group: Vec, + pub role: Vec, + pub user: Vec, + pub admin: Vec, + // MSSQL + pub authorization_owner: Option, +} + +impl fmt::Display for CreateRole { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + names = display_separated(&self.names, ", "), + superuser = match self.superuser { + Some(true) => " SUPERUSER", + Some(false) => " NOSUPERUSER", + None => "" + }, + create_db = match self.create_db { + Some(true) => " CREATEDB", + Some(false) => " NOCREATEDB", + None => "" + }, + create_role = match self.create_role { + Some(true) => " CREATEROLE", + Some(false) => " NOCREATEROLE", + None => "" + }, + inherit = match self.inherit { + Some(true) => " INHERIT", + Some(false) => " NOINHERIT", + None => "" + }, + login = match self.login { + Some(true) => " LOGIN", + Some(false) => " NOLOGIN", + None => "" + }, + replication = match self.replication { + Some(true) => " REPLICATION", + Some(false) => " NOREPLICATION", + None => "" + }, + bypassrls = match self.bypassrls { + Some(true) => " BYPASSRLS", + Some(false) => " NOBYPASSRLS", + None => "" + } + )?; + if let Some(limit) = &self.connection_limit { + write!(f, " CONNECTION LIMIT {limit}")?; + } + match &self.password { + Some(Password::Password(pass)) => write!(f, " PASSWORD {pass}")?, + Some(Password::NullPassword) => write!(f, " PASSWORD NULL")?, + None => {} + }; + if let Some(until) = &self.valid_until { + write!(f, " VALID UNTIL {until}")?; + } + if !self.in_role.is_empty() { + write!(f, " IN ROLE {}", display_comma_separated(&self.in_role))?; + } + if !self.in_group.is_empty() { + write!(f, " IN GROUP {}", display_comma_separated(&self.in_group))?; + } + if !self.role.is_empty() { + write!(f, " ROLE {}", display_comma_separated(&self.role))?; + } + if !self.user.is_empty() { + write!(f, " USER {}", display_comma_separated(&self.user))?; + } + if !self.admin.is_empty() { + write!(f, " ADMIN {}", display_comma_separated(&self.admin))?; + } + if let Some(owner) = &self.authorization_owner { + write!(f, " AUTHORIZATION {owner}")?; + } + Ok(()) + } +} + +impl Spanned for CreateRole { + fn span(&self) -> Span { + Span::empty() + } +} diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 42df95d1e..2e4eacbf8 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -19,7 +19,7 @@ //! (commonly referred to as Data Definition Language, or DDL) #[cfg(not(feature = "std"))] -use alloc::{boxed::Box, string::String, vec::Vec}; +use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; use core::fmt::{self, Display, Write}; #[cfg(feature = "serde")] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 580b0a288..31e9f5bde 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -56,7 +56,7 @@ pub use self::data_type::{ ExactNumberInfo, IntervalFields, StructBracketKind, TimezoneInfo, }; pub use self::dcl::{ - AlterRoleOperation, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, + AlterRoleOperation, CreateRole, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, @@ -3273,28 +3273,7 @@ pub enum Statement { /// CREATE ROLE /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrole.html) - CreateRole { - names: Vec, - if_not_exists: bool, - // Postgres - login: Option, - inherit: Option, - bypassrls: Option, - password: Option, - superuser: Option, - create_db: Option, - create_role: Option, - replication: Option, - connection_limit: Option, - valid_until: Option, - in_role: Vec, - in_group: Vec, - role: Vec, - user: Vec, - admin: Vec, - // MSSQL - authorization_owner: Option, - }, + CreateRole(CreateRole), /// ```sql /// CREATE SECRET /// ``` @@ -4870,98 +4849,7 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::CreateRole { - names, - if_not_exists, - inherit, - login, - bypassrls, - password, - create_db, - create_role, - superuser, - replication, - connection_limit, - valid_until, - in_role, - in_group, - role, - user, - admin, - authorization_owner, - } => { - write!( - f, - "CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - names = display_separated(names, ", "), - superuser = match *superuser { - Some(true) => " SUPERUSER", - Some(false) => " NOSUPERUSER", - None => "" - }, - create_db = match *create_db { - Some(true) => " CREATEDB", - Some(false) => " NOCREATEDB", - None => "" - }, - create_role = match *create_role { - Some(true) => " CREATEROLE", - Some(false) => " NOCREATEROLE", - None => "" - }, - inherit = match *inherit { - Some(true) => " INHERIT", - Some(false) => " NOINHERIT", - None => "" - }, - login = match *login { - Some(true) => " LOGIN", - Some(false) => " NOLOGIN", - None => "" - }, - replication = match *replication { - Some(true) => " REPLICATION", - Some(false) => " NOREPLICATION", - None => "" - }, - bypassrls = match *bypassrls { - Some(true) => " BYPASSRLS", - Some(false) => " NOBYPASSRLS", - None => "" - } - )?; - if let Some(limit) = connection_limit { - write!(f, " CONNECTION LIMIT {limit}")?; - } - match password { - Some(Password::Password(pass)) => write!(f, " PASSWORD {pass}"), - Some(Password::NullPassword) => write!(f, " PASSWORD NULL"), - None => Ok(()), - }?; - if let Some(until) = valid_until { - write!(f, " VALID UNTIL {until}")?; - } - if !in_role.is_empty() { - write!(f, " IN ROLE {}", display_comma_separated(in_role))?; - } - if !in_group.is_empty() { - write!(f, " IN GROUP {}", display_comma_separated(in_group))?; - } - if !role.is_empty() { - write!(f, " ROLE {}", display_comma_separated(role))?; - } - if !user.is_empty() { - write!(f, " USER {}", display_comma_separated(user))?; - } - if !admin.is_empty() { - write!(f, " ADMIN {}", display_comma_separated(admin))?; - } - if let Some(owner) = authorization_owner { - write!(f, " AUTHORIZATION {owner}")?; - } - Ok(()) - } + Statement::CreateRole(create_role) => write!(f, "{create_role}"), Statement::CreateSecret { or_replace, temporary, @@ -10728,6 +10616,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(cr: CreateRole) -> Self { + Self::CreateRole(cr) + } +} + impl From for Statement { fn from(c: CaseStatement) -> Self { Self::Case(c) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e9a89ebd3..fe3b0c536 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -361,7 +361,7 @@ impl Spanned for Statement { .chain(module_args.iter().map(|i| i.span)), ), Statement::CreateIndex(create_index) => create_index.span(), - Statement::CreateRole { .. } => Span::empty(), + Statement::CreateRole(create_role) => create_role.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 98c806393..c442646c8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6210,7 +6210,7 @@ impl<'a> Parser<'a> { }? } - Ok(Statement::CreateRole { + Ok(CreateRole { names, if_not_exists, login, @@ -6229,7 +6229,8 @@ impl<'a> Parser<'a> { user, admin, authorization_owner, - }) + } + .into()) } pub fn parse_owner(&mut self) -> Result { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c6af59e84..331bc6f26 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -9406,21 +9406,17 @@ fn parse_drop_index() { fn parse_create_role() { let sql = "CREATE ROLE consultant"; match verified_stmt(sql) { - Statement::CreateRole { names, .. } => { - assert_eq_vec(&["consultant"], &names); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["consultant"], &create_role.names); } _ => unreachable!(), } let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; match verified_stmt(sql) { - Statement::CreateRole { - names, - if_not_exists, - .. - } => { - assert_eq_vec(&["mysql_a", "mysql_b"], &names); - assert!(if_not_exists); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mysql_a", "mysql_b"], &create_role.names); + assert!(create_role.if_not_exists); } _ => unreachable!(), } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b1ad422ec..cd7dd1e99 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -772,14 +772,10 @@ fn parse_mssql_bin_literal() { fn parse_mssql_create_role() { let sql = "CREATE ROLE mssql AUTHORIZATION helena"; match ms().verified_stmt(sql) { - Statement::CreateRole { - names, - authorization_owner, - .. - } => { - assert_eq_vec(&["mssql"], &names); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mssql"], &create_role.names); assert_eq!( - authorization_owner, + create_role.authorization_owner, Some(ObjectName::from(vec![Ident { value: "helena".into(), quote_style: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7b7385544..70d3e576f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3832,47 +3832,29 @@ fn parse_custom_operator() { fn parse_create_role() { let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b"; match pg().verified_stmt(sql) { - Statement::CreateRole { - names, - if_not_exists, - .. - } => { - assert_eq_vec(&["mysql_a", "mysql_b"], &names); - assert!(if_not_exists); + Statement::CreateRole(create_role) => { + assert_eq_vec(&["mysql_a", "mysql_b"], &create_role.names); + assert!(create_role.if_not_exists); } _ => unreachable!(), } let sql = "CREATE ROLE abc LOGIN PASSWORD NULL"; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - login, - password, - .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq!(*login, Some(true)); - assert_eq!(*password, Some(Password::NullPassword)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.password, Some(Password::NullPassword)); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE abc WITH LOGIN PASSWORD NULL"; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - login, - password, - .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq!(*login, Some(true)); - assert_eq!(*password, Some(Password::NullPassword)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.password, Some(Password::NullPassword)); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } @@ -3880,69 +3862,44 @@ fn parse_create_role() { let sql = "CREATE ROLE magician WITH SUPERUSER CREATEROLE NOCREATEDB BYPASSRLS INHERIT PASSWORD 'abcdef' LOGIN VALID UNTIL '2025-01-01' IN ROLE role1, role2 ROLE role3 ADMIN role4, role5 REPLICATION"; // Roundtrip order of optional parameters is not preserved match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, - if_not_exists, - bypassrls, - login, - inherit, - password, - superuser, - create_db, - create_role, - replication, - connection_limit, - valid_until, - in_role, - in_group, - role, - user: _, - admin, - authorization_owner, - }], - ) => { - assert_eq_vec(&["magician"], names); - assert!(!*if_not_exists); - assert_eq!(*login, Some(true)); - assert_eq!(*inherit, Some(true)); - assert_eq!(*bypassrls, Some(true)); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["magician"], &create_role.names); + assert!(!create_role.if_not_exists); + assert_eq!(create_role.login, Some(true)); + assert_eq!(create_role.inherit, Some(true)); + assert_eq!(create_role.bypassrls, Some(true)); assert_eq!( - *password, + create_role.password, Some(Password::Password(Expr::Value( (Value::SingleQuotedString("abcdef".into())).with_empty_span() ))) ); - assert_eq!(*superuser, Some(true)); - assert_eq!(*create_db, Some(false)); - assert_eq!(*create_role, Some(true)); - assert_eq!(*replication, Some(true)); - assert_eq!(*connection_limit, None); + assert_eq!(create_role.superuser, Some(true)); + assert_eq!(create_role.create_db, Some(false)); + assert_eq!(create_role.create_role, Some(true)); + assert_eq!(create_role.replication, Some(true)); + assert_eq!(create_role.connection_limit, None); assert_eq!( - *valid_until, + create_role.valid_until, Some(Expr::Value( (Value::SingleQuotedString("2025-01-01".into())).with_empty_span() )) ); - assert_eq_vec(&["role1", "role2"], in_role); - assert!(in_group.is_empty()); - assert_eq_vec(&["role3"], role); - assert_eq_vec(&["role4", "role5"], admin); - assert_eq!(*authorization_owner, None); + assert_eq_vec(&["role1", "role2"], &create_role.in_role); + assert!(create_role.in_group.is_empty()); + assert_eq_vec(&["role3"], &create_role.role); + assert_eq_vec(&["role4", "role5"], &create_role.admin); + assert_eq!(create_role.authorization_owner, None); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } let sql = "CREATE ROLE abc WITH USER foo, bar ROLE baz "; match pg().parse_sql_statements(sql).as_deref() { - Ok( - [Statement::CreateRole { - names, user, role, .. - }], - ) => { - assert_eq_vec(&["abc"], names); - assert_eq_vec(&["foo", "bar"], user); - assert_eq_vec(&["baz"], role); + Ok([Statement::CreateRole(create_role)]) => { + assert_eq_vec(&["abc"], &create_role.names); + assert_eq_vec(&["foo", "bar"], &create_role.user); + assert_eq_vec(&["baz"], &create_role.role); } err => panic!("Failed to parse CREATE ROLE test case: {err:?}"), } From 1e18b873b98eaa786a36ac967f4d62e6ba02ae3a Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 14:25:56 +0200 Subject: [PATCH 07/12] =?UTF-8?q?Moved=20`CreateExtension`=20and=20`DropEx?= =?UTF-8?q?tension`=20structs=20out=20of=20`Statement`=C2=A0enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ast/ddl.rs | 82 +++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 86 ++++++++++--------------------------- src/ast/spans.rs | 4 +- src/parser/mod.rs | 9 ++-- tests/sqlparser_postgres.rs | 40 ++++++++--------- 5 files changed, 132 insertions(+), 89 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 2e4eacbf8..406c6fde0 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3663,3 +3663,85 @@ impl Spanned for CreateView { Span::union_iter(spans) } } + +/// CREATE EXTENSION 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 CreateExtension { + pub name: Ident, + pub if_not_exists: bool, + pub cascade: bool, + pub schema: Option, + pub version: Option, +} + +impl fmt::Display for CreateExtension { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE EXTENSION {if_not_exists}{name}", + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + name = self.name + )?; + if self.cascade || self.schema.is_some() || self.version.is_some() { + write!(f, " WITH")?; + + if let Some(name) = &self.schema { + write!(f, " SCHEMA {name}")?; + } + if let Some(version) = &self.version { + write!(f, " VERSION {version}")?; + } + if self.cascade { + write!(f, " CASCADE")?; + } + } + + Ok(()) + } +} + +impl Spanned for CreateExtension { + fn span(&self) -> Span { + Span::empty() + } +} + +/// DROP EXTENSION statement +/// Note: this is a PostgreSQL-specific statement +/// https://www.postgresql.org/docs/current/sql-dropextension.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 DropExtension { + pub names: Vec, + pub if_exists: bool, + /// `CASCADE` or `RESTRICT` + pub cascade_or_restrict: Option, +} + +impl fmt::Display for DropExtension { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DROP EXTENSION")?; + if self.if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {}", display_comma_separated(&self.names))?; + if let Some(cascade_or_restrict) = &self.cascade_or_restrict { + write!(f, " {cascade_or_restrict}")?; + } + Ok(()) + } +} + +impl Spanned for DropExtension { + fn span(&self) -> Span { + Span::empty() + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 31e9f5bde..5cc6aa6d0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -64,13 +64,13 @@ pub use self::ddl::{ AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, - CreateFunction, CreateIndex, CreateTable, CreateTrigger, CreateView, Deduplicate, - DeferrableInitial, DropBehavior, DropTrigger, GeneratedAs, GeneratedExpressionMode, - IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, - NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, - ReplicaIdentity, TableConstraint, TagsColumnOption, Truncate, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + CreateExtension, CreateFunction, CreateIndex, CreateTable, CreateTrigger, CreateView, + Deduplicate, DeferrableInitial, DropBehavior, DropExtension, DropTrigger, GeneratedAs, + GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, + KeyOrIndexDisplay, Msck, NullsDistinctOption, Owner, Partition, ProcedureParam, + ReferentialAction, RenameTableNameKind, ReplicaIdentity, TableConstraint, TagsColumnOption, + Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -3534,25 +3534,14 @@ pub enum Statement { /// ``` /// /// Note: this is a PostgreSQL-specific statement, - CreateExtension { - name: Ident, - if_not_exists: bool, - cascade: bool, - schema: Option, - version: Option, - }, + CreateExtension(CreateExtension), /// ```sql /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] /// /// Note: this is a PostgreSQL-specific statement. /// https://www.postgresql.org/docs/current/sql-dropextension.html /// ``` - DropExtension { - names: Vec, - if_exists: bool, - /// `CASCADE` or `RESTRICT` - cascade_or_restrict: Option, - }, + DropExtension(DropExtension), /// ```sql /// FETCH /// ``` @@ -4806,49 +4795,8 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateIndex(create_index) => create_index.fmt(f), - Statement::CreateExtension { - name, - if_not_exists, - cascade, - schema, - version, - } => { - write!( - f, - "CREATE EXTENSION {if_not_exists}{name}", - if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" } - )?; - if *cascade || schema.is_some() || version.is_some() { - write!(f, " WITH")?; - - if let Some(name) = schema { - write!(f, " SCHEMA {name}")?; - } - if let Some(version) = version { - write!(f, " VERSION {version}")?; - } - if *cascade { - write!(f, " CASCADE")?; - } - } - - Ok(()) - } - Statement::DropExtension { - names, - if_exists, - cascade_or_restrict, - } => { - write!(f, "DROP EXTENSION")?; - if *if_exists { - write!(f, " IF EXISTS")?; - } - write!(f, " {}", display_comma_separated(names))?; - if let Some(cascade_or_restrict) = cascade_or_restrict { - write!(f, " {cascade_or_restrict}")?; - } - Ok(()) - } + Statement::CreateExtension(create_extension) => write!(f, "{create_extension}"), + Statement::DropExtension(drop_extension) => write!(f, "{drop_extension}"), Statement::CreateRole(create_role) => write!(f, "{create_role}"), Statement::CreateSecret { or_replace, @@ -10622,6 +10570,18 @@ impl From for Statement { } } +impl From for Statement { + fn from(ce: CreateExtension) -> Self { + Self::CreateExtension(ce) + } +} + +impl From for Statement { + fn from(de: DropExtension) -> Self { + Self::DropExtension(de) + } +} + impl From for Statement { fn from(c: CaseStatement) -> Self { Self::Case(c) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index fe3b0c536..79b78c9ed 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -362,6 +362,8 @@ impl Spanned for Statement { ), Statement::CreateIndex(create_index) => create_index.span(), Statement::CreateRole(create_role) => create_role.span(), + Statement::CreateExtension(create_extension) => create_extension.span(), + Statement::DropExtension(drop_extension) => drop_extension.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), @@ -405,8 +407,6 @@ impl Spanned for Statement { Statement::DropProcedure { .. } => Span::empty(), Statement::DropSecret { .. } => Span::empty(), Statement::Declare { .. } => Span::empty(), - Statement::CreateExtension { .. } => Span::empty(), - Statement::DropExtension { .. } => Span::empty(), Statement::Fetch { .. } => Span::empty(), Statement::Flush { .. } => Span::empty(), Statement::Discard { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c442646c8..ceada1b03 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7180,13 +7180,14 @@ impl<'a> Parser<'a> { (None, None, false) }; - Ok(Statement::CreateExtension { + Ok(CreateExtension { name, if_not_exists, schema, version, cascade, - }) + } + .into()) } /// Parse a PostgreSQL-specific [Statement::DropExtension] statement. @@ -7195,7 +7196,7 @@ impl<'a> Parser<'a> { let names = self.parse_comma_separated(|p| p.parse_identifier())?; let cascade_or_restrict = self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]); - Ok(Statement::DropExtension { + Ok(Statement::DropExtension(DropExtension { names, if_exists, cascade_or_restrict: cascade_or_restrict @@ -7205,7 +7206,7 @@ impl<'a> Parser<'a> { _ => self.expected("CASCADE or RESTRICT", self.peek_token()), }) .transpose()?, - }) + })) } //TODO: Implement parsing for Skewed diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 70d3e576f..3506f7cd0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -673,93 +673,93 @@ fn parse_create_extension() { fn parse_drop_extension() { assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: None, - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name, extension_name2 CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into(), "extension_name2".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION extension_name, extension_name2 RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into(), "extension_name2".into()], if_exists: false, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: None, - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic().verified_stmt("DROP EXTENSION IF EXISTS extension_name RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); assert_eq!( pg_and_generic() .verified_stmt("DROP EXTENSION IF EXISTS extension_name1, extension_name2 CASCADE"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name1".into(), "extension_name2".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Cascade), - } + }) ); assert_eq!( pg_and_generic() .verified_stmt("DROP EXTENSION IF EXISTS extension_name1, extension_name2 RESTRICT"), - Statement::DropExtension { + Statement::DropExtension(DropExtension { names: vec!["extension_name1".into(), "extension_name2".into()], if_exists: true, cascade_or_restrict: Some(ReferentialAction::Restrict), - } + }) ); } From fa34d81c619d8f2c55b9edf306a6ddf6948f7b15 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 14:29:18 +0200 Subject: [PATCH 08/12] Fixed documentation error --- src/ast/ddl.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 406c6fde0..f9c8fdc43 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3715,7 +3715,11 @@ impl Spanned for CreateExtension { /// DROP EXTENSION statement /// Note: this is a PostgreSQL-specific statement -/// https://www.postgresql.org/docs/current/sql-dropextension.html +/// +/// # References +/// +/// PostgreSQL Documentation: +/// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] From 112a96db56f84e772b292e995a3aafb2608e0e13 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 14:33:42 +0200 Subject: [PATCH 09/12] Reformatted code --- src/ast/ddl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index f9c8fdc43..effa930a6 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3715,9 +3715,9 @@ impl Spanned for CreateExtension { /// DROP EXTENSION statement /// Note: this is a PostgreSQL-specific statement -/// +/// /// # References -/// +/// /// PostgreSQL Documentation: /// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] From 8f70b00fb5f68503594085afbf3d312e428dc672 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 15:25:08 +0200 Subject: [PATCH 10/12] Moved `AlterTable` struct out of `Statement` --- src/ast/ddl.rs | 68 +++++++++++++++++++++++---- src/ast/mod.rs | 88 +++++++++-------------------------- src/ast/spans.rs | 31 ++++++------ src/parser/mod.rs | 5 +- src/test_utils.rs | 23 +++------ tests/sqlparser_clickhouse.rs | 28 ++++++----- tests/sqlparser_common.rs | 12 ++--- tests/sqlparser_mysql.rs | 16 +++---- tests/sqlparser_postgres.rs | 18 +++---- 9 files changed, 140 insertions(+), 149 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index effa930a6..6d1ddf34f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -30,15 +30,16 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ - display_comma_separated, display_separated, ArgMode, CommentDef, ConditionalStatements, - CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, - CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, FunctionCalledOnNull, - FunctionDeterminismSpecifier, FunctionParallel, HiveDistributionStyle, HiveFormat, - HiveIOFormat, HiveRowFormat, Ident, InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, - OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, - RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, - Tag, TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, - ValueWithSpan, WrappedCollection, + display_comma_separated, display_separated, ArgMode, AttachedToken, CommentDef, + ConditionalStatements, CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, + CreateTableOptions, CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, + FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, HiveDistributionStyle, + HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, InitializeKind, + MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, + OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, SequenceOptions, + Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, + TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, + WrappedCollection, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; @@ -3749,3 +3750,52 @@ impl Spanned for DropExtension { Span::empty() } } + +/// ALTER TABLE 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 AlterTable { + /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, + pub if_exists: bool, + pub only: bool, + pub operations: Vec, + pub location: Option, + /// ClickHouse dialect supports `ON CLUSTER` clause for ALTER TABLE + /// For example: `ALTER TABLE table_name ON CLUSTER cluster_name ADD COLUMN c UInt32` + /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) + pub on_cluster: Option, + /// Snowflake "ICEBERG" clause for Iceberg tables + /// + pub iceberg: bool, + /// Token that represents the end of the statement (semicolon or EOF) + pub end_token: AttachedToken, +} + +impl fmt::Display for AlterTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.iceberg { + write!(f, "ALTER ICEBERG TABLE ")?; + } else { + write!(f, "ALTER TABLE ")?; + } + + if self.if_exists { + write!(f, "IF EXISTS ")?; + } + if self.only { + write!(f, "ONLY ")?; + } + write!(f, "{} ", &self.name)?; + if let Some(cluster) = &self.on_cluster { + write!(f, "ON CLUSTER {cluster} ")?; + } + write!(f, "{}", display_comma_separated(&self.operations))?; + if let Some(loc) = &self.location { + write!(f, " {loc}")? + } + Ok(()) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5cc6aa6d0..4b77d0f05 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -60,17 +60,18 @@ pub use self::dcl::{ }; pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, - AlterSchema, AlterSchemaOperation, AlterTableAlgorithm, AlterTableLock, AlterTableOperation, - AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, - AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, - ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, - CreateExtension, CreateFunction, CreateIndex, CreateTable, CreateTrigger, CreateView, - Deduplicate, DeferrableInitial, DropBehavior, DropExtension, DropTrigger, GeneratedAs, - GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, - IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, - KeyOrIndexDisplay, Msck, NullsDistinctOption, Owner, Partition, ProcedureParam, - ReferentialAction, RenameTableNameKind, ReplicaIdentity, TableConstraint, TagsColumnOption, - Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, + AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, + ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, + ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, + CreateIndex, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, + DropBehavior, DropExtension, DropTrigger, GeneratedAs, GeneratedExpressionMode, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, + NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, + ReplicaIdentity, TableConstraint, TagsColumnOption, Truncate, + UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; pub use self::dml::{Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -3311,24 +3312,7 @@ pub enum Statement { /// ```sql /// ALTER TABLE /// ``` - AlterTable { - /// Table name - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - name: ObjectName, - if_exists: bool, - only: bool, - operations: Vec, - location: Option, - /// ClickHouse dialect supports `ON CLUSTER` clause for ALTER TABLE - /// For example: `ALTER TABLE table_name ON CLUSTER cluster_name ADD COLUMN c UInt32` - /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) - on_cluster: Option, - /// Snowflake "ICEBERG" clause for Iceberg tables - /// - iceberg: bool, - /// Token that represents the end of the statement (semicolon or EOF) - end_token: AttachedToken, - }, + AlterTable(AlterTable), /// ```sql /// ALTER SCHEMA /// ``` @@ -3537,10 +3521,9 @@ pub enum Statement { CreateExtension(CreateExtension), /// ```sql /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] - /// + /// ``` /// Note: this is a PostgreSQL-specific statement. /// https://www.postgresql.org/docs/current/sql-dropextension.html - /// ``` DropExtension(DropExtension), /// ```sql /// FETCH @@ -4879,42 +4862,7 @@ impl fmt::Display for Statement { Ok(()) } Statement::CreateConnector(create_connector) => create_connector.fmt(f), - Statement::AlterTable { - name, - if_exists, - only, - operations, - location, - on_cluster, - iceberg, - end_token: _, - } => { - if *iceberg { - write!(f, "ALTER ICEBERG TABLE ")?; - } else { - write!(f, "ALTER TABLE ")?; - } - - if *if_exists { - write!(f, "IF EXISTS ")?; - } - if *only { - write!(f, "ONLY ")?; - } - write!(f, "{name} ")?; - if let Some(cluster) = on_cluster { - write!(f, "ON CLUSTER {cluster} ")?; - } - write!( - f, - "{operations}", - operations = display_comma_separated(operations) - )?; - if let Some(loc) = location { - write!(f, " {loc}")? - } - Ok(()) - } + Statement::AlterTable(alter_table) => write!(f, "{alter_table}"), Statement::AlterIndex { name, operation } => { write!(f, "ALTER INDEX {name} {operation}") } @@ -10570,6 +10518,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(at: AlterTable) -> Self { + Self::AlterTable(at) + } +} + impl From for Statement { fn from(ce: CreateExtension) -> Self { Self::CreateExtension(ce) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 79b78c9ed..b1c2cf61b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -16,8 +16,8 @@ // under the License. use crate::ast::{ - ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, ColumnOptions, - ExportData, Owner, TypedString, + ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, AlterTable, + ColumnOptions, ExportData, Owner, TypedString, }; use core::iter; @@ -367,21 +367,7 @@ impl Spanned for Statement { Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), - Statement::AlterTable { - name, - if_exists: _, - only: _, - operations, - location: _, - on_cluster, - iceberg: _, - end_token, - } => union_spans( - core::iter::once(name.span()) - .chain(operations.iter().map(|i| i.span())) - .chain(on_cluster.iter().map(|i| i.span)) - .chain(core::iter::once(end_token.0.span)), - ), + Statement::AlterTable(alter_table) => alter_table.span(), Statement::AlterIndex { name, operation } => name.span().union(&operation.span()), Statement::AlterView { name, @@ -2416,6 +2402,17 @@ impl Spanned for AlterSchema { } } +impl Spanned for AlterTable { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()) + .chain(self.operations.iter().map(|i| i.span())) + .chain(self.on_cluster.iter().map(|i| i.span)) + .chain(core::iter::once(self.end_token.0.span)), + ) + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ceada1b03..d4a1929a8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9359,7 +9359,7 @@ impl<'a> Parser<'a> { self.get_current_token().clone() }; - Ok(Statement::AlterTable { + Ok(AlterTable { name: table_name, if_exists, only, @@ -9368,7 +9368,8 @@ impl<'a> Parser<'a> { on_cluster, iceberg, end_token: AttachedToken(end_token), - }) + } + .into()) } pub fn parse_alter_view(&mut self) -> Result { diff --git a/src/test_utils.rs b/src/test_utils.rs index ab2cf89b2..65040271f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -343,21 +343,12 @@ pub fn expr_from_projection(item: &SelectItem) -> &Expr { pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTableOperation { match stmt { - Statement::AlterTable { - name, - if_exists, - only: is_only, - operations, - on_cluster: _, - location: _, - iceberg, - end_token: _, - } => { - assert_eq!(name.to_string(), expected_name); - assert!(!if_exists); - assert!(!is_only); - assert!(!iceberg); - only(operations) + Statement::AlterTable(alter_table) => { + assert_eq!(alter_table.name.to_string(), expected_name); + assert!(!alter_table.if_exists); + assert!(!alter_table.only); + assert!(!alter_table.iceberg); + only(alter_table.operations) } _ => panic!("Expected ALTER TABLE statement"), } @@ -484,7 +475,7 @@ pub fn index_column(stmt: Statement) -> Expr { _ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"), } } - Statement::AlterTable { operations, .. } => match operations.first().unwrap() { + Statement::AlterTable(alter_table) => match alter_table.operations.first().unwrap() { AlterTableOperation::AddConstraint { constraint, .. } => { match constraint { TableConstraint::Index { columns, .. } => { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index f8cf3f2ca..44bfcda42 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -243,12 +243,10 @@ fn parse_alter_table_attach_and_detach_partition() { match clickhouse_and_generic() .verified_stmt(format!("ALTER TABLE t0 {operation} PARTITION part").as_str()) { - Statement::AlterTable { - name, operations, .. - } => { - pretty_assertions::assert_eq!("t0", name.to_string()); + Statement::AlterTable(alter_table) => { + pretty_assertions::assert_eq!("t0", alter_table.name.to_string()); pretty_assertions::assert_eq!( - operations[0], + alter_table.operations[0], if operation == &"ATTACH" { AlterTableOperation::AttachPartition { partition: Partition::Expr(Identifier(Ident::new("part"))), @@ -266,9 +264,9 @@ fn parse_alter_table_attach_and_detach_partition() { match clickhouse_and_generic() .verified_stmt(format!("ALTER TABLE t1 {operation} PART part").as_str()) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { pretty_assertions::assert_eq!("t1", name.to_string()); pretty_assertions::assert_eq!( operations[0], @@ -308,9 +306,9 @@ fn parse_alter_table_add_projection() { "ALTER TABLE t0 ADD PROJECTION IF NOT EXISTS my_name", " (SELECT a, b GROUP BY a ORDER BY b)", )) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( @@ -380,9 +378,9 @@ fn parse_alter_table_add_projection() { fn parse_alter_table_drop_projection() { match clickhouse_and_generic().verified_stmt("ALTER TABLE t0 DROP PROJECTION IF EXISTS my_name") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( @@ -413,9 +411,9 @@ fn parse_alter_table_clear_and_materialize_projection() { format!("ALTER TABLE t0 {keyword} PROJECTION IF EXISTS my_name IN PARTITION p0",) .as_str(), ) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, operations, .. - } => { + }) => { assert_eq!(name, ObjectName::from(vec!["t0".into()])); assert_eq!(1, operations.len()); assert_eq!( @@ -1518,7 +1516,7 @@ fn parse_freeze_and_unfreeze_partition() { Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(), )); match clickhouse_and_generic().verified_stmt(&sql) { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!(operations.len(), 1); let expected_operation = if operation_name == &"FREEZE" { AlterTableOperation::FreezePartition { @@ -1542,7 +1540,7 @@ fn parse_freeze_and_unfreeze_partition() { let sql = format!("ALTER TABLE t {operation_name} PARTITION '2024-08-14' WITH NAME 'hello'"); match clickhouse_and_generic().verified_stmt(&sql) { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!(operations.len(), 1); let expected_partition = Partition::Expr(Expr::Value( Value::SingleQuotedString("2024-08-14".to_string()).with_empty_span(), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 331bc6f26..73f3f6e38 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4838,9 +4838,9 @@ fn test_alter_table_with_on_cluster() { match all_dialects() .verified_stmt("ALTER TABLE t ON CLUSTER 'cluster' ADD CONSTRAINT bar PRIMARY KEY (baz)") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, on_cluster, .. - } => { + }) => { assert_eq!(name.to_string(), "t"); assert_eq!(on_cluster, Some(Ident::with_quote('\'', "cluster"))); } @@ -4850,9 +4850,9 @@ fn test_alter_table_with_on_cluster() { match all_dialects() .verified_stmt("ALTER TABLE t ON CLUSTER cluster_name ADD CONSTRAINT bar PRIMARY KEY (baz)") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, on_cluster, .. - } => { + }) => { assert_eq!(name.to_string(), "t"); assert_eq!(on_cluster, Some(Ident::new("cluster_name"))); } @@ -17223,9 +17223,9 @@ fn parse_invisible_column() { let sql = r#"ALTER TABLE t ADD COLUMN bar INT INVISIBLE"#; let stmt = verified_stmt(sql); match stmt { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(alter_table) => { assert_eq!( - operations, + alter_table.operations, vec![AlterTableOperation::AddColumn { column_keyword: true, if_not_exists: false, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 40d4374fe..3558bfbf9 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2727,7 +2727,7 @@ fn parse_delete_with_limit() { #[test] fn parse_alter_table_add_column() { match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT FIRST") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, @@ -2736,7 +2736,7 @@ fn parse_alter_table_add_column() { location: _, on_cluster: _, end_token: _, - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); assert!(!iceberg); @@ -2759,13 +2759,13 @@ fn parse_alter_table_add_column() { } match mysql().verified_stmt("ALTER TABLE tab ADD COLUMN b INT AFTER foo") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, .. - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); assert!(!only); @@ -2796,13 +2796,13 @@ fn parse_alter_table_add_columns() { match mysql() .verified_stmt("ALTER TABLE tab ADD COLUMN a TEXT FIRST, ADD COLUMN b INT AFTER foo") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, .. - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); assert!(!only); @@ -3024,7 +3024,7 @@ fn parse_alter_table_with_algorithm() { "ALTER TABLE users DROP COLUMN password_digest, ALGORITHM = COPY, RENAME COLUMN name TO username"; let stmt = mysql_and_generic().verified_stmt(sql); match stmt { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![ @@ -3072,7 +3072,7 @@ fn parse_alter_table_with_lock() { "ALTER TABLE users DROP COLUMN password_digest, LOCK = EXCLUSIVE, RENAME COLUMN name TO username"; let stmt = mysql_and_generic().verified_stmt(sql); match stmt { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![ diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 3506f7cd0..ccddee943 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -605,7 +605,7 @@ fn parse_alter_table_constraints_unique_nulls_distinct() { match pg_and_generic() .verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)") { - Statement::AlterTable { operations, .. } => match &operations[0] { + Statement::AlterTable(alter_table) => match &alter_table.operations[0] { AlterTableOperation::AddConstraint { constraint: TableConstraint::Unique { nulls_distinct, .. }, .. @@ -828,13 +828,13 @@ fn parse_alter_table_alter_column_add_generated() { #[test] fn parse_alter_table_add_columns() { match pg().verified_stmt("ALTER TABLE IF EXISTS ONLY tab ADD COLUMN a TEXT, ADD COLUMN b INT") { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists, only, operations, .. - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert!(if_exists); assert!(only); @@ -908,13 +908,13 @@ fn parse_alter_table_owner_to() { for case in test_cases { match pg_and_generic().verified_stmt(case.sql) { - Statement::AlterTable { + Statement::AlterTable(AlterTable { name, if_exists: _, only: _, operations, .. - } => { + }) => { assert_eq!(name.to_string(), "tab"); assert_eq!( operations, @@ -6360,7 +6360,7 @@ fn parse_varbit_datatype() { #[test] fn parse_alter_table_replica_identity() { match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY FULL") { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![AlterTableOperation::ReplicaIdentity { @@ -6372,7 +6372,7 @@ fn parse_alter_table_replica_identity() { } match pg_and_generic().verified_stmt("ALTER TABLE foo REPLICA IDENTITY USING INDEX foo_idx") { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![AlterTableOperation::ReplicaIdentity { @@ -6420,7 +6420,7 @@ fn parse_alter_table_constraint_not_valid() { match pg_and_generic().verified_stmt( "ALTER TABLE foo ADD CONSTRAINT bar FOREIGN KEY (baz) REFERENCES other(ref) NOT VALID", ) { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![AlterTableOperation::AddConstraint { @@ -6445,7 +6445,7 @@ fn parse_alter_table_constraint_not_valid() { #[test] fn parse_alter_table_validate_constraint() { match pg_and_generic().verified_stmt("ALTER TABLE foo VALIDATE CONSTRAINT bar") { - Statement::AlterTable { operations, .. } => { + Statement::AlterTable(AlterTable { operations, .. }) => { assert_eq!( operations, vec![AlterTableOperation::ValidateConstraint { name: "bar".into() }] From 20d1c9d933f377cf10721d08e1518cce7b9d1db3 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 15:41:50 +0200 Subject: [PATCH 11/12] =?UTF-8?q?Moved=20`DropFunction`=20struct=20out=20o?= =?UTF-8?q?f=20`Statement`=C2=A0enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ast/ddl.rs | 47 +++++++++++++++++++++++++++++++------ src/ast/mod.rs | 35 ++++++++------------------- src/ast/spans.rs | 2 +- src/parser/mod.rs | 4 ++-- tests/sqlparser_postgres.rs | 12 +++++----- 5 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6d1ddf34f..990501a25 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -33,13 +33,13 @@ use crate::ast::{ display_comma_separated, display_separated, ArgMode, AttachedToken, CommentDef, ConditionalStatements, CreateFunctionBody, CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, CreateViewParams, DataType, Expr, FileFormat, FunctionBehavior, - FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, HiveDistributionStyle, - HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, InitializeKind, - MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, - OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, SequenceOptions, - Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, TriggerEvent, - TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, ValueWithSpan, - WrappedCollection, + FunctionCalledOnNull, FunctionDesc, FunctionDeterminismSpecifier, FunctionParallel, + HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, HiveSetLocation, Ident, + InitializeKind, MySQLColumnPosition, ObjectName, OnCommit, OneOrManyWithParens, + OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RefreshModeKind, RowAccessPolicy, + SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, TableVersion, Tag, + TriggerEvent, TriggerExecBody, TriggerObject, TriggerPeriod, TriggerReferencing, Value, + ValueWithSpan, WrappedCollection, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; @@ -3799,3 +3799,36 @@ impl fmt::Display for AlterTable { Ok(()) } } + +/// DROP FUNCTION 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 DropFunction { + pub if_exists: bool, + /// One or more functions to drop + pub func_desc: Vec, + /// `CASCADE` or `RESTRICT` + pub drop_behavior: Option, +} + +impl fmt::Display for DropFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "DROP FUNCTION{} {}", + if self.if_exists { " IF EXISTS" } else { "" }, + display_comma_separated(&self.func_desc), + )?; + if let Some(op) = &self.drop_behavior { + write!(f, " {op}")?; + } + Ok(()) + } +} + +impl Spanned for DropFunction { + fn span(&self) -> Span { + Span::empty() + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4b77d0f05..30d256ee0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -66,7 +66,7 @@ pub use self::ddl::{ ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, - DropBehavior, DropExtension, DropTrigger, GeneratedAs, GeneratedExpressionMode, + DropBehavior, DropExtension, DropFunction, DropTrigger, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, @@ -3448,13 +3448,7 @@ pub enum Statement { /// ```sql /// DROP FUNCTION /// ``` - DropFunction { - if_exists: bool, - /// One or more function to drop - func_desc: Vec, - /// `CASCADE` or `RESTRICT` - drop_behavior: Option, - }, + DropFunction(DropFunction), /// ```sql /// DROP DOMAIN /// ``` @@ -3523,7 +3517,7 @@ pub enum Statement { /// DROP EXTENSION [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] /// ``` /// Note: this is a PostgreSQL-specific statement. - /// https://www.postgresql.org/docs/current/sql-dropextension.html + /// DropExtension(DropExtension), /// ```sql /// FETCH @@ -4965,22 +4959,7 @@ impl fmt::Display for Statement { }; Ok(()) } - Statement::DropFunction { - if_exists, - func_desc, - drop_behavior, - } => { - write!( - f, - "DROP FUNCTION{} {}", - if *if_exists { " IF EXISTS" } else { "" }, - display_comma_separated(func_desc), - )?; - if let Some(op) = drop_behavior { - write!(f, " {op}")?; - } - Ok(()) - } + Statement::DropFunction(drop_function) => write!(f, "{drop_function}"), Statement::DropDomain(DropDomain { if_exists, name, @@ -10524,6 +10503,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(df: DropFunction) -> Self { + Self::DropFunction(df) + } +} + impl From for Statement { fn from(ce: CreateExtension) -> Self { Self::CreateExtension(ce) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index b1c2cf61b..c3379def3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -388,7 +388,7 @@ impl Spanned for Statement { Statement::AttachDuckDBDatabase { .. } => Span::empty(), Statement::DetachDuckDBDatabase { .. } => Span::empty(), Statement::Drop { .. } => Span::empty(), - Statement::DropFunction { .. } => Span::empty(), + Statement::DropFunction(drop_function) => drop_function.span(), Statement::DropDomain { .. } => Span::empty(), Statement::DropProcedure { .. } => Span::empty(), Statement::DropSecret { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d4a1929a8..0e708b974 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6508,11 +6508,11 @@ impl<'a> Parser<'a> { let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let func_desc = self.parse_comma_separated(Parser::parse_function_desc)?; let drop_behavior = self.parse_optional_drop_behavior(); - Ok(Statement::DropFunction { + Ok(Statement::DropFunction(DropFunction { if_exists, func_desc, drop_behavior, - }) + })) } /// ```sql diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ccddee943..20eb5eb8b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4488,7 +4488,7 @@ fn parse_drop_function() { let sql = "DROP FUNCTION IF EXISTS test_func"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![FunctionDesc { name: ObjectName::from(vec![Ident { @@ -4499,13 +4499,13 @@ fn parse_drop_function() { args: None }], drop_behavior: None - } + }) ); let sql = "DROP FUNCTION IF EXISTS test_func(a INTEGER, IN b INTEGER = 1)"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![FunctionDesc { name: ObjectName::from(vec![Ident { @@ -4526,13 +4526,13 @@ fn parse_drop_function() { ]), }], drop_behavior: None - } + }) ); let sql = "DROP FUNCTION IF EXISTS test_func1(a INTEGER, IN b INTEGER = 1), test_func2(a VARCHAR, IN b INTEGER = 1)"; assert_eq!( pg().verified_stmt(sql), - Statement::DropFunction { + Statement::DropFunction(DropFunction { if_exists: true, func_desc: vec![ FunctionDesc { @@ -4573,7 +4573,7 @@ fn parse_drop_function() { } ], drop_behavior: None - } + }) ); } From 7782556ad893f6438335fa9550617ff4a84f9b90 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 Oct 2025 15:46:02 +0200 Subject: [PATCH 12/12] =?UTF-8?q?Restored=20previous=20implementation=20of?= =?UTF-8?q?=20`Spanned`=C2=A0trait=20for=20`CreateView`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ast/ddl.rs | 24 ------------------------ src/ast/spans.rs | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 990501a25..1134cf4cb 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3641,30 +3641,6 @@ impl fmt::Display for CreateView { } } -impl Spanned for CreateView { - fn span(&self) -> Span { - let name_span = self.name.span(); - let query_span = self.query.span(); - let options_span = self.options.span(); - - // Union all the relevant spans - let mut spans = vec![name_span, query_span, options_span]; - - // Add column spans - spans.extend(self.columns.iter().map(|col| col.span())); - - // Add cluster_by spans - spans.extend(self.cluster_by.iter().map(|ident| ident.span)); - - // Add to span if present - if let Some(ref to) = self.to { - spans.push(to.span()); - } - - Span::union_iter(spans) - } -} - /// CREATE EXTENSION statement /// Note: this is a PostgreSQL-specific statement #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index c3379def3..6ab2e476e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -17,7 +17,7 @@ use crate::ast::{ ddl::AlterSchema, query::SelectItemQualifiedWildcardKind, AlterSchemaOperation, AlterTable, - ColumnOptions, ExportData, Owner, TypedString, + ColumnOptions, CreateView, ExportData, Owner, TypedString, }; use core::iter; @@ -2402,6 +2402,19 @@ impl Spanned for AlterSchema { } } +impl Spanned for CreateView { + fn span(&self) -> Span { + union_spans( + core::iter::once(self.name.span()) + .chain(self.columns.iter().map(|i| i.span())) + .chain(core::iter::once(self.query.span())) + .chain(core::iter::once(self.options.span())) + .chain(self.cluster_by.iter().map(|i| i.span)) + .chain(self.to.iter().map(|i| i.span())), + ) + } +} + impl Spanned for AlterTable { fn span(&self) -> Span { union_spans(