From b33da30ba98fc0d5d42ba73f746fe39125d136a3 Mon Sep 17 00:00:00 2001 From: Przemyslaw Denkiewicz Date: Wed, 12 Aug 2026 15:36:11 +0200 Subject: [PATCH] =?UTF-8?q?Snowflake:=20role=20tagging=20grammar=20?= =?UTF-8?q?=E2=80=94=20ALTER=20ROLE=20[IF=20EXISTS]=20SET/UNSET=20TAG,=20C?= =?UTF-8?q?REATE=20ROLE=20...=20WITH=20TAG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route ALTER ROLE { 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. Add if_exists to Statement::SetTags (also enables ALTER DATABASE|SCHEMA|ROLE IF EXISTS ... SET TAG). Add a trailing-only WITH TAG (...) clause to CREATE ROLE via a new with_tags field. Co-Authored-By: Claude Opus 4.8 --- src/ast/dcl.rs | 8 +++++++- src/ast/mod.rs | 9 ++++++++- src/dialect/snowflake.rs | 13 +++++++++++++ src/parser/mod.rs | 14 ++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index fcc15c7da..6eb528ca9 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; @@ -349,6 +349,9 @@ 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 { @@ -424,6 +427,9 @@ 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/mod.rs b/src/ast/mod.rs index 5fdfab535..e8493c890 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5405,6 +5405,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`. @@ -8023,11 +8025,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..ae1712d05 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -377,6 +377,17 @@ impl Dialect for SnowflakeDialect { return Some(Ok(stmt)); } + // ALTER ROLE [IF EXISTS] { SET TAG | UNSET TAG } — intercept only + // the tag form (mirroring ALTER SCHEMA); every other ALTER ROLE form + // (RENAME TO, SET/UNSET COMMENT) fails the closure and falls through to + // the generic grammar. + if let Ok(Some(stmt)) = parser.maybe_parse(|p| { + p.expect_keywords(&[Keyword::ALTER, Keyword::ROLE])?; + parse_alter_object_set_tags(p, ObjectType::Role) + }) { + return Some(Ok(stmt)); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::STAGE]) { // ALTER STAGE return Some(parse_alter_stage(parser)); @@ -3331,6 +3342,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 +3374,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..13077ff56 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7316,6 +7316,19 @@ impl<'a> Parser<'a> { 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) { @@ -7534,6 +7547,7 @@ impl<'a> Parser<'a> { user, admin, authorization_owner, + with_tags, }) }