Skip to content
Closed
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
10 changes: 9 additions & 1 deletion crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ impl Parser {
{
return self.parse_execute_using_policy_context();
}
if self.current + 1 >= self.tokens.len() || self.check_next(TokenType::Semicolon) {
if self.current + 1 >= self.tokens.len()
|| self.check_next(TokenType::Semicolon)
|| self.applies_snowflake_context_rules()
{
return Err(self.parse_error("Expected an object kind and name after EXECUTE"));
}
}
Expand Down Expand Up @@ -835,6 +838,11 @@ impl Parser {
break;
}

if self.applies_snowflake_context_rules()
&& (self.check(TokenType::Eq) || self.is_snowflake_reserved_keyword())
{
return Err(self.parse_error("Expected a variable name"));
}
let name = self.parse_set_item_name()?;

if self.match_token(TokenType::Eq)
Expand Down
14 changes: 13 additions & 1 deletion crates/sqlquack-syntax/src/parser/_helpers/ddl/scripting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::expressions::tree::models::Expression;
use crate::expressions::types::models::Tuple;
use crate::parser::constants::ENGINE;
use crate::parser::constants::MUTEX;
use crate::parser::constants::PARAMETERS;
use crate::parser::constants::PLAN;
use crate::parser::constants::REPRODUCTION;
use crate::parser::constants::SECOND_PREVIOUS_OFFSET;
Expand Down Expand Up @@ -654,6 +655,7 @@ impl Parser {

fn parse_show_scope(&mut self, this: &str) -> Result<(Option<String>, Option<Expression>)> {
Ok(if self.match_token(TokenType::In) {
let snowflake = self.applies_snowflake_context_rules();
let (kind, scope_obj) = if self.match_keyword("ACCOUNT") {
(Some("ACCOUNT".to_string()), None)
} else if self.match_token(TokenType::Database) {
Expand Down Expand Up @@ -689,7 +691,10 @@ impl Parser {
&& !self.check(TokenType::Semicolon)
&& !self.check_keyword_text("STARTS")
{
let table = self.parse_table_ref()?;
let table = match snowflake {
true => self.parse_object_name()?,
false => self.parse_table_ref()?,
};
Some(Expression::Table(Box::new(table)))
} else {
None
Expand All @@ -708,6 +713,13 @@ impl Parser {
None
};
(Some("VIEW".to_string()), scope_obj)
} else if snowflake && this == PARAMETERS && self.match_identifier("USER") {
let named = !self.is_at_end() && !self.check(TokenType::Semicolon);
let scope_obj = match named {
true => Some(Expression::Table(Box::new(self.parse_object_name()?))),
false => None,
};
(Some("USER".to_string()), scope_obj)
} else if self.match_keyword("CLASS") {
let scope_obj = if !self.is_at_end() {
let table = self.parse_table_ref()?;
Expand Down
179 changes: 179 additions & 0 deletions crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
use crate::expressions::names::models::Identifier;
use crate::expressions::operators::models::Call;
use crate::expressions::schema_objects::models::ExecuteObject;
use crate::expressions::schema_objects::models::ObjectProperty;
use crate::expressions::statements::models::CreateTask;
use crate::expressions::statements::models::ExecuteUsingPolicyContext;
use crate::expressions::tree::models::Expression;
use crate::parser::_helpers::ddl::snowflake_alter::property;
use crate::parser::constants::EXECUTE_DCM_PROJECT;
use crate::parser::constants::SNOWFLAKE_RESERVED_KEYWORDS;
use crate::parser::models::Parser;
use crate::tokens::models::ParserToken;
use crate::tokens::models::TokenType;
use crate::tokens::types::Result;
use crate::traversal::types::ExpressionWalk;

/// The action, alias and properties of an EXECUTE DCM PROJECT deploy, plan or preview.
type DcmRun = (String, Option<Identifier>, Vec<ObjectProperty>);

impl Parser {
/// In Snowflake, refuse an unquoted reserved keyword in any dotted part of a COMMENT ON name.
pub(super) fn check_snowflake_comment_name(&self) -> Result<()> {
Expand Down Expand Up @@ -181,6 +187,10 @@ impl Parser {
}
} else if self.match_text_seq(&["TEST", "ALL"]) {
node.action = Some("TEST ALL".to_owned());
} else if let Some((action, alias, properties)) = self.parse_dcm_project_run()? {
node.action = Some(action);
node.alias = alias;
node.properties = properties;
} else {
self.current = start;
return Ok(None);
Expand All @@ -190,6 +200,73 @@ impl Parser {
}
}

impl Parser {
/// Reads `DEPLOY [ AS <alias> ] <source>`, `PLAN [ DELTA ] <source> [ OUTPUT_PATH '<path>' ]` or `PREVIEW <table> USING CONFIGURATION <config> FROM '<path>' [ LIMIT <n> ]` of EXECUTE DCM PROJECT; none for other actions.
fn parse_dcm_project_run(&mut self) -> Result<Option<DcmRun>> {
if self.match_text_seq(&["DEPLOY"]) {
let alias = match self.match_token(TokenType::As) {
true => Some(self.parse_simple_name()?),
false => None,
};
return Ok(Some((
"DEPLOY".to_owned(),
alias,
self.parse_dcm_project_source()?,
)));
}
if self.match_text_seq(&["PLAN"]) {
let action = match self.match_text_seq(&["DELTA"]) {
true => "PLAN DELTA",
false => "PLAN",
};
let mut properties = self.parse_dcm_project_source()?;
if self.match_text_seq(&["OUTPUT_PATH"]) {
properties.push(property(
"OUTPUT_PATH",
None,
Some(self.parse_string_value()?),
));
}
return Ok(Some((action.to_owned(), None, properties)));
}
if !self.match_text_seq(&["PREVIEW"]) {
return Ok(None);
}
let table = Expression::Table(Box::new(self.parse_object_name()?));
let mut properties = vec![property("PREVIEW", None, Some(table))];
if !self.check_text_seq(&["USING"]) {
return Err(self.parse_error("Expected USING CONFIGURATION"));
}
properties.extend(self.parse_dcm_project_source()?);
if self.match_token(TokenType::Limit) {
if !self.check(TokenType::Number) {
return Err(self.parse_error("Expected a row count after LIMIT"));
}
properties.push(property("LIMIT", None, Some(self.parse_primary()?)));
}
Ok(Some(("PREVIEW".to_owned(), None, properties)))
}

/// Reads `[ USING CONFIGURATION <config> ] FROM '<path>'` or `FROM DEPLOYMENT <deployment>`.
fn parse_dcm_project_source(&mut self) -> Result<Vec<ObjectProperty>> {
let mut properties: Vec<ObjectProperty> = Vec::new();
let configured = self.match_text_seq(&["USING"]);
if configured {
self.expect_alter_words(&[&["CONFIGURATION"]])?;
let config = Expression::Table(Box::new(self.parse_object_name()?));
properties.push(property("USING CONFIGURATION", None, Some(config)));
}
self.expect_token(TokenType::From)?;
if !configured && self.match_text_seq(&["DEPLOYMENT"]) {
let deployment = Expression::Table(Box::new(self.parse_object_name()?));
properties.push(property("FROM DEPLOYMENT", None, Some(deployment)));
} else {
properties.push(property("FROM", None, Some(self.parse_string_value()?)));
}
Ok(properties)
}
}

impl Parser {
/// Refuses a Snowflake GET/LIST/REMOVE lacking a stage, with a literal stage part, an empty option or GET's `file://` target.
pub(crate) fn check_snowflake_stage_command(&self, needs_target: bool) -> Result<()> {
Expand Down Expand Up @@ -231,6 +308,108 @@ impl Parser {
Ok(())
}

/// Refuses the EXECUTE IMMEDIATE shapes Snowflake rejects: no target, a stage without FROM, a broken FROM location or option, and an empty or unbalanced USING list.
pub(crate) fn check_snowflake_execute_immediate(&self) -> Result<()> {
if !self.applies_snowflake_context_rules() {
return Ok(());
}
let rest = &self.tokens[(self.current + 1).min(self.tokens.len())..];
let end = rest
.iter()
.position(|token| token.token_type == TokenType::Semicolon)
.unwrap_or(rest.len());
let tokens = &rest[..end];
let refuse = |message: &str| Err(self.parse_error(message));
let Some(first) = tokens.first() else {
return refuse("Expected a statement, a variable or FROM after EXECUTE IMMEDIATE");
};
if first.token_type == TokenType::DAt || first.text.starts_with('@') {
return refuse("Expected FROM before the stage");
}
if first.token_type != TokenType::From {
let target = match first.token_type {
TokenType::Dollar | TokenType::Parameter if first.text.len() <= 1 => {
match tokens.get(1) {
Some(name) if name.span.start == first.span.end => 2,
_ => return refuse("Expected a variable name after $"),
}
}
_ => 1,
};
return match &tokens[target..] {
[] => Ok(()),
[using, list @ ..] if using.text.eq_ignore_ascii_case("USING") => {
Self::check_immediate_list(list, false).map_or_else(
|| refuse("Expected a non-empty USING ( ... ) list"),
|_| Ok(()),
)
}
_ => refuse("Expected USING or the end after the EXECUTE IMMEDIATE target"),
};
}
let location = &tokens[1..];
let Some(head) = location.first() else {
return refuse("Expected a stage or a path after FROM");
};
let mut next = 1;
if head.token_type != TokenType::String {
while location
.get(next)
.is_some_and(|token| token.span.start == location[next - 1].span.end)
{
next += 1;
}
if location[..next]
.iter()
.any(|token| token.token_type == TokenType::String)
{
return refuse("A stage location part cannot be a string literal");
}
}
let mut options = &location[next..];
if options
.first()
.is_some_and(|token| token.text.eq_ignore_ascii_case("USING"))
{
match Self::check_immediate_list(&options[1..], true) {
Some(used) => options = &options[1 + used..],
None => return refuse("Expected USING ( <name> => <value> , ... )"),
}
}
match options {
[] => Ok(()),
[dry_run, eq, value]
if dry_run.text.eq_ignore_ascii_case("DRY_RUN")
&& eq.token_type == TokenType::Eq
&& matches!(value.token_type, TokenType::True | TokenType::False) =>
{
Ok(())
}
_ => refuse("Expected USING, DRY_RUN = TRUE | FALSE or the end after the location"),
}
}

/// The tokens a non-empty `( <item> , ... )` list takes, each item `<name> => <value>` when `named`; none when it is malformed.
fn check_immediate_list(tokens: &[ParserToken], named: bool) -> Option<usize> {
if tokens.first()?.token_type != TokenType::LParen {
return None;
}
let close = tokens
.iter()
.position(|token| token.token_type == TokenType::RParen)?;
let items = &tokens[1..close];
if items.is_empty() {
return None;
}
let well_formed = items
.split(|token| token.token_type == TokenType::Comma)
.all(|item| match named {
true => matches!(item, [name, arrow, _, ..] if name.token_type != TokenType::FArrow && arrow.token_type == TokenType::FArrow),
false => !item.is_empty(),
});
well_formed.then_some(close + 1)
}

/// Snowflake DCM `DEFINE`, refused for kinds kept as opaque text or not yet checked closely.
pub(crate) fn parse_snowflake_define(&mut self) -> Result<Expression> {
let definition = self.parse_create()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,26 @@ impl Parser {
operands.push(self.parse_primary()?);
"SPECIFICATION"
}
_ if self.check(TokenType::Type) || self.check_identifier("TYPE") => return Ok(None),
_ if self.check(TokenType::Type) || self.check_identifier("TYPE") => {
self.skip();
self.expect_token(TokenType::Eq)?;
let experiment_type = match self.check(TokenType::String) {
true => self.parse_primary()?,
false => Expression::Identifier(self.parse_simple_name()?),
};
properties.push(property("TYPE", None, Some(experiment_type)));
if !self.check(TokenType::From) {
""
} else {
self.skip();
self.expect_alter_words(&[&["SPECIFICATION"]])?;
if !self.check(TokenType::String) && !self.check(TokenType::DollarString) {
return Err(self.parse_error("Expected the specification text"));
}
operands.push(self.parse_primary()?);
"SPECIFICATION"
}
}
_ => "",
};
if !self.is_at_end() && !self.check(TokenType::Semicolon) {
Expand Down
20 changes: 19 additions & 1 deletion crates/sqlquack-syntax/src/parser/_helpers/functions/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use crate::expressions::typed_functions::models::ValueFunc;
use crate::expressions::types::models::DataType;
use crate::expressions::types::models::Tuple;
use crate::parser::constants::BINARY_ARITY;
use crate::parser::constants::EMPTY_CALL_SYNTAX_ERRORS;
use crate::parser::constants::FIRST_VALUE;
use crate::parser::constants::LEAD;
use crate::parser::constants::TIME_TO_TIME_STR;
Expand Down Expand Up @@ -265,7 +266,24 @@ impl Parser {
return Ok(expr);
}
let comments = self.call_comments(saved);
let typed = self.parse_typed_function_lowered(name, upper_name, quoted)?;
let typed = match self.parse_typed_function_lowered(name, upper_name, quoted) {
Ok(typed) => typed,
Err(_)
if self.tokens[saved].token_type == TokenType::RParen
&& !EMPTY_CALL_SYNTAX_ERRORS.contains(&upper_name) =>
{
self.restore_comments(comments);
self.current = saved;
let mut expr = self.parse_generic_function(name, quoted)?;
if self.config.record_call_spans {
self.record_call_span(&mut expr, saved);
}
self.faithful_calls
.insert(saved, (expr.clone(), self.current));
return Ok(expr);
}
Err(error) => return Err(error),
};
let mut expr = match Self::call_name(&typed) {
Some(kept) if kept.eq_ignore_ascii_case(name) => typed,
_ if crate::functions::names::main::faithful_keyword_syntax_name::faithful_keyword_syntax_name(upper_name)
Expand Down
13 changes: 12 additions & 1 deletion crates/sqlquack-syntax/src/parser/_helpers/statements/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1289,12 +1289,23 @@ impl Parser {
/// Parses ALTER SESSION SET/UNSET.
pub(crate) fn parse_alter_session(&mut self) -> Result<Option<Expression>> {
if self.match_token(TokenType::Set) {
let snowflake = self.applies_snowflake_context_rules();
let mut expressions: Vec<Expression> = Vec::new();
loop {
let valueless = self.check_next(TokenType::Eq)
&& self.tokens.get(self.current + 2).is_none_or(|token| {
matches!(token.token_type, TokenType::Semicolon | TokenType::Comma)
});
if snowflake && (self.is_at_end() || self.check(TokenType::Semicolon) || valueless)
{
return Err(self.parse_error("Expected <parameter> = <value>"));
}
if let Some(item) = self.parse_set_item_assignment()? {
expressions.push(item);
}
if !self.match_token(TokenType::Comma) {
let listed = self.match_token(TokenType::Comma)
|| snowflake && !self.is_at_end() && self.check_next(TokenType::Eq);
if !listed {
break;
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/sqlquack-syntax/src/parser/_helpers/statements/dml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,9 @@ impl Parser {
["CLONE", "FROM", "AS", "USING", "LIKE", "("]
.iter()
.any(|word| t.text.eq_ignore_ascii_case(word))
|| t.text.eq_ignore_ascii_case("TYPE")
&& self.tokens.get(self.current + 2).map(|eq| eq.token_type)
== Some(TokenType::Eq)
})
{
return Err(self.parse_error("Expected an object kind and a name"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ impl Parser {
== Some(true)
{
self.skip();
self.check_snowflake_execute_immediate()?;
self.parse_command()?.ok_or_else(|| {
self.parse_error("Failed to parse EXECUTE IMMEDIATE statement")
})
Expand Down
4 changes: 4 additions & 0 deletions crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,10 @@ impl Parser {
"TIMESTAMPLTZ" | "TIMESTAMP_LTZ" => self.parse_timestampltz_data_type(),
"TIMESTAMPNTZ" | "TIMESTAMP_NTZ" => self.parse_timestampntz_data_type(),
"INTERVAL" => self.parse_interval_data_type(),
"PRECISION" | "VARYING" if self.applies_snowflake_context_rules() => Err(self
.parse_error(format!(
"{name} only follows DOUBLE or CHARACTER in a Snowflake type"
))),
_ => self.parse_other_scalar_data_type(name, raw_name),
}
}
Expand Down
Loading
Loading