From e18153f4bda305cc49e8a3ef3318e3dd9e7692a7 Mon Sep 17 00:00:00 2001 From: hovaesco Date: Wed, 12 Aug 2026 15:09:30 +0200 Subject: [PATCH] =?UTF-8?q?Snowflake:=20warehouse=20tagging=20grammar=20?= =?UTF-8?q?=E2=80=94=20ALTER=20WAREHOUSE=20[IF=20EXISTS]=20SET/UNSET=20TAG?= =?UTF-8?q?,=20CREATE=20WAREHOUSE=20...=20WITH=20TAG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route ALTER WAREHOUSE { SET | UNSET } TAG through parse_alter_object_set_tags (mirroring the ALTER SCHEMA maybe_parse form) so the tag form requires a name and non-tag forms keep falling through to parse_alter_warehouse. Add if_exists to Statement::SetTags (also enables ALTER DATABASE|SCHEMA IF EXISTS ... SET TAG). Add a trailing-only WITH TAG (...) clause to CREATE WAREHOUSE via a new with_tags field. Co-Authored-By: Claude Opus 4.8 --- src/ast/mod.rs | 18 ++++++++++++++++-- src/dialect/snowflake.rs | 14 ++++++++++++++ src/parser/mod.rs | 22 +++++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5fdfab535..0d72cc226 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4976,6 +4976,8 @@ 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 } @@ -5405,6 +5407,8 @@ pub enum Statement { 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`. @@ -7589,13 +7593,18 @@ 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, @@ -8023,11 +8032,16 @@ impl fmt::Display for Statement { Statement::SetTags { object_type, object_name, + if_exists, unset, set_tags, unset_tags, } => { - write!(f, "ALTER {object_type} {object_name} ")?; + 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 { diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 0090f0bda..e87bf906f 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -377,6 +377,18 @@ impl Dialect for SnowflakeDialect { return Some(Ok(stmt)); } + // ALTER WAREHOUSE [IF EXISTS] { SET TAG | UNSET TAG } — intercept + // only the tag form. Every other ALTER WAREHOUSE form (SET , + // UNSET, SUSPEND, RESUME, RENAME TO, ABORT ALL QUERIES) fails the closure + // and falls through to Parser::parse_alter_warehouse, which also keeps + // the bare (name-less) form a syntax error — the tag form requires a name. + if let Ok(Some(stmt)) = parser.maybe_parse(|p| { + p.expect_keywords(&[Keyword::ALTER, Keyword::WAREHOUSE])?; + parse_alter_object_set_tags(p, ObjectType::Warehouse) + }) { + return Some(Ok(stmt)); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::STAGE]) { // ALTER STAGE return Some(parse_alter_stage(parser)); @@ -3331,6 +3343,7 @@ 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 = match parser.expect_one_of_keywords(&[Keyword::SET, Keyword::UNSET])? { Keyword::UNSET => true, @@ -3362,6 +3375,7 @@ fn parse_alter_object_set_tags( Ok(Statement::SetTags { object_type, object_name, + if_exists, unset, set_tags, unset_tags, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ef771a2de..e0ce3b26b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5727,10 +5727,29 @@ impl<'a> Parser<'a> { 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(); } @@ -5740,6 +5759,7 @@ impl<'a> Parser<'a> { or_replace, if_not_exists, name, + with_tags, }) }