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
12 changes: 12 additions & 0 deletions corpora/polyglot-check/expected_differences.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,18 @@ marker = "WITHOUTTIMEZONE"
field = "to"
note = "Snowflake documents TIMESTAMP WITHOUT TIME ZONE as a synonym of TIMESTAMP_NTZ, so SQLQuack's cast target is TIMESTAMPNTZ, as for TIMESTAMP_NTZ; polyglot reads it as bare TIMESTAMP, whose meaning follows TIMESTAMP_TYPE_MAPPING; crates/sqlquack-syntax/tests/parser/test_fixes.rs pins the tree"

[[kept_field]]
id = "snowflake_clone_time_travel"
marker = "CLONE"
field = "at_clause"
note = "a database or schema clone's AT | BEFORE ( TIMESTAMP | OFFSET | STATEMENT => <expr> ) parses into its keyword, kind and expression; polyglot keeps the clause as raw text; crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs pins the tree"

[[kept_field]]
id = "snowflake_table_clone_time_travel"
marker = "CLONE"
field = "clone_at_clause"
note = "a table clone's AT | BEFORE ( TIMESTAMP | OFFSET | STATEMENT => <expr> ) parses into its keyword, kind and expression; polyglot keeps the clause as raw text; crates/sqlquack-syntax/tests/parser/test_snowflake_create_clone.rs pins the tree"

# Statements polyglot keeps as one opaque command where SQLQuack parses them into structured nodes.
# The check counts an entry only when polyglot's tree is a single command, SQLQuack's tree starts
# with `sqlquack_node`, and the statement, without whitespace and ignoring case, contains `marker`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,9 @@ pub struct CreateDatabase {
/// AT/BEFORE clause for time travel (Snowflake)
#[serde(default)]
pub at_clause: Option<Expression>,
/// Snowflake clone options, e.g. IGNORE HYBRID TABLES
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub properties: Vec<Expression>,
}

/// Database option
Expand All @@ -385,6 +388,7 @@ impl CreateDatabase {
options: Vec::new(),
clone_from: None,
at_clause: None,
properties: Vec::new(),
}
}
}
Expand Down
141 changes: 56 additions & 85 deletions crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1222,49 +1222,10 @@ impl Parser {
None
};

