Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Identifier>,
/// OR REPLACE (Snowflake)
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub or_replace: bool,
pub if_not_exists: bool,
pub authorization: Option<Identifier>,
/// CLONE source parts, possibly dot-qualified
Expand All @@ -302,6 +305,7 @@ impl CreateSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: vec![Identifier::new(name)],
or_replace: false,
if_not_exists: false,
authorization: None,
clone_from: None,
Expand Down Expand Up @@ -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<DatabaseOption>,
/// Snowflake CLONE source
Expand Down Expand Up @@ -384,6 +391,7 @@ impl CreateDatabase {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: Identifier::new(name),
or_replace: false,
if_not_exists: false,
options: Vec::new(),
clone_from: None,
Expand Down
11 changes: 9 additions & 2 deletions crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,7 @@ impl Parser {
pub(crate) fn parse_create_schema(
&mut self,
leading_comments: Vec<String>,
or_replace: bool,
) -> Result<Expression> {
self.expect_token(TokenType::Schema)?;

Expand Down Expand Up @@ -1271,6 +1272,7 @@ impl Parser {

Ok(Expression::CreateSchema(Box::new(CreateSchema {
name,
or_replace,
if_not_exists,
authorization,
clone_from,
Expand Down Expand Up @@ -1299,12 +1301,16 @@ impl Parser {
}

/// Parse CREATE DATABASE statement
pub(crate) fn parse_create_database(&mut self) -> Result<Expression> {
pub(crate) fn parse_create_database(&mut self, or_replace: bool) -> Result<Expression> {
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()?)
Expand Down Expand Up @@ -1378,6 +1384,7 @@ impl Parser {

Ok(Expression::CreateDatabase(Box::new(CreateDatabase {
name,
or_replace,
if_not_exists,
options,
clone_from,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl Parser {
if self.match_token(TokenType::Eq) {
self.expect_token(TokenType::LParen)?;
let mut kinds: Vec<Expression> = 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)));
Expand Down Expand Up @@ -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)
}
_ => {
Expand Down Expand Up @@ -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..]
Expand Down
4 changes: 2 additions & 2 deletions crates/sqlquack-syntax/src/parser/_helpers/statements/dml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
48 changes: 48 additions & 0 deletions crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;",
Expand Down Expand Up @@ -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 {
Expand Down