diff --git a/examples/cli.rs b/examples/cli.rs index 3c4299b209..51a63a7ea1 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -71,9 +71,9 @@ $ cargo run --example cli - [--dialectname] .expect("failed to read from stdin"); String::from_utf8(buf).expect("stdin content wasn't valid utf8") } else { - println!("Parsing from file '{}' using {:?}", &filename, dialect); + println!("Parsing from file '{}' using {:?}", filename, dialect); fs::read_to_string(&filename) - .unwrap_or_else(|_| panic!("Unable to read the file {}", &filename)) + .unwrap_or_else(|_| panic!("Unable to read the file {}", filename)) }; let without_bom = if contents.chars().next().unwrap() as u64 != 0xfeff { contents.as_str() diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 3c50a81c06..4b7fd73358 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -31,7 +31,7 @@ use sqlparser_derive::{Visit, VisitMut}; use super::{display_comma_separated, Expr, Ident, Password, Spanned}; use crate::ast::{ display_separated, CascadeOption, CurrentGrantsKind, GrantObjects, Grantee, ObjectName, - Privileges, + Privileges, Tag, }; use crate::tokenizer::Span; @@ -311,6 +311,8 @@ impl fmt::Display for SecondaryRoles { pub struct CreateRole { /// Role names to create. pub names: Vec, + /// Whether `OR REPLACE` was specified. + pub or_replace: bool, /// Whether `IF NOT EXISTS` was specified. pub if_not_exists: bool, // Postgres @@ -347,13 +349,17 @@ pub struct CreateRole { // MSSQL /// Optional authorization owner. pub authorization_owner: Option, + // Snowflake + /// Trailing `WITH TAG ( = '' [, ...])` clause; empty when absent. + pub with_tags: Vec, } 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}", + "CREATE {or_replace}ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}", + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, names = display_separated(&self.names, ", "), superuser = match self.superuser { @@ -421,6 +427,13 @@ impl fmt::Display for CreateRole { if let Some(owner) = &self.authorization_owner { write!(f, " AUTHORIZATION {owner}")?; } + if !self.with_tags.is_empty() { + write!( + f, + " WITH TAG ({})", + display_comma_separated(&self.with_tags) + )?; + } Ok(()) } } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index a8821fc5b2..d13c225eca 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -34,6 +34,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +use crate::ast::helpers::key_value_options::KeyValueOptions; use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, @@ -120,6 +121,24 @@ impl fmt::Display for ReplicaIdentity { } } +/// A single ` = ''` pair of a Snowflake external-table +/// `ADD PARTITION` clause. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ExternalTablePartitionColumn { + /// The partition column. + pub column: Ident, + /// The partition value, always a string literal. + pub value: String, +} + +impl fmt::Display for ExternalTablePartitionColumn { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} = '{}'", self.column, self.value) + } +} + /// An `ALTER TABLE` (`Statement::AlterTable`) operation #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -143,6 +162,18 @@ pub enum AlterTableOperation { /// MySQL `ALTER TABLE` only [FIRST | AFTER column_name] column_position: Option, }, + /// `ADD [COLUMN] [IF NOT EXISTS] [, , ...]` + /// + /// Snowflake multi-column add: a single `ADD [COLUMN]` keyword followed by + /// a comma-separated list of column definitions. + AddColumns { + /// `[COLUMN]`. + column_keyword: bool, + /// `[IF NOT EXISTS]` + if_not_exists: bool, + /// The column definitions to add. + column_defs: Vec, + }, /// `ADD PROJECTION [IF NOT EXISTS] name ( SELECT [GROUP BY] [ORDER BY])` /// /// Note: this is a ClickHouse-specific operation. @@ -459,6 +490,13 @@ pub enum AlterTableOperation { }, /// Remove the clustering key from the table. DropClusteringKey, + /// `DROP ROW ACCESS POLICY ` (Snowflake). + DropRowAccessPolicy { + /// The row access policy name being detached. + policy_name: ObjectName, + }, + /// `DROP ALL ROW ACCESS POLICIES` (Snowflake). + DropAllRowAccessPolicies, /// Redshift `ALTER SORTKEY (column_list)` /// AlterSortKey { @@ -478,6 +516,44 @@ pub enum AlterTableOperation { /// Optional subpath for external table refresh subpath: Option, }, + /// `ADD FILES ( '' [, ... ] )` + /// + /// Snowflake external table: register specific staged files. + /// + AddFiles { + /// Relative staged file paths to register. + files: Vec, + }, + /// `REMOVE FILES ( '' [, ... ] )` + /// + /// Snowflake external table: unregister specific staged files. + RemoveFiles { + /// Relative staged file paths to unregister. + files: Vec, + }, + /// `SET AUTO_REFRESH = { TRUE | FALSE }` + /// + /// Snowflake external table auto-refresh toggle. + SetAutoRefresh { + /// The new auto-refresh value. + value: bool, + }, + /// `ADD PARTITION ( = '' [, ... ] ) LOCATION ''` + /// + /// Snowflake user-specified partition addition (external table). + AddExternalPartition { + /// Column/value pairs defining the partition. + partitions: Vec, + /// The staged subpath the partition maps to. + location: String, + }, + /// `DROP PARTITION LOCATION ''` + /// + /// Snowflake user-specified partition removal (external table). + DropExternalPartition { + /// The staged subpath whose partition is dropped. + location: String, + }, /// `SUSPEND` /// /// Note: this is Snowflake specific for dynamic tables @@ -746,6 +822,21 @@ impl fmt::Display for AlterTableOperation { Ok(()) } + AlterTableOperation::AddColumns { + column_keyword, + if_not_exists, + column_defs, + } => { + write!(f, "ADD")?; + if *column_keyword { + write!(f, " COLUMN")?; + } + if *if_not_exists { + write!(f, " IF NOT EXISTS")?; + } + write!(f, " {}", display_comma_separated(column_defs))?; + Ok(()) + } AlterTableOperation::AddProjection { if_not_exists, name, @@ -1008,6 +1099,12 @@ impl fmt::Display for AlterTableOperation { write!(f, "DROP CLUSTERING KEY")?; Ok(()) } + AlterTableOperation::DropRowAccessPolicy { policy_name } => { + write!(f, "DROP ROW ACCESS POLICY {policy_name}") + } + AlterTableOperation::DropAllRowAccessPolicies => { + write!(f, "DROP ALL ROW ACCESS POLICIES") + } AlterTableOperation::AlterSortKey { columns } => { write!(f, "ALTER SORTKEY({})", display_comma_separated(columns))?; Ok(()) @@ -1027,6 +1124,48 @@ impl fmt::Display for AlterTableOperation { } Ok(()) } + AlterTableOperation::AddFiles { files } => { + write!( + f, + "ADD FILES ({})", + files + .iter() + .map(|file| format!("'{file}'")) + .collect::>() + .join(", ") + ) + } + AlterTableOperation::RemoveFiles { files } => { + write!( + f, + "REMOVE FILES ({})", + files + .iter() + .map(|file| format!("'{file}'")) + .collect::>() + .join(", ") + ) + } + AlterTableOperation::SetAutoRefresh { value } => { + write!( + f, + "SET AUTO_REFRESH = {}", + if *value { "TRUE" } else { "FALSE" } + ) + } + AlterTableOperation::AddExternalPartition { + partitions, + location, + } => { + write!( + f, + "ADD PARTITION ({}) LOCATION '{location}'", + display_comma_separated(partitions) + ) + } + AlterTableOperation::DropExternalPartition { location } => { + write!(f, "DROP PARTITION LOCATION '{location}'") + } AlterTableOperation::Suspend => { write!(f, "SUSPEND") } @@ -1297,6 +1436,15 @@ pub enum AlterColumnOperation { had_set: bool, }, + /// `COMMENT ''` + /// + /// Snowflake: set the column comment + /// (`ALTER TABLE t ALTER COLUMN c COMMENT ''`). + Comment { + /// The comment text. + comment: String, + }, + /// `ADD GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ]` /// /// Note: this is a PostgreSQL-specific operation. @@ -1306,6 +1454,116 @@ pub enum AlterColumnOperation { /// Optional sequence options for identity generation. sequence_options: Option>, }, + + /// `SET MASKING POLICY [USING (, ...)] [FORCE]` + /// + /// Snowflake: attach a masking policy to the column + /// (`ALTER TABLE t MODIFY COLUMN c SET MASKING POLICY p`). + SetMaskingPolicy { + /// The policy to attach. + policy_name: ObjectName, + /// Optional `USING (, ...)` conditional-masking column list. + using_columns: Option>, + /// Whether the `FORCE` keyword was present. + force: bool, + }, + + /// `UNSET MASKING POLICY` + /// + /// Snowflake: detach the masking policy from the column. + UnsetMaskingPolicy, +} + +/// An operation on a masking policy in an `ALTER MASKING POLICY` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterMaskingPolicyOperation { + /// `SET BODY -> ` + SetBody { + /// The replacement body expression. + body: Expr, + }, + /// `RENAME TO ` + RenameTo { + /// The new policy name. + new_name: ObjectName, + }, + /// `SET COMMENT = ''` + SetComment { + /// The replacement comment text. + comment: String, + }, + /// `UNSET COMMENT` + UnsetComment, +} + +impl fmt::Display for AlterMaskingPolicyOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterMaskingPolicyOperation::SetBody { body } => write!(f, "SET BODY -> {body}"), + AlterMaskingPolicyOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterMaskingPolicyOperation::SetComment { comment } => { + write!(f, "SET COMMENT = '{}'", escape_single_quote_string(comment)) + } + AlterMaskingPolicyOperation::UnsetComment => write!(f, "UNSET COMMENT"), + } + } +} + +/// An operation in an `ALTER TAG` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTagOperation { + /// `RENAME TO ` + RenameTo { + /// The new tag name. + new_name: ObjectName, + }, + /// `SET MASKING POLICY

[, MASKING POLICY

...] [FORCE]` + SetMaskingPolicy { + /// The masking policies to attach. + policies: Vec, + /// `FORCE` flag. + force: bool, + }, + /// `UNSET MASKING POLICY

[, MASKING POLICY

...]` + UnsetMaskingPolicy { + /// The masking policies to detach. + policies: Vec, + }, +} + +impl fmt::Display for AlterTagOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterTagOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterTagOperation::SetMaskingPolicy { policies, force } => { + write!(f, "SET ")?; + for (i, p) in policies.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "MASKING POLICY {p}")?; + } + if *force { + write!(f, " FORCE")?; + } + Ok(()) + } + AlterTagOperation::UnsetMaskingPolicy { policies } => { + write!(f, "UNSET ")?; + for (i, p) in policies.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "MASKING POLICY {p}")?; + } + Ok(()) + } + } + } } impl fmt::Display for AlterColumnOperation { @@ -1333,6 +1591,9 @@ impl fmt::Display for AlterColumnOperation { } Ok(()) } + AlterColumnOperation::Comment { comment } => { + write!(f, "COMMENT '{}'", escape_single_quote_string(comment)) + } AlterColumnOperation::AddGenerated { generated_as, sequence_options, @@ -1355,6 +1616,21 @@ impl fmt::Display for AlterColumnOperation { } Ok(()) } + AlterColumnOperation::SetMaskingPolicy { + policy_name, + using_columns, + force, + } => { + write!(f, "SET MASKING POLICY {policy_name}")?; + if let Some(columns) = using_columns { + write!(f, " USING ({})", display_comma_separated(columns))?; + } + if *force { + write!(f, " FORCE")?; + } + Ok(()) + } + AlterColumnOperation::UnsetMaskingPolicy => write!(f, "UNSET MASKING POLICY"), } } } @@ -1572,6 +1848,42 @@ pub struct ColumnDef { pub options: Vec, } +impl ColumnDef { + /// Drop any [`ConstraintCharacteristics`] the column's inline constraints + /// carry, so they render as bare constraints. Counterpart to + /// [`TableConstraint::clear_characteristics`] for dialects whose grammar has + /// no equivalent of the characteristics the source dialect accepts. + pub fn clear_constraint_characteristics(&mut self) { + for option in &mut self.options { + match &mut option.option { + ColumnOption::PrimaryKey(constraint) => constraint.characteristics = None, + ColumnOption::Unique(constraint) => constraint.characteristics = None, + ColumnOption::ForeignKey(constraint) => constraint.characteristics = None, + ColumnOption::Null + | ColumnOption::NotNull + | ColumnOption::Default(_) + | ColumnOption::Materialized(_) + | ColumnOption::Ephemeral(_) + | ColumnOption::Alias(_) + | ColumnOption::Check(_) + | ColumnOption::DialectSpecific(_) + | ColumnOption::CharacterSet(_) + | ColumnOption::Collation(_) + | ColumnOption::Comment(_) + | ColumnOption::OnUpdate(_) + | ColumnOption::Generated { .. } + | ColumnOption::Options(_) + | ColumnOption::Identity(_) + | ColumnOption::OnConflict(_) + | ColumnOption::Policy(_) + | ColumnOption::Tags(_) + | ColumnOption::Srid(_) + | ColumnOption::Invisible => {} + } + } + } +} + impl fmt::Display for ColumnDef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.data_type == DataType::Unspecified { @@ -2257,9 +2569,13 @@ pub(crate) fn display_option_spaced(option: &Option) -> impl display_option(" ", "", option) } -/// ` = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]` +/// ` = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ] [ ENABLE | DISABLE ] [ VALIDATE | NOVALIDATE ] [ RELY | NORELY ]` /// /// Used in UNIQUE and foreign key constraints. The individual settings may occur in any order. +/// +/// `ENABLE`/`DISABLE`, `VALIDATE`/`NOVALIDATE` and `RELY`/`NORELY` are only +/// parsed for dialects returning true from +/// `Dialect::supports_informational_constraint_properties`. #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Default, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -2270,6 +2586,12 @@ pub struct ConstraintCharacteristics { pub initially: Option, /// `[ ENFORCED | NOT ENFORCED ]` pub enforced: Option, + /// `[ ENABLE | DISABLE ]` + pub enabled: Option, + /// `[ VALIDATE | NOVALIDATE ]` + pub validated: Option, + /// `[ RELY | NORELY ]` + pub rely: Option, } /// Initial setting for deferrable constraints (`INITIALLY IMMEDIATE` or `INITIALLY DEFERRED`). @@ -2313,26 +2635,35 @@ impl ConstraintCharacteristics { }, ) } + + fn enabled_text(&self) -> Option<&'static str> { + self.enabled + .map(|enabled| if enabled { "ENABLE" } else { "DISABLE" }) + } + + fn validated_text(&self) -> Option<&'static str> { + self.validated + .map(|validated| if validated { "VALIDATE" } else { "NOVALIDATE" }) + } + + fn rely_text(&self) -> Option<&'static str> { + self.rely.map(|rely| if rely { "RELY" } else { "NORELY" }) + } } impl fmt::Display for ConstraintCharacteristics { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let deferrable = self.deferrable_text(); - let initially_immediate = self.initially_immediate_text(); - let enforced = self.enforced_text(); - - match (deferrable, initially_immediate, enforced) { - (None, None, None) => Ok(()), - (None, None, Some(enforced)) => write!(f, "{enforced}"), - (None, Some(initial), None) => write!(f, "{initial}"), - (None, Some(initial), Some(enforced)) => write!(f, "{initial} {enforced}"), - (Some(deferrable), None, None) => write!(f, "{deferrable}"), - (Some(deferrable), None, Some(enforced)) => write!(f, "{deferrable} {enforced}"), - (Some(deferrable), Some(initial), None) => write!(f, "{deferrable} {initial}"), - (Some(deferrable), Some(initial), Some(enforced)) => { - write!(f, "{deferrable} {initial} {enforced}") - } - } + let properties = [ + self.deferrable_text(), + self.initially_immediate_text(), + self.enforced_text(), + self.enabled_text(), + self.validated_text(), + self.rely_text(), + ]; + + let set: Vec<&str> = properties.into_iter().flatten().collect(); + write!(f, "{}", display_separated(&set, " ")) } } @@ -3030,6 +3361,9 @@ pub struct CreateTable { /// Snowflake "CHANGE_TRACKING" clause /// pub change_tracking: Option, + /// Snowflake table-level "STAGE_FILE_FORMAT" clause + /// + pub stage_file_format: Option, /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause /// pub data_retention_time_in_days: Option, @@ -3060,6 +3394,12 @@ pub struct CreateTable { /// Snowflake "CATALOG" clause for Iceberg tables /// pub catalog: Option, + /// Snowflake "CATALOG_TABLE_NAME" clause for externally-managed Iceberg tables + /// + pub catalog_table_name: Option, + /// Snowflake "AUTO_REFRESH" clause for externally-managed Iceberg tables + /// + pub auto_refresh: Option, /// Snowflake "CATALOG_SYNC" clause for Iceberg tables /// pub catalog_sync: Option, @@ -3069,9 +3409,23 @@ pub struct CreateTable { /// Snowflake "TARGET_LAG" clause for dybamic tables /// pub target_lag: Option, - /// Snowflake "WAREHOUSE" clause for dybamic tables + /// Snowflake "WAREHOUSE" clause for dybamic tables. Stored verbatim (not + /// as an `Ident`) so the user's original casing survives — SHOW / GET_DDL + /// echo the warehouse exactly as written. + /// + pub warehouse: Option, + /// Snowflake "INITIALIZATION_WAREHOUSE" clause for dynamic tables. Stored + /// verbatim for the same case-fidelity reason as `warehouse`. /// - pub warehouse: Option, + pub initialization_warehouse: Option, + /// Snowflake "SCHEDULER" clause for dynamic tables (a quoted string or a + /// bare keyword such as `DISABLE`); the value is stored verbatim. + /// + pub scheduler: Option, + /// Snowflake `IMMUTABLE WHERE ()` clause for dynamic tables. + /// Stored as the serialized predicate text so its casing survives. + /// + pub immutable_where: Option, /// Snowflake "REFRESH_MODE" clause for dybamic tables /// pub refresh_mode: Option, @@ -3093,6 +3447,34 @@ pub struct CreateTable { /// Redshift `BACKUP` option: `BACKUP { YES | NO }` /// pub backup: Option, + /// Snowflake external table `PATTERN = ''` clause. + /// + pub pattern: Option, + /// Snowflake external table `REFRESH_ON_CREATE = { TRUE | FALSE }` clause. + pub refresh_on_create: Option, + /// Snowflake external table `PARTITION_TYPE = { USER_SPECIFIED | ... }` clause. + pub partition_type: Option, + /// Snowflake external table `TABLE_FORMAT = { DELTA | ... }` clause. + pub table_format: Option, + /// Snowflake external table `AWS_SNS_TOPIC = ''` clause. + pub aws_sns_topic: Option, +} + +impl CreateTable { + /// Whether this is a Snowflake-shaped `CREATE EXTERNAL TABLE` (which renders + /// `LOCATION=@stage FILE_FORMAT=(...)` etc.) as opposed to the Hive form + /// (`STORED AS ... LOCATION '...'`). The two grammars share the `external` + /// flag but never the Snowflake-only clauses. + fn is_snowflake_external(&self) -> bool { + self.external + && self.file_format.is_none() + && (self.stage_file_format.is_some() + || self.pattern.is_some() + || self.refresh_on_create.is_some() + || self.partition_type.is_some() + || self.table_format.is_some() + || self.aws_sns_topic.is_some()) + } } impl fmt::Display for CreateTable { @@ -3149,8 +3531,10 @@ impl fmt::Display for CreateTable { && self.like.is_none() && self.clone.is_none() && self.partition_of.is_none() + && !self.iceberg { - // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens + // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens. + // Externally-managed Iceberg tables legitimately have no column list. f.write_str(" ()")?; } else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like { write!(f, " ({like_in_columns_list})")?; @@ -3252,7 +3636,7 @@ impl fmt::Display for CreateTable { } } } - if self.external { + if self.external && !self.is_snowflake_external() { if let Some(file_format) = self.file_format { write!(f, " STORED AS {file_format}")?; } @@ -3260,6 +3644,33 @@ impl fmt::Display for CreateTable { write!(f, " LOCATION '{location}'")?; } } + if self.is_snowflake_external() { + if let Some(location) = &self.location { + write!(f, " LOCATION={location}")?; + } + if let Some(stage_file_format) = &self.stage_file_format { + write!(f, " FILE_FORMAT=({stage_file_format})")?; + } + if let Some(pattern) = &self.pattern { + write!(f, " PATTERN='{pattern}'")?; + } + if let Some(refresh_on_create) = self.refresh_on_create { + write!( + f, + " REFRESH_ON_CREATE={}", + if refresh_on_create { "TRUE" } else { "FALSE" } + )?; + } + if let Some(partition_type) = &self.partition_type { + write!(f, " PARTITION_TYPE={partition_type}")?; + } + if let Some(table_format) = &self.table_format { + write!(f, " TABLE_FORMAT={table_format}")?; + } + if let Some(aws_sns_topic) = &self.aws_sns_topic { + write!(f, " AWS_SNS_TOPIC='{aws_sns_topic}'")?; + } + } match &self.table_options { options @ CreateTableOptions::With(_) @@ -3294,6 +3705,18 @@ impl fmt::Display for CreateTable { write!(f, " CATALOG='{catalog}'")?; } + if let Some(catalog_table_name) = self.catalog_table_name.as_ref() { + write!(f, " CATALOG_TABLE_NAME='{catalog_table_name}'")?; + } + + if let Some(auto_refresh) = self.auto_refresh { + write!( + f, + " AUTO_REFRESH={}", + if auto_refresh { "TRUE" } else { "FALSE" } + )?; + } + if self.iceberg { if let Some(base_location) = self.base_location.as_ref() { write!(f, " BASE_LOCATION='{base_location}'")?; @@ -3331,6 +3754,12 @@ impl fmt::Display for CreateTable { )?; } + if !self.is_snowflake_external() { + if let Some(stage_file_format) = &self.stage_file_format { + write!(f, " STAGE_FILE_FORMAT=({stage_file_format})")?; + } + } + if let Some(data_retention_time_in_days) = self.data_retention_time_in_days { write!( f, @@ -3373,6 +3802,18 @@ impl fmt::Display for CreateTable { write!(f, " WAREHOUSE={warehouse}")?; } + if let Some(initialization_warehouse) = &self.initialization_warehouse { + write!(f, " INITIALIZATION_WAREHOUSE={initialization_warehouse}")?; + } + + if let Some(scheduler) = &self.scheduler { + write!(f, " SCHEDULER='{scheduler}'")?; + } + + if let Some(immutable_where) = &self.immutable_where { + write!(f, " IMMUTABLE WHERE ({immutable_where})")?; + } + if let Some(refresh_mode) = &self.refresh_mode { write!(f, " REFRESH_MODE={refresh_mode}")?; } @@ -4224,6 +4665,9 @@ pub struct Truncate { pub partitions: Option>, /// TABLE - optional keyword pub table: bool, + /// Snowflake-specific: `MATERIALIZED VIEW` target instead of a table. + /// + pub materialized_view: bool, /// Snowflake/Redshift-specific option: [ IF EXISTS ] pub if_exists: bool, /// Postgres-specific option: [ RESTART IDENTITY | CONTINUE IDENTITY ] @@ -4237,7 +4681,13 @@ pub struct Truncate { impl fmt::Display for Truncate { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let table = if self.table { "TABLE " } else { "" }; + let table = if self.materialized_view { + "MATERIALIZED VIEW " + } else if self.table { + "TABLE " + } else { + "" + }; let if_exists = if self.if_exists { "IF EXISTS " } else { "" }; write!( @@ -4692,6 +5142,9 @@ pub enum AlterTableType { /// External table type /// External, + /// Materialized view type + /// + MaterializedView, } /// ALTER TABLE statement @@ -4726,6 +5179,7 @@ impl fmt::Display for AlterTable { Some(AlterTableType::Iceberg) => write!(f, "ALTER ICEBERG TABLE ")?, Some(AlterTableType::Dynamic) => write!(f, "ALTER DYNAMIC TABLE ")?, Some(AlterTableType::External) => write!(f, "ALTER EXTERNAL TABLE ")?, + Some(AlterTableType::MaterializedView) => write!(f, "ALTER MATERIALIZED VIEW ")?, None => write!(f, "ALTER TABLE ")?, } @@ -4735,7 +5189,7 @@ impl fmt::Display for AlterTable { if self.only { write!(f, "ONLY ")?; } - write!(f, "{} ", &self.name)?; + write!(f, "{} ", self.name)?; if let Some(cluster) = &self.on_cluster { write!(f, "ON CLUSTER {cluster} ")?; } @@ -5604,6 +6058,80 @@ impl Spanned for AlterFunction { } } +/// Snowflake `ALTER PROCEDURE [IF EXISTS] ( [ [, ...]] ) `. +/// +/// Kept distinct from [`AlterFunction`] because the procedure grammar carries +/// the `EXECUTE AS { CALLER | OWNER }` rights operation, which has no function +/// analog. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterProcedure { + /// `IF EXISTS` flag. + pub if_exists: bool, + /// Procedure name. + pub name: ObjectName, + /// Argument-type signature (`(NUMBER, VARCHAR)`), used to disambiguate + /// overloads. Empty when the parentheses hold no arguments. + pub args: Vec, + /// Operation applied to the procedure. + pub operation: AlterProcedureOperation, +} + +/// Operation for [`AlterProcedure`]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterProcedureOperation { + /// `RENAME TO ` + RenameTo { + /// New procedure name. + new_name: ObjectName, + }, + /// `SET COMMENT = ` + SetComment { + /// The comment value expression. + comment: Expr, + }, + /// `UNSET COMMENT` + UnsetComment, + /// `EXECUTE AS { CALLER | OWNER }` + ExecuteAs(ProcedureExecuteAs), +} + +impl fmt::Display for AlterProcedure { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ALTER PROCEDURE ")?; + if self.if_exists { + write!(f, "IF EXISTS ")?; + } + write!( + f, + "{}({}) {}", + self.name, + display_comma_separated(&self.args), + self.operation + ) + } +} + +impl fmt::Display for AlterProcedureOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterProcedureOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterProcedureOperation::SetComment { comment } => write!(f, "SET COMMENT = {comment}"), + AlterProcedureOperation::UnsetComment => write!(f, "UNSET COMMENT"), + AlterProcedureOperation::ExecuteAs(execute_as) => write!(f, "EXECUTE AS {execute_as}"), + } + } +} + +impl Spanned for AlterProcedure { + fn span(&self) -> Span { + Span::empty() + } +} + /// CREATE POLICY statement. /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index ab2feb6930..f68743a15a 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -24,6 +24,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; +use crate::ast::helpers::key_value_options::KeyValueOptions; use crate::ast::{ ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions, DistStyle, Expr, FileFormat, ForValues, HiveDistributionStyle, HiveFormat, Ident, @@ -139,6 +140,8 @@ pub struct CreateTableBuilder { pub enable_schema_evolution: Option, /// Optional change tracking flag. pub change_tracking: Option, + /// Optional table-level stage file format options. + pub stage_file_format: Option, /// Optional data retention time in days. pub data_retention_time_in_days: Option, /// Optional max data extension time in days. @@ -159,6 +162,10 @@ pub struct CreateTableBuilder { pub external_volume: Option, /// Optional catalog name. pub catalog: Option, + /// Optional externally-managed catalog table name. + pub catalog_table_name: Option, + /// Optional auto-refresh flag for externally-managed Iceberg tables. + pub auto_refresh: Option, /// Optional catalog synchronization option. pub catalog_sync: Option, /// Optional storage serialization policy. @@ -167,8 +174,14 @@ pub struct CreateTableBuilder { pub table_options: CreateTableOptions, /// Optional target lag configuration. pub target_lag: Option, - /// Optional warehouse identifier. - pub warehouse: Option, + /// Optional warehouse name, stored verbatim. + pub warehouse: Option, + /// Optional initialization warehouse name, stored verbatim. + pub initialization_warehouse: Option, + /// Optional scheduler value (quoted string or bare keyword). + pub scheduler: Option, + /// Optional `IMMUTABLE WHERE` predicate text. + pub immutable_where: Option, /// Optional refresh mode for materialized tables. pub refresh_mode: Option, /// Optional initialization kind for the table. @@ -183,6 +196,16 @@ pub struct CreateTableBuilder { pub sortkey: Option>, /// Redshift `BACKUP` option. pub backup: Option, + /// Snowflake external table `PATTERN` clause. + pub pattern: Option, + /// Snowflake external table `REFRESH_ON_CREATE` clause. + pub refresh_on_create: Option, + /// Snowflake external table `PARTITION_TYPE` clause. + pub partition_type: Option, + /// Snowflake external table `TABLE_FORMAT` clause. + pub table_format: Option, + /// Snowflake external table `AWS_SNS_TOPIC` clause. + pub aws_sns_topic: Option, } impl CreateTableBuilder { @@ -226,6 +249,7 @@ impl CreateTableBuilder { copy_grants: false, enable_schema_evolution: None, change_tracking: None, + stage_file_format: None, data_retention_time_in_days: None, max_data_extension_time_in_days: None, default_ddl_collation: None, @@ -236,11 +260,16 @@ impl CreateTableBuilder { base_location: None, external_volume: None, catalog: None, + catalog_table_name: None, + auto_refresh: None, catalog_sync: None, storage_serialization_policy: None, table_options: CreateTableOptions::None, target_lag: None, warehouse: None, + initialization_warehouse: None, + scheduler: None, + immutable_where: None, refresh_mode: None, initialize: None, require_user: false, @@ -248,6 +277,11 @@ impl CreateTableBuilder { distkey: None, sortkey: None, backup: None, + pattern: None, + refresh_on_create: None, + partition_type: None, + table_format: None, + aws_sns_topic: None, } } /// Set `OR REPLACE` for the CREATE TABLE statement. @@ -434,6 +468,11 @@ impl CreateTableBuilder { self.change_tracking = change_tracking; self } + /// Set the table-level stage file format options. + pub fn stage_file_format(mut self, stage_file_format: Option) -> Self { + self.stage_file_format = stage_file_format; + self + } /// Set data retention time (in days). pub fn data_retention_time_in_days(mut self, data_retention_time_in_days: Option) -> Self { self.data_retention_time_in_days = data_retention_time_in_days; @@ -516,11 +555,26 @@ impl CreateTableBuilder { self.target_lag = target_lag; self } - /// Associate the table with a warehouse identifier. - pub fn warehouse(mut self, warehouse: Option) -> Self { + /// Associate the table with a warehouse name (stored verbatim). + pub fn warehouse(mut self, warehouse: Option) -> Self { self.warehouse = warehouse; self } + /// Set the initialization warehouse name (stored verbatim). + pub fn initialization_warehouse(mut self, initialization_warehouse: Option) -> Self { + self.initialization_warehouse = initialization_warehouse; + self + } + /// Set the scheduler value (quoted string or bare keyword). + pub fn scheduler(mut self, scheduler: Option) -> Self { + self.scheduler = scheduler; + self + } + /// Set the `IMMUTABLE WHERE` predicate text. + pub fn immutable_where(mut self, immutable_where: Option) -> Self { + self.immutable_where = immutable_where; + self + } /// Set refresh mode for materialized/managed tables. pub fn refresh_mode(mut self, refresh_mode: Option) -> Self { self.refresh_mode = refresh_mode; @@ -556,6 +610,31 @@ impl CreateTableBuilder { self.backup = backup; self } + /// Set the Snowflake external table `PATTERN` clause. + pub fn pattern(mut self, pattern: Option) -> Self { + self.pattern = pattern; + self + } + /// Set the Snowflake external table `REFRESH_ON_CREATE` clause. + pub fn refresh_on_create(mut self, refresh_on_create: Option) -> Self { + self.refresh_on_create = refresh_on_create; + self + } + /// Set the Snowflake external table `PARTITION_TYPE` clause. + pub fn partition_type(mut self, partition_type: Option) -> Self { + self.partition_type = partition_type; + self + } + /// Set the Snowflake external table `TABLE_FORMAT` clause. + pub fn table_format(mut self, table_format: Option) -> Self { + self.table_format = table_format; + self + } + /// Set the Snowflake external table `AWS_SNS_TOPIC` clause. + pub fn aws_sns_topic(mut self, aws_sns_topic: Option) -> Self { + self.aws_sns_topic = aws_sns_topic; + self + } /// Consume the builder and produce a `CreateTable`. pub fn build(self) -> CreateTable { CreateTable { @@ -596,6 +675,7 @@ impl CreateTableBuilder { copy_grants: self.copy_grants, enable_schema_evolution: self.enable_schema_evolution, change_tracking: self.change_tracking, + stage_file_format: self.stage_file_format, data_retention_time_in_days: self.data_retention_time_in_days, max_data_extension_time_in_days: self.max_data_extension_time_in_days, default_ddl_collation: self.default_ddl_collation, @@ -606,11 +686,16 @@ impl CreateTableBuilder { base_location: self.base_location, external_volume: self.external_volume, catalog: self.catalog, + catalog_table_name: self.catalog_table_name, + auto_refresh: self.auto_refresh, catalog_sync: self.catalog_sync, storage_serialization_policy: self.storage_serialization_policy, table_options: self.table_options, target_lag: self.target_lag, warehouse: self.warehouse, + initialization_warehouse: self.initialization_warehouse, + scheduler: self.scheduler, + immutable_where: self.immutable_where, refresh_mode: self.refresh_mode, initialize: self.initialize, require_user: self.require_user, @@ -618,6 +703,11 @@ impl CreateTableBuilder { distkey: self.distkey, sortkey: self.sortkey, backup: self.backup, + pattern: self.pattern, + refresh_on_create: self.refresh_on_create, + partition_type: self.partition_type, + table_format: self.table_format, + aws_sns_topic: self.aws_sns_topic, } } } @@ -677,6 +767,7 @@ impl From for CreateTableBuilder { copy_grants: table.copy_grants, enable_schema_evolution: table.enable_schema_evolution, change_tracking: table.change_tracking, + stage_file_format: table.stage_file_format, data_retention_time_in_days: table.data_retention_time_in_days, max_data_extension_time_in_days: table.max_data_extension_time_in_days, default_ddl_collation: table.default_ddl_collation, @@ -687,11 +778,16 @@ impl From for CreateTableBuilder { base_location: table.base_location, external_volume: table.external_volume, catalog: table.catalog, + catalog_table_name: table.catalog_table_name, + auto_refresh: table.auto_refresh, catalog_sync: table.catalog_sync, storage_serialization_policy: table.storage_serialization_policy, table_options: table.table_options, target_lag: table.target_lag, warehouse: table.warehouse, + initialization_warehouse: table.initialization_warehouse, + scheduler: table.scheduler, + immutable_where: table.immutable_where, refresh_mode: table.refresh_mode, initialize: table.initialize, require_user: table.require_user, @@ -699,6 +795,11 @@ impl From for CreateTableBuilder { distkey: table.distkey, sortkey: table.sortkey, backup: table.backup, + pattern: table.pattern, + refresh_on_create: table.refresh_on_create, + partition_type: table.partition_type, + table_format: table.table_format, + aws_sns_topic: table.aws_sns_topic, } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dbbc9103fc..ddb008bfe5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -64,18 +64,20 @@ pub use self::dcl::{ pub use self::ddl::{ Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner, AlterFunction, AlterFunctionAction, AlterFunctionKind, AlterFunctionOperation, - AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, - AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, - AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, - AlterTableLock, AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, - AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, - ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, - ColumnPolicyProperty, ConstraintCharacteristics, CreateCollation, CreateCollationDefinition, - CreateConnector, CreateDomain, CreateExtension, CreateFunction, CreateIndex, CreateOperator, - CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, - CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, - DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, - DropOperatorSignature, DropPolicy, DropTrigger, ForValues, FunctionReturnType, GeneratedAs, + AlterIndexOperation, AlterMaskingPolicyOperation, AlterOperator, AlterOperatorClass, + AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, + AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, AlterProcedure, + AlterProcedureOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, + AlterTableLock, AlterTableOperation, AlterTableType, AlterTagOperation, AlterType, + AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, + AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, + ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateCollation, + CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, CreateFunction, + CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, + CreatePolicyCommand, CreatePolicyType, CreateTable, CreateTrigger, CreateView, Deduplicate, + DeferrableInitial, DistStyle, DropBehavior, DropExtension, DropFunction, DropOperator, + DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, + ExternalTablePartitionColumn, ForValues, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes, OperatorClassItem, @@ -1030,6 +1032,23 @@ pub enum Expr { /// Optional escape character. escape_char: Option, }, + /// Snowflake ` [NOT] {LIKE|ILIKE} {ANY|ALL} (, ..., ) [ESCAPE ]` + /// matching a subject against a parenthesized list of patterns. + /// + LikeAnyAll { + /// `true` when `NOT` is present. + negated: bool, + /// `true` for `ILIKE`, `false` for `LIKE`. + ilike: bool, + /// `true` for the `ALL` quantifier, `false` for `ANY`. + all: bool, + /// Subject expression to match. + expr: Box, + /// List of pattern expressions. + patterns: Vec, + /// Optional escape character applied to every pattern. + escape_char: Option, + }, /// `SIMILAR TO` regex SimilarTo { /// `true` when `NOT` is present. @@ -1852,6 +1871,28 @@ impl fmt::Display for Expr { pattern ), }, + Expr::LikeAnyAll { + negated, + ilike, + all, + expr, + patterns, + escape_char, + } => { + write!( + f, + "{} {}{} {} ({})", + expr, + if *negated { "NOT " } else { "" }, + if *ilike { "ILIKE" } else { "LIKE" }, + if *all { "ALL" } else { "ANY" }, + display_comma_separated(patterns), + )?; + if let Some(ch) = escape_char { + write!(f, " ESCAPE {ch}")?; + } + Ok(()) + } Expr::RLike { negated, expr, @@ -2502,6 +2543,32 @@ impl fmt::Display for ShowCreateObject { } } +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// Key-constraint catalog selected by a `SHOW ... KEYS` statement. +pub enum ShowKeysKind { + /// `SHOW [TERSE] PRIMARY KEYS` + Primary, + /// `SHOW IMPORTED KEYS` + Imported, + /// `SHOW EXPORTED KEYS` + Exported, + /// `SHOW [TERSE] UNIQUE KEYS` + Unique, +} + +impl fmt::Display for ShowKeysKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ShowKeysKind::Primary => f.write_str("PRIMARY"), + ShowKeysKind::Imported => f.write_str("IMPORTED"), + ShowKeysKind::Exported => f.write_str("EXPORTED"), + ShowKeysKind::Unique => f.write_str("UNIQUE"), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -3914,16 +3981,23 @@ pub enum Statement { from_obj: Option, /// Optional alias for the source object. from_obj_alias: Option, + /// Optional inline stage table-function args on the FROM stage + /// (`@stage (FILE_FORMAT => …, PATTERN => …)`), as used in a + /// `COPY INTO FROM (SELECT … FROM @stage (…))` load. + from_obj_args: Option, /// Stage-specific parameters (e.g., credentials, path). stage_params: StageParamsObject, /// Optional list of transformations applied when loading. from_transformations: Option>, /// Optional source query instead of a staged object. from_query: Option>, - /// Optional list of specific file names to load. - files: Option>, - /// Optional filename matching pattern. - pattern: Option, + /// Optional list of specific file names to load. Each entry is a + /// string literal or a wire bind placeholder (`Value::Placeholder`), + /// kept distinct so a bound `?` can be told from a literal. + files: Option>, + /// Optional filename matching pattern: a string literal or a wire bind + /// placeholder (`Value::Placeholder`). + pattern: Option, /// File format options. file_format: KeyValueOptions, /// Additional copy options. @@ -3987,6 +4061,21 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createrole.html) CreateRole(CreateRole), /// ```sql + /// CREATE [OR REPLACE] DATABASE ROLE [IF NOT EXISTS] [COMMENT = '...'] + /// ``` + /// Snowflake database role (scoped to a database, distinct from an + /// account-level role). + CreateDatabaseRole { + /// `true` when `OR REPLACE` was specified. + or_replace: bool, + /// `true` when `IF NOT EXISTS` was specified. + if_not_exists: bool, + /// The (optionally database-qualified) role name. + name: ObjectName, + /// Optional `COMMENT = '...'` clause. + comment: Option, + }, + /// ```sql /// CREATE SECRET /// ``` /// See [DuckDB](https://duckdb.org/docs/sql/statements/create_secret.html) @@ -4066,6 +4155,20 @@ pub enum Statement { with_options: Vec, }, /// ```sql + /// ALTER VIEW { MODIFY | ALTER } COLUMN + /// { SET MASKING POLICY

[USING (, ...)] [FORCE] | UNSET MASKING POLICY } + /// ``` + /// Snowflake column-level masking-policy operation on a view. + AlterViewColumn { + /// View name. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + name: ObjectName, + /// Target column. + column_name: Ident, + /// The column operation to apply. + op: AlterColumnOperation, + }, + /// ```sql /// ALTER FUNCTION /// ALTER AGGREGATE /// ``` @@ -4208,6 +4311,18 @@ pub enum Statement { table: Option, }, /// ```sql + /// UNDROP

