diff --git a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs index 29520d987..bb4745e3e 100644 --- a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs +++ b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs @@ -1188,7 +1188,7 @@ pub struct CreateObject { pub if_not_exists: bool, /// The object name pub name: TableRef, - /// The source's keywords: "BACKUP SET", "SHARE", "REPLICA" or "ARCHIVE" + /// The source's keywords: "BACKUP SET", "SHARE", "REPLICA" or "ARCHIVE"; empty without one pub source: String, /// The source's operands in order: names, an identifier string or a condition #[serde(default)] diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_create_clone.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_create_clone.rs index 623ec47ee..3d02e0bbf 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_create_clone.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_create_clone.rs @@ -260,6 +260,33 @@ impl Parser { return Err(self.parse_error("Expected IF NOT EXISTS")); } let name = self.parse_object_name()?; + if matches!(kind, "DATABASE ROLE") && !self.check_identifier("CLONE") { + let mut properties: Vec = Vec::new(); + if self.match_token(TokenType::Comment) { + self.expect_token(TokenType::Eq)?; + let comment = if self.is_identifier_token() && !self.check_keyword() { + self.parse_primary()? + } else { + self.parse_string_value()? + }; + properties.push(property("COMMENT", None, Some(comment))); + } + if !self.is_at_end() && !self.check(TokenType::Semicolon) { + return Err( + self.parse_error("Expected CLONE or COMMENT after CREATE DATABASE ROLE ") + ); + } + return Ok(Some(Expression::CreateObject(Box::new(CreateObject { + kind: kind.to_owned(), + modifier, + or_replace, + if_not_exists, + name, + source: String::new(), + operands: Vec::new(), + properties, + })))); + } if !self.match_identifier("CLONE") { let lone_word = self.is_identifier_token() && !self.check_keyword() @@ -295,7 +322,7 @@ impl Parser { } /// Whether DATABASE ROLE starts a role; alone or before CLONE, ROLE names a database. - fn names_a_database_role(&self) -> bool { + pub(crate) fn names_a_database_role(&self) -> bool { self.check_text_seq(&["DATABASE", "ROLE"]) && self.tokens.get(self.current + 2).is_some_and(|t| { t.token_type != TokenType::Semicolon && !t.text.eq_ignore_ascii_case("CLONE") diff --git a/crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs b/crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs index c20dc1ae0..44affb7eb 100644 --- a/crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs +++ b/crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs @@ -38,6 +38,30 @@ fn given_a_snowflake_clone_or_replica_when_parsing_then_only_documented_forms_ar dialect: Dialect::Snowflake, expected_accepted: true, }, + AcceptanceTestCase { + description: "a database role is created by name (recorded success)", + sql: "CREATE DATABASE ROLE v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a database role takes OR REPLACE, IF NOT EXISTS and a comment (recorded success)", + sql: "CREATE OR REPLACE DATABASE ROLE IF NOT EXISTS v1 COMMENT = 'm' ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a database role takes no second name (recorded errno 1003)", + sql: "CREATE DATABASE ROLE v1 v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a database role comment needs a value (recorded errno 1003)", + sql: "CREATE DATABASE ROLE v1 COMMENT = ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, AcceptanceTestCase { description: "a database named ROLE clones (recorded errno 2003, after parsing)", sql: "CREATE DATABASE ROLE CLONE v1 ;",