let at_clause = if self.match_identifier("AT") || self.match_token(TokenType::Before) {
let keyword = self.previous_text().to_ascii_uppercase();
self.expect_token(TokenType::LParen)?;
let mut result = format!("{} (", keyword);
let mut prev_token_type: Option<TokenType> = None;
let mut paren_depth = 1;
while !self.is_at_end() && paren_depth > 0 {
let token = self.advance()?;
if token.token_type == TokenType::LParen {
paren_depth += 1;
} else if token.token_type == TokenType::RParen {
paren_depth -= 1;
if paren_depth == 0 {
break;
}
}
let needs_space = !result.ends_with('(')
&& prev_token_type != Some(TokenType::Arrow)
&& prev_token_type != Some(TokenType::Dash)
&& prev_token_type != Some(TokenType::LParen)
&& token.token_type != TokenType::LParen;
if needs_space
&& token.token_type != TokenType::RParen
&& token.token_type != TokenType::Comma
{
result.push(' ');
}
if token.token_type == TokenType::String {
result.push('\'');
result.push_str(&token.text.replace('\'', "''"));
result.push('\'');
} else {
result.push_str(&token.text);
}
if token.token_type == TokenType::Arrow || token.token_type == TokenType::Comma {
result.push(' ');
}
prev_token_type = Some(token.token_type);
}
result.push(')');
Some(Expression::Raw(Raw { sql: result }))
let at_clause = if self.is_snowflake() {
self.parse_clone_time_travel()?
} else {
None
self.parse_raw_at_clause()?
};

let authorization = if self.match_token(TokenType::Authorization) {
Expand All @@ -1273,7 +1234,7 @@ impl Parser {
None
};

let mut properties: Vec<Expression> = Vec::new();
let mut properties = self.parse_clone_container_properties(clone_from.is_some())?;

if self.match_token(TokenType::With) {
self.expect_token(TokenType::LParen)?;
Expand Down Expand Up @@ -1351,53 +1312,15 @@ impl Parser {
None
};

let at_clause = if self.match_identifier("AT") || self.match_token(TokenType::Before) {
let keyword = self.previous_text().to_ascii_uppercase();
self.expect_token(TokenType::LParen)?;
let mut result = format!("{} (", keyword);
let mut prev_token_type: Option<TokenType> = None;
let mut paren_depth = 1;
while !self.is_at_end() && paren_depth > 0 {
let token = self.advance()?;
if token.token_type == TokenType::LParen {
paren_depth += 1;
} else if token.token_type == TokenType::RParen {
paren_depth -= 1;
if paren_depth == 0 {
break;
}
}
let needs_space = !result.ends_with('(')
&& prev_token_type != Some(TokenType::Arrow)
&& prev_token_type != Some(TokenType::Dash)
&& prev_token_type != Some(TokenType::LParen)
&& token.token_type != TokenType::LParen;
if needs_space
&& token.token_type != TokenType::RParen
&& token.token_type != TokenType::Comma
{
result.push(' ');
}
if token.token_type == TokenType::String {
result.push('\'');
result.push_str(&token.text.replace('\'', "''"));
result.push('\'');
} else {
result.push_str(&token.text);
}
if token.token_type == TokenType::Arrow || token.token_type == TokenType::Comma {
result.push(' ');
}
prev_token_type = Some(token.token_type);
}
result.push(')');
Some(Expression::Raw(Raw { sql: result }))
let at_clause = if self.is_snowflake() {
self.parse_clone_time_travel()?
} else {
None
self.parse_raw_at_clause()?
};

let _on_cluster = self.parse_on_cluster_clause()?;

let properties = self.parse_clone_container_properties(clone_from.is_some())?;
let mut options: Vec<DatabaseOption> = Vec::new();

while !self.is_at_end() && !self.check(TokenType::Semicolon) {
Expand Down Expand Up @@ -1459,9 +1382,57 @@ impl Parser {
options,
clone_from,
at_clause,
properties,
})))
}

/// A schema or database clone's `AT | BEFORE ( ... )` kept as written, outside Snowflake.
fn parse_raw_at_clause(&mut self) -> Result<Option<Expression>> {
if !(self.match_identifier("AT") || self.match_token(TokenType::Before)) {
return Ok(None);
}
let keyword = self.previous_text().to_ascii_uppercase();
self.expect_token(TokenType::LParen)?;
let mut result = format!("{} (", keyword);
let mut prev_token_type: Option<TokenType> = None;
let mut paren_depth = 1;
while !self.is_at_end() && paren_depth > 0 {
let token = self.advance()?;
if token.token_type == TokenType::LParen {
paren_depth += 1;
} else if token.token_type == TokenType::RParen {
paren_depth -= 1;
if paren_depth == 0 {
break;
}
}
let needs_space = !result.ends_with('(')
&& prev_token_type != Some(TokenType::Arrow)
&& prev_token_type != Some(TokenType::Dash)
&& prev_token_type != Some(TokenType::LParen)
&& token.token_type != TokenType::LParen;
if needs_space
&& token.token_type != TokenType::RParen
&& token.token_type != TokenType::Comma
{
result.push(' ');
}
if token.token_type == TokenType::String {
result.push('\'');
result.push_str(&token.text.replace('\'', "''"));
result.push('\'');
} else {
result.push_str(&token.text);
}
if token.token_type == TokenType::Arrow || token.token_type == TokenType::Comma {
result.push(' ');
}
prev_token_type = Some(token.token_type);
}
result.push(')');
Ok(Some(Expression::Raw(Raw { sql: result })))
}

/// Parse DROP DATABASE statement
pub(crate) fn parse_drop_database(&mut self) -> Result<Expression> {
self.expect_token(TokenType::Database)?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::expressions::statements::models::Version;
use crate::expressions::tables::models::CreateTable;
use crate::expressions::tables::models::PartitionedOfProperty;
use crate::expressions::tree::models::Expression;
use crate::parser::_helpers::ddl::snowflake_create_clone::CloneOptions;
use crate::parser::models::CreateTableCloneKind;
use crate::parser::models::CreateTableHead;
use crate::parser::models::CreateTableLeadingOptions;
Expand Down Expand Up @@ -180,8 +181,23 @@ impl Parser {

self.skip();

let source = self.parse_table_ref()?;
let at_clause = if self.match_identifier("AT") || self.match_token(TokenType::Before) {
let snowflake = self.is_snowflake();
if snowflake {
for part in [&name.catalog, &name.schema, &Some(name.name.clone())]
.into_iter()
.flatten()
{
self.reject_reserved_name_part(part)?;
}
}
let source = if snowflake {
self.parse_object_name()?
} else {
self.parse_table_ref()?
};
let at_clause = if self.is_snowflake() {
self.parse_clone_time_travel()?
} else if self.match_identifier("AT") || self.match_token(TokenType::Before) {
let keyword = self.previous_text().to_ascii_uppercase();
self.expect_token(TokenType::LParen)?;
let mut result = format!("{} (", keyword);
Expand Down Expand Up @@ -259,6 +275,11 @@ impl Parser {
} else {
None
};
let properties = if snowflake {
Self::clone_options_as_properties(self.parse_clone_options(CloneOptions::Copy)?)
} else {
Vec::new()
};
Ok(Expression::CreateTable(Box::new(CreateTable {
name,
on_cluster: on_cluster.clone(),
Expand All @@ -284,7 +305,7 @@ impl Parser {
with_statistics: None,
teradata_indexes: Vec::new(),
with_cte: None,
properties: Vec::new(),
properties,
partition_of: None,
post_table_properties: Vec::new(),
mysql_table_options: Vec::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub(crate) mod snowflake_alter_objects;
pub(crate) mod snowflake_alter_resources;
pub(crate) mod snowflake_alter_tables;
pub(crate) mod snowflake_commands;
pub(crate) mod snowflake_create_clone;
pub(crate) mod snowflake_create_from;
pub(crate) mod snowflake_show;
pub(crate) mod snowflake_statements;
Expand Down
Loading