+ /// ``` + /// Snowflake: restore the most recently dropped object of the given name + /// within the Time Travel retention window. + /// + Undrop { + /// The type of object to restore. + object_type: ObjectType, + /// The name of the object to restore. + name: ObjectName, + }, + /// ```sql /// DROP FUNCTION /// ``` DropFunction(DropFunction), @@ -4325,6 +4440,48 @@ pub enum Statement { /// Optional target table to fetch rows into. into: Option, }, + /// Snowflake scripting `FETCH INTO [, ...]`. + /// + /// Unlike the ISO/PostgreSQL [`Statement::Fetch`], the scripting form has + /// no direction and no `FROM`/`IN`; it binds the current cursor row into + /// one or more local variables. + FetchInto { + /// Cursor name. + cursor: Ident, + /// One or more variable targets. + into: Vec, + }, + /// Snowflake `CALL () INTO [, ...]`. + /// + /// Like [`Statement::Call`] but captures the procedure result into one or + /// more local variables. + CallInto { + /// The procedure call. + function: Function, + /// One or more variable targets. + into: Vec, + }, + /// Snowflake `ALTER PROCEDURE`. + AlterProcedure(AlterProcedure), + /// Snowflake anonymous procedure: + /// `WITH AS PROCEDURE () RETURNS LANGUAGE + /// [EXECUTE AS ...] AS CALL ()`. + WithProcedure { + /// Procedure name introduced by the `WITH` clause. + name: Ident, + /// Optional procedure parameters. + params: Option>, + /// Optional return type. + returns: Option, + /// Optional language identifier. + language: Option, + /// Optional `EXECUTE AS { CALLER | OWNER }` rights clause. + execute_as: Option, + /// Procedure body statements. + body: ConditionalStatements, + /// The trailing `CALL ()` statement. + call: Box, + }, /// ```sql /// FLUSH [NO_WRITE_TO_BINLOG | LOCAL] flush_option [, flush_option] ... | tables_option /// ``` @@ -4359,8 +4516,8 @@ pub enum Statement { /// /// Note: this is a Presto-specific statement. ShowFunctions { - /// Optional filter for which functions to display. - filter: Option, + /// Options controlling the SHOW output (filter, `IN `, etc.). + show_options: ShowStatementOptions, }, /// ```sql /// SHOW @@ -4663,6 +4820,9 @@ pub enum Statement { clone: Option, /// Optional schema comment (Snowflake `COMMENT = '...'`). comment: Option, + /// Snowflake inline `[ WITH ] TAG ( = '' [, ...] )` clause; + /// `None` when absent. + with_tags: Option>, }, /// ```sql /// CREATE DATABASE @@ -4796,6 +4956,8 @@ pub enum Statement { copy_options: KeyValueOptions, /// Optional comment. comment: Option, + /// Trailing `WITH TAG ( = '' [, ...])` clause; empty when absent. + with_tags: Vec, }, /// ```sql /// ALTER STAGE [IF EXISTS] { SET ... | RENAME TO } @@ -4819,6 +4981,35 @@ pub enum Statement { if_not_exists: bool, /// Warehouse name. name: ObjectName, + /// Trailing `WITH TAG ( = '' [, ...])` clause; empty when absent. + with_tags: Vec, + }, + /// ```sql + /// CREATE [OR REPLACE] STREAM [IF NOT EXISTS] ON { TABLE | VIEW } + /// [ { AT | BEFORE } ( => ) ] + /// ``` + CreateStream { + /// `OR REPLACE` flag. + or_replace: bool, + /// `IF NOT EXISTS` flag. + if_not_exists: bool, + /// Stream name. + name: ObjectName, + /// Whether the source is a table (`ON TABLE`) or a view (`ON VIEW`). + source_kind: StreamSourceKind, + /// The source object the stream tracks (the `` after + /// `ON TABLE`/`ON VIEW`). + source_table: ObjectName, + /// The optional `{ AT | BEFORE } ( => )` time-travel clause + /// seeding the new stream's offset from an existing point. Stored as the + /// whole `AT(...)` / `BEFORE(...)` function-call expression, mirroring + /// how [`crate::ast::TableVersion::Function`] keeps a table-version + /// clause. Absent for a plain `ON {TABLE|VIEW} `. + at_before: Option, + /// The optional `APPEND_ONLY = { TRUE | FALSE }` property, following the + /// `{ AT | BEFORE }` clause. `None` when the option is omitted (which + /// Snowflake treats as `FALSE`). + append_only: Option, }, /// ```sql /// ALTER WAREHOUSE [IF EXISTS] [] @@ -4960,6 +5151,99 @@ pub enum Statement { show_options: ShowStatementOptions, }, /// ```sql + /// SHOW [TERSE] STREAMS [LIKE ''] + /// [IN { ACCOUNT | DATABASE | SCHEMA }] + /// ``` + /// See + ShowStreams { + /// `true` when terse output format was requested. + terse: bool, + /// Additional options for `SHOW` statements. + show_options: ShowStatementOptions, + }, + /// ```sql + /// CREATE [OR REPLACE] PIPE [IF NOT EXISTS] + /// [AUTO_INGEST = { TRUE | FALSE }] + /// [ERROR_INTEGRATION = ] + /// [AWS_SNS_TOPIC = ''] + /// [INTEGRATION = ''] + /// [COMMENT = ''] + /// AS + /// ``` + /// See + CreatePipe { + /// `OR REPLACE` flag. + or_replace: bool, + /// `IF NOT EXISTS` flag. + if_not_exists: bool, + /// Pipe name. + name: ObjectName, + /// Optional `AUTO_INGEST = { TRUE | FALSE }` clause. + auto_ingest: Option, + /// Optional `ERROR_INTEGRATION = ` clause. + error_integration: Option, + /// Optional `AWS_SNS_TOPIC = ''` clause. + aws_sns_topic: Option, + /// Optional `INTEGRATION = ''` clause. + integration: Option, + /// Optional `COMMENT = ''` clause. + comment: Option, + /// The `COPY INTO` statement the pipe wraps (after `AS`). + copy_statement: Box, + }, + /// ```sql + /// ALTER PIPE [IF EXISTS] + /// { SET ... | UNSET ... | REFRESH [PREFIX = ''] [MODIFIED_AFTER = ''] } + /// ``` + /// See + AlterPipe { + /// `IF EXISTS` flag. + if_exists: bool, + /// Pipe name. + name: ObjectName, + /// The alter operation. + operation: AlterPipeOperation, + }, + /// ```sql + /// SHOW [TERSE] PIPES [LIKE ''] + /// [IN { ACCOUNT | DATABASE | SCHEMA }] + /// ``` + /// See + ShowPipes { + /// `true` when terse output format was requested. + terse: bool, + /// Additional options for `SHOW` statements. + show_options: ShowStatementOptions, + }, + /// ```sql + /// ALTER STREAM [IF EXISTS] { SET COMMENT = '' | UNSET COMMENT } + /// ``` + /// See + AlterStream { + /// `IF EXISTS` flag. + if_exists: bool, + /// Stream name. + name: ObjectName, + /// The alter action. + operation: AlterStreamOperation, + }, + /// ```sql + /// ALTER SEQUENCE [IF EXISTS] + /// { RENAME TO + /// | [SET] INCREMENT [BY] [=] + /// | SET COMMENT = '' + /// | UNSET COMMENT } + /// ``` + /// See + AlterSequence { + /// `IF EXISTS` flag. + if_exists: bool, + /// Sequence name. + name: ObjectName, + /// The alter action. + operation: AlterSequenceOperation, + }, + /// ```sql /// CREATE [OR REPLACE] EXTERNAL VOLUME [IF NOT EXISTS] /// ``` /// See @@ -5080,6 +5364,226 @@ pub enum Statement { show_options: ShowStatementOptions, }, /// ```sql + /// CREATE [OR REPLACE] TAG [IF NOT EXISTS] + /// [ ALLOWED_VALUES '' [ , '' ... ] ] [ COMMENT = '' ] + /// ``` + /// See + CreateTag { + /// `OR REPLACE` flag. + or_replace: bool, + /// `IF NOT EXISTS` flag. + if_not_exists: bool, + /// Tag name. + name: ObjectName, + /// Optional `ALLOWED_VALUES` list (stored for SHOW fidelity only). + allowed_values: Vec, + /// Optional comment. + comment: Option, + }, + /// ```sql + /// ALTER TAG [IF EXISTS] RENAME TO + /// ALTER TAG [IF EXISTS] SET MASKING POLICY

