diff --git a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs index 9b0424ec6..29520d987 100644 --- a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs +++ b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs @@ -282,6 +282,9 @@ impl AlterIndex { pub struct CreateSchema { /// Schema name parts, possibly dot-qualified (e.g. [mydb, hr] for "mydb.hr") pub name: Vec, + /// OR REPLACE (Snowflake) + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub or_replace: bool, pub if_not_exists: bool, pub authorization: Option, /// CLONE source parts, possibly dot-qualified @@ -302,6 +305,7 @@ impl CreateSchema { pub fn new(name: impl Into) -> Self { Self { name: vec![Identifier::new(name)], + or_replace: false, if_not_exists: false, authorization: None, clone_from: None, @@ -356,6 +360,9 @@ impl DropNamespace { #[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CreateDatabase { pub name: Identifier, + /// OR REPLACE (Snowflake) + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub or_replace: bool, pub if_not_exists: bool, pub options: Vec, /// Snowflake CLONE source @@ -384,6 +391,7 @@ impl CreateDatabase { pub fn new(name: impl Into) -> Self { Self { name: Identifier::new(name), + or_replace: false, if_not_exists: false, options: Vec::new(), clone_from: None, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs index e20765603..9c5b288da 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs @@ -1209,6 +1209,7 @@ impl Parser { pub(crate) fn parse_create_schema( &mut self, leading_comments: Vec, + or_replace: bool, ) -> Result { self.expect_token(TokenType::Schema)?; @@ -1271,6 +1272,7 @@ impl Parser { Ok(Expression::CreateSchema(Box::new(CreateSchema { name, + or_replace, if_not_exists, authorization, clone_from, @@ -1299,12 +1301,16 @@ impl Parser { } /// Parse CREATE DATABASE statement - pub(crate) fn parse_create_database(&mut self) -> Result { + pub(crate) fn parse_create_database(&mut self, or_replace: bool) -> Result { self.expect_token(TokenType::Database)?; let if_not_exists = self.match_keywords(&[TokenType::If, TokenType::Not, TokenType::Exists]); - let name = self.parse_unquoted_identifier(Self::expect_identifier)?; + let name = if self.is_snowflake() { + self.parse_quoting_identifier(Self::expect_identifier)? + } else { + self.parse_unquoted_identifier(Self::expect_identifier)? + }; let clone_from = if self.match_identifier("CLONE") { Some(self.expect_identifier_or_safe_keyword_with_quoted()?) @@ -1378,6 +1384,7 @@ impl Parser { Ok(Expression::CreateDatabase(Box::new(CreateDatabase { name, + or_replace, if_not_exists, options, clone_from, 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 07150ef25..623ec47ee 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 @@ -57,7 +57,7 @@ impl Parser { if self.match_token(TokenType::Eq) { self.expect_token(TokenType::LParen)?; let mut kinds: Vec = Vec::new(); - loop { + while !self.check(TokenType::RParen) || !kinds.is_empty() { let kind = self.expect_alter_words(&[&["COMMENTS"], &["TAGS"], &["GRANTS"]])?; kinds.push(Expression::Identifier(Identifier::new(kind))); @@ -234,7 +234,8 @@ impl Parser { None if !temporary && self.match_text_seq(&["FILE", "FORMAT"]) => { ("FILE FORMAT", CloneOptions::Bare) } - None if !temporary && self.match_text_seq(&["DATABASE", "ROLE"]) => { + None if !temporary && self.names_a_database_role() => { + self.match_text_seq(&["DATABASE", "ROLE"]); ("DATABASE ROLE", CloneOptions::Bare) } _ => { @@ -293,6 +294,14 @@ impl Parser { })))) } + /// Whether DATABASE ROLE starts a role; alone or before CLONE, ROLE names a database. + 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") + }) + } + /// Whether the rest of the statement contains REPLICA or AS OF, which make it a replica. fn statement_names_a_replica(&self) -> bool { self.tokens[self.current..] diff --git a/crates/sqlquack-syntax/src/parser/_helpers/statements/dml.rs b/crates/sqlquack-syntax/src/parser/_helpers/statements/dml.rs index 92cc1809b..35b7f0aec 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/statements/dml.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/statements/dml.rs @@ -357,8 +357,8 @@ impl Parser { } } TokenType::Index => self.parse_create_index_with_clustered(false, None), - TokenType::Schema => self.parse_create_schema(leading_comments), - TokenType::Database => self.parse_create_database(), + TokenType::Schema => self.parse_create_schema(leading_comments, or_replace), + TokenType::Database => self.parse_create_database(or_replace), TokenType::Function => { self.parse_create_function(or_replace, or_alter, temporary, false) } 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 2c764cefc..c20dc1ae0 100644 --- a/crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs +++ b/crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs @@ -32,6 +32,30 @@ fn given_a_snowflake_clone_or_replica_when_parsing_then_only_documented_forms_ar dialect: Dialect::Snowflake, expected_accepted: true, }, + AcceptanceTestCase { + description: "CREATE DATABASE ROLE alone creates a database named ROLE (recorded success)", + sql: "CREATE DATABASE ROLE ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a database named ROLE clones (recorded errno 2003, after parsing)", + sql: "CREATE DATABASE ROLE CLONE v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "COPY = ( ) names no kind (recorded errno 1422, after parsing)", + sql: "CREATE STREAM v1 CLONE v1 COPY = ( ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a COPY list does not end in a comma", + sql: "CREATE TABLE v1 CLONE v1 COPY = ( TAGS , ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, AcceptanceTestCase { description: "a role has no CLONE (recorded errno 1003)", sql: "CREATE ROLE v1 CLONE v1 ;", @@ -179,6 +203,30 @@ fn given_a_snowflake_clone_or_replica_when_parsing_then_the_tree_keeps_its_parts pointer: "/0/create_object/source", expected_value: json!("REPLICA"), }, + ProjectionTestCase { + description: "a database named ROLE clones as a database", + sql: "CREATE DATABASE ROLE CLONE d2", + pointer: "/0/create_database/name/name", + expected_value: json!("ROLE"), + }, + ProjectionTestCase { + description: "a database clone keeps OR REPLACE", + sql: "CREATE OR REPLACE DATABASE d1 CLONE d2", + pointer: "/0/create_database/or_replace", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "a database clone keeps its name's quoting", + sql: "CREATE DATABASE \"v1 Q\" CLONE d2", + pointer: "/0/create_database/name/quoted", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "a schema clone keeps OR REPLACE", + sql: "CREATE OR REPLACE SCHEMA s1 CLONE s2", + pointer: "/0/create_schema/or_replace", + expected_value: json!(true), + }, ]; for test_case in test_cases {