From e40bdea7e372764c4206702e20a3141bff5610ae Mon Sep 17 00:00:00 2001 From: Wojciech Padlo Date: Mon, 8 Jun 2026 15:19:18 +0200 Subject: [PATCH 1/2] Snowflake: parse ALTER STAGE (SET / RENAME TO) Co-Authored-By: Claude Opus 4.8 --- src/ast/mod.rs | 78 +++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 93 +++++++++++++++++++--- tests/sqlparser_snowflake.rs | 146 +++++++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+), 10 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1839c4b9b..c16d189c0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4766,6 +4766,18 @@ pub enum Statement { comment: Option, }, /// ```sql + /// ALTER STAGE [IF EXISTS] { SET ... | RENAME TO } + /// ``` + /// See + AlterStage { + /// Stage name. + name: ObjectName, + /// `IF EXISTS` flag. + if_exists: bool, + /// The alter operation. + operation: AlterStageOperation, + }, + /// ```sql /// CREATE [OR REPLACE] WAREHOUSE [IF NOT EXISTS] /// ``` CreateWarehouse { @@ -6839,6 +6851,17 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::AlterStage { + name, + if_exists, + operation, + } => { + write!( + f, + "ALTER STAGE {if_exists}{name} {operation}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } Statement::CreateWarehouse { or_replace, if_not_exists, @@ -12138,6 +12161,61 @@ impl fmt::Display for AlterFileFormatOperation { } } +/// Operations for `ALTER STAGE`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterStageOperation { + /// `RENAME TO ` + RenameTo(ObjectName), + /// `SET ` — the same property groups as `CREATE STAGE`. + Set { + /// Internal/external stage parameters (URL, STORAGE_INTEGRATION, + /// CREDENTIALS, ENCRYPTION, ENDPOINT). + stage_params: StageParamsObject, + /// Directory table parameters (`DIRECTORY = (...)`). + directory_table_params: KeyValueOptions, + /// File format options (`FILE_FORMAT = (...)`). + file_format: KeyValueOptions, + /// Copy options (`COPY_OPTIONS = (...)`). + copy_options: KeyValueOptions, + /// Optional `COMMENT` value. + comment: Option, + }, +} + +impl fmt::Display for AlterStageOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterStageOperation::RenameTo(name) => { + write!(f, "RENAME TO {name}") + } + AlterStageOperation::Set { + stage_params, + directory_table_params, + file_format, + copy_options, + comment, + } => { + write!(f, "SET{stage_params}")?; + if !directory_table_params.options.is_empty() { + write!(f, " DIRECTORY=({directory_table_params})")?; + } + if !file_format.options.is_empty() { + write!(f, " FILE_FORMAT=({file_format})")?; + } + if !copy_options.options.is_empty() { + write!(f, " COPY_OPTIONS=({copy_options})")?; + } + if let Some(comment) = comment { + write!(f, " COMMENT='{comment}'")?; + } + Ok(()) + } + } + } +} + /// A ` = ` pair inside `ALTER WAREHOUSE … SET …`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0d057ae39..84649476c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -462,6 +462,7 @@ impl Spanned for Statement { Statement::CreateProcedure { .. } => Span::empty(), Statement::CreateMacro { .. } => Span::empty(), Statement::CreateStage { .. } => Span::empty(), + Statement::AlterStage { .. } => Span::empty(), Statement::Assert { .. } => Span::empty(), Statement::Grant { .. } => Span::empty(), Statement::Deny { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 3a1bce5d4..455909bec 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -27,8 +27,8 @@ use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ - AlterExternalVolumeOperation, AlterFileFormatOperation, AlterTable, AlterTableOperation, - AlterTableType, + AlterExternalVolumeOperation, AlterFileFormatOperation, AlterStageOperation, AlterTable, + AlterTableOperation, AlterTableType, CatalogRestAuthentication, CatalogRestConfig, CatalogSource, CatalogSyncNamespaceMode, CatalogTableFormat, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTable, CreateTableLikeKind, DollarQuotedString, @@ -289,6 +289,11 @@ impl Dialect for SnowflakeDialect { return Some(parse_alter_file_format(parser)); } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::STAGE]) { + // ALTER STAGE + return Some(parse_alter_stage(parser)); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { @@ -1285,14 +1290,50 @@ pub fn parse_create_stage( //[ IF NOT EXISTS ] let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = parser.parse_object_name(false)?; + + let StageProperties { + stage_params, + directory_table_params, + file_format, + copy_options, + comment, + } = parse_stage_properties(parser)?; + + Ok(Statement::CreateStage { + or_replace, + temporary, + if_not_exists, + name, + stage_params, + directory_table_params, + file_format, + copy_options, + comment, + }) +} + +/// The shared property groups parsed by both `CREATE STAGE` and +/// `ALTER STAGE ... SET`. +struct StageProperties { + stage_params: StageParamsObject, + directory_table_params: KeyValueOptions, + file_format: KeyValueOptions, + copy_options: KeyValueOptions, + comment: Option, +} + +/// Parse the stage property groups (`internalStageParams`/`externalStageParams`, +/// `DIRECTORY`, `FILE_FORMAT`, `COPY_OPTIONS`, `COMMENT`) shared by +/// `CREATE STAGE` and `ALTER STAGE ... SET`. +fn parse_stage_properties(parser: &mut Parser) -> Result { + // [ internalStageParams | externalStageParams ] + let stage_params = parse_stage_params(parser)?; + let mut directory_table_params = Vec::new(); let mut file_format = Vec::new(); let mut copy_options = Vec::new(); let mut comment = None; - // [ internalStageParams | externalStageParams ] - let stage_params = parse_stage_params(parser)?; - // [ directoryTableParams ] if parser.parse_keyword(Keyword::DIRECTORY) { parser.expect_token(&Token::Eq)?; @@ -1317,11 +1358,7 @@ pub fn parse_create_stage( comment = Some(parser.parse_comment_value()?); } - Ok(Statement::CreateStage { - or_replace, - temporary, - if_not_exists, - name, + Ok(StageProperties { stage_params, directory_table_params: KeyValueOptions { options: directory_table_params, @@ -1339,6 +1376,42 @@ pub fn parse_create_stage( }) } +/// Parse `ALTER STAGE [IF EXISTS] { SET ... | RENAME TO }` +fn parse_alter_stage(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::RENAME, Keyword::TO]) { + AlterStageOperation::RenameTo(parser.parse_object_name(false)?) + } else if parser.parse_keyword(Keyword::SET) { + let StageProperties { + stage_params, + directory_table_params, + file_format, + copy_options, + comment, + } = parse_stage_properties(parser)?; + AlterStageOperation::Set { + stage_params, + directory_table_params, + file_format, + copy_options, + comment, + } + } else { + return parser.expected( + "SET or RENAME TO after ALTER STAGE ", + parser.peek_token(), + ); + }; + + Ok(Statement::AlterStage { + name, + if_exists, + operation, + }) +} + pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result { let mut ident = String::new(); while let Some(next_token) = parser.next_token_no_skip() { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 6fd2528e7..0ec3d0971 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -7443,6 +7443,152 @@ fn test_alter_file_format_unset_no_options_rejected() { .expect_err("UNSET requires at least one option"); } +#[test] +fn test_alter_stage_rename() { + match snowflake().verified_stmt("ALTER STAGE s RENAME TO t") { + Statement::AlterStage { + name, + if_exists, + operation, + } => { + assert_eq!("s", name.to_string()); + assert!(!if_exists); + match operation { + AlterStageOperation::RenameTo(target) => { + assert_eq!("t", target.to_string()); + } + other => panic!("expected RenameTo, got {other:?}"), + } + } + _ => unreachable!(), + } +} + +#[test] +fn test_alter_stage_rename_if_exists_cross_schema() { + match snowflake().verified_stmt("ALTER STAGE IF EXISTS db.sch.s RENAME TO db.sch.t") { + Statement::AlterStage { + name, + if_exists, + operation, + } => { + assert_eq!("db.sch.s", name.to_string()); + assert!(if_exists); + match operation { + AlterStageOperation::RenameTo(target) => { + assert_eq!("db.sch.t", target.to_string()); + } + other => panic!("expected RenameTo, got {other:?}"), + } + } + _ => unreachable!(), + } +} + +#[test] +fn test_alter_stage_set_stage_params() { + let sql = concat!( + "ALTER STAGE my_ext_stage SET ", + "URL='s3://load/files/' ", + "STORAGE_INTEGRATION=myint ", + "CREDENTIALS=(AWS_KEY_ID='1a2b3c' AWS_SECRET_KEY='4x5y6z') ", + "ENCRYPTION=(MASTER_KEY='key' TYPE='AWS_SSE_KMS')" + ); + match snowflake().verified_stmt(sql) { + Statement::AlterStage { + name, + if_exists, + operation, + } => { + assert_eq!("my_ext_stage", name.to_string()); + assert!(!if_exists); + match operation { + AlterStageOperation::Set { + stage_params, + comment, + .. + } => { + assert_eq!("s3://load/files/", stage_params.url.unwrap()); + assert_eq!("myint", stage_params.storage_integration.unwrap()); + assert!(stage_params.credentials.options.contains(&KeyValueOption { + option_name: "AWS_KEY_ID".to_string(), + option_value: KeyValueOptionKind::Single( + Value::SingleQuotedString("1a2b3c".to_string()).with_empty_span() + ), + })); + assert!(stage_params.encryption.options.contains(&KeyValueOption { + option_name: "TYPE".to_string(), + option_value: KeyValueOptionKind::Single( + Value::SingleQuotedString("AWS_SSE_KMS".to_string()).with_empty_span() + ), + })); + assert!(comment.is_none()); + } + other => panic!("expected Set, got {other:?}"), + } + } + _ => unreachable!(), + } +} + +#[test] +fn test_alter_stage_set_file_format_copy_options_comment() { + let sql = concat!( + "ALTER STAGE IF EXISTS my_stage SET ", + "DIRECTORY=(ENABLE=true) ", + "FILE_FORMAT=(TYPE=CSV) ", + "COPY_OPTIONS=(ON_ERROR=CONTINUE) ", + "COMMENT='updated'" + ); + match snowflake().verified_stmt(sql) { + Statement::AlterStage { + if_exists, + operation, + .. + } => { + assert!(if_exists); + match operation { + AlterStageOperation::Set { + directory_table_params, + file_format, + copy_options, + comment, + .. + } => { + assert!(directory_table_params.options.contains(&KeyValueOption { + option_name: "ENABLE".to_string(), + option_value: KeyValueOptionKind::Single( + Value::Boolean(true).with_empty_span() + ), + })); + assert!(file_format.options.contains(&KeyValueOption { + option_name: "TYPE".to_string(), + option_value: KeyValueOptionKind::Single( + Value::Placeholder("CSV".to_string()).with_empty_span() + ), + })); + assert!(copy_options.options.contains(&KeyValueOption { + option_name: "ON_ERROR".to_string(), + option_value: KeyValueOptionKind::Single( + Value::Placeholder("CONTINUE".to_string()).with_empty_span() + ), + })); + assert_eq!(Some("updated".to_string()), comment); + } + other => panic!("expected Set, got {other:?}"), + } + } + _ => unreachable!(), + } +} + +#[test] +fn test_alter_stage_invalid_operation_rejected() { + snowflake() + .parse_sql_statements("ALTER STAGE s UNSET COMMENT") + .expect_err("ALTER STAGE only supports SET and RENAME TO"); +} + #[test] fn test_drop_file_format() { match snowflake().verified_stmt("DROP FILE FORMAT f") { From 855bcd95db94734c338b7f0e2d90ceb5bc7c04d3 Mon Sep 17 00:00:00 2001 From: Wojciech Padlo Date: Mon, 8 Jun 2026 16:25:09 +0200 Subject: [PATCH 2/2] Fix pre-existing CI failures on the snowflake branch lineage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These checks were already red on the base branch (lav-379-2-for-over-cursor), inherited from the cursor/scripting/ACCOUNT commits — unrelated to ALTER STAGE, but required for green CI on this PR: - codestyle: run cargo fmt --all across the crate - lint: collapse a match-in-match (parser scripting list), replace a forbidden unreachable! in DESCRIBE object-type parsing with a parse error, and collapse nested match arms in the warehouse/account tests - docs: drop unresolved intra-doc link brackets in scripting-list doc comments - compile-no-std: gate 'use alloc::vec' behind not(feature = std) in spans.rs Co-Authored-By: Claude Opus 4.8 --- src/ast/mod.rs | 30 ++------- src/ast/spans.rs | 34 +++++------ src/dialect/snowflake.rs | 32 +++++----- src/parser/mod.rs | 41 ++++++------- tests/sqlparser_snowflake.rs | 115 ++++++++++++++++++----------------- 5 files changed, 114 insertions(+), 138 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c16d189c0..d90848f77 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -7024,18 +7024,10 @@ impl fmt::Display for Statement { } write!(f, ")")?; if let Some(val) = allow_writes { - write!( - f, - " ALLOW_WRITES = {}", - if *val { "TRUE" } else { "FALSE" } - )?; + write!(f, " ALLOW_WRITES = {}", if *val { "TRUE" } else { "FALSE" })?; } if let Some(ref c) = comment { - write!( - f, - " COMMENT = '{}'", - value::escape_single_quote_string(c) - )?; + write!(f, " COMMENT = '{}'", value::escape_single_quote_string(c))?; } Ok(()) } @@ -7163,20 +7155,12 @@ impl fmt::Display for Statement { if let Some(ref auth) = rest_authentication { write!(f, " REST_AUTHENTICATION = ({auth})")?; } - write!( - f, - " ENABLED = {}", - if *enabled { "TRUE" } else { "FALSE" } - )?; + write!(f, " ENABLED = {}", if *enabled { "TRUE" } else { "FALSE" })?; if let Some(secs) = refresh_interval_seconds { write!(f, " REFRESH_INTERVAL_SECONDS = {secs}")?; } if let Some(ref c) = comment { - write!( - f, - " COMMENT = '{}'", - value::escape_single_quote_string(c) - )?; + write!(f, " COMMENT = '{}'", value::escape_single_quote_string(c))?; } Ok(()) } @@ -12466,11 +12450,7 @@ impl fmt::Display for CatalogRestConfig { write!(f, " CATALOG_API_TYPE = {t}")?; } if let Some(ref w) = self.warehouse { - write!( - f, - " WAREHOUSE = '{}'", - value::escape_single_quote_string(w) - )?; + write!(f, " WAREHOUSE = '{}'", value::escape_single_quote_string(w))?; } if let Some(ref m) = self.access_delegation_mode { write!(f, " ACCESS_DELEGATION_MODE = {m}")?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 84649476c..38449a2fb 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -23,6 +23,8 @@ use crate::{ }, tokenizer::TokenWithSpan, }; +#[cfg(not(feature = "std"))] +use alloc::vec; use core::iter; use crate::tokenizer::Span; @@ -35,20 +37,19 @@ use super::{ ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForIterationSource, ForStatement, ForValues, FromTable, Function, - FunctionArg, - FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, - HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr, - Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, - LoopControlStatement, LoopStatement, MatchRecognizePattern, Measure, Merge, MergeAction, - MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, NamedParenthesizedList, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, - PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, RaiseStatement, - RaiseStatementValue, ReferentialAction, RenameSelectItem, RepeatStatement, ReplaceSelectElement, - ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript, - SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, - TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, ViewColumnDef, - WhileStatement, WildcardAdditionalOptions, With, WithFill, + FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, + GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, + InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, + LimitClause, LoopControlStatement, LoopStatement, MatchRecognizePattern, Measure, Merge, + MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, + NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, + OnConflictAction, OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, + Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, + RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, RepeatStatement, + ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, + Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, + TableFactor, TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, + Use, Values, ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -832,10 +833,7 @@ impl Spanned for RepeatStatement { impl Spanned for LoopControlStatement { fn span(&self) -> Span { let LoopControlStatement { kind: _, label } = self; - label - .as_ref() - .map(|l| l.span) - .unwrap_or_else(Span::empty) + label.as_ref().map(|l| l.span).unwrap_or_else(Span::empty) } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 455909bec..f299bd83a 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -28,16 +28,16 @@ use crate::ast::helpers::stmt_data_loading::{ }; use crate::ast::{ AlterExternalVolumeOperation, AlterFileFormatOperation, AlterStageOperation, AlterTable, - AlterTableOperation, AlterTableType, - CatalogRestAuthentication, CatalogRestConfig, CatalogSource, CatalogSyncNamespaceMode, - CatalogTableFormat, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, - CopyIntoSnowflakeKind, CreateTable, CreateTableLikeKind, DollarQuotedString, - ExternalVolumeEncryption, ExternalVolumeStorageLocation, Ident, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - InitializeKind, Insert, MultiTableInsertIntoClause, MultiTableInsertType, MultiTableInsertValue, - MultiTableInsertValues, MultiTableInsertWhenClause, ObjectName, ObjectNamePart, - RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, StorageLifecyclePolicy, - StorageSerializationPolicy, TableObject, TagsColumnOption, Value, WrappedCollection, + AlterTableOperation, AlterTableType, CatalogRestAuthentication, CatalogRestConfig, + CatalogSource, CatalogSyncNamespaceMode, CatalogTableFormat, ColumnOption, ColumnPolicy, + ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTable, CreateTableLikeKind, + DollarQuotedString, ExternalVolumeEncryption, ExternalVolumeStorageLocation, Ident, + IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + IdentityPropertyOrder, InitializeKind, Insert, MultiTableInsertIntoClause, + MultiTableInsertType, MultiTableInsertValue, MultiTableInsertValues, + MultiTableInsertWhenClause, ObjectName, ObjectNamePart, RefreshModeKind, RowAccessPolicy, + ShowObjects, SqlOption, Statement, StorageLifecyclePolicy, StorageSerializationPolicy, + TableObject, TagsColumnOption, Value, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -319,7 +319,10 @@ impl Dialect for SnowflakeDialect { return Some(parse_drop_file_format(parser)); } - if parser.parse_one_of_keywords(&[Keyword::DESC, Keyword::DESCRIBE]).is_some() { + if parser + .parse_one_of_keywords(&[Keyword::DESC, Keyword::DESCRIBE]) + .is_some() + { if parser.parse_keywords(&[Keyword::EXTERNAL, Keyword::VOLUME]) { // DESC[RIBE] EXTERNAL VOLUME return Some(parse_describe_external_volume(parser)); @@ -2206,9 +2209,7 @@ fn parse_external_volume_storage_location( } let storage_base_url = storage_base_url.ok_or_else(|| { - ParserError::ParserError( - "STORAGE_BASE_URL is required in STORAGE_LOCATION".to_string(), - ) + ParserError::ParserError("STORAGE_BASE_URL is required in STORAGE_LOCATION".to_string()) })?; Ok(ExternalVolumeStorageLocation { @@ -2316,7 +2317,8 @@ fn parse_create_file_format( } // `LIKE` is mutually exclusive with `TYPE`/options per Snowflake's grammar. - if like_source.is_some() && !matches!(parser.peek_token().token, Token::EOF | Token::SemiColon) { + if like_source.is_some() && !matches!(parser.peek_token().token, Token::EOF | Token::SemiColon) + { return parser.expected( "end of statement after CREATE FILE FORMAT ... LIKE", parser.peek_token(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9f96e5208..67e27f886 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -979,7 +979,8 @@ impl<'a> Parser<'a> { } }; - let conditional_statements = self.parse_scripting_conditional_statements(terminal_keywords)?; + let conditional_statements = + self.parse_scripting_conditional_statements(terminal_keywords)?; Ok(ConditionalStatementBlock { start_token: AttachedToken(start_token), @@ -1013,13 +1014,13 @@ impl<'a> Parser<'a> { Ok(conditional_statements) } - /// Like [`parse_conditional_statements`], but the bare-sequence branch - /// dispatches to [`parse_scripting_statement_list`] so that body-only + /// Like `parse_conditional_statements`, but the bare-sequence branch + /// dispatches to `parse_scripting_statement_list` so that body-only /// scripting forms (loop control, `LET`, bare assignment) parse when the /// body omits an inner `BEGIN … END`. Used by Snowflake-scripting loop /// and conditional constructs; **not** used by `CREATE TRIGGER` / /// `CREATE PROCEDURE` bodies, which retain their existing semantics via - /// [`parse_conditional_statements`]. + /// `parse_conditional_statements`. fn parse_scripting_conditional_statements( &mut self, terminal_keywords: &[Keyword], @@ -1042,7 +1043,7 @@ impl<'a> Parser<'a> { /// Parse a list of statements inside a `BEGIN...END` scripting block. /// - /// Extends [`parse_statement_list`] with Snowflake-scripting-specific forms: + /// Extends `parse_statement_list` with Snowflake-scripting-specific forms: /// - Bare assignment: `var := expr` /// - `LET` declaration: `LET var [data_type] := expr` pub(crate) fn parse_scripting_statement_list( @@ -1053,10 +1054,10 @@ impl<'a> Parser<'a> { loop { match &self.peek_nth_token_ref(0).token { Token::EOF => break, - Token::Word(w) => { - if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { - break; - } + Token::Word(w) + if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) => + { + break } _ => {} } @@ -8160,12 +8161,7 @@ impl<'a> Parser<'a> { // The word could be a variable name. Check that the token // after it looks like the rest of a declaration (type, `:=`, // `DEFAULT`, or `;` for a bare declaration with no type/init). - matches!( - tok1, - Token::Assignment - | Token::Word(_) - | Token::SemiColon - ) + matches!(tok1, Token::Assignment | Token::Word(_) | Token::SemiColon) } _ => false, } @@ -11476,10 +11472,7 @@ impl<'a> Parser<'a> { })?; AlterAccountOperation::Set { params } } else { - return self.expected( - "SET or RENAME TO after ALTER ACCOUNT", - self.peek_token(), - ); + return self.expected("SET or RENAME TO after ALTER ACCOUNT", self.peek_token()); }; Ok(Statement::AlterAccount { name, operation }) @@ -12684,8 +12677,7 @@ impl<'a> Parser<'a> { let peek_token = parser.peek_token(); let span = peek_token.span; match peek_token.token { - Token::DollarQuotedString(s) - if dialect_of!(parser is PostgreSqlDialect | GenericDialect | SnowflakeDialect) => + Token::DollarQuotedString(s) if dialect_of!(parser is PostgreSqlDialect | GenericDialect | SnowflakeDialect) => { parser.next_token(); Ok(Expr::Value(Value::DollarQuotedString(s).with_span(span))) @@ -14493,7 +14485,10 @@ impl<'a> Parser<'a> { // Snowflake-style: DESC if self.dialect.describe_requires_table_keyword() - && matches!(describe_alias, DescribeAlias::Desc | DescribeAlias::Describe) + && matches!( + describe_alias, + DescribeAlias::Desc | DescribeAlias::Describe + ) { if let Some(kw) = self.parse_one_of_keywords(&[ Keyword::TABLE, @@ -14508,7 +14503,7 @@ impl<'a> Parser<'a> { Keyword::DATABASE => DescribeObjectType::Database, Keyword::SCHEMA => DescribeObjectType::Schema, Keyword::TASK => DescribeObjectType::Task, - _ => unreachable!(), + _ => return self.expected("a describe object type", self.peek_token()), }; let object_name = self.parse_object_name(false)?; return Ok(Statement::DescribeObject { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 0ec3d0971..f082169ae 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4793,8 +4793,7 @@ fn test_exception_handler_body_bare_assignment() { /// outer body. #[test] fn test_exception_handler_body_let_declaration() { - let sql = - "BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN LET t VARCHAR := 'caught'; RETURN t; END"; + let sql = "BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN LET t VARCHAR := 'caught'; RETURN t; END"; let stmts = snowflake().parse_sql_statements(sql).expect(sql); let Statement::StartTransaction { exception, .. } = stmts.into_iter().next().unwrap() else { panic!("expected StartTransaction"); @@ -5449,9 +5448,7 @@ fn test_create_external_volume_if_not_exists() { "(NAME = 'loc1' STORAGE_PROVIDER = 'S3' STORAGE_BASE_URL = 's3://bucket/'))", ); match snowflake().verified_stmt(sql) { - Statement::CreateExternalVolume { - if_not_exists, .. - } => { + Statement::CreateExternalVolume { if_not_exists, .. } => { assert!(if_not_exists); } _ => unreachable!(), @@ -5623,9 +5620,7 @@ fn test_alter_external_volume_add_storage_location() { fn test_alter_external_volume_set_allow_writes() { let sql = "ALTER EXTERNAL VOLUME my_vol SET ALLOW_WRITES = TRUE"; match snowflake().verified_stmt(sql) { - Statement::AlterExternalVolume { - operation, .. - } => { + Statement::AlterExternalVolume { operation, .. } => { assert_eq!( AlterExternalVolumeOperation::SetAllowWrites(true), operation @@ -5636,9 +5631,7 @@ fn test_alter_external_volume_set_allow_writes() { let sql = "ALTER EXTERNAL VOLUME my_vol SET ALLOW_WRITES = FALSE"; match snowflake().verified_stmt(sql) { - Statement::AlterExternalVolume { - operation, .. - } => { + Statement::AlterExternalVolume { operation, .. } => { assert_eq!( AlterExternalVolumeOperation::SetAllowWrites(false), operation @@ -5663,9 +5656,7 @@ fn test_alter_external_volume_if_exists() { fn test_alter_external_volume_remove_storage_location() { let sql = "ALTER EXTERNAL VOLUME my_vol REMOVE STORAGE_LOCATION 'loc1'"; match snowflake().verified_stmt(sql) { - Statement::AlterExternalVolume { - operation, .. - } => { + Statement::AlterExternalVolume { operation, .. } => { assert_eq!( AlterExternalVolumeOperation::RemoveStorageLocation("loc1".to_string()), operation @@ -6207,9 +6198,7 @@ fn test_alter_warehouse_resume_if_suspended() { let sql = "ALTER WAREHOUSE foo RESUME IF SUSPENDED"; match snowflake().verified_stmt(sql) { Statement::AlterWarehouse { - name, - operation, - .. + name, operation, .. } => { assert_eq!("foo", name.unwrap().to_string()); assert_eq!( @@ -6227,7 +6216,9 @@ fn test_alter_warehouse_resume_plain() { match snowflake().verified_stmt(sql) { Statement::AlterWarehouse { operation, .. } => { assert_eq!( - AlterWarehouseOperation::Resume { if_suspended: false }, + AlterWarehouseOperation::Resume { + if_suspended: false + }, operation ); } @@ -6239,12 +6230,12 @@ fn test_alter_warehouse_resume_plain() { fn test_alter_warehouse_rename_to() { let sql = "ALTER WAREHOUSE foo RENAME TO bar"; match snowflake().verified_stmt(sql) { - Statement::AlterWarehouse { operation, .. } => match operation { - AlterWarehouseOperation::RenameTo { new_name } => { - assert_eq!("bar", new_name.to_string()); - } - _ => unreachable!(), - }, + Statement::AlterWarehouse { + operation: AlterWarehouseOperation::RenameTo { new_name }, + .. + } => { + assert_eq!("bar", new_name.to_string()); + } _ => unreachable!(), } } @@ -6264,14 +6255,14 @@ fn test_alter_warehouse_abort_all_queries() { fn test_alter_warehouse_unset_multi() { let sql = "ALTER WAREHOUSE foo UNSET COMMENT, AUTO_SUSPEND"; match snowflake().verified_stmt(sql) { - Statement::AlterWarehouse { operation, .. } => match operation { - AlterWarehouseOperation::Unset { params } => { - assert_eq!(2, params.len()); - assert_eq!("COMMENT", params[0].to_string()); - assert_eq!("AUTO_SUSPEND", params[1].to_string()); - } - _ => unreachable!(), - }, + Statement::AlterWarehouse { + operation: AlterWarehouseOperation::Unset { params }, + .. + } => { + assert_eq!(2, params.len()); + assert_eq!("COMMENT", params[0].to_string()); + assert_eq!("AUTO_SUSPEND", params[1].to_string()); + } _ => unreachable!(), } } @@ -6280,14 +6271,14 @@ fn test_alter_warehouse_unset_multi() { fn test_alter_warehouse_set_multi_params() { let sql = "ALTER WAREHOUSE foo SET WAREHOUSE_SIZE = LARGE, AUTO_SUSPEND = 60"; match snowflake().verified_stmt(sql) { - Statement::AlterWarehouse { operation, .. } => match operation { - AlterWarehouseOperation::Set { params } => { - assert_eq!(2, params.len()); - assert_eq!("WAREHOUSE_SIZE", params[0].name.to_string()); - assert_eq!("AUTO_SUSPEND", params[1].name.to_string()); - } - _ => unreachable!(), - }, + Statement::AlterWarehouse { + operation: AlterWarehouseOperation::Set { params }, + .. + } => { + assert_eq!(2, params.len()); + assert_eq!("WAREHOUSE_SIZE", params[0].name.to_string()); + assert_eq!("AUTO_SUSPEND", params[1].name.to_string()); + } _ => unreachable!(), } } @@ -6355,14 +6346,14 @@ fn test_alter_account_set_named() { fn test_alter_account_set_multi() { let sql = "ALTER ACCOUNT acc1 SET STATEMENT_TIMEOUT_IN_SECONDS = 3600, TIMEZONE = 'UTC'"; match snowflake().verified_stmt(sql) { - Statement::AlterAccount { operation, .. } => match operation { - AlterAccountOperation::Set { params } => { - assert_eq!(2, params.len()); - assert_eq!("TIMEZONE", params[1].name.to_string()); - assert_eq!("'UTC'", params[1].value.to_string()); - } - _ => unreachable!(), - }, + Statement::AlterAccount { + operation: AlterAccountOperation::Set { params }, + .. + } => { + assert_eq!(2, params.len()); + assert_eq!("TIMEZONE", params[1].name.to_string()); + assert_eq!("'UTC'", params[1].value.to_string()); + } _ => unreachable!(), } } @@ -6656,7 +6647,9 @@ fn test_describe_task() { let sql = "DESCRIBE TASK foo"; match snowflake().verified_stmt(sql) { Statement::DescribeObject { - object_type, object_name, .. + object_type, + object_name, + .. } => { assert_eq!(DescribeObjectType::Task, object_type); assert_eq!("foo", object_name.to_string()); @@ -6775,9 +6768,7 @@ fn test_for_reverse_to_end_for() { fn test_for_in_cursor_end_for() { let sql = "FOR rec IN cur DO RETURN rec.price; END FOR"; match snowflake().verified_stmt(sql) { - Statement::For(ForStatement { - var, iteration, .. - }) => { + Statement::For(ForStatement { var, iteration, .. }) => { assert_eq!(var.value, "rec"); match iteration { ForIterationSource::Cursor(source) => { @@ -7113,7 +7104,8 @@ fn test_create_file_format_or_replace() { #[test] fn test_create_file_format_if_not_exists_with_options() { let sql = "CREATE FILE FORMAT IF NOT EXISTS f TYPE = CSV FIELD_DELIMITER = '|' SKIP_HEADER = 1"; - let canonical = "CREATE FILE FORMAT IF NOT EXISTS f TYPE = CSV FIELD_DELIMITER='|' SKIP_HEADER=1"; + let canonical = + "CREATE FILE FORMAT IF NOT EXISTS f TYPE = CSV FIELD_DELIMITER='|' SKIP_HEADER=1"; match snowflake().one_statement_parses_to(sql, canonical) { Statement::CreateFileFormat { if_not_exists, @@ -7180,7 +7172,8 @@ fn test_create_file_format_temp_synonym() { "CREATE TEMP FILE FORMAT f TYPE = CSV", "CREATE VOLATILE FILE FORMAT f TYPE = CSV", ] { - match snowflake().one_statement_parses_to(sql, "CREATE TEMPORARY FILE FORMAT f TYPE = CSV") { + match snowflake().one_statement_parses_to(sql, "CREATE TEMPORARY FILE FORMAT f TYPE = CSV") + { Statement::CreateFileFormat { temporary, .. } => assert!(temporary), _ => unreachable!(), } @@ -7213,7 +7206,10 @@ fn test_create_file_format_like_one_part() { assert_eq!("f", name.to_string()); assert!(format_type.is_none()); assert!(options.options.is_empty()); - assert_eq!(Some("other".to_string()), like_source.map(|s| s.to_string())); + assert_eq!( + Some("other".to_string()), + like_source.map(|s| s.to_string()) + ); } _ => unreachable!(), } @@ -7401,7 +7397,10 @@ fn test_alter_file_format_unset_multiple() { unset_comment, } => { assert_eq!( - vec![Ident::new("FIELD_DELIMITER"), Ident::new("RECORD_DELIMITER")], + vec![ + Ident::new("FIELD_DELIMITER"), + Ident::new("RECORD_DELIMITER") + ], options ); assert!(!unset_comment); @@ -7623,7 +7622,9 @@ fn test_describe_file_format() { #[test] fn test_desc_file_format() { - match snowflake().one_statement_parses_to("DESC FILE FORMAT db.sch.f", "DESCRIBE FILE FORMAT db.sch.f") { + match snowflake() + .one_statement_parses_to("DESC FILE FORMAT db.sch.f", "DESCRIBE FILE FORMAT db.sch.f") + { Statement::DescribeFileFormat { name } => { assert_eq!("db.sch.f", name.to_string()); }