[, MASKING POLICY

...] [FORCE] + /// ALTER TAG [IF EXISTS] UNSET MASKING POLICY

[, MASKING POLICY

...] + /// ``` + AlterTag { + /// `IF EXISTS` flag. + if_exists: bool, + /// Tag name. + name: ObjectName, + /// The operation to apply to the tag. + operation: AlterTagOperation, + }, + /// ```sql + /// DROP TAG [IF EXISTS] + /// ``` + DropTag { + /// Tag name. + name: ObjectName, + /// `IF EXISTS` flag. + if_exists: bool, + }, + /// ```sql + /// ALTER SET TAG = '' [, ...] + /// ALTER UNSET TAG [, ...] + /// ``` + /// Object-level tag assignment / removal. + SetTags { + /// The domain of the target object (e.g. `DATABASE`). + object_type: ObjectType, + /// The target object name. + object_name: ObjectName, + /// Whether `IF EXISTS` was specified, suppressing the missing-target error. + if_exists: bool, + /// Whether this is `UNSET TAG` (`true`) or `SET TAG` (`false`). + unset: bool, + /// Tags to set (`SET TAG`); empty for `UNSET TAG`. + set_tags: Vec, + /// Tag names to unset (`UNSET TAG`); empty for `SET TAG`. + unset_tags: Vec, + }, + /// ```sql + /// SHOW TAGS [ LIKE '' ] [ IN ... ] + /// ``` + ShowTags { + /// Whether to show terse output. + terse: bool, + /// Options controlling the SHOW output (`LIKE` / `IN` / ...). + show_options: ShowStatementOptions, + }, + /// ```sql + /// SHOW [TERSE] SEQUENCES [ LIKE '' ] [ IN ... ] ... + /// ``` + ShowSequences { + /// Whether to show terse output. + terse: bool, + /// Options controlling the SHOW output (`LIKE` / `IN` / `LIMIT` / ...). + show_options: ShowStatementOptions, + }, + /// ```sql + /// SHOW [TERSE] PRIMARY KEYS [ IN ... ] + /// SHOW IMPORTED KEYS [ IN ... ] + /// SHOW EXPORTED KEYS [ IN ... ] + /// ``` + ShowKeys { + /// Which key-constraint catalog to show. + kind: ShowKeysKind, + /// Whether to show terse output (only meaningful for `PRIMARY KEYS`). + terse: bool, + /// Options controlling the SHOW output (`LIKE` / `IN` / `LIMIT` / ...). + show_options: ShowStatementOptions, + }, + /// ```sql + /// CREATE [OR REPLACE] ROW ACCESS POLICY [IF NOT EXISTS] + /// AS () RETURNS BOOLEAN -> + /// ``` + /// See + CreateRowAccessPolicy { + /// `OR REPLACE` flag. + or_replace: bool, + /// `IF NOT EXISTS` flag. + if_not_exists: bool, + /// Policy name. + name: ObjectName, + /// Signature arguments (name + type). + args: Vec, + /// The declared return type (`BOOLEAN`). + return_type: DataType, + /// The policy body expression after `->`. + policy_expr: Expr, + }, + /// ```sql + /// ALTER ROW ACCESS POLICY [IF EXISTS] RENAME TO + /// ``` + AlterRowAccessPolicy { + /// `IF EXISTS` flag. + if_exists: bool, + /// Policy name. + name: ObjectName, + /// New policy name. + new_name: ObjectName, + }, + /// ```sql + /// DROP ROW ACCESS POLICY [IF EXISTS] + /// ``` + DropRowAccessPolicy { + /// `IF EXISTS` flag. + if_exists: bool, + /// Policy name. + name: ObjectName, + }, + /// ```sql + /// DESC[RIBE] ROW ACCESS POLICY + /// ``` + DescribeRowAccessPolicy { + /// Policy name. + name: ObjectName, + }, + /// ```sql + /// SHOW ROW ACCESS POLICIES [ LIKE '' ] + /// ``` + ShowRowAccessPolicies { + /// Optional `LIKE` filter. + filter: Option, + }, + /// ```sql + /// CREATE [OR REPLACE] MASKING POLICY [IF NOT EXISTS] + /// AS ( [, ...]) RETURNS -> [COMMENT = ''] + /// ``` + /// See + CreateMaskingPolicy { + /// `OR REPLACE` flag. + or_replace: bool, + /// `IF NOT EXISTS` flag. + if_not_exists: bool, + /// Policy name. + name: ObjectName, + /// Signature arguments (name + type), in declaration order. + args: Vec, + /// The declared return type. + return_type: DataType, + /// The policy body expression after `->`. + policy_expr: Expr, + /// Optional `COMMENT = ''`. + comment: Option, + }, + /// ```sql + /// ALTER MASKING POLICY [IF EXISTS] + /// { SET BODY -> | RENAME TO | SET COMMENT = '' | UNSET COMMENT } + /// ``` + /// See + AlterMaskingPolicy { + /// `IF EXISTS` flag. + if_exists: bool, + /// Policy name. + name: ObjectName, + /// The operation to apply. + operation: AlterMaskingPolicyOperation, + }, + /// ```sql + /// DROP MASKING POLICY [IF EXISTS] + /// ``` + DropMaskingPolicy { + /// `IF EXISTS` flag. + if_exists: bool, + /// Policy name. + name: ObjectName, + }, + /// ```sql + /// DESC[RIBE] MASKING POLICY + /// ``` + DescribeMaskingPolicy { + /// Policy name. + name: ObjectName, + }, + /// ```sql + /// SHOW MASKING POLICIES [ LIKE '' ] [ IN ] + /// ``` + ShowMaskingPolicies { + /// Options controlling the SHOW output (filter, `IN `, etc.). + show_options: ShowStatementOptions, + }, + /// ```sql + /// SHOW PROCEDURES [ LIKE '' ] [ IN ] + /// ``` + ShowProcedures { + /// Options controlling the SHOW output (filter, `IN `, etc.). + show_options: ShowStatementOptions, + }, + /// ```sql + /// SHOW CONNECTIONS [ LIKE '' ] + /// ``` + ShowConnections { + /// Optional `LIKE` filter. + filter: Option, + }, + /// ```sql + /// SHOW SHARES [ LIKE '' ] + /// ``` + ShowShares { + /// Optional `LIKE` filter. + filter: Option, + }, + /// ```sql /// CREATE [OR REPLACE] CATALOG INTEGRATION [IF NOT EXISTS] ... /// ``` /// See @@ -5124,22 +5628,70 @@ pub enum Statement { filter: Option, }, /// ```sql - /// ASSERT [AS ] + /// CREATE [OR REPLACE] STORAGE INTEGRATION [IF NOT EXISTS] ... /// ``` - Assert { - /// Assertion condition expression. - condition: Expr, - /// Optional message expression. - message: Option, + /// See + CreateStorageIntegration { + /// `OR REPLACE` flag. + or_replace: bool, + /// `IF NOT EXISTS` flag. + if_not_exists: bool, + /// Storage integration name. + name: ObjectName, + /// Configuration parameters (`TYPE`, `ENABLED`, `STORAGE_PROVIDER`, …). + params: KeyValueOptions, }, /// ```sql - /// GRANT privileges ON objects TO grantees + /// ALTER STORAGE INTEGRATION [IF EXISTS] SET ... /// ``` - Grant(Grant), + AlterStorageIntegration { + /// Storage integration name. + name: ObjectName, + /// `IF EXISTS` flag. + if_exists: bool, + /// The `SET` options. + set_options: KeyValueOptions, + }, /// ```sql - /// DENY privileges ON object TO grantees + /// DROP STORAGE INTEGRATION [IF EXISTS] /// ``` - Deny(DenyStatement), + DropStorageIntegration { + /// Storage integration name. + name: ObjectName, + /// `IF EXISTS` flag. + if_exists: bool, + }, + /// ```sql + /// DESC[RIBE] STORAGE INTEGRATION + /// ``` + DescribeStorageIntegration { + /// Storage integration name. + name: ObjectName, + }, + /// ```sql + /// SHOW STORAGE INTEGRATIONS [LIKE ''] + /// ``` + ShowStorageIntegrations { + /// Optional filter (e.g. `LIKE`). + filter: Option, + }, + /// ```sql + /// ASSERT [AS ] + /// ``` + Assert { + /// Assertion condition expression. + condition: Expr, + /// Optional message expression. + message: Option, + }, + /// ```sql + /// GRANT privileges ON objects TO grantees + /// ``` + Grant(Grant), + /// ```sql + /// DENY privileges ON object TO grantees + /// ``` + Deny(DenyStatement), /// ```sql /// REVOKE privileges ON objects FROM grantees /// ``` @@ -5239,6 +5791,8 @@ pub enum Statement { /// The object name (may be qualified: db.schema.table) #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] object_name: ObjectName, + /// Optional `TYPE = { COLUMNS | STAGE }` modifier (Snowflake `DESC TABLE`). + table_type: Option, }, /// ```sql /// DESC | DESCRIBE RESULT @@ -5339,6 +5893,8 @@ pub enum Statement { CreateSequence { /// Whether the sequence is temporary. temporary: bool, + /// `OR REPLACE` flag. + or_replace: bool, /// `IF NOT EXISTS` flag. if_not_exists: bool, /// Sequence name. @@ -5626,6 +6182,23 @@ pub enum Statement { /// branches. /// [Snowflake](https://docs.snowflake.com/en/developer-guide/snowflake-scripting/snowflake-scripting) Null, + /// A `PUT` / `GET` file-transfer statement appearing inside a Snowflake + /// Scripting block (procedure / anonymous-block body). + /// + /// ```sql + /// PUT file:///tmp/x.csv @my_stage AUTO_COMPRESS = FALSE; + /// GET @my_stage file:///tmp/dir; + /// ``` + /// + /// Snowflake recognises these inside a body only so the block parses; it + /// rejects executing them ("Unsupported statement type 'PUT_FILES'"). The + /// operands are not modelled — an unquoted `file://` path triggers the `//` + /// single-line-comment lexer rule, so the tail is not tokenizable — only + /// the `get` discriminant is kept. `get = true` for `GET`. + PutGetFiles { + /// `true` for `GET`, `false` for `PUT`. + get: bool, + }, } impl From for Statement { @@ -5789,8 +6362,13 @@ impl fmt::Display for Statement { describe_alias, object_type, object_name, + table_type, } => { - write!(f, "{describe_alias} {object_type} {object_name}") + write!(f, "{describe_alias} {object_type} {object_name}")?; + if let Some(table_type) = table_type { + write!(f, " TYPE = {table_type}")?; + } + Ok(()) } Statement::DescribeResult { describe_alias, @@ -5853,6 +6431,37 @@ impl fmt::Display for Statement { Ok(()) } + Statement::FetchInto { cursor, into } => { + write!(f, "FETCH {cursor} INTO {}", display_comma_separated(into)) + } + Statement::CallInto { function, into } => { + write!(f, "CALL {function} INTO {}", display_comma_separated(into)) + } + Statement::AlterProcedure(alter_procedure) => write!(f, "{alter_procedure}"), + Statement::WithProcedure { + name, + params, + returns, + language, + execute_as, + body, + call, + } => { + write!(f, "WITH {name} AS PROCEDURE")?; + if let Some(p) = params { + write!(f, " ({})", display_comma_separated(p))?; + } + if let Some(ret) = returns { + write!(f, " RETURNS {ret}")?; + } + if let Some(language) = language { + write!(f, " LANGUAGE {language}")?; + } + if let Some(execute_as) = execute_as { + write!(f, " EXECUTE AS {execute_as}")?; + } + write!(f, " AS {body} {call}") + } Statement::Directory { overwrite, local, @@ -6237,6 +6846,27 @@ impl fmt::Display for Statement { write!(f, "{drop_operator_class}") } Statement::CreateRole(create_role) => write!(f, "{create_role}"), + Statement::CreateDatabaseRole { + or_replace, + if_not_exists, + name, + comment, + } => { + write!( + f, + "CREATE {or_replace}DATABASE ROLE {if_not_exists}{name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if let Some(comment) = comment { + write!( + f, + " COMMENT = '{}'", + value::escape_single_quote_string(comment) + )?; + } + Ok(()) + } Statement::CreateSecret { or_replace, temporary, @@ -6301,6 +6931,13 @@ impl fmt::Display for Statement { } write!(f, " AS {query}") } + Statement::AlterViewColumn { + name, + column_name, + op, + } => { + write!(f, "ALTER VIEW {name} ALTER COLUMN {column_name} {op}") + } Statement::AlterFunction(alter_function) => write!(f, "{alter_function}"), Statement::AlterType(AlterType { name, operation }) => { write!(f, "ALTER TYPE {name} {operation}") @@ -6388,6 +7025,9 @@ impl fmt::Display for Statement { }; Ok(()) } + Statement::Undrop { object_type, name } => { + write!(f, "UNDROP {object_type} {name}") + } Statement::DropFunction(drop_function) => write!(f, "{drop_function}"), Statement::DropDomain(DropDomain { if_exists, @@ -6570,12 +7210,18 @@ impl fmt::Display for Statement { } Statement::ShowObjects(ShowObjects { terse, + dynamic, show_options, }) => { write!( f, - "SHOW {terse}OBJECTS{show_options}", + "SHOW {terse}{kind}{show_options}", terse = if *terse { "TERSE " } else { "" }, + kind = if *dynamic { + "DYNAMIC TABLES" + } else { + "OBJECTS" + }, )?; Ok(()) } @@ -6611,11 +7257,8 @@ impl fmt::Display for Statement { )?; Ok(()) } - Statement::ShowFunctions { filter } => { - write!(f, "SHOW FUNCTIONS")?; - if let Some(filter) = filter { - write!(f, " {filter}")?; - } + Statement::ShowFunctions { show_options } => { + write!(f, "SHOW FUNCTIONS{show_options}")?; Ok(()) } Statement::Use(use_expr) => use_expr.fmt(f), @@ -6712,6 +7355,7 @@ impl fmt::Display for Statement { default_collate_spec, clone, comment, + with_tags, } => { write!( f, @@ -6742,6 +7386,10 @@ impl fmt::Display for Statement { write!(f, " CLONE {clone}")?; } + if let Some(tags) = with_tags { + write!(f, " WITH TAG ({})", display_comma_separated(tags))?; + } + if let Some(comment) = comment { match comment { CommentDef::WithEq(c) => write!(f, " COMMENT = '{c}'")?, @@ -6876,6 +7524,7 @@ impl fmt::Display for Statement { } Statement::CreateSequence { temporary, + or_replace, if_not_exists, name, data_type, @@ -6891,7 +7540,8 @@ impl fmt::Display for Statement { }; write!( f, - "CREATE {temporary}SEQUENCE {if_not_exists}{name}{as_type}", + "CREATE {or_replace}{temporary}SEQUENCE {if_not_exists}{name}{as_type}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, temporary = if *temporary { "TEMPORARY " } else { "" }, name = name, @@ -6915,6 +7565,7 @@ impl fmt::Display for Statement { file_format, copy_options, comment, + with_tags, .. } => { write!( @@ -6936,6 +7587,9 @@ impl fmt::Display for Statement { if comment.is_some() { write!(f, " COMMENT='{}'", comment.as_ref().unwrap())?; } + if !with_tags.is_empty() { + write!(f, " WITH TAG ({})", display_comma_separated(with_tags))?; + } Ok(()) } Statement::AlterStage { @@ -6953,13 +7607,45 @@ impl fmt::Display for Statement { or_replace, if_not_exists, name, + with_tags, } => { write!( f, "CREATE {or_replace}WAREHOUSE {if_not_exists}{name}", or_replace = if *or_replace { "OR REPLACE " } else { "" }, if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, - ) + )?; + if !with_tags.is_empty() { + write!(f, " WITH TAG ({})", display_comma_separated(with_tags))?; + } + Ok(()) + } + Statement::CreateStream { + or_replace, + if_not_exists, + name, + source_kind, + source_table, + at_before, + append_only, + } => { + write!( + f, + "CREATE {or_replace}STREAM {if_not_exists}{name} ON {source_kind} {source_table}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if let Some(at_before) = at_before { + write!(f, " {at_before}")?; + } + if let Some(append_only) = append_only { + write!( + f, + " APPEND_ONLY = {}", + if *append_only { "TRUE" } else { "FALSE" } + )?; + } + Ok(()) } Statement::AlterWarehouse { name, @@ -7089,6 +7775,99 @@ impl fmt::Display for Statement { )?; Ok(()) } + Statement::ShowStreams { + terse, + show_options, + } => { + write!( + f, + "SHOW {terse}STREAMS{show_options}", + terse = if *terse { "TERSE " } else { "" }, + )?; + Ok(()) + } + Statement::AlterStream { + if_exists, + name, + operation, + } => { + write!(f, "ALTER STREAM")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {name} {operation}") + } + Statement::CreatePipe { + or_replace, + if_not_exists, + name, + auto_ingest, + error_integration, + aws_sns_topic, + integration, + comment, + copy_statement, + } => { + write!( + f, + "CREATE {or_replace}PIPE {if_not_exists}{name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if let Some(auto_ingest) = auto_ingest { + write!( + f, + " AUTO_INGEST = {}", + if *auto_ingest { "TRUE" } else { "FALSE" } + )?; + } + if let Some(error_integration) = error_integration { + write!(f, " ERROR_INTEGRATION = {error_integration}")?; + } + if let Some(aws_sns_topic) = aws_sns_topic { + write!(f, " AWS_SNS_TOPIC = '{aws_sns_topic}'")?; + } + if let Some(integration) = integration { + write!(f, " INTEGRATION = '{integration}'")?; + } + if let Some(comment) = comment { + write!(f, " COMMENT = '{comment}'")?; + } + write!(f, " AS {copy_statement}") + } + Statement::AlterPipe { + if_exists, + name, + operation, + } => { + write!(f, "ALTER PIPE")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {name} {operation}") + } + Statement::ShowPipes { + terse, + show_options, + } => { + write!( + f, + "SHOW {terse}PIPES{show_options}", + terse = if *terse { "TERSE " } else { "" }, + )?; + Ok(()) + } + Statement::AlterSequence { + if_exists, + name, + operation, + } => { + write!(f, "ALTER SEQUENCE")?; + if *if_exists { + write!(f, " IF EXISTS")?; + } + write!(f, " {name} {operation}") + } Statement::CreateExternalVolume { or_replace, if_not_exists, @@ -7118,106 +7897,312 @@ impl fmt::Display for Statement { } Ok(()) } - Statement::AlterExternalVolume { - name, + Statement::AlterExternalVolume { + name, + if_exists, + operation, + } => { + write!( + f, + "ALTER EXTERNAL VOLUME {if_exists}{name} {operation}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::DropExternalVolume { name, if_exists } => { + write!( + f, + "DROP EXTERNAL VOLUME {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::DescribeExternalVolume { name } => { + write!(f, "DESCRIBE EXTERNAL VOLUME {name}") + } + Statement::ShowExternalVolumes { filter } => { + write!(f, "SHOW EXTERNAL VOLUMES")?; + if let Some(ref filter) = filter { + write!(f, " {filter}")?; + } + Ok(()) + } + Statement::CreateFileFormat { + or_replace, + temporary, + if_not_exists, + name, + format_type, + options, + like_source, + comment, + } => { + write!( + f, + "CREATE {or_replace}{temporary}FILE FORMAT {if_not_exists}{name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + temporary = if *temporary { "TEMPORARY " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if let Some(ref source) = like_source { + write!(f, " LIKE {source}")?; + } else { + if let Some(ref ft) = format_type { + write!(f, " TYPE = {ft}")?; + } + if !options.options.is_empty() { + write!(f, " {options}")?; + } + } + if let Some(ref c) = comment { + write!(f, " COMMENT = '{}'", value::escape_single_quote_string(c))?; + } + Ok(()) + } + Statement::AlterFileFormat { + name, + if_exists, + operation, + } => { + write!( + f, + "ALTER FILE FORMAT {if_exists}{name} {operation}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::DropFileFormat { name, if_exists } => { + write!( + f, + "DROP FILE FORMAT {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::DescribeFileFormat { name } => { + write!(f, "DESCRIBE FILE FORMAT {name}") + } + Statement::ShowFileFormats { + terse, + show_options, + } => { + write!( + f, + "SHOW {terse}FILE FORMATS{show_options}", + terse = if *terse { "TERSE " } else { "" }, + ) + } + Statement::ShowStages { + terse, + show_options, + } => { + write!( + f, + "SHOW {terse}STAGES{show_options}", + terse = if *terse { "TERSE " } else { "" }, + ) + } + Statement::CreateTag { + or_replace, + if_not_exists, + name, + allowed_values, + comment, + } => { + write!( + f, + "CREATE {or_replace}TAG {if_not_exists}{name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if !allowed_values.is_empty() { + write!(f, " ALLOWED_VALUES ")?; + for (i, v) in allowed_values.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "'{}'", value::escape_single_quote_string(v))?; + } + } + if let Some(ref c) = comment { + write!(f, " COMMENT = '{}'", value::escape_single_quote_string(c))?; + } + Ok(()) + } + Statement::AlterTag { + if_exists, + name, + operation, + } => { + write!( + f, + "ALTER TAG {if_exists}{name} {operation}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::DropTag { name, if_exists } => { + write!( + f, + "DROP TAG {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::SetTags { + object_type, + object_name, + if_exists, + unset, + set_tags, + unset_tags, + } => { + write!(f, "ALTER {object_type} ")?; + if *if_exists { + write!(f, "IF EXISTS ")?; + } + write!(f, "{object_name} ")?; + if *unset { + write!(f, "UNSET TAG {}", display_comma_separated(unset_tags)) + } else { + write!(f, "SET TAG {}", display_comma_separated(set_tags)) + } + } + Statement::ShowTags { + terse, + show_options, + } => { + write!( + f, + "SHOW {terse}TAGS{show_options}", + terse = if *terse { "TERSE " } else { "" }, + ) + } + Statement::ShowSequences { + terse, + show_options, + } => { + write!( + f, + "SHOW {terse}SEQUENCES{show_options}", + terse = if *terse { "TERSE " } else { "" }, + ) + } + Statement::ShowKeys { + kind, + terse, + show_options, + } => { + write!( + f, + "SHOW {terse}{kind} KEYS{show_options}", + terse = if *terse { "TERSE " } else { "" }, + ) + } + Statement::CreateRowAccessPolicy { + or_replace, + if_not_exists, + name, + args, + return_type, + policy_expr, + } => { + write!( + f, + "CREATE {or_replace}ROW ACCESS POLICY {if_not_exists}{name} AS ({args}) RETURNS {return_type} -> {policy_expr}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + args = display_comma_separated(args), + ) + } + Statement::AlterRowAccessPolicy { if_exists, - operation, + name, + new_name, } => { write!( f, - "ALTER EXTERNAL VOLUME {if_exists}{name} {operation}", + "ALTER ROW ACCESS POLICY {if_exists}{name} RENAME TO {new_name}", if_exists = if *if_exists { "IF EXISTS " } else { "" }, ) } - Statement::DropExternalVolume { name, if_exists } => { + Statement::DropRowAccessPolicy { if_exists, name } => { write!( f, - "DROP EXTERNAL VOLUME {if_exists}{name}", + "DROP ROW ACCESS POLICY {if_exists}{name}", if_exists = if *if_exists { "IF EXISTS " } else { "" }, ) } - Statement::DescribeExternalVolume { name } => { - write!(f, "DESCRIBE EXTERNAL VOLUME {name}") + Statement::DescribeRowAccessPolicy { name } => { + write!(f, "DESCRIBE ROW ACCESS POLICY {name}") } - Statement::ShowExternalVolumes { filter } => { - write!(f, "SHOW EXTERNAL VOLUMES")?; - if let Some(ref filter) = filter { + Statement::ShowRowAccessPolicies { filter } => { + write!(f, "SHOW ROW ACCESS POLICIES")?; + if let Some(filter) = filter { write!(f, " {filter}")?; } Ok(()) } - Statement::CreateFileFormat { + Statement::CreateMaskingPolicy { or_replace, - temporary, if_not_exists, name, - format_type, - options, - like_source, + args, + return_type, + policy_expr, comment, } => { write!( f, - "CREATE {or_replace}{temporary}FILE FORMAT {if_not_exists}{name}", + "CREATE {or_replace}MASKING POLICY {if_not_exists}{name} AS ({args}) RETURNS {return_type} -> {policy_expr}", or_replace = if *or_replace { "OR REPLACE " } else { "" }, - temporary = if *temporary { "TEMPORARY " } else { "" }, if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + args = display_comma_separated(args), )?; - if let Some(ref source) = like_source { - write!(f, " LIKE {source}")?; - } else { - if let Some(ref ft) = format_type { - write!(f, " TYPE = {ft}")?; - } - if !options.options.is_empty() { - write!(f, " {options}")?; - } - } - if let Some(ref c) = comment { - write!(f, " COMMENT = '{}'", value::escape_single_quote_string(c))?; + if let Some(comment) = comment { + write!( + f, + " COMMENT = '{}'", + value::escape_single_quote_string(comment) + )?; } Ok(()) } - Statement::AlterFileFormat { - name, + Statement::AlterMaskingPolicy { if_exists, + name, operation, } => { write!( f, - "ALTER FILE FORMAT {if_exists}{name} {operation}", + "ALTER MASKING POLICY {if_exists}{name} {operation}", if_exists = if *if_exists { "IF EXISTS " } else { "" }, ) } - Statement::DropFileFormat { name, if_exists } => { + Statement::DropMaskingPolicy { if_exists, name } => { write!( f, - "DROP FILE FORMAT {if_exists}{name}", + "DROP MASKING POLICY {if_exists}{name}", if_exists = if *if_exists { "IF EXISTS " } else { "" }, ) } - Statement::DescribeFileFormat { name } => { - write!(f, "DESCRIBE FILE FORMAT {name}") + Statement::DescribeMaskingPolicy { name } => { + write!(f, "DESCRIBE MASKING POLICY {name}") } - Statement::ShowFileFormats { - terse, - show_options, - } => { - write!( - f, - "SHOW {terse}FILE FORMATS{show_options}", - terse = if *terse { "TERSE " } else { "" }, - ) + Statement::ShowMaskingPolicies { show_options } => { + write!(f, "SHOW MASKING POLICIES{show_options}") } - Statement::ShowStages { - terse, - show_options, - } => { - write!( - f, - "SHOW {terse}STAGES{show_options}", - terse = if *terse { "TERSE " } else { "" }, - ) + Statement::ShowProcedures { show_options } => { + write!(f, "SHOW PROCEDURES{show_options}")?; + Ok(()) + } + Statement::ShowConnections { filter } => { + write!(f, "SHOW CONNECTIONS")?; + if let Some(filter) = filter { + write!(f, " {filter}")?; + } + Ok(()) + } + Statement::ShowShares { filter } => { + write!(f, "SHOW SHARES")?; + if let Some(filter) = filter { + write!(f, " {filter}")?; + } + Ok(()) } Statement::CreateCatalogIntegration { or_replace, @@ -7275,12 +8260,62 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateStorageIntegration { + or_replace, + if_not_exists, + name, + params, + } => { + write!( + f, + "CREATE {or_replace}STORAGE INTEGRATION {if_not_exists}{name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if !params.options.is_empty() { + write!(f, " {params}")?; + } + Ok(()) + } + Statement::AlterStorageIntegration { + name, + if_exists, + set_options, + } => { + write!( + f, + "ALTER STORAGE INTEGRATION {if_exists}{name} SET", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + )?; + if !set_options.options.is_empty() { + write!(f, " {set_options}")?; + } + Ok(()) + } + Statement::DropStorageIntegration { name, if_exists } => { + write!( + f, + "DROP STORAGE INTEGRATION {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::DescribeStorageIntegration { name } => { + write!(f, "DESCRIBE STORAGE INTEGRATION {name}") + } + Statement::ShowStorageIntegrations { filter } => { + write!(f, "SHOW STORAGE INTEGRATIONS")?; + if let Some(ref filter) = filter { + write!(f, " {filter}")?; + } + Ok(()) + } Statement::CopyIntoSnowflake { kind, into, into_columns, from_obj, from_obj_alias, + from_obj_args, stage_params, from_transformations, from_query, @@ -7305,6 +8340,9 @@ impl fmt::Display for Statement { from_stage, stage_params )?; + if let Some(args) = from_obj_args { + write!(f, " ({})", display_comma_separated(&args.args))?; + } } if let Some(from_obj_alias) = from_obj_alias { write!(f, " AS {from_obj_alias}")?; @@ -7313,6 +8351,9 @@ impl fmt::Display for Statement { } else if let Some(from_obj) = from_obj { // Standard data load write!(f, " FROM {from_obj}{stage_params}")?; + if let Some(args) = from_obj_args { + write!(f, " ({})", display_comma_separated(&args.args))?; + } if let Some(from_obj_alias) = from_obj_alias { write!(f, " AS {from_obj_alias}")?; } @@ -7322,10 +8363,10 @@ impl fmt::Display for Statement { } if let Some(files) = files { - write!(f, " FILES = ('{}')", display_separated(files, "', '"))?; + write!(f, " FILES = ({})", display_separated(files, ", "))?; } if let Some(pattern) = pattern { - write!(f, " PATTERN = '{pattern}'")?; + write!(f, " PATTERN = {pattern}")?; } if let Some(partition) = partition { write!(f, " PARTITION BY {partition}")?; @@ -7497,6 +8538,9 @@ impl fmt::Display for Statement { write!(f, " := {value}") } Statement::Null => write!(f, "NULL"), + Statement::PutGetFiles { get } => { + write!(f, "{}", if *get { "GET" } else { "PUT" }) + } } } } @@ -7523,6 +8567,8 @@ pub enum SequenceOptions { Cache(Expr), /// `CYCLE` or `NO CYCLE` option. Cycle(bool), + /// `ORDER` or `NOORDER` option (Snowflake); `true` = `ORDER`, `false` = `NOORDER`. + Order(bool), } impl fmt::Display for SequenceOptions { @@ -7562,6 +8608,9 @@ impl fmt::Display for SequenceOptions { SequenceOptions::Cycle(no) => { write!(f, " {}CYCLE", if *no { "NO " } else { "" }) } + SequenceOptions::Order(order) => { + write!(f, " {}", if *order { "ORDER" } else { "NOORDER" }) + } } } } @@ -8167,6 +9216,8 @@ pub enum Action { /// Read access. Read, + /// Write access. + Write, /// Read session-level access. ReadSession, /// References with optional column list. @@ -8260,6 +9311,7 @@ impl fmt::Display for Action { Action::Ownership => f.write_str("OWNERSHIP")?, Action::PurchaseDataExchangeListing => f.write_str("PURCHASE DATA EXCHANGE LISTING")?, Action::Read => f.write_str("READ")?, + Action::Write => f.write_str("WRITE")?, Action::ReadSession => f.write_str("READ SESSION")?, Action::References { .. } => f.write_str("REFERENCES")?, Action::Replicate => f.write_str("REPLICATE")?, @@ -8303,8 +9355,13 @@ pub enum ActionCreateObjectType { ComputePool, /// A data exchange listing. DataExchangeListing, + /// A class object identified by a qualified name, e.g. + /// `CREATE SNOWFLAKE.ML.ANOMALY_DETECTION`. + Class(ObjectName), /// A database object. Database, + /// A database role object. + DatabaseRole, /// An external volume object. ExternalVolume, /// A failover group object. @@ -8323,6 +9380,8 @@ pub enum ActionCreateObjectType { Schema, /// A share object. Share, + /// A table object. + Table, /// A user object. User, /// A warehouse object. @@ -8337,7 +9396,9 @@ impl fmt::Display for ActionCreateObjectType { ActionCreateObjectType::ApplicationPackage => write!(f, "APPLICATION PACKAGE"), ActionCreateObjectType::ComputePool => write!(f, "COMPUTE POOL"), ActionCreateObjectType::DataExchangeListing => write!(f, "DATA EXCHANGE LISTING"), + ActionCreateObjectType::Class(name) => write!(f, "{name}"), ActionCreateObjectType::Database => write!(f, "DATABASE"), + ActionCreateObjectType::DatabaseRole => write!(f, "DATABASE ROLE"), ActionCreateObjectType::ExternalVolume => write!(f, "EXTERNAL VOLUME"), ActionCreateObjectType::FailoverGroup => write!(f, "FAILOVER GROUP"), ActionCreateObjectType::Integration => write!(f, "INTEGRATION"), @@ -8347,6 +9408,7 @@ impl fmt::Display for ActionCreateObjectType { ActionCreateObjectType::Role => write!(f, "ROLE"), ActionCreateObjectType::Schema => write!(f, "SCHEMA"), ActionCreateObjectType::Share => write!(f, "SHARE"), + ActionCreateObjectType::Table => write!(f, "TABLE"), ActionCreateObjectType::User => write!(f, "USER"), ActionCreateObjectType::Warehouse => write!(f, "WAREHOUSE"), } @@ -8679,6 +9741,46 @@ pub enum GrantObjects { /// The target schema names. schemas: Vec, }, + /// Grant privileges on `ALL SCHEMAS IN DATABASE [, ...]` + AllSchemasInDatabase { + /// The target database names. + databases: Vec, + }, + /// Grant privileges on `ALL TABLES IN DATABASE [, ...]` + AllTablesInDatabase { + /// The target database names. + databases: Vec, + }, + /// Grant privileges on `ALL STAGES IN SCHEMA [, ...]` + AllStagesInSchema { + /// The target schema names. + schemas: Vec, + }, + /// Grant privileges on `ALL FILE FORMATS IN SCHEMA [, ...]` + AllFileFormatsInSchema { + /// The target schema names. + schemas: Vec, + }, + /// Grant privileges on `FUTURE TABLES IN DATABASE [, ...]` + FutureTablesInDatabase { + /// The target database names. + databases: Vec, + }, + /// Grant privileges on `FUTURE STAGES IN SCHEMA [, ...]` + FutureStagesInSchema { + /// The target schema names. + schemas: Vec, + }, + /// Grant privileges on `FUTURE FILE FORMATS IN SCHEMA [, ...]` + FutureFileFormatsInSchema { + /// The target schema names. + schemas: Vec, + }, + /// Grant privileges on `FUTURE FUNCTIONS IN SCHEMA [, ...]` + FutureFunctionsInSchema { + /// The target schema names. + schemas: Vec, + }, /// Grant privileges on specific databases Databases(Vec), /// Grant privileges on specific schemas @@ -8697,6 +9799,10 @@ pub enum GrantObjects { ResourceMonitors(Vec), /// Grant privileges on users Users(Vec), + /// Grant privileges on specific stages + Stages(Vec), + /// Grant privileges on specific file formats + FileFormats(Vec), /// Grant privileges on compute pools ComputePools(Vec), /// Grant privileges on connections @@ -8840,12 +9946,74 @@ impl fmt::Display for GrantObjects { display_comma_separated(schemas) ) } + GrantObjects::AllSchemasInDatabase { databases } => { + write!( + f, + "ALL SCHEMAS IN DATABASE {}", + display_comma_separated(databases) + ) + } + GrantObjects::AllTablesInDatabase { databases } => { + write!( + f, + "ALL TABLES IN DATABASE {}", + display_comma_separated(databases) + ) + } + GrantObjects::AllStagesInSchema { schemas } => { + write!( + f, + "ALL STAGES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllFileFormatsInSchema { schemas } => { + write!( + f, + "ALL FILE FORMATS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureTablesInDatabase { databases } => { + write!( + f, + "FUTURE TABLES IN DATABASE {}", + display_comma_separated(databases) + ) + } + GrantObjects::FutureStagesInSchema { schemas } => { + write!( + f, + "FUTURE STAGES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureFileFormatsInSchema { schemas } => { + write!( + f, + "FUTURE FILE FORMATS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::FutureFunctionsInSchema { schemas } => { + write!( + f, + "FUTURE FUNCTIONS IN SCHEMA {}", + display_comma_separated(schemas) + ) + } GrantObjects::ResourceMonitors(objects) => { write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects)) } GrantObjects::Users(objects) => { write!(f, "USER {}", display_comma_separated(objects)) } + GrantObjects::Stages(objects) => { + write!(f, "STAGE {}", display_comma_separated(objects)) + } + GrantObjects::FileFormats(objects) => { + write!(f, "FILE FORMAT {}", display_comma_separated(objects)) + } GrantObjects::ComputePools(objects) => { write!(f, "COMPUTE POOL {}", display_comma_separated(objects)) } @@ -9578,6 +10746,12 @@ pub enum ObjectType { View, /// A materialized view. MaterializedView, + /// A dynamic table (Snowflake). + /// + DynamicTable, + /// An external table (Snowflake). + /// + ExternalTable, /// An index. Index, /// A schema. @@ -9586,6 +10760,8 @@ pub enum ObjectType { Database, /// A role. Role, + /// A database role (Snowflake). + DatabaseRole, /// A sequence. Sequence, /// A stage. @@ -9600,6 +10776,9 @@ pub enum ObjectType { Warehouse, /// A task. Task, + /// A pipe (Snowflake). + /// + Pipe, } impl fmt::Display for ObjectType { @@ -9609,10 +10788,13 @@ impl fmt::Display for ObjectType { ObjectType::Table => "TABLE", ObjectType::View => "VIEW", ObjectType::MaterializedView => "MATERIALIZED VIEW", + ObjectType::DynamicTable => "DYNAMIC TABLE", + ObjectType::ExternalTable => "EXTERNAL TABLE", ObjectType::Index => "INDEX", ObjectType::Schema => "SCHEMA", ObjectType::Database => "DATABASE", ObjectType::Role => "ROLE", + ObjectType::DatabaseRole => "DATABASE ROLE", ObjectType::Sequence => "SEQUENCE", ObjectType::Stage => "STAGE", ObjectType::Type => "TYPE", @@ -9620,6 +10802,29 @@ impl fmt::Display for ObjectType { ObjectType::Stream => "STREAM", ObjectType::Warehouse => "WAREHOUSE", ObjectType::Task => "TASK", + ObjectType::Pipe => "PIPE", + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// The kind of object a Snowflake stream tracks, i.e. whether it was created +/// with `ON TABLE ` or `ON VIEW `. +/// +pub enum StreamSourceKind { + /// `ON TABLE `. + Table, + /// `ON VIEW `. + View, +} + +impl fmt::Display for StreamSourceKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + StreamSourceKind::Table => "TABLE", + StreamSourceKind::View => "VIEW", }) } } @@ -9779,8 +10984,17 @@ impl fmt::Display for HiveDescribeFormat { pub enum DescribeObjectType { /// `TABLE` Table, + /// `DYNAMIC TABLE` (Snowflake) + /// + DynamicTable, + /// `EXTERNAL TABLE` (Snowflake) + /// + ExternalTable, /// `VIEW` View, + /// `MATERIALIZED VIEW` (Snowflake) + /// + MaterializedView, /// `DATABASE` Database, /// `SCHEMA` @@ -9789,17 +11003,50 @@ pub enum DescribeObjectType { Task, /// `STAGE` Stage, + /// `STREAM` + Stream, + /// `SEQUENCE` + Sequence, + /// `PIPE` + Pipe, } impl fmt::Display for DescribeObjectType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { DescribeObjectType::Table => "TABLE", + DescribeObjectType::DynamicTable => "DYNAMIC TABLE", + DescribeObjectType::ExternalTable => "EXTERNAL TABLE", DescribeObjectType::View => "VIEW", + DescribeObjectType::MaterializedView => "MATERIALIZED VIEW", DescribeObjectType::Database => "DATABASE", DescribeObjectType::Schema => "SCHEMA", DescribeObjectType::Task => "TASK", DescribeObjectType::Stage => "STAGE", + DescribeObjectType::Stream => "STREAM", + DescribeObjectType::Sequence => "SEQUENCE", + DescribeObjectType::Pipe => "PIPE", + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// `TYPE = { COLUMNS | STAGE }` modifier of a Snowflake `DESC TABLE` statement. +/// +pub enum DescribeTableType { + /// `TYPE = COLUMNS` (the default): describe the table's columns. + Columns, + /// `TYPE = STAGE`: describe the table's implicit internal stage properties. + Stage, +} + +impl fmt::Display for DescribeTableType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + DescribeTableType::Columns => "COLUMNS", + DescribeTableType::Stage => "STAGE", }) } } @@ -12072,6 +13319,9 @@ impl fmt::Display for ShowCharset { pub struct ShowObjects { /// Whether to show terse output. pub terse: bool, + /// Whether this is `SHOW DYNAMIC TABLES` rather than `SHOW OBJECTS` + /// (Snowflake). Both share the option grammar (`LIKE` / `IN` / …). + pub dynamic: bool, /// Additional options controlling the SHOW output. pub show_options: ShowStatementOptions, } @@ -12469,6 +13719,105 @@ impl fmt::Display for AlterTaskAction { } } +/// Action for [`Statement::AlterStream`]. +/// +/// See . +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterStreamOperation { + /// `SET COMMENT = ''` + SetComment(String), + /// `UNSET COMMENT` + UnsetComment, +} + +impl fmt::Display for AlterStreamOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterStreamOperation::SetComment(value) => write!(f, "SET COMMENT = '{value}'"), + AlterStreamOperation::UnsetComment => write!(f, "UNSET COMMENT"), + } + } +} + +/// Action for [`Statement::AlterPipe`]. +/// +/// See . +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterPipeOperation { + /// `SET

