From 1949f6190e44c43ac26115a5d555cc599056f0ed Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:39:45 +0000 Subject: [PATCH 1/2] fix(syntax): route Snowflake ALTER APPLICATION ROLE and DATABASE ROLE to the role parser The ALTER APPLICATION and ALTER DATABASE matchers took the kind word before checking for a following ROLE, so role actions such as RENAME TO and SET COMMENT were refused. --- .../_helpers/ddl/snowflake_alter_objects.rs | 5 +++++ .../sqlquack-syntax/tests/parser/test_fixes.rs | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs index 3e051aae8..a80fd680b 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs @@ -25,6 +25,11 @@ type Parts = (Vec, Vec); impl Parser { /// Parse a Snowflake ALTER of a kind this module owns, or None when the next words name another kind. pub(crate) fn parse_alter_snowflake_objects(&mut self) -> Result> { + if self.check_text_seq(&["APPLICATION", "ROLE"]) + || self.check_text_seq(&["DATABASE", "ROLE"]) + { + return Ok(None); + } if self.match_text_seq(&["APPLICATION", "PACKAGE"]) { return self.parse_alter_application_package().map(Some); } diff --git a/crates/sqlquack-syntax/tests/parser/test_fixes.rs b/crates/sqlquack-syntax/tests/parser/test_fixes.rs index 0b5a3ddee..397bc8529 100644 --- a/crates/sqlquack-syntax/tests/parser/test_fixes.rs +++ b/crates/sqlquack-syntax/tests/parser/test_fixes.rs @@ -779,6 +779,24 @@ fn given_a_snowflake_alter_of_an_application_database_or_service_kind_when_parsi dialect: Dialect::Snowflake, expected_accepted: false, }, + AcceptanceTestCase { + description: "a database role is no database (recorded accepted)", + sql: "ALTER DATABASE ROLE v1 RENAME TO v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a database role unsets a tag (recorded accepted)", + sql: "ALTER DATABASE ROLE IF EXISTS v1 UNSET TAG v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an application role is no application (recorded accepted)", + sql: "ALTER APPLICATION ROLE v1 SET COMMENT = 'm' ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, ]; for test_case in test_cases { From 2f0c38a72e97be921c79cbea598b1cf4a04ae8fc Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:03:39 +0000 Subject: [PATCH 2/2] fix(syntax): parse Snowflake ALTER APPLICATION ROLE and DATABASE ROLE by their grammar Both kinds take RENAME TO, SET or UNSET; when one of those follows the kind, Snowflake reads ROLE as the application or database name. All 109 receipted role cases agree: 58 parse and 51 are refused. --- .../_helpers/ddl/snowflake_alter_objects.rs | 27 +++++++++++++++--- .../tests/parser/test_fixes.rs | 28 +++++++++++++++---- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs index a80fd680b..45655ecca 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs @@ -25,10 +25,17 @@ type Parts = (Vec, Vec); impl Parser { /// Parse a Snowflake ALTER of a kind this module owns, or None when the next words name another kind. pub(crate) fn parse_alter_snowflake_objects(&mut self) -> Result> { - if self.check_text_seq(&["APPLICATION", "ROLE"]) - || self.check_text_seq(&["DATABASE", "ROLE"]) - { - return Ok(None); + for kind in ["APPLICATION", "DATABASE"] { + if self.check_text_seq(&[kind, "ROLE"]) + && !self + .text_at(2) + .is_some_and(|word| matches!(word.as_str(), "RENAME" | "SET" | "UNSET")) + { + self.current += 2; + return self + .parse_alter_scoped_role(&format!("{kind} ROLE")) + .map(Some); + } } if self.match_text_seq(&["APPLICATION", "PACKAGE"]) { return self.parse_alter_application_package().map(Some); @@ -680,6 +687,18 @@ impl Parser { Ok(alter_object("BACKUP SET", head, action, parts)) } + /// Parse ALTER APPLICATION ROLE or ALTER DATABASE ROLE after its kind. + fn parse_alter_scoped_role(&mut self, kind: &str) -> Result { + let head = self.parse_alter_object_head()?; + let action = self.expect_alter_words(&[&["RENAME"], &["SET"], &["UNSET"]])?; + let parts = match action.as_str() { + "RENAME" => self.parse_alter_target("TO")?, + "SET" => self.parse_alter_set(true)?, + _ => self.parse_alter_set(false)?, + }; + Ok(alter_object(kind, head, action, parts)) + } + /// Consume the first of `words` that comes next and return it. fn match_one_of(&mut self, words: &[&str]) -> Option { let word = words.iter().find(|word| self.check_text_seq(&[word]))?; diff --git a/crates/sqlquack-syntax/tests/parser/test_fixes.rs b/crates/sqlquack-syntax/tests/parser/test_fixes.rs index 9dadf0fc4..107b92262 100644 --- a/crates/sqlquack-syntax/tests/parser/test_fixes.rs +++ b/crates/sqlquack-syntax/tests/parser/test_fixes.rs @@ -1276,23 +1276,41 @@ fn given_a_snowflake_alter_of_an_application_database_or_service_kind_when_parsi expected_accepted: false, }, AcceptanceTestCase { - description: "a database role is no database (recorded accepted)", + description: "a database role renames (recorded errno 2003, after parsing)", sql: "ALTER DATABASE ROLE v1 RENAME TO v1 ;", dialect: Dialect::Snowflake, expected_accepted: true, }, AcceptanceTestCase { - description: "a database role unsets a tag (recorded accepted)", - sql: "ALTER DATABASE ROLE IF EXISTS v1 UNSET TAG v1 ;", + description: "a database role unsets a tag (recorded errno 2003, after parsing)", + sql: "ALTER DATABASE ROLE \"SELECT\" UNSET TAG v1 ;", dialect: Dialect::Snowflake, expected_accepted: true, }, AcceptanceTestCase { - description: "an application role is no application (recorded accepted)", - sql: "ALTER APPLICATION ROLE v1 SET COMMENT = 'm' ;", + description: "an application role sets a comment (recorded accepted)", + sql: "ALTER APPLICATION ROLE IF EXISTS v1 SET COMMENT = 'm' ;", dialect: Dialect::Snowflake, expected_accepted: true, }, + AcceptanceTestCase { + description: "with no role name, ROLE names the database (recorded errno 2003)", + sql: "ALTER DATABASE ROLE RENAME TO v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an application role needs a property to unset (recorded errno 1003)", + sql: "ALTER APPLICATION ROLE v1 UNSET ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a database role name is no string (recorded errno 1003)", + sql: "ALTER DATABASE ROLE 'v1' RENAME TO v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, ]; for test_case in test_cases {