AS `, + // with or without parentheses around the expression. + let parenthesised = parser.consume_token(&Token::LParen); + let expr = parser.parse_expr()?; + if parenthesised { + parser.expect_token(&Token::RParen)?; + } + Ok(Ok(Some(ColumnOption::Generated { + generated_as: GeneratedAs::Always, + sequence_options: None, + generation_expr: Some(expr), + generation_expr_mode: None, + generated_keyword: false, + }))) } else { Err(ParserError::ParserError("not found match".to_string())) } @@ -734,6 +1004,11 @@ impl Dialect for SnowflakeDialect { true } + /// See: + fn supports_informational_constraint_properties(&self) -> bool { + true + } + /// See: fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] { &RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR @@ -747,6 +1022,14 @@ impl Dialect for SnowflakeDialect { true } + fn supports_comma_separated_add_column_list(&self) -> bool { + true + } + + fn supports_alter_column_comment(&self) -> bool { + true + } + fn is_identifier_generating_function_name( &self, ident: &Ident, @@ -830,21 +1113,43 @@ fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result +/// fn parse_alter_dynamic_table(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); // Use parse_object_name(true) to support IDENTIFIER() function let table_name = parser.parse_object_name(true)?; - // Parse the operation (REFRESH, SUSPEND, or RESUME) let operation = if parser.parse_keyword(Keyword::REFRESH) { AlterTableOperation::Refresh { subpath: None } } else if parser.parse_keyword(Keyword::SUSPEND) { AlterTableOperation::Suspend } else if parser.parse_keyword(Keyword::RESUME) { AlterTableOperation::Resume + } else if parser.parse_keyword(Keyword::RENAME) { + parser.expect_keyword_is(Keyword::TO)?; + let new_name = parser.parse_object_name(false)?; + AlterTableOperation::RenameTable { + table_name: RenameTableNameKind::To(new_name), + } + } else if parser.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { + parser.expect_token(&Token::LParen)?; + let exprs = parser.parse_comma_separated(|p| p.parse_expr())?; + parser.expect_token(&Token::RParen)?; + AlterTableOperation::ClusterBy { exprs } + } else if parser.parse_keywords(&[Keyword::DROP, Keyword::CLUSTERING, Keyword::KEY]) { + AlterTableOperation::DropClusteringKey + } else if parser.parse_keyword(Keyword::SET) { + AlterTableOperation::SetOptionsParens { + options: parse_alter_dynamic_table_properties(parser, false)?, + } + } else if parser.parse_keyword(Keyword::UNSET) { + AlterTableOperation::SetOptionsParens { + options: parse_alter_dynamic_table_properties(parser, true)?, + } } else { return parser.expected_ref( - "REFRESH, SUSPEND, or RESUME after ALTER DYNAMIC TABLE", + "REFRESH, SUSPEND, RESUME, RENAME, SET, UNSET, CLUSTER BY, \ + or DROP CLUSTERING KEY after ALTER DYNAMIC TABLE", parser.peek_token_ref(), ); }; @@ -857,7 +1162,7 @@ fn parse_alter_dynamic_table(parser: &mut Parser) -> Result Result` is encoded as ` = NULL`. +fn parse_alter_dynamic_table_properties( + parser: &mut Parser, + unset: bool, +) -> Result, ParserError> { + let mut options = vec![parse_alter_dynamic_table_property(parser, unset)?]; + loop { + let _ = parser.consume_token(&Token::Comma); + if matches!(parser.peek_token().token, Token::EOF | Token::SemiColon) { + break; + } + options.push(parse_alter_dynamic_table_property(parser, unset)?); + } + Ok(options) +} + +/// Parse one `ALTER DYNAMIC TABLE … SET/UNSET` property into an +/// [`SqlOption::KeyValue`]. See [`parse_alter_dynamic_table_properties`]. +fn parse_alter_dynamic_table_property( + parser: &mut Parser, + unset: bool, +) -> Result { + let key_token = parser.next_token(); + let key = match &key_token.token { + Token::Word(w) if w.quote_style.is_none() => w.value.to_uppercase(), + _ => return parser.expected("a dynamic table property name", key_token), + }; + + if key == "IMMUTABLE" { + parser.expect_keyword_is(Keyword::WHERE)?; + let value = if unset { + Value::Null + } else { + parser.expect_token(&Token::LParen)?; + let predicate = parser.parse_expr()?; + parser.expect_token(&Token::RParen)?; + Value::SingleQuotedString(predicate.to_string()) + }; + return Ok(SqlOption::KeyValue { + key: Ident::new("IMMUTABLE_WHERE"), + value: Expr::Value(value.into()), + }); + } + + if unset { + if !matches!( + key.as_str(), + "COMMENT" | "INITIALIZATION_WAREHOUSE" | "SCHEDULER" + ) { + return parser.expected( + "COMMENT, INITIALIZATION_WAREHOUSE, SCHEDULER, or IMMUTABLE WHERE after UNSET", + key_token, + ); + } + return Ok(SqlOption::KeyValue { + key: Ident::new(key), + value: Expr::Value(Value::Null.into()), + }); + } + + if !matches!( + key.as_str(), + "TARGET_LAG" | "WAREHOUSE" | "INITIALIZATION_WAREHOUSE" | "COMMENT" | "SCHEDULER" + ) { + return parser.expected("a dynamic table property name", key_token); + } + parser.expect_token(&Token::Eq)?; + let value_token = parser.next_token(); + let value = match &value_token.token { + Token::SingleQuotedString(s) => s.clone(), + // WAREHOUSE / INITIALIZATION_WAREHOUSE keep the user's verbatim + // spelling; other keyword values (bare DOWNSTREAM) are uppercased. + Token::Word(w) + if w.quote_style.is_none() + && key != "WAREHOUSE" + && key != "INITIALIZATION_WAREHOUSE" => + { + w.value.to_uppercase() + } + Token::Word(w) if w.quote_style.is_none() => w.value.clone(), + _ => return parser.expected("a property value", value_token), + }; + Ok(SqlOption::KeyValue { + key: Ident::new(key), + value: Expr::Value(Value::SingleQuotedString(value).into()), + }) +} + +/// Parse Snowflake scripting `FETCH INTO [, ...]`. +/// +/// The caller has verified the next keyword is `FETCH` and runs this via +/// `maybe_parse`, so a non-scripting `FETCH` simply errors out and rewinds. +fn parse_fetch_into(parser: &mut Parser) -> Result { + parser.expect_keyword(Keyword::FETCH)?; + let cursor = parser.parse_identifier()?; + parser.expect_keyword(Keyword::INTO)?; + let into = parser.parse_scripting_into_targets()?; + Ok(Statement::FetchInto { cursor, into }) +} + +/// Parse `ALTER PROCEDURE [IF EXISTS] ( [ [, ...]] ) +/// { RENAME TO ... | SET COMMENT = ... | UNSET COMMENT | EXECUTE AS CALLER|OWNER }`. +/// +/// The `ALTER PROCEDURE` keywords are already consumed by the caller. +fn parse_alter_procedure(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + + parser.expect_token(&Token::LParen)?; + let args = if parser.peek_token_ref().token == Token::RParen { + vec![] + } else { + parser.parse_comma_separated(Parser::parse_data_type)? + }; + parser.expect_token(&Token::RParen)?; + + let operation = if parser.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterProcedureOperation::RenameTo { + new_name: parser.parse_object_name(false)?, + } + } else if parser.parse_keywords(&[Keyword::EXECUTE, Keyword::AS]) { + let execute_as = if parser.parse_keyword(Keyword::CALLER) { + ProcedureExecuteAs::Caller + } else { + parser.expect_keyword_is(Keyword::OWNER)?; + ProcedureExecuteAs::Owner + }; + AlterProcedureOperation::ExecuteAs(execute_as) + } else if parser.parse_keyword(Keyword::SET) { + parser.expect_keyword_is(Keyword::COMMENT)?; + parser.expect_token(&Token::Eq)?; + AlterProcedureOperation::SetComment { + comment: parser.parse_expr()?, + } + } else if parser.parse_keyword(Keyword::UNSET) { + parser.expect_keyword_is(Keyword::COMMENT)?; + AlterProcedureOperation::UnsetComment + } else { + return parser.expected_ref( + "RENAME TO, SET COMMENT, UNSET COMMENT, or EXECUTE AS after ALTER PROCEDURE", + parser.peek_token_ref(), + ); + }; + + Ok(Statement::AlterProcedure(AlterProcedure { + if_exists, + name, + args, + operation, + })) +} + +/// Parse Snowflake anonymous procedure: +/// `WITH AS PROCEDURE () RETURNS LANGUAGE +/// [EXECUTE AS ...] AS CALL ()`. +/// +/// The caller runs this via `maybe_parse`, so an ordinary CTE fails the +/// `AS PROCEDURE` probe and rewinds. +fn parse_with_procedure(parser: &mut Parser) -> Result { + parser.expect_keyword(Keyword::WITH)?; + let name = parser.parse_identifier()?; + parser.expect_keyword_is(Keyword::AS)?; + parser.expect_keyword_is(Keyword::PROCEDURE)?; + + let params = parser.parse_optional_procedure_parameters()?; + + let returns = if parser.parse_keyword(Keyword::RETURNS) { + Some(parser.parse_data_type()?) + } else { + None + }; + // Snowflake allows a `NOT NULL` return-type annotation; drop it. + let _ = parser.parse_keywords(&[Keyword::NOT, Keyword::NULL]); + + let language = if parser.parse_keyword(Keyword::LANGUAGE) { + Some(parser.parse_identifier()?) + } else { + None + }; + + let execute_as = if parser.parse_keywords(&[Keyword::EXECUTE, Keyword::AS]) { + if parser.parse_keyword(Keyword::CALLER) { + Some(ProcedureExecuteAs::Caller) + } else { + parser.expect_keyword_is(Keyword::OWNER)?; + Some(ProcedureExecuteAs::Owner) + } + } else { + None + }; + + parser.expect_keyword_is(Keyword::AS)?; + let body = parser.parse_procedure_body()?; + + parser.expect_keyword(Keyword::CALL)?; + let call = Box::new(parser.parse_call()?); + + Ok(Statement::WithProcedure { + name, + params, + returns, + language, + execute_as, + body, + call, + }) +} + +/// Parse snowflake alter materialized view. +/// +/// +/// Every clause except `RENAME TO` is an accept-and-no-op downstream; only the +/// object identity and, for `RENAME TO`, the new name carry meaning. The +/// operation is tagged [`AlterTableType::MaterializedView`] so the emulator can +/// route it to the view machinery. +fn parse_alter_materialized_view(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(true)?; + + let operation = if parser.parse_keyword(Keyword::RENAME) { + parser.expect_keyword_is(Keyword::TO)?; + let new_name = parser.parse_object_name(false)?; + AlterTableOperation::RenameTable { + table_name: RenameTableNameKind::To(new_name), + } + } else if parser.parse_keywords(&[Keyword::CLUSTER, Keyword::BY]) { + parser.expect_token(&Token::LParen)?; + let exprs = parser.parse_comma_separated(|p| p.parse_expr())?; + parser.expect_token(&Token::RParen)?; + AlterTableOperation::ClusterBy { exprs } + } else if parser.parse_keywords(&[Keyword::DROP, Keyword::CLUSTERING, Keyword::KEY]) { + AlterTableOperation::DropClusteringKey + } else if parser.parse_keyword(Keyword::SUSPEND) { + let _ = parser.parse_keyword(Keyword::RECLUSTER); + AlterTableOperation::Suspend + } else if parser.parse_keyword(Keyword::RESUME) { + let _ = parser.parse_keyword(Keyword::RECLUSTER); + AlterTableOperation::Resume + } else if parser.parse_keyword(Keyword::SET) { + AlterTableOperation::SetOptionsParens { + options: parse_alter_materialized_view_properties(parser, false)?, + } + } else if parser.parse_keyword(Keyword::UNSET) { + AlterTableOperation::SetOptionsParens { + options: parse_alter_materialized_view_properties(parser, true)?, + } + } else { + return parser.expected_ref( + "RENAME, CLUSTER BY, DROP CLUSTERING KEY, SUSPEND, RESUME, SET, \ + or UNSET after ALTER MATERIALIZED VIEW", + parser.peek_token_ref(), + ); + }; + + let end_token = if parser.peek_token_ref().token == Token::SemiColon { + parser.peek_token_ref().clone() + } else { + parser.get_current_token().clone() + }; + + Ok(Statement::AlterTable(AlterTable { + name, + if_exists, + only: false, + operations: vec![operation], + location: None, + on_cluster: None, + table_type: Some(AlterTableType::MaterializedView), + end_token: AttachedToken(end_token), + })) +} + +/// Parse the comma-separated property list of `ALTER MATERIALIZED VIEW … +/// SET/UNSET { SECURE | COMMENT | CONTACT | DATA_METRIC_SCHEDULE }`. The list is +/// only preserved for round-tripping; the emulator treats every property as a +/// no-op. +fn parse_alter_materialized_view_properties( + parser: &mut Parser, + unset: bool, +) -> Result, ParserError> { + let mut options = vec![parse_alter_materialized_view_property(parser, unset)?]; + while parser.consume_token(&Token::Comma) { + options.push(parse_alter_materialized_view_property(parser, unset)?); + } + Ok(options) +} + +/// Parse one `SET`/`UNSET` property. `SECURE` is a bare flag; `CONTACT` takes a +/// `purpose[= contact]` pair; everything else takes `= ` on SET. +fn parse_alter_materialized_view_property( + parser: &mut Parser, + unset: bool, +) -> Result { + let key_token = parser.next_token(); + let key = match &key_token.token { + Token::Word(w) => w.value.to_uppercase(), + _ => return parser.expected("a materialized view property name", key_token), + }; + + let value = if key == "SECURE" { + Expr::Value(Value::Boolean(!unset).into()) + } else if key == "CONTACT" { + let purpose = parser.parse_identifier()?; + if unset { + Expr::Value(Value::SingleQuotedString(purpose.value).into()) + } else { + parser.expect_token(&Token::Eq)?; + let contact = parser.parse_object_name(false)?; + Expr::Value(Value::SingleQuotedString(format!("{}={contact}", purpose.value)).into()) + } + } else if unset { + Expr::Value(Value::Null.into()) + } else { + parser.expect_token(&Token::Eq)?; + parser.parse_expr()? + }; + + Ok(SqlOption::KeyValue { + key: Ident::new(key), + value, + }) +} + /// Parse snowflake alter external table. /// fn parse_alter_external_table(parser: &mut Parser) -> Result { let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let table_name = parser.parse_object_name(true)?; - // Parse the operation (REFRESH for now) let operation = if parser.parse_keyword(Keyword::REFRESH) { // Optional subpath for refreshing specific partitions let subpath = match parser.peek_token().token { @@ -884,9 +1516,47 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result None, }; AlterTableOperation::Refresh { subpath } + } else if parser.parse_keywords(&[Keyword::ADD, Keyword::FILES]) { + AlterTableOperation::AddFiles { + files: parse_external_table_file_list(parser)?, + } + } else if parser.parse_keywords(&[Keyword::REMOVE, Keyword::FILES]) { + AlterTableOperation::RemoveFiles { + files: parse_external_table_file_list(parser)?, + } + } else if parser.parse_keywords(&[Keyword::ADD, Keyword::PARTITION]) { + parser.expect_token(&Token::LParen)?; + let mut partitions = Vec::new(); + loop { + let column = parser.parse_identifier()?; + parser.expect_token(&Token::Eq)?; + let value = parser.parse_literal_string()?; + partitions.push(ExternalTablePartitionColumn { column, value }); + if !parser.consume_token(&Token::Comma) { + break; + } + } + parser.expect_token(&Token::RParen)?; + parser.expect_keyword_is(Keyword::LOCATION)?; + let location = parser.parse_literal_string()?; + AlterTableOperation::AddExternalPartition { + partitions, + location, + } + } else if parser.parse_keywords(&[Keyword::DROP, Keyword::PARTITION]) { + parser.expect_keyword_is(Keyword::LOCATION)?; + let location = parser.parse_literal_string()?; + AlterTableOperation::DropExternalPartition { location } + } else if parser.parse_keywords(&[Keyword::SET, Keyword::AUTO_REFRESH]) { + let _ = parser.consume_token(&Token::Eq); + let value = parser.parse_keyword(Keyword::TRUE); + if !value { + parser.expect_keyword_is(Keyword::FALSE)?; + } + AlterTableOperation::SetAutoRefresh { value } } else { return parser.expected_ref( - "REFRESH after ALTER EXTERNAL TABLE", + "REFRESH, ADD FILES, REMOVE FILES, ADD/DROP PARTITION or SET AUTO_REFRESH after ALTER EXTERNAL TABLE", parser.peek_token_ref(), ); }; @@ -909,6 +1579,229 @@ fn parse_alter_external_table(parser: &mut Parser) -> Result +fn parse_create_external_table( + or_replace: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let table_name = parser.parse_object_name(false)?; + let (columns, constraints) = parser.parse_columns()?; + + // The Hive `CREATE EXTERNAL TABLE … STORED AS … LOCATION ''` form is + // reachable under the Snowflake dialect too, and other dialects depend on + // it. When the trailing clauses are Hive-shaped, hand off to the Hive + // grammar rather than the Snowflake one. + if is_hive_external_table_tail(parser) { + return parse_hive_external_table_tail( + parser, + table_name, + columns, + constraints, + or_replace, + if_not_exists, + ); + } + + let mut builder = CreateTableBuilder::new(table_name) + .or_replace(or_replace) + .if_not_exists(if_not_exists) + .external(true) + .hive_formats(None) + .columns(columns) + .constraints(constraints); + + // Snowflake does not fix the order of the trailing clauses, so parse them in + // a loop until an unrecognised token (e.g. `;` or EOF) is reached. + loop { + if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { + let exprs = if parser.consume_token(&Token::LParen) { + let exprs = parser.parse_comma_separated(|p| p.parse_expr())?; + parser.expect_token(&Token::RParen)?; + exprs + } else { + vec![parser.parse_expr()?] + }; + let expr = if exprs.len() == 1 { + exprs.into_iter().next().expect("len checked") + } else { + Expr::Tuple(exprs) + }; + builder = builder.partition_by(Some(Box::new(expr))); + } else if parser.parse_keyword(Keyword::LOCATION) { + let _ = parser.consume_token(&Token::Eq); + builder = builder.location(Some(parse_external_table_location(parser)?)); + } else if parser.parse_keyword(Keyword::FILE_FORMAT) { + parser.expect_token(&Token::Eq)?; + let options = parser.parse_key_value_options(true, &[], false)?; + builder = builder.stage_file_format(Some(options)); + } else if parser.parse_keyword(Keyword::PATTERN) { + let _ = parser.consume_token(&Token::Eq); + builder = builder.pattern(Some(parser.parse_literal_string()?)); + } else if parser.parse_keyword(Keyword::REFRESH_ON_CREATE) { + let _ = parser.consume_token(&Token::Eq); + builder = builder.refresh_on_create(Some(parser.parse_boolean_string()?)); + } else if parser.parse_keyword(Keyword::AUTO_REFRESH) { + let _ = parser.consume_token(&Token::Eq); + builder.auto_refresh = Some(parser.parse_boolean_string()?); + } else if parser.parse_keyword(Keyword::PARTITION_TYPE) { + let _ = parser.consume_token(&Token::Eq); + builder = builder.partition_type(Some(parser.parse_identifier()?.value)); + } else if parser.parse_keyword(Keyword::TABLE_FORMAT) { + let _ = parser.consume_token(&Token::Eq); + builder = builder.table_format(Some(parser.parse_identifier()?.value)); + } else if parser.parse_keyword(Keyword::AWS_SNS_TOPIC) { + let _ = parser.consume_token(&Token::Eq); + builder = builder.aws_sns_topic(Some(parser.parse_literal_string()?)); + } else if parser.parse_keywords(&[Keyword::COPY, Keyword::GRANTS]) { + builder = builder.copy_grants(true); + } else if parser.parse_keyword(Keyword::COMMENT) { + parser.prev_token(); + if let Some(comment) = parser.parse_optional_inline_comment()? { + builder = builder.comment_after_column_def(Some(comment)); + } + } else if parser.parse_keywords(&[Keyword::WITH, Keyword::TAG]) + || parser.parse_keyword(Keyword::TAG) + { + parser.expect_token(&Token::LParen)?; + let tags = parser.parse_comma_separated(Parser::parse_tag)?; + parser.expect_token(&Token::RParen)?; + builder = builder.with_tags(Some(tags)); + } else if parser.parse_keywords(&[Keyword::WITH, Keyword::ROW]) + || parser.parse_keyword(Keyword::ROW) + { + parser.expect_keywords(&[Keyword::ACCESS, Keyword::POLICY])?; + let policy = parser.parse_object_name(false)?; + parser.expect_keyword_is(Keyword::ON)?; + parser.expect_token(&Token::LParen)?; + let policy_columns = parser.parse_comma_separated(|p| p.parse_identifier())?; + parser.expect_token(&Token::RParen)?; + builder = + builder.with_row_access_policy(Some(RowAccessPolicy::new(policy, policy_columns))); + } else { + break; + } + } + + Ok(Statement::CreateTable(builder.build())) +} + +/// Whether the clauses following the column list are Hive-shaped (`STORED AS`, +/// `ROW FORMAT`, `PARTITIONED BY`, `CLUSTERED BY`, `TBLPROPERTIES`, or a +/// `LOCATION ''` rather than the Snowflake `LOCATION=@stage`). +fn is_hive_external_table_tail(parser: &Parser) -> bool { + match &parser.peek_token_ref().token { + Token::Word(w) => match w.keyword { + Keyword::STORED + | Keyword::ROW + | Keyword::PARTITIONED + | Keyword::CLUSTERED + | Keyword::TBLPROPERTIES => true, + Keyword::LOCATION => { + matches!( + parser.peek_nth_token_ref(1).token, + Token::SingleQuotedString(_) + ) + } + _ => false, + }, + _ => false, + } +} + +/// Build a Hive-form `CREATE EXTERNAL TABLE` from an already-parsed name and +/// column list, mirroring [`Parser::parse_create_external_table`]. +fn parse_hive_external_table_tail( + parser: &mut Parser, + table_name: ObjectName, + columns: Vec, + constraints: Vec, + or_replace: bool, + if_not_exists: bool, +) -> Result { + let hive_distribution = parser.parse_hive_distribution()?; + let hive_formats = parser.parse_hive_formats()?; + let file_format = hive_formats + .as_ref() + .and_then(|hf| hf.storage.as_ref()) + .and_then(|storage| match storage { + crate::ast::HiveIOFormat::FileFormat { format } => Some(*format), + crate::ast::HiveIOFormat::IOF { .. } | crate::ast::HiveIOFormat::Using { .. } => None, + }); + let location = hive_formats.as_ref().and_then(|hf| hf.location.clone()); + let table_properties = parser.parse_options(Keyword::TBLPROPERTIES)?; + let table_options = if table_properties.is_empty() { + crate::ast::CreateTableOptions::None + } else { + crate::ast::CreateTableOptions::TableProperties(table_properties) + }; + Ok(Statement::CreateTable( + CreateTableBuilder::new(table_name) + .columns(columns) + .constraints(constraints) + .hive_distribution(hive_distribution) + .hive_formats(hive_formats) + .table_options(table_options) + .or_replace(or_replace) + .if_not_exists(if_not_exists) + .external(true) + .file_format(file_format) + .location(location) + .build(), + )) +} + +/// Parse a Snowflake `DROP EXTERNAL TABLE` statement. The `DROP EXTERNAL TABLE` +/// keywords have already been consumed. +/// +fn parse_drop_external_table(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let names = parser.parse_comma_separated(|p| p.parse_object_name(false))?; + let cascade = parser.parse_keyword(Keyword::CASCADE); + let restrict = !cascade && parser.parse_keyword(Keyword::RESTRICT); + Ok(Statement::Drop { + object_type: ObjectType::ExternalTable, + if_exists, + names, + cascade, + restrict, + purge: false, + temporary: false, + table: None, + }) +} + +/// Parse the `@stage[/subpath]` reference used as an external-table `LOCATION`, +/// carrying the whole reference (including any `/` subpath) through as one +/// string. +fn parse_external_table_location(parser: &mut Parser) -> Result { + let mut parts = Vec::new(); + loop { + parts.push(parse_stage_name_identifier(parser)?.value); + if !parser.consume_token(&Token::Period) { + break; + } + } + Ok(parts.join(".")) +} + +/// Parse a parenthesised, comma-separated list of quoted staged file paths, +/// as used by `ALTER EXTERNAL TABLE … ADD/REMOVE FILES ( '' [, …] )`. +fn parse_external_table_file_list(parser: &mut Parser) -> Result, ParserError> { + parser.expect_token(&Token::LParen)?; + let mut files = Vec::new(); + loop { + files.push(parser.parse_literal_string()?); + if !parser.consume_token(&Token::Comma) { + break; + } + } + parser.expect_token(&Token::RParen)?; + Ok(files) +} + /// Parse snowflake alter session. /// fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { @@ -1008,6 +1901,11 @@ pub fn parse_create_table( parser.expect_token(&Token::Eq)?; builder = builder.change_tracking(Some(parser.parse_boolean_string()?)); } + Keyword::STAGE_FILE_FORMAT => { + parser.expect_token(&Token::Eq)?; + let options = parser.parse_key_value_options(true, &[], false)?; + builder = builder.stage_file_format(Some(options)); + } Keyword::DATA_RETENTION_TIME_IN_DAYS => { parser.expect_token(&Token::Eq)?; let data_retention_time_in_days = parser.parse_literal_uint()?; @@ -1083,6 +1981,14 @@ pub fn parse_create_table( parser.expect_token(&Token::Eq)?; builder.catalog = Some(parser.parse_literal_string()?); } + Keyword::CATALOG_TABLE_NAME => { + parser.expect_token(&Token::Eq)?; + builder.catalog_table_name = Some(parser.parse_literal_string()?); + } + Keyword::AUTO_REFRESH => { + parser.expect_token(&Token::Eq)?; + builder.auto_refresh = Some(parser.parse_boolean_string()?); + } Keyword::BASE_LOCATION => { parser.expect_token(&Token::Eq)?; builder.base_location = Some(parser.parse_literal_string()?); @@ -1102,13 +2008,43 @@ pub fn parse_create_table( } Keyword::TARGET_LAG => { parser.expect_token(&Token::Eq)?; - let target_lag = parser.parse_literal_string()?; + // TARGET_LAG accepts a quoted duration ('1 minute') or the + // bare keyword DOWNSTREAM. + let target_lag = if parser.parse_keyword(Keyword::DOWNSTREAM) { + "DOWNSTREAM".to_string() + } else { + parser.parse_literal_string()? + }; builder = builder.target_lag(Some(target_lag)); } Keyword::WAREHOUSE => { parser.expect_token(&Token::Eq)?; let warehouse = parser.parse_identifier()?; - builder = builder.warehouse(Some(warehouse)); + builder = builder.warehouse(Some(warehouse.value)); + } + Keyword::INITIALIZATION_WAREHOUSE => { + parser.expect_token(&Token::Eq)?; + let warehouse = parser.parse_identifier()?; + builder = builder.initialization_warehouse(Some(warehouse.value)); + } + Keyword::SCHEDULER => { + parser.expect_token(&Token::Eq)?; + // SCHEDULER accepts a quoted string ('DISABLE') or a bare + // keyword (the GET_DDL spelling, e.g. DISABLE). + let value_token = parser.next_token(); + let scheduler = match &value_token.token { + Token::SingleQuotedString(s) => s.clone(), + Token::Word(w) => w.value.clone(), + _ => return parser.expected("a scheduler value", value_token), + }; + builder = builder.scheduler(Some(scheduler)); + } + Keyword::IMMUTABLE => { + parser.expect_keyword_is(Keyword::WHERE)?; + parser.expect_token(&Token::LParen)?; + let predicate = parser.parse_expr()?; + parser.expect_token(&Token::RParen)?; + builder = builder.immutable_where(Some(predicate.to_string())); } Keyword::AT | Keyword::BEFORE => { parser.prev_token(); @@ -1152,6 +2088,8 @@ pub fn parse_create_table( let (columns, constraints) = parser.parse_columns()?; builder = builder.columns(columns).constraints(constraints); } + // Snowflake accepts Iceberg table options either space- or comma-separated. + Token::Comma => {} Token::EOF => { break; } @@ -1172,7 +2110,18 @@ pub fn parse_create_table( builder = builder.table_options(table_options); - if iceberg && builder.base_location.is_none() { + // Snowflake-managed Iceberg tables require BASE_LOCATION. Tables bound to + // an external catalog integration (an explicit non-SNOWFLAKE CATALOG, or + // CATALOG_TABLE_NAME for externally-managed reads) do not. + let external_catalog = builder + .catalog + .as_deref() + .is_some_and(|c| !c.eq_ignore_ascii_case("SNOWFLAKE")); + if iceberg + && builder.base_location.is_none() + && builder.catalog_table_name.is_none() + && !external_catalog + { return Err(ParserError::ParserError( "BASE_LOCATION is required for ICEBERG tables".to_string(), )); @@ -1321,6 +2270,16 @@ pub fn parse_create_stage( comment, } = parse_stage_properties(parser)?; + // Trailing `WITH TAG ( = '' [, ...])`. The property loop above breaks + // on the `WITH` keyword, so the clause is naturally trailing-only. + let mut with_tags = Vec::new(); + if parser.parse_keyword(Keyword::WITH) { + parser.expect_keyword(Keyword::TAG)?; + parser.expect_token(&Token::LParen)?; + with_tags = parser.parse_comma_separated(Parser::parse_tag)?; + parser.expect_token(&Token::RParen)?; + } + Ok(Statement::CreateStage { or_replace, temporary, @@ -1331,6 +2290,7 @@ pub fn parse_create_stage( file_format, copy_options, comment, + with_tags, }) } @@ -1385,17 +2345,17 @@ fn parse_stage_properties(parser: &mut Parser) -> Result` // is sugar for `FILE_FORMAT = (FORMAT_NAME = )` — @@ -1415,7 +2375,7 @@ fn parse_stage_properties(parser: &mut Parser) -> Result Result { + parser.prev_token(); + break; + } Token::AtSign => ident.push('@'), Token::Tilde => ident.push('~'), Token::Mod => ident.push('%'), @@ -1528,6 +2492,10 @@ pub fn parse_snowflake_stage_name(parser: &mut Parser) -> Result Ok(ObjectName::from(vec![Ident::new(s)])), _ => { parser.prev_token(); Ok(parser.parse_object_name(false)?) @@ -1546,10 +2514,11 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { _ => CopyIntoSnowflakeKind::Table, }; - let mut files: Vec = vec![]; + let mut files: Vec = vec![]; let mut from_transformations: Option> = None; let mut from_stage_alias = None; let mut from_stage = None; + let mut from_stage_args = None; let mut stage_params = StageParamsObject { url: None, encryption: KeyValueOptions { @@ -1589,6 +2558,11 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { parser.expect_keyword_is(Keyword::FROM)?; from_stage = Some(parse_snowflake_stage_name(parser)?); + // Inline stage table-function args (querying-stage syntax): + // `@stage (FILE_FORMAT => …, PATTERN => …)`. + if parser.consume_token(&Token::LParen) { + from_stage_args = Some(parser.parse_table_function_args()?); + } stage_params = parse_stage_params(parser)?; // Parse an optional alias @@ -1623,7 +2597,40 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { // FILE_FORMAT if parser.parse_keyword(Keyword::FILE_FORMAT) { parser.expect_token(&Token::Eq)?; - file_format = parser.parse_key_value_options(true, &[])?.options; + if parser.peek_token().token == Token::LParen { + let paren_span = parser.peek_token().span; + file_format = parser.parse_key_value_options(true, &[], false)?.options; + if file_format.is_empty() { + // Snowflake parses an empty `FILE_FORMAT = ()` as a reference + // to its internal empty-constant-list token and then resolves + // that as a format name, so the clause fails with + // "File format 'TOK_CONSTANT_LIST' does not exist". Mirror + // that by lowering `()` to `FORMAT_NAME = TOK_CONSTANT_LIST`. + file_format = vec![KeyValueOption { + option_name: "FORMAT_NAME".to_string(), + option_value: KeyValueOptionKind::Single( + Value::Placeholder("TOK_CONSTANT_LIST".to_string()) + .with_span(paren_span), + ), + }]; + } + } else { + // Shorthand `FILE_FORMAT = ''` / `FILE_FORMAT = ` + // is sugar for `FILE_FORMAT = (FORMAT_NAME = )` — + // normalize it (mirrors CREATE STAGE). + let tok = parser.peek_token(); + let value = match tok.token { + Token::Word(w) => { + parser.next_token(); + Value::Placeholder(w.value.clone()).with_span(tok.span) + } + _ => parser.parse_value()?, + }; + file_format = vec![KeyValueOption { + option_name: "FORMAT_NAME".to_string(), + option_value: KeyValueOptionKind::Single(value), + }]; + } // PARTITION BY } else if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) { partition = Some(Box::new(parser.parse_expr()?)) @@ -1636,7 +2643,13 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { continue_loop = false; let next_token = parser.next_token(); match next_token.token { - Token::SingleQuotedString(s) => files.push(s), + // A bind placeholder is accepted here so the statement + // parses; whether it is a legal FILES value is decided + // downstream (real Snowflake rejects a bound `?`). + Token::SingleQuotedString(_) | Token::Placeholder(_) => { + parser.prev_token(); + files.push(parser.parse_value()?); + } _ => parser.expected("file token", next_token)?, }; if parser.next_token().token.eq(&Token::Comma) { @@ -1651,7 +2664,10 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { parser.expect_token(&Token::Eq)?; let next_token = parser.next_token(); pattern = Some(match next_token.token { - Token::SingleQuotedString(s) => s, + Token::SingleQuotedString(_) | Token::Placeholder(_) => { + parser.prev_token(); + parser.parse_value()? + } _ => parser.expected("pattern", next_token)?, }); // VALIDATION MODE @@ -1661,14 +2677,21 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { // COPY OPTIONS } else if parser.parse_keyword(Keyword::COPY_OPTIONS) { parser.expect_token(&Token::Eq)?; - copy_options = parser.parse_key_value_options(true, &[])?.options; + copy_options = parser.parse_key_value_options(true, &[], false)?.options; } else { match parser.next_token().token { - Token::SemiColon | Token::EOF => break, + // Leave the statement terminator for the caller: inside a + // `BEGIN … END` body the surrounding statement list expects to + // consume the `;` itself. + Token::SemiColon => { + parser.prev_token(); + break; + } + Token::EOF => break, Token::Comma => continue, // In `COPY INTO ` the copy options do not have a shared key // like in `COPY INTO
` - Token::Word(key) => copy_options.push(parser.parse_key_value_option(&key)?), + Token::Word(key) => copy_options.push(parser.parse_key_value_option(&key, false)?), _ => { return parser .expected_ref("another copy option, ; or EOF'", parser.peek_token_ref()) @@ -1683,6 +2706,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result { into_columns, from_obj: from_stage, from_obj_alias: from_stage_alias, + from_obj_args: from_stage_args, stage_params, from_transformations, from_query, @@ -1784,6 +2808,16 @@ fn parse_select_item_for_data_load( }?); } + // The data-load item grammar only covers [.]$[:] [AS ]. + // If the item was not fully consumed here — e.g. a dotted sub-path (`$1:a.b`) or a + // `::TYPE` cast follows — roll back by failing so the caller's general select-item + // fallback parses the extended shape. Items are separated by `,` and terminated by + // the trailing `FROM @stage`, so anything else means the item continues. + if !matches!(parser.peek_token_ref().token, Token::Comma) && !parser.peek_keyword(Keyword::FROM) + { + return parser.expected_ref("end of data load select item", parser.peek_token_ref()); + } + Ok(StageLoadSelectItem { alias, file_col_num, @@ -1831,7 +2865,7 @@ fn parse_stage_params(parser: &mut Parser) -> Result Result { parser.advance_token(); if set { - let option = parser.parse_key_value_option(&key)?; + let option = parser.parse_key_value_option(&key, false)?; options.push(option); } else { options.push(KeyValueOption { @@ -1981,10 +3015,15 @@ fn parse_column_tags(parser: &mut Parser, with: bool) -> Result -fn parse_show_objects(terse: bool, parser: &mut Parser) -> Result { +fn parse_show_objects( + terse: bool, + dynamic: bool, + parser: &mut Parser, +) -> Result { let show_options = parser.parse_show_stmt_options()?; Ok(Statement::ShowObjects(ShowObjects { terse, + dynamic, show_options, })) } @@ -2382,7 +3421,17 @@ fn parse_create_file_format( like_source = Some(parser.parse_object_name(false)?); } else if parser.parse_keyword(Keyword::TYPE) { parser.expect_token(&Token::Eq)?; - format_type = Some(parser.parse_identifier()?); + // A bind placeholder is accepted here so the statement parses; real + // Snowflake rejects it downstream as an invalid type. A genuine bind + // marker is an unquoted `Ident` whose value is the marker text, which + // no literal unquoted type identifier can be. + format_type = Some(match parser.peek_token().token { + Token::Placeholder(s) => { + parser.next_token(); + Ident::new(s) + } + _ => parser.parse_identifier()?, + }); } // `LIKE` is mutually exclusive with `TYPE`/options per Snowflake's grammar. @@ -2411,7 +3460,7 @@ fn parse_create_file_format( if matches!(parser.peek_token().token, Token::EOF | Token::SemiColon) { break; } - let parsed = parser.parse_key_value_options(false, &[Keyword::COMMENT])?; + let parsed = parser.parse_key_value_options(false, &[Keyword::COMMENT], false)?; if parsed.options.is_empty() { break; } @@ -2446,7 +3495,7 @@ fn parse_alter_file_format(parser: &mut Parser) -> Result Result +/// [ ALLOWED_VALUES '' [ , ... ] ] [ COMMENT = '' ]` +fn parse_create_tag(or_replace: bool, parser: &mut Parser) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + + let mut allowed_values = Vec::new(); + if parser.parse_keyword(Keyword::ALLOWED_VALUES) { + loop { + allowed_values.push(parser.parse_literal_string()?); + if !parser.consume_token(&Token::Comma) { + break; + } + } + } + + let comment = if parser.parse_keyword(Keyword::COMMENT) { + parser.expect_token(&Token::Eq)?; + Some(parser.parse_comment_value()?) + } else { + None + }; + + Ok(Statement::CreateTag { + or_replace, + if_not_exists, + name, + allowed_values, + comment, + }) +} + +/// Parse a comma-separated list of `MASKING POLICY ` items, where the +/// `MASKING POLICY` keywords are repeated before each policy name. +fn parse_masking_policy_list(parser: &mut Parser) -> Result, ParserError> { + let mut policies = Vec::new(); + loop { + parser.expect_keywords(&[Keyword::MASKING, Keyword::POLICY])?; + policies.push(parser.parse_object_name(false)?); + if !parser.consume_token(&Token::Comma) { + break; + } + } + Ok(policies) +} + +/// Parse `ALTER TAG [IF EXISTS] { RENAME TO +/// | SET MASKING POLICY

[, MASKING POLICY

...] [FORCE] +/// | UNSET MASKING POLICY

[, MASKING POLICY

...] }` +fn parse_alter_tag(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + let operation = if parser.parse_keyword(Keyword::SET) { + let policies = parse_masking_policy_list(parser)?; + let force = parser.parse_keyword(Keyword::FORCE); + AlterTagOperation::SetMaskingPolicy { policies, force } + } else if parser.parse_keyword(Keyword::UNSET) { + let policies = parse_masking_policy_list(parser)?; + AlterTagOperation::UnsetMaskingPolicy { policies } + } else { + parser.expect_keywords(&[Keyword::RENAME, Keyword::TO])?; + let new_name = parser.parse_object_name(false)?; + AlterTagOperation::RenameTo { new_name } + }; + Ok(Statement::AlterTag { + if_exists, + name, + operation, + }) +} + +/// Parse `DROP TAG [IF EXISTS] ` +fn parse_drop_tag(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + Ok(Statement::DropTag { name, if_exists }) +} + +/// Parse the tail of `ALTER { SET TAG = '' [, ...] +/// | UNSET TAG [, ...] }` after the object-type keyword has been consumed. +fn parse_alter_object_set_tags( + parser: &mut Parser, + object_type: ObjectType, +) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let object_name = parser.parse_object_name(false)?; + let unset = matches!( + parser.expect_one_of_keywords(&[Keyword::SET, Keyword::UNSET])?, + Keyword::UNSET + ); + parser.expect_keyword(Keyword::TAG)?; + + let mut set_tags = Vec::new(); + let mut unset_tags = Vec::new(); + if unset { + loop { + unset_tags.push(parser.parse_object_name(false)?); + if !parser.consume_token(&Token::Comma) { + break; + } + } + } else { + loop { + let key = parser.parse_object_name(false)?; + parser.expect_token(&Token::Eq)?; + let value = parser.parse_literal_string()?; + set_tags.push(Tag::new(key, value)); + if !parser.consume_token(&Token::Comma) { + break; + } + } + } + + Ok(Statement::SetTags { + object_type, + object_name, + if_exists, + unset, + set_tags, + unset_tags, + }) +} + +/// Parse `SHOW [TERSE] TAGS [ ... ]` +fn parse_show_tags(terse: bool, parser: &mut Parser) -> Result { + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowTags { + terse, + show_options, + }) +} + +/// Parse `SHOW [TERSE] SEQUENCES [ ... ]` +fn parse_show_sequences(terse: bool, parser: &mut Parser) -> Result { + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowSequences { + terse, + show_options, + }) +} + +/// Parse `SHOW [TERSE] { PRIMARY | IMPORTED | EXPORTED } KEYS [ ... ]` +fn parse_show_keys( + kind: ShowKeysKind, + terse: bool, + parser: &mut Parser, +) -> Result { + parser.expect_keyword(Keyword::KEYS)?; + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowKeys { + kind, + terse, + show_options, + }) +} + /// Parse `DESC[RIBE] WAREHOUSE ` fn parse_describe_warehouse(parser: &mut Parser) -> Result { let name = parser.parse_object_name(false)?; Ok(Statement::DescribeWarehouse { name }) } +/// Parse `CREATE [OR REPLACE] ROW ACCESS POLICY [IF NOT EXISTS] +/// AS ( [, ...]) RETURNS -> ` +fn parse_create_row_access_policy( + or_replace: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + parser.expect_keyword_is(Keyword::AS)?; + parser.expect_token(&Token::LParen)?; + let args = parser.parse_comma_separated(|p| { + let arg_name = p.parse_identifier()?; + let data_type = p.parse_data_type()?; + Ok(OperateFunctionArg { + mode: None, + name: Some(arg_name), + data_type, + default_expr: None, + }) + })?; + parser.expect_token(&Token::RParen)?; + parser.expect_keyword_is(Keyword::RETURNS)?; + let return_type = parser.parse_data_type()?; + parser.expect_token(&Token::Arrow)?; + let policy_expr = parser.parse_expr()?; + Ok(Statement::CreateRowAccessPolicy { + or_replace, + if_not_exists, + name, + args, + return_type, + policy_expr, + }) +} + +/// Parse `ALTER ROW ACCESS POLICY [IF EXISTS] RENAME TO ` +fn parse_alter_row_access_policy(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + parser.expect_keywords(&[Keyword::RENAME, Keyword::TO])?; + let new_name = parser.parse_object_name(false)?; + Ok(Statement::AlterRowAccessPolicy { + if_exists, + name, + new_name, + }) +} + +/// Parse `DROP ROW ACCESS POLICY [IF EXISTS] ` +fn parse_drop_row_access_policy(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + Ok(Statement::DropRowAccessPolicy { if_exists, name }) +} + +/// Parse `DESC[RIBE] ROW ACCESS POLICY ` +fn parse_describe_row_access_policy(parser: &mut Parser) -> Result { + let name = parser.parse_object_name(false)?; + Ok(Statement::DescribeRowAccessPolicy { name }) +} + +/// Parse `SHOW ROW ACCESS POLICIES [LIKE '']` +fn parse_show_row_access_policies(parser: &mut Parser) -> Result { + let filter = parser.parse_show_statement_filter()?; + Ok(Statement::ShowRowAccessPolicies { filter }) +} + +/// Parse `CREATE [OR REPLACE] MASKING POLICY [IF NOT EXISTS] +/// AS ( [, ...]) RETURNS -> [COMMENT = '']` +fn parse_create_masking_policy( + or_replace: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + parser.expect_keyword_is(Keyword::AS)?; + parser.expect_token(&Token::LParen)?; + let args = parser.parse_comma_separated(|p| { + let arg_name = p.parse_identifier()?; + let data_type = p.parse_data_type()?; + Ok(OperateFunctionArg { + mode: None, + name: Some(arg_name), + data_type, + default_expr: None, + }) + })?; + parser.expect_token(&Token::RParen)?; + parser.expect_keyword_is(Keyword::RETURNS)?; + let return_type = parser.parse_data_type()?; + parser.expect_token(&Token::Arrow)?; + let policy_expr = parser.parse_expr()?; + let comment = if parser.parse_keyword(Keyword::COMMENT) { + parser.expect_token(&Token::Eq)?; + Some(parser.parse_comment_value()?) + } else { + None + }; + Ok(Statement::CreateMaskingPolicy { + or_replace, + if_not_exists, + name, + args, + return_type, + policy_expr, + comment, + }) +} + +/// Parse `ALTER MASKING POLICY [IF EXISTS] +/// { SET BODY -> | RENAME TO | SET COMMENT = '' | UNSET COMMENT }` +fn parse_alter_masking_policy(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + let operation = if parser.parse_keywords(&[Keyword::SET, Keyword::BODY]) { + parser.expect_token(&Token::Arrow)?; + AlterMaskingPolicyOperation::SetBody { + body: parser.parse_expr()?, + } + } else if parser.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterMaskingPolicyOperation::RenameTo { + new_name: parser.parse_object_name(false)?, + } + } else if parser.parse_keywords(&[Keyword::SET, Keyword::COMMENT]) { + parser.expect_token(&Token::Eq)?; + AlterMaskingPolicyOperation::SetComment { + comment: parser.parse_comment_value()?, + } + } else if parser.parse_keywords(&[Keyword::UNSET, Keyword::COMMENT]) { + AlterMaskingPolicyOperation::UnsetComment + } else { + return parser.expected_ref( + "SET BODY, RENAME TO, SET COMMENT, or UNSET COMMENT", + parser.peek_token_ref(), + ); + }; + Ok(Statement::AlterMaskingPolicy { + if_exists, + name, + operation, + }) +} + +/// Parse `DROP MASKING POLICY [IF EXISTS] ` +fn parse_drop_masking_policy(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + Ok(Statement::DropMaskingPolicy { if_exists, name }) +} + +/// Parse `DESC[RIBE] MASKING POLICY ` +fn parse_describe_masking_policy(parser: &mut Parser) -> Result { + let name = parser.parse_object_name(false)?; + Ok(Statement::DescribeMaskingPolicy { name }) +} + +/// Parse `SHOW MASKING POLICIES [LIKE ''] [IN ]` +fn parse_show_masking_policies(parser: &mut Parser) -> Result { + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowMaskingPolicies { show_options }) +} + +/// Parse `SHOW PROCEDURES [LIKE ''] [IN ]` +fn parse_show_procedures(parser: &mut Parser) -> Result { + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowProcedures { show_options }) +} + /// Parse `SHOW WAREHOUSES [LIKE '']` fn parse_show_warehouses(parser: &mut Parser) -> Result { let filter = parser.parse_show_statement_filter()?; Ok(Statement::ShowWarehouses { filter }) } +/// Parse `SHOW CONNECTIONS [LIKE '']` +fn parse_show_connections(parser: &mut Parser) -> Result { + let filter = parser.parse_show_statement_filter()?; + Ok(Statement::ShowConnections { filter }) +} + +/// Parse `SHOW SHARES [LIKE '']` +fn parse_show_shares(parser: &mut Parser) -> Result { + let filter = parser.parse_show_statement_filter()?; + Ok(Statement::ShowShares { filter }) +} + /// Parse `SHOW ACCOUNTS [HISTORY] [LIKE '']` fn parse_show_accounts(parser: &mut Parser) -> Result { let history = parser.parse_keyword(Keyword::HISTORY); @@ -2694,6 +4079,29 @@ fn parse_catalog_rest_config(parser: &mut Parser) -> Result Result { + let mut scope = parse_oauth_scope_segment(parser)?; + while parser.consume_token(&Token::Colon) { + scope.push(':'); + scope.push_str(&parse_oauth_scope_segment(parser)?); + } + Ok(scope) +} + +/// A single segment of an OAuth scope: a bare word (keyword or not) or a +/// quoted string. +fn parse_oauth_scope_segment(parser: &mut Parser) -> Result { + let token = parser.next_token(); + match token.token { + Token::Word(w) => Ok(w.value), + Token::SingleQuotedString(s) | Token::DoubleQuotedString(s) => Ok(s), + _ => parser.expected("OAUTH scope", token), + } +} + /// Parse the body of `REST_AUTHENTICATION = ( … )`. fn parse_catalog_rest_authentication( parser: &mut Parser, @@ -2722,7 +4130,7 @@ fn parse_catalog_rest_authentication( parser.expect_token(&Token::Eq)?; parser.expect_token(&Token::LParen)?; loop { - oauth_allowed_scopes.push(parser.parse_literal_string()?); + oauth_allowed_scopes.push(parse_oauth_scope(parser)?); if !parser.consume_token(&Token::Comma) { break; } @@ -2784,3 +4192,59 @@ fn parse_show_catalog_integrations(parser: &mut Parser) -> Result `. +/// +/// Params (`TYPE`, `ENABLED`, `STORAGE_PROVIDER`, `STORAGE_AWS_ROLE_ARN`, +/// `STORAGE_ALLOWED_LOCATIONS`, `STORAGE_BLOCKED_LOCATIONS`, `COMMENT`, plus +/// GCS/Azure provider variants) are captured generically as key-value options, +/// so the parser stays agnostic to the provider-specific property set. +fn parse_create_storage_integration( + or_replace: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + let params = parser.parse_key_value_options(false, &[], false)?; + Ok(Statement::CreateStorageIntegration { + or_replace, + if_not_exists, + name, + params, + }) +} + +/// Parse `ALTER STORAGE INTEGRATION [IF EXISTS] SET `. +/// +/// Only the `SET` form is modeled; the emulator consumes it in a follow-up +/// task. The `SET` options are captured generically as key-value options. +fn parse_alter_storage_integration(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + parser.expect_keyword(Keyword::SET)?; + let set_options = parser.parse_key_value_options(false, &[], false)?; + Ok(Statement::AlterStorageIntegration { + name, + if_exists, + set_options, + }) +} + +/// Parse `DROP STORAGE INTEGRATION [IF EXISTS] `. +fn parse_drop_storage_integration(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + Ok(Statement::DropStorageIntegration { name, if_exists }) +} + +/// Parse `DESC[RIBE] STORAGE INTEGRATION `. +fn parse_describe_storage_integration(parser: &mut Parser) -> Result { + let name = parser.parse_object_name(false)?; + Ok(Statement::DescribeStorageIntegration { name }) +} + +/// Parse `SHOW STORAGE INTEGRATIONS [LIKE '']`. +fn parse_show_storage_integrations(parser: &mut Parser) -> Result { + let filter = parser.parse_show_statement_filter()?; + Ok(Statement::ShowStorageIntegrations { filter }) +} diff --git a/src/keywords.rs b/src/keywords.rs index 743329d971..02b6281d10 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -115,6 +115,7 @@ define_keywords!( ALIGNMENT, ALL, ALLOCATE, + ALLOWED_VALUES, ALLOWOVERWRITE, ALLOW_WRITES, ALTER, @@ -123,6 +124,7 @@ define_keywords!( AND, ANTI, ANY, + APPEND_ONLY, APPLICATION, APPLY, APPLYBUDGET, @@ -147,6 +149,8 @@ define_keywords!( AUTOEXTEND_SIZE, AUTOINCREMENT, AUTO_INCREMENT, + AUTO_INGEST, + AUTO_REFRESH, AVG, AVG_ROW_LENGTH, AVRO, @@ -155,6 +159,7 @@ define_keywords!( AWS_SECRET_ACCESS_KEY, AWS_SERVICE, AWS_SIGV4, + AWS_SNS_TOPIC, BACKUP, BACKWARD, BASE64, @@ -177,6 +182,7 @@ define_keywords!( BLOCK, BLOOM, BLOOMFILTER, + BODY, BOOL, BOOLEAN, BOOST, @@ -213,6 +219,7 @@ define_keywords!( CATALOG_SYNC, CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER, CATALOG_SYNC_NAMESPACE_MODE, + CATALOG_TABLE_NAME, CATALOG_URI, CATCH, CATEGORY, @@ -266,6 +273,7 @@ define_keywords!( CONFLICT, CONNECT, CONNECTION, + CONNECTIONS, CONNECTOR, CONNECT_BY_ROOT, CONSTRAINT, @@ -403,6 +411,7 @@ define_keywords!( EPOCH, EQUALS, ERROR, + ERROR_INTEGRATION, ESCAPE, ESCAPED, ESTIMATE, @@ -426,6 +435,7 @@ define_keywords!( EXPLAIN, EXPLICIT, EXPORT, + EXPORTED, EXTEND, EXTENDED, EXTENSION, @@ -514,6 +524,7 @@ define_keywords!( HOUR, HOURS, HUGEINT, + HYBRID, IAM_ROLE, ICEBERG, ICEBERG_REST, @@ -539,6 +550,7 @@ define_keywords!( INDICATOR, INHERIT, INHERITS, + INITIALIZATION_WAREHOUSE, INITIALIZE, INITIALLY, INNER, @@ -686,6 +698,7 @@ define_keywords!( MIN_ROWS, MOD, MODE, + MODIFIED_AFTER, MODIFIES, MODIFY, MODULE, @@ -726,6 +739,7 @@ define_keywords!( NOLOGIN, NONE, NOORDER, + NORELY, NOREPLICATION, NORMALIZE, NORMALIZED, @@ -735,6 +749,7 @@ define_keywords!( NOTHING, NOTIFY, NOTNULL, + NOVALIDATE, NOWAIT, NO_WRITE_TO_BINLOG, NTH_VALUE, @@ -807,6 +822,7 @@ define_keywords!( PARTITION, PARTITIONED, PARTITIONS, + PARTITION_TYPE, PASSEDBYVALUE, PASSING, PASSKEY, @@ -823,6 +839,8 @@ define_keywords!( PERIOD, PERMISSIVE, PERSISTENT, + PIPE, + PIPES, PIVOT, PLACING, PLAIN, @@ -830,6 +848,7 @@ define_keywords!( PLANS, POINT, POLARIS, + POLICIES, POLICY, POLYGON, POOL, @@ -842,6 +861,7 @@ define_keywords!( PRECEDING, PRECISION, PREFERRED, + PREFIX, PREPARE, PRESERVE, PRESET, @@ -851,6 +871,7 @@ define_keywords!( PRIOR, PRIVILEGES, PROCEDURE, + PROCEDURES, PROCESSLIST, PROFILE, PROGRAM, @@ -882,6 +903,7 @@ define_keywords!( REFRESH, REFRESH_INTERVAL_SECONDS, REFRESH_MODE, + REFRESH_ON_CREATE, REGCLASS, REGEXP, REGION, @@ -900,6 +922,7 @@ define_keywords!( RELAY, RELEASE, RELEASES, + RELY, REMAINDER, REMOTE, REMOVE, @@ -957,6 +980,7 @@ define_keywords!( SAMPLE, SAVEPOINT, SCHEDULE, + SCHEDULER, SCHEMA, SCHEMAS, SCOPE, @@ -993,6 +1017,7 @@ define_keywords!( SETTINGS, SHARE, SHARED, + SHARES, SHARING, SHOW, SIGNED, @@ -1026,6 +1051,7 @@ define_keywords!( STABLE, STAGE, STAGES, + STAGE_FILE_FORMAT, START, STARTS, STATEMENT, @@ -1053,6 +1079,7 @@ define_keywords!( STORED, STRAIGHT_JOIN, STREAM, + STREAMS, STRICT, STRING, STRUCT, @@ -1085,6 +1112,7 @@ define_keywords!( TABLESPACE, TABLE_FORMAT, TAG, + TAGS, TARGET, TARGET_LAG, TASK, @@ -1156,6 +1184,7 @@ define_keywords!( UNCACHE, UNCOMMITTED, UNDEFINED, + UNDROP, UNFREEZE, UNION, UNIQUE, @@ -1178,6 +1207,7 @@ define_keywords!( USE, USER, USER_RESOURCES, + USER_SPECIFIED, USING, USMALLINT, UTINYINT, diff --git a/src/parser/alter.rs b/src/parser/alter.rs index 4000eb26ba..9625d871bf 100644 --- a/src/parser/alter.rs +++ b/src/parser/alter.rs @@ -259,7 +259,7 @@ impl Parser<'_> { }; let set_tag = if self.parse_keywords(&[Keyword::SET, Keyword::TAG]) { - self.parse_key_value_options(false, &[])? + self.parse_key_value_options(false, &[], false)? } else { KeyValueOptions { delimiter: KeyValueOptionsDelimiter::Comma, @@ -277,7 +277,7 @@ impl Parser<'_> { }; let set_props = if self.parse_keyword(Keyword::SET) { - self.parse_key_value_options(false, &[])? + self.parse_key_value_options(false, &[], false)? } else { KeyValueOptions { delimiter: KeyValueOptionsDelimiter::Comma, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b44490b461..e52ead45e3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -650,6 +650,7 @@ impl<'a> Parser<'a> { Keyword::CREATE => self.parse_create(), Keyword::CACHE => self.parse_cache_table(), Keyword::DROP => self.parse_drop(), + Keyword::UNDROP => self.parse_undrop(), Keyword::DISCARD => self.parse_discard(), Keyword::DECLARE => self.parse_declare(), Keyword::FETCH => self.parse_fetch_statement(), @@ -1063,6 +1064,19 @@ impl<'a> Parser<'a> { _ => {} } + // A `PUT` / `GET` file-transfer statement inside a scripting body. + // These are recognised so the block parses, but never tokenize + // cleanly: an unquoted `file://` path triggers the Snowflake `//` + // single-line-comment rule, which eats the statement's `;` + // terminator and the rest of the line. Scan the raw token stream + // (comments included) to the terminator rather than parse operands, + // and skip the shared `;` expectation below. + if let Some(get) = self.peek_put_get_files() { + self.consume_put_get_files(); + values.push(Statement::PutGetFiles { get }); + continue; + } + let stmt = if self.peek_keyword(Keyword::LET) { self.next_token(); // consume LET let name = self.parse_identifier()?; @@ -1090,9 +1104,12 @@ impl<'a> Parser<'a> { && self.peek_nth_token_ref(1).token == Token::Assignment { // bare assignment: var := expr + // Accepts `(EXECUTE IMMEDIATE …)` / `(SHOW …)` payloads the same + // way a RESULTSET declaration initializer does, so a dynamic + // query can be assigned to an existing RESULTSET variable. let target = self.parse_identifier()?; self.expect_token(&Token::Assignment)?; - let value = self.parse_expr()?; + let value = self.parse_snowflake_declaration_payload_expr()?; Statement::Assignment { target, value } } else if let Some(kind) = loop_control_keyword(&self.peek_nth_token_ref(0).token) { // Loop-control statements only make sense inside a scripting @@ -1119,6 +1136,54 @@ impl<'a> Parser<'a> { Ok(values) } + /// If the next scripting statement is a `PUT` / `GET` file-transfer + /// statement, return `Some(get)` (`get = true` for `GET`). A `GET` + /// followed by `STACKED` / `DIAGNOSTICS` is `GET DIAGNOSTICS`, not a + /// file transfer, and yields `None`. + fn peek_put_get_files(&self) -> Option { + let Token::Word(w) = &self.peek_nth_token_ref(0).token else { + return None; + }; + if w.quote_style.is_some() { + return None; + } + if w.value.eq_ignore_ascii_case("PUT") { + return Some(false); + } + if w.keyword == Keyword::GET { + if let Token::Word(next) = &self.peek_nth_token_ref(1).token { + if next.value.eq_ignore_ascii_case("STACKED") + || next.value.eq_ignore_ascii_case("DIAGNOSTICS") + { + return None; + } + } + return Some(true); + } + None + } + + /// Consume the raw tokens of a `PUT` / `GET` file-transfer statement up to + /// and including its terminator — either a real `;` (quoted paths keep it) + /// or the `//` single-line comment that swallowed it (unquoted `file://` + /// paths), scanning whitespace/comment tokens directly. + fn consume_put_get_files(&mut self) { + while let Some(tok) = self.tokens.get(self.index) { + match &tok.token { + Token::EOF => break, + Token::SemiColon => { + self.index += 1; + break; + } + Token::Whitespace(Whitespace::SingleLineComment { .. }) => { + self.index += 1; + break; + } + _ => self.index += 1, + } + } + } + /// Parse a `RAISE` statement. /// /// See [Statement::Raise] @@ -1334,7 +1399,8 @@ impl<'a> Parser<'a> { /// Parse `TRUNCATE` statement. pub fn parse_truncate(&mut self) -> Result { - let table = self.parse_keyword(Keyword::TABLE); + let materialized_view = self.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEW]); + let table = !materialized_view && self.parse_keyword(Keyword::TABLE); let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); let table_names = self.parse_comma_separated(|p| { @@ -1376,6 +1442,7 @@ impl<'a> Parser<'a> { table_names, partitions, table, + materialized_view, if_exists, identity, cascade, @@ -1886,16 +1953,21 @@ impl<'a> Parser<'a> { // callee name (an `ObjectNamePart::Function`) and the trailing // `(args)` is the actual call — mirror the object-name grammar // so the resulting `Expr::Function` matches the CALL path shape. - if self.dialect.is_identifier_generating_function_name(&ident, &[]) { + if self + .dialect + .is_identifier_generating_function_name(&ident, &[]) + { let checkpoint = self.index; self.expect_token(&Token::LParen)?; - let args = self - .parse_comma_separated0(Self::parse_function_args, Token::RParen)?; + let args = + self.parse_comma_separated0(Self::parse_function_args, Token::RParen)?; self.expect_token(&Token::RParen)?; if self.peek_token_ref().token == Token::LParen { - let name = ObjectName(vec![ObjectNamePart::Function( - ObjectNamePartFunction { name: ident, args }, - )]); + let name = + ObjectName(vec![ObjectNamePart::Function(ObjectNamePartFunction { + name: ident, + args, + })]); return self.parse_function(name); } self.index = checkpoint; @@ -4275,46 +4347,31 @@ impl<'a> Parser<'a> { let negated = self.parse_keyword(Keyword::NOT); let regexp = self.parse_keyword(Keyword::REGEXP); let rlike = self.parse_keyword(Keyword::RLIKE); - let null = if !self.in_column_definition_state() { - self.parse_keyword(Keyword::NULL) - } else { - false - }; if regexp || rlike { - Ok(Expr::RLike { + return Ok(Expr::RLike { negated, expr: Box::new(expr), pattern: Box::new( self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?, ), regexp, - }) - } else if negated && null { + }); + } + let null = if !self.in_column_definition_state() { + self.parse_keyword(Keyword::NULL) + } else { + false + }; + if negated && null { Ok(Expr::IsNotNull(Box::new(expr))) } else if self.parse_keyword(Keyword::IN) { self.parse_in(expr, negated) } else if self.parse_keyword(Keyword::BETWEEN) { self.parse_between(expr, negated) } else if self.parse_keyword(Keyword::LIKE) { - Ok(Expr::Like { - negated, - any: self.parse_keyword(Keyword::ANY), - expr: Box::new(expr), - pattern: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?, - ), - escape_char: self.parse_escape_char()?, - }) + self.parse_like_expr(negated, false, expr) } else if self.parse_keyword(Keyword::ILIKE) { - Ok(Expr::ILike { - negated, - any: self.parse_keyword(Keyword::ANY), - expr: Box::new(expr), - pattern: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?, - ), - escape_char: self.parse_escape_char()?, - }) + self.parse_like_expr(negated, true, expr) } else if self.parse_keywords(&[Keyword::SIMILAR, Keyword::TO]) { Ok(Expr::SimilarTo { negated, @@ -4377,6 +4434,54 @@ impl<'a> Parser<'a> { } } + /// Parse the tail of a `LIKE` / `ILIKE` predicate after the keyword has + /// been consumed. Handles Snowflake's `{ANY|ALL} (, ..., )` + /// multi-pattern list form as well as the ordinary single-pattern form. + fn parse_like_expr( + &mut self, + negated: bool, + ilike: bool, + expr: Expr, + ) -> Result { + let quantifier = self.parse_one_of_keywords(&[Keyword::ANY, Keyword::ALL]); + if let Some(kw) = quantifier { + if self.consume_token(&Token::LParen) { + let patterns = self.parse_comma_separated0(Parser::parse_expr, Token::RParen)?; + self.expect_token(&Token::RParen)?; + return Ok(Expr::LikeAnyAll { + negated, + ilike, + all: kw == Keyword::ALL, + expr: Box::new(expr), + patterns, + escape_char: self.parse_escape_char()?, + }); + } + // `ANY` without a parenthesized list falls back to the legacy + // single-pattern form (`ALL` has no single-pattern meaning here). + } + let any = quantifier == Some(Keyword::ANY); + let pattern = Box::new(self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?); + let escape_char = self.parse_escape_char()?; + if ilike { + Ok(Expr::ILike { + negated, + any, + expr: Box::new(expr), + pattern, + escape_char, + }) + } else { + Ok(Expr::Like { + negated, + any, + expr: Box::new(expr), + pattern, + escape_char, + }) + } + } + /// Parse the `ESCAPE CHAR` portion of `LIKE`, `ILIKE`, and `SIMILAR TO` pub fn parse_escape_char(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::ESCAPE) { @@ -5005,7 +5110,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(expected) { Ok(self.get_current_token().clone()) } else { - self.expected_ref(format!("{:?}", &expected).as_str(), self.peek_token_ref()) + self.expected_ref(format!("{:?}", expected).as_str(), self.peek_token_ref()) } } @@ -5018,7 +5123,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(expected) { Ok(()) } else { - self.expected_ref(format!("{:?}", &expected).as_str(), self.peek_token_ref()) + self.expected_ref(format!("{:?}", expected).as_str(), self.peek_token_ref()) } } @@ -5452,9 +5557,17 @@ impl<'a> Parser<'a> { self.parse_create_procedure(or_alter, or_replace) } else if self.parse_keyword(Keyword::SCHEMA) { self.parse_create_schema(or_replace, transient) + } else if self.parse_keyword(Keyword::ROLE) { + self.parse_create_role(or_replace).map(Into::into) + } else if self.parse_keyword(Keyword::SEQUENCE) { + self.parse_create_sequence(or_replace, temporary) + } else if self.parse_keyword(Keyword::STREAM) { + self.parse_create_stream(or_replace) + } else if self.parse_keyword(Keyword::PIPE) { + self.parse_create_pipe(or_replace) } else if or_replace { self.expected_ref( - "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or WAREHOUSE or TASK or PROCEDURE or SCHEMA after CREATE OR REPLACE", + "[EXTERNAL] TABLE or [MATERIALIZED] VIEW or FUNCTION or WAREHOUSE or TASK or PROCEDURE or SCHEMA or ROLE or SEQUENCE after CREATE OR REPLACE", self.peek_token_ref(), ) } else if self.parse_keyword(Keyword::EXTENSION) { @@ -5466,11 +5579,11 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::VIRTUAL) { self.parse_create_virtual_table() } else if self.parse_keyword(Keyword::DATABASE) { - self.parse_create_database() - } else if self.parse_keyword(Keyword::ROLE) { - self.parse_create_role().map(Into::into) - } else if self.parse_keyword(Keyword::SEQUENCE) { - self.parse_create_sequence(temporary) + if self.parse_keyword(Keyword::ROLE) { + self.parse_create_database_role(or_replace) + } else { + self.parse_create_database() + } } else if self.parse_keyword(Keyword::COLLATION) { self.parse_create_collation().map(Into::into) } else if self.parse_keyword(Keyword::TYPE) { @@ -5497,11 +5610,11 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = self.parse_identifier()?; let options = self - .parse_key_value_options(false, &[Keyword::WITH, Keyword::TAG])? + .parse_key_value_options(false, &[Keyword::WITH, Keyword::TAG], true)? .options; let with_tags = self.parse_keyword(Keyword::WITH); let tags = if self.parse_keyword(Keyword::TAG) { - self.parse_key_value_options(true, &[])?.options + self.parse_key_value_options(true, &[], false)?.options } else { vec![] }; @@ -5521,13 +5634,125 @@ impl<'a> Parser<'a> { }) } + /// `CREATE [OR REPLACE] STREAM [IF NOT EXISTS] ON { TABLE | VIEW } ` + /// `[ { AT | BEFORE } ( => ) ]` + fn parse_create_stream(&mut self, or_replace: bool) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + self.expect_keyword(Keyword::ON)?; + let source_kind = if self.parse_keyword(Keyword::VIEW) { + StreamSourceKind::View + } else { + self.expect_keyword(Keyword::TABLE)?; + StreamSourceKind::Table + }; + let source_table = self.parse_object_name(false)?; + // Optional `{ AT | BEFORE } ( => )` clause, kept whole as + // the function-call expression (the same shape `TableVersion::Function` + // uses for a table-version anchor). + let at_before = if self.peek_keyword(Keyword::AT) || self.peek_keyword(Keyword::BEFORE) { + let func_name = self.parse_object_name(false)?; + Some(self.parse_function(func_name)?) + } else { + None + }; + // Optional `APPEND_ONLY = { TRUE | FALSE }`, which must follow the + // `{ AT | BEFORE }` clause per Snowflake's grammar. + let append_only = if self.parse_keyword(Keyword::APPEND_ONLY) { + self.expect_token(&Token::Eq)?; + Some(self.parse_boolean_string()?) + } else { + None + }; + Ok(Statement::CreateStream { + or_replace, + if_not_exists, + name, + source_kind, + source_table, + at_before, + append_only, + }) + } + + /// `CREATE [OR REPLACE] PIPE [IF NOT EXISTS] + /// [AUTO_INGEST = { TRUE | FALSE }] [ERROR_INTEGRATION = ] + /// [AWS_SNS_TOPIC = ''] [INTEGRATION = ''] [COMMENT = ''] + /// AS ` + fn parse_create_pipe(&mut self, or_replace: bool) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let mut auto_ingest: Option = None; + let mut error_integration: Option = None; + let mut aws_sns_topic: Option = None; + let mut integration: Option = None; + let mut comment: Option = None; + + loop { + if self.parse_keyword(Keyword::AS) { + let copy_statement = Box::new(self.parse_statement()?); + return Ok(Statement::CreatePipe { + or_replace, + if_not_exists, + name, + auto_ingest, + error_integration, + aws_sns_topic, + integration, + comment, + copy_statement, + }); + } + if self.parse_keyword(Keyword::AUTO_INGEST) { + self.expect_token(&Token::Eq)?; + auto_ingest = Some(self.parse_boolean_string()?); + } else if self.parse_keyword(Keyword::ERROR_INTEGRATION) { + self.expect_token(&Token::Eq)?; + error_integration = Some(self.parse_identifier()?); + } else if self.parse_keyword(Keyword::AWS_SNS_TOPIC) { + self.expect_token(&Token::Eq)?; + aws_sns_topic = Some(self.parse_literal_string()?); + } else if self.parse_keyword(Keyword::INTEGRATION) { + self.expect_token(&Token::Eq)?; + integration = Some(self.parse_literal_string()?); + } else if self.parse_keyword(Keyword::COMMENT) { + self.expect_token(&Token::Eq)?; + comment = Some(self.parse_literal_string()?); + } else { + return self.expected( + "AUTO_INGEST, ERROR_INTEGRATION, AWS_SNS_TOPIC, INTEGRATION, COMMENT, or AS in CREATE PIPE", + self.peek_token(), + ); + } + } + } + fn parse_create_warehouse(&mut self, or_replace: bool) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = self.parse_object_name(false)?; - // Skip any warehouse parameters (SIZE, MAX_CLUSTER_COUNT, etc.) + // Skip any warehouse parameters (SIZE, MAX_CLUSTER_COUNT, etc.) up to the + // trailing `WITH TAG ( = '' [, ...])` clause, which is trailing-only: + // no parameter may follow it. + let mut with_tags = Vec::new(); loop { match self.peek_token().token { Token::SemiColon | Token::EOF => break, + Token::Word(w) if w.keyword == Keyword::WITH => { + self.next_token(); + self.expect_keyword(Keyword::TAG)?; + self.expect_token(&Token::LParen)?; + with_tags = self.parse_comma_separated(Parser::parse_tag)?; + self.expect_token(&Token::RParen)?; + match self.peek_token().token { + Token::SemiColon | Token::EOF => break, + _ => { + return self.expected( + "end of statement after WITH TAG (...)", + self.peek_token(), + ) + } + } + } _ => { self.next_token(); } @@ -5537,6 +5762,7 @@ impl<'a> Parser<'a> { or_replace, if_not_exists, name, + with_tags, }) } @@ -5820,6 +6046,21 @@ impl<'a> Parser<'a> { let with_managed_access = self.parse_keywords(&[Keyword::WITH, Keyword::MANAGED, Keyword::ACCESS]); + // Snowflake inline `[ WITH ] TAG ( = '', ... )` clause. The + // optional `WITH` shares its keyword with the Trino option list below, + // so intercept `WITH TAG` (and the bare `TAG`) here; `parse_keywords` + // backtracks when `TAG` does not follow, leaving `WITH (k='v')` intact. + let with_tags = if self.parse_keywords(&[Keyword::WITH, Keyword::TAG]) + || self.parse_keyword(Keyword::TAG) + { + self.expect_token(&Token::LParen)?; + let tags = self.parse_comma_separated(Parser::parse_tag)?; + self.expect_token(&Token::RParen)?; + Some(tags) + } else { + None + }; + let with = if !with_managed_access && self.peek_keyword(Keyword::WITH) { Some(self.parse_options(Keyword::WITH)?) } else { @@ -5851,6 +6092,7 @@ impl<'a> Parser<'a> { default_collate_spec, clone, comment, + with_tags, }) } @@ -6042,6 +6284,7 @@ impl<'a> Parser<'a> { body.called_on_null = Some(FunctionCalledOnNull::Strict); } let mut set_params: Vec = Vec::new(); + let mut options: Vec = Vec::new(); loop { fn ensure_not_set(field: &Option, name: &str) -> Result<(), ParserError> { if field.is_some() { @@ -6135,6 +6378,10 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::RETURN) { ensure_not_set(&body.function_body, "RETURN")?; body.function_body = Some(CreateFunctionBody::Return(self.parse_expr()?)); + } else if dialect_of!(self is SnowflakeDialect | GenericDialect) + && self.peek_snowflake_function_property() + { + options.push(self.parse_snowflake_function_property()?); } else { break; } @@ -6158,11 +6405,44 @@ impl<'a> Parser<'a> { if_not_exists: false, using: None, determinism_specifier: None, - options: None, + options: if options.is_empty() { + None + } else { + Some(options) + }, remote_connection: None, }) } + /// True when the next token is a Snowflake Python-UDF property name + /// (`RUNTIME_VERSION`, `HANDLER`, `IMPORTS`, `PACKAGES`). None of these are + /// reserved keywords, so they are recognised by their bare-word spelling. + fn peek_snowflake_function_property(&self) -> bool { + matches!(&self.peek_token_ref().token, Token::Word(w) + if w.quote_style.is_none() + && matches!( + w.value.to_ascii_uppercase().as_str(), + "RUNTIME_VERSION" | "HANDLER" | "IMPORTS" | "PACKAGES" + )) + } + + /// Parse a Snowflake Python-UDF property clause `key = value` or + /// `key = ('a' [, 'b' …])` into an [`SqlOption::KeyValue`]. Parenthesised + /// lists are stored as an [`Expr::Tuple`]. + fn parse_snowflake_function_property(&mut self) -> Result { + let key = self.parse_identifier()?; + self.expect_token(&Token::Eq)?; + let value = if self.peek_token_ref().token == Token::LParen { + self.expect_token(&Token::LParen)?; + let values = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + Expr::Tuple(values) + } else { + self.parse_expr()? + }; + Ok(SqlOption::KeyValue { key, value }) + } + /// Parse `CREATE FUNCTION` for [Hive] /// /// [Hive]: https://cwiki.apache.org/confluence/display/hive/languagemanual+ddl#LanguageManualDDL-Create/Drop/ReloadFunction @@ -7071,10 +7351,23 @@ impl<'a> Parser<'a> { } /// Parse a `CREATE ROLE` statement. - pub fn parse_create_role(&mut self) -> Result { + pub fn parse_create_role(&mut self, or_replace: bool) -> Result { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let names = self.parse_comma_separated(|p| p.parse_object_name(false))?; + // Snowflake: trailing `WITH TAG ( = '' [, ...] )`. It is + // trailing-only, so no other role option may follow. `WITH TAG` is + // consumed atomically here; a bare `WITH` (or `WITH `) is left + // for the generic option loop below. + let mut with_tags = Vec::new(); + if dialect_of!(self is SnowflakeDialect) + && self.parse_keywords(&[Keyword::WITH, Keyword::TAG]) + { + self.expect_token(&Token::LParen)?; + with_tags = self.parse_comma_separated(Parser::parse_tag)?; + self.expect_token(&Token::RParen)?; + } + let _ = self.parse_keyword(Keyword::WITH); // [ WITH ] let optional_keywords = if dialect_of!(self is MsSqlDialect) { @@ -7275,6 +7568,7 @@ impl<'a> Parser<'a> { Ok(CreateRole { names, + or_replace, if_not_exists, login, inherit, @@ -7292,6 +7586,29 @@ impl<'a> Parser<'a> { user, admin, authorization_owner, + with_tags, + }) + } + + /// Parse a Snowflake `CREATE [OR REPLACE] DATABASE ROLE` statement. The + /// leading `DATABASE ROLE` keywords have already been consumed. + pub fn parse_create_database_role( + &mut self, + or_replace: bool, + ) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let comment = if self.parse_keyword(Keyword::COMMENT) { + self.expect_token(&Token::Eq)?; + Some(self.parse_literal_string()?) + } else { + None + }; + Ok(Statement::CreateDatabaseRole { + or_replace, + if_not_exists, + name, + comment, }) } @@ -7761,7 +8078,11 @@ impl<'a> Parser<'a> { let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); - let object_type = if self.parse_keyword(Keyword::TABLE) { + let object_type = if self.parse_keywords(&[Keyword::DYNAMIC, Keyword::TABLE]) { + ObjectType::DynamicTable + } else if self.parse_keyword(Keyword::TABLE) + || self.parse_keywords(&[Keyword::ICEBERG, Keyword::TABLE]) + { ObjectType::Table } else if self.parse_keyword(Keyword::COLLATION) { ObjectType::Collation @@ -7776,7 +8097,11 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::SCHEMA) { ObjectType::Schema } else if self.parse_keyword(Keyword::DATABASE) { - ObjectType::Database + if self.parse_keyword(Keyword::ROLE) { + ObjectType::DatabaseRole + } else { + ObjectType::Database + } } else if self.parse_keyword(Keyword::SEQUENCE) { ObjectType::Sequence } else if self.parse_keyword(Keyword::STAGE) { @@ -7793,6 +8118,8 @@ impl<'a> Parser<'a> { return self.parse_drop_account(); } else if self.parse_keyword(Keyword::TASK) { ObjectType::Task + } else if self.parse_keyword(Keyword::PIPE) { + ObjectType::Pipe } else if self.parse_keyword(Keyword::FUNCTION) { return self.parse_drop_function().map(Into::into); } else if self.parse_keyword(Keyword::POLICY) { @@ -7859,6 +8186,33 @@ impl<'a> Parser<'a> { }) } + /// Parse a Snowflake `UNDROP ` statement. + /// + /// Grammar for the whole UNDROP family (`TABLE`, `DYNAMIC TABLE`, + /// `SCHEMA`, `DATABASE`) plus `VIEW`, which parses so it can be rejected + /// downstream with Snowflake's unsupported-feature error rather than a + /// parse error. + pub fn parse_undrop(&mut self) -> Result { + let object_type = if self.parse_keywords(&[Keyword::DYNAMIC, Keyword::TABLE]) { + ObjectType::DynamicTable + } else if self.parse_keyword(Keyword::TABLE) { + ObjectType::Table + } else if self.parse_keyword(Keyword::VIEW) { + ObjectType::View + } else if self.parse_keyword(Keyword::SCHEMA) { + ObjectType::Schema + } else if self.parse_keyword(Keyword::DATABASE) { + ObjectType::Database + } else { + return self.expected_ref( + "DATABASE, DYNAMIC TABLE, SCHEMA, TABLE or VIEW after UNDROP", + self.peek_token_ref(), + ); + }; + let name = self.parse_object_name(false)?; + Ok(Statement::Undrop { object_type, name }) + } + fn parse_optional_drop_behavior(&mut self) -> Option { match self.parse_one_of_keywords(&[Keyword::CASCADE, Keyword::RESTRICT]) { Some(Keyword::CASCADE) => Some(DropBehavior::Cascade), @@ -9609,7 +9963,25 @@ impl<'a> Parser<'a> { if let Some(constraint) = self.parse_optional_table_constraint()? { constraints.push(constraint); } else if let Token::Word(_) = &self.peek_token_ref().token { - columns.push(self.parse_column_def()?); + // A bare column name with no data type (`CREATE TABLE t(id) AS + // SELECT ...`) is only accepted when the dialect opts in and the + // name is immediately followed by a column-list terminator, so a + // genuinely malformed data type still surfaces its own error. + if self.dialect.supports_create_table_optional_column_type() + && matches!( + self.peek_nth_token_ref(1).token, + Token::Comma | Token::RParen + ) + { + let name = self.parse_identifier()?; + columns.push(ColumnDef { + name, + data_type: DataType::Unspecified, + options: vec![], + }); + } else { + columns.push(self.parse_column_def()?); + } } else { return self.expected_ref( "column name or constraint definition", @@ -10188,18 +10560,64 @@ impl<'a> Parser<'a> { && self.parse_keywords(&[Keyword::NOT, Keyword::ENFORCED]) { cc.enforced = Some(false); - } else { + } else if !self.parse_informational_constraint_property(&mut cc) { break; } } - if cc.deferrable.is_some() || cc.initially.is_some() || cc.enforced.is_some() { - Ok(Some(cc)) - } else { + if cc == ConstraintCharacteristics::default() { Ok(None) + } else { + Ok(Some(cc)) } } + /// Parse one of the informational constraint properties `{ ENABLE | DISABLE }`, + /// `{ VALIDATE | NOVALIDATE }` or `{ RELY | NORELY }`, returning whether one was + /// consumed. Only dialects opting in via + /// [`Dialect::supports_informational_constraint_properties`] accept them, as + /// `ENABLE`, `DISABLE` and `VALIDATE` are keywords used elsewhere. + fn parse_informational_constraint_property( + &mut self, + cc: &mut ConstraintCharacteristics, + ) -> bool { + if !self.dialect.supports_informational_constraint_properties() { + return false; + } + + if cc.enabled.is_none() { + if self.parse_keyword(Keyword::ENABLE) { + cc.enabled = Some(true); + return true; + } + if self.parse_keyword(Keyword::DISABLE) { + cc.enabled = Some(false); + return true; + } + } + if cc.validated.is_none() { + if self.parse_keyword(Keyword::VALIDATE) { + cc.validated = Some(true); + return true; + } + if self.parse_keyword(Keyword::NOVALIDATE) { + cc.validated = Some(false); + return true; + } + } + if cc.rely.is_none() { + if self.parse_keyword(Keyword::RELY) { + cc.rely = Some(true); + return true; + } + if self.parse_keyword(Keyword::NORELY) { + cc.rely = Some(false); + return true; + } + } + false + } + /// Parse an optional table constraint (e.g. `PRIMARY KEY`, `UNIQUE`, `FOREIGN KEY`, `CHECK`). pub fn parse_optional_table_constraint( &mut self, @@ -10704,6 +11122,18 @@ impl<'a> Parser<'a> { Ok(AlterTableOperation::AlterSortKey { columns }) } + /// Peek whether the upcoming tokens are a bare ` COMMENT ...` + /// continuation of a comma-separated `ALTER COLUMN ... COMMENT` list, i.e. + /// with the `COLUMN` keyword omitted. This shape is unambiguous against + /// every other `ALTER TABLE` operation, which are all keyword-led. + fn peek_bare_column_comment_continuation(&self) -> bool { + matches!(self.peek_nth_token(0).token, Token::Word(_)) + && matches!( + self.peek_nth_token(1).token, + Token::Word(w) if w.keyword == Keyword::COMMENT + ) + } + /// Parse a single `ALTER TABLE` operation and return an `AlterTableOperation`. pub fn parse_alter_table_operation(&mut self) -> Result { let operation = if self.parse_keyword(Keyword::ADD) { @@ -10744,15 +11174,38 @@ impl<'a> Parser<'a> { false }; - let column_def = self.parse_column_def()?; + if self.dialect.supports_comma_separated_add_column_list() { + let mut column_defs = vec![self.parse_column_def()?]; + while self.consume_token(&Token::Comma) { + column_defs.push(self.parse_column_def()?); + } + if column_defs.len() == 1 { + let column_def = column_defs.swap_remove(0); + let column_position = self.parse_column_position()?; + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + column_position, + } + } else { + AlterTableOperation::AddColumns { + column_keyword, + if_not_exists, + column_defs, + } + } + } else { + let column_def = self.parse_column_def()?; - let column_position = self.parse_column_position()?; + let column_position = self.parse_column_position()?; - AlterTableOperation::AddColumn { - column_keyword, - if_not_exists, - column_def, - column_position, + AlterTableOperation::AddColumn { + column_keyword, + if_not_exists, + column_def, + column_position, + } } } } @@ -10918,6 +11371,16 @@ impl<'a> Parser<'a> { AlterTableOperation::DropProjection { if_exists, name } } else if self.parse_keywords(&[Keyword::CLUSTERING, Keyword::KEY]) { AlterTableOperation::DropClusteringKey + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::ROW, + Keyword::ACCESS, + Keyword::POLICIES, + ]) { + AlterTableOperation::DropAllRowAccessPolicies + } else if self.parse_keywords(&[Keyword::ROW, Keyword::ACCESS, Keyword::POLICY]) { + let policy_name = self.parse_object_name(false)?; + AlterTableOperation::DropRowAccessPolicy { policy_name } } else { let has_column_keyword = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); @@ -10969,19 +11432,42 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::MODIFY) { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let col_name = self.parse_identifier()?; - let data_type = self.parse_data_type()?; - let mut options = vec![]; - while let Some(option) = self.parse_optional_column_option()? { - options.push(option); - } + if let Some(op) = self.maybe_parse_column_masking_policy()? { + AlterTableOperation::AlterColumn { + column_name: col_name, + op, + } + } else { + let data_type = self.parse_data_type()?; + let mut options = vec![]; + while let Some(option) = self.parse_optional_column_option()? { + options.push(option); + } - let column_position = self.parse_column_position()?; + let column_position = self.parse_column_position()?; - AlterTableOperation::ModifyColumn { - col_name, - data_type, - options, - column_position, + AlterTableOperation::ModifyColumn { + col_name, + data_type, + options, + column_position, + } + } + } else if self.dialect.supports_alter_column_comment() + && (self.parse_keyword(Keyword::COLUMN) || self.peek_bare_column_comment_continuation()) + { + // Continuation of a comma-separated `ALTER COLUMN ... COMMENT` list, + // e.g. `... ALTER COLUMN c1 COMMENT 's1', COLUMN c2 COMMENT 's2'`. + // The second and later items carry `COLUMN` without a leading `ALTER`, + // and Snowflake also accepts the bare form with `COLUMN` omitted: + // `... ALTER c1 COMMENT 's1', c2 COMMENT 's2'`. + let column_name = self.parse_identifier()?; + self.expect_keyword_is(Keyword::COMMENT)?; + AlterTableOperation::AlterColumn { + column_name, + op: AlterColumnOperation::Comment { + comment: self.parse_literal_string()?, + }, } } else if self.parse_keyword(Keyword::ALTER) { if self.peek_keyword(Keyword::SORTKEY) { @@ -11011,6 +11497,12 @@ impl<'a> Parser<'a> { self.parse_set_data_type(true)? } else if self.parse_keyword(Keyword::TYPE) { self.parse_set_data_type(false)? + } else if self.dialect.supports_alter_column_comment() + && self.parse_keyword(Keyword::COMMENT) + { + AlterColumnOperation::Comment { + comment: self.parse_literal_string()?, + } } else if self.parse_keywords(&[Keyword::ADD, Keyword::GENERATED]) { let generated_as = if self.parse_keyword(Keyword::ALWAYS) { Some(GeneratedAs::Always) @@ -11034,6 +11526,8 @@ impl<'a> Parser<'a> { generated_as, sequence_options, } + } else if let Some(op) = self.maybe_parse_column_masking_policy()? { + op } else { let message = if is_postgresql { "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE, or ADD GENERATED after ALTER COLUMN" @@ -11184,6 +11678,35 @@ impl<'a> Parser<'a> { Ok(operation) } + /// Try to parse a Snowflake column masking-policy operation + /// (`SET MASKING POLICY

[USING (, ...)] [FORCE]` or + /// `UNSET MASKING POLICY`) following the column name in an `ALTER TABLE` + /// / `ALTER VIEW` `{MODIFY|ALTER} COLUMN` clause. Returns `None` if the + /// upcoming tokens are not a masking-policy operation, leaving the parser + /// position unchanged. + fn maybe_parse_column_masking_policy( + &mut self, + ) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::SET, Keyword::MASKING, Keyword::POLICY]) { + let policy_name = self.parse_object_name(false)?; + let using_columns = if self.parse_keyword(Keyword::USING) { + Some(self.parse_parenthesized_column_list(Mandatory, false)?) + } else { + None + }; + let force = self.parse_keyword(Keyword::FORCE); + Ok(Some(AlterColumnOperation::SetMaskingPolicy { + policy_name, + using_columns, + force, + })) + } else if self.parse_keywords(&[Keyword::UNSET, Keyword::MASKING, Keyword::POLICY]) { + Ok(Some(AlterColumnOperation::UnsetMaskingPolicy)) + } else { + Ok(None) + } + } + fn parse_set_data_type(&mut self, had_set: bool) -> Result { let data_type = self.parse_data_type()?; let using = if self.dialect.supports_alter_column_type_using() @@ -11232,6 +11755,9 @@ impl<'a> Parser<'a> { Keyword::WAREHOUSE, Keyword::ACCOUNT, Keyword::TASK, + Keyword::STREAM, + Keyword::SEQUENCE, + Keyword::PIPE, ])?; match object_type { Keyword::SCHEMA => { @@ -11283,9 +11809,12 @@ impl<'a> Parser<'a> { Keyword::WAREHOUSE => self.parse_alter_warehouse(), Keyword::ACCOUNT => self.parse_alter_account(), Keyword::TASK => self.parse_alter_task(), + Keyword::STREAM => self.parse_alter_stream(), + Keyword::SEQUENCE => self.parse_alter_sequence(), + Keyword::PIPE => self.parse_alter_pipe(), // unreachable because expect_one_of_keywords used above unexpected_keyword => Err(ParserError::ParserError( - format!("Internal parser error: expected any of {{VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR, WAREHOUSE, ACCOUNT, TASK}}, got {unexpected_keyword:?}"), + format!("Internal parser error: expected any of {{VIEW, TYPE, COLLATION, TABLE, INDEX, FUNCTION, AGGREGATE, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR, WAREHOUSE, ACCOUNT, TASK, STREAM, SEQUENCE, PIPE}}, got {unexpected_keyword:?}"), )), } } @@ -11622,6 +12151,104 @@ impl<'a> Parser<'a> { }) } + /// Parse `ALTER STREAM [IF EXISTS] { SET COMMENT = '' | UNSET COMMENT }`. + pub fn parse_alter_stream(&mut self) -> Result { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = self.parse_object_name(false)?; + let operation = if self.parse_keyword(Keyword::SET) { + self.expect_keyword(Keyword::COMMENT)?; + self.expect_token(&Token::Eq)?; + AlterStreamOperation::SetComment(self.parse_literal_string()?) + } else if self.parse_keyword(Keyword::UNSET) { + self.expect_keyword(Keyword::COMMENT)?; + AlterStreamOperation::UnsetComment + } else { + return self.expected("SET or UNSET after ALTER STREAM", self.peek_token()); + }; + Ok(Statement::AlterStream { + if_exists, + name, + operation, + }) + } + + /// Parse `ALTER PIPE [IF EXISTS] + /// { SET