diff --git a/corpora/polyglot-check/expected_differences.toml b/corpora/polyglot-check/expected_differences.toml index b08e1b036..9aa233a9f 100644 --- a/corpora/polyglot-check/expected_differences.toml +++ b/corpora/polyglot-check/expected_differences.toml @@ -1142,3 +1142,80 @@ id = "snowflake_scripting_block" marker = "BEGIN" sqlquack_node = "scripting_block" note = "a Snowflake Scripting block parses into its declarations, statements and handlers; crates/sqlquack-syntax/tests/parser/test_scripting.rs pins the tree" + +[[rejection]] +id = "snowflake_stage_reserved_name" +sql = "SELECT @select" +dialects = ["Snowflake"] +sqlquack_error = "Expected a stage name after @" +note = "Snowflake reads @ as a stage reference and refuses a bare reserved word as the stage name (recorded syntax_error)" + +[[rejection]] +id = "snowflake_stage_quoted_blank_name" +sql = "SELECT @`a b`" +dialects = ["Snowflake"] +sqlquack_error = "Expected a stage name after @" +note = "Snowflake refuses a stage reference whose quoted name holds a blank (recorded syntax_error); backticks are no Snowflake quote either" + +[[rejection]] +id = "snowflake_stage_string_name" +sql = "SELECT @JOIN, @'foo'" +dialects = ["Snowflake"] +sqlquack_error = "Expected a stage name after @" +note = "Snowflake reads @ as a stage reference and refuses a string as the stage name (recorded syntax_error for @'v1')" + +[[rejection]] +id = "snowflake_bare_interval_type_double_colon" +sql = "SELECT interval '00:00:01'::interval AS foo" +dialects = ["Snowflake"] +sqlquack_error = "Expected an interval field such as DAY or YEAR TO MONTH" +note = "Snowflake's INTERVAL type needs a field (recorded errno 1003 for CAST ( NULL AS INTERVAL ))" + +[[rejection]] +id = "snowflake_bare_interval_type_cast" +sql = "SELECT CAST(INTERVAL '00:00:01' AS INTERVAL) AS foo" +dialects = ["Snowflake"] +sqlquack_error = "Expected an interval field such as DAY or YEAR TO MONTH" +note = "Snowflake's INTERVAL type needs a field (recorded errno 1003 for CAST ( NULL AS INTERVAL ))" + +[[rejection]] +id = "snowflake_bare_interval_type_double_colon_window" +sql = "SELECT ROW_NUMBER() OVER(PARTITION BY event_time + interval '00:00:01'::interval) AS foo FROM t" +dialects = ["Snowflake"] +sqlquack_error = "Expected an interval field such as DAY or YEAR TO MONTH" +note = "Snowflake's INTERVAL type needs a field (recorded errno 1003 for CAST ( NULL AS INTERVAL ))" + +[[rejection]] +id = "snowflake_bare_interval_type_cast_window" +sql = "SELECT ROW_NUMBER() OVER (PARTITION BY event_time + CAST(INTERVAL '00:00:01' AS INTERVAL)) AS foo FROM t" +dialects = ["Snowflake"] +sqlquack_error = "Expected an interval field such as DAY or YEAR TO MONTH" +note = "Snowflake's INTERVAL type needs a field (recorded errno 1003 for CAST ( NULL AS INTERVAL ))" + +[[rejection]] +id = "snowflake_bare_interval_type_cast_string" +sql = "CAST('1 DAY' AS INTERVAL)" +dialects = ["Snowflake"] +sqlquack_error = "Expected an interval field such as DAY or YEAR TO MONTH" +note = "Snowflake's INTERVAL type needs a field (recorded errno 1003 for CAST ( NULL AS INTERVAL ))" + +[[rejection]] +id = "snowflake_type_bare_interval_cast" +sql = "SELECT CAST(x AS INTERVAL)" +dialects = ["Snowflake"] +sqlquack_error = "Expected an interval field such as DAY or YEAR TO MONTH" +note = "Snowflake's INTERVAL type needs a field (recorded errno 1003 for CAST ( NULL AS INTERVAL ))" + +[[rejection]] +id = "snowflake_type_bare_interval_try_cast" +sql = "SELECT TRY_CAST(x AS INTERVAL)" +dialects = ["Snowflake"] +sqlquack_error = "Expected an interval field such as DAY or YEAR TO MONTH" +note = "Snowflake's INTERVAL type needs a field (recorded errno 1003 for CAST ( NULL AS INTERVAL ))" + +[[rejection]] +id = "snowflake_type_bare_interval_double_colon" +sql = "SELECT x::INTERVAL" +dialects = ["Snowflake"] +sqlquack_error = "Expected an interval field such as DAY or YEAR TO MONTH" +note = "Snowflake's INTERVAL type needs a field (recorded errno 1003 for CAST ( NULL AS INTERVAL ))" diff --git a/crates/sqlquack-core/src/classification/main/cast_type_name.rs b/crates/sqlquack-core/src/classification/main/cast_type_name.rs index b525b24c8..a55057a16 100644 --- a/crates/sqlquack-core/src/classification/main/cast_type_name.rs +++ b/crates/sqlquack-core/src/classification/main/cast_type_name.rs @@ -46,7 +46,7 @@ pub fn cast_type_name(data_type: &DataType) -> String { DataType::Enum { .. } => "ENUM", DataType::Set { .. } => "SET", DataType::Union { .. } => "UNION", - DataType::Nullable { inner } => return cast_type_name(inner), + DataType::Nullable { inner } | DataType::NotNull { inner } => return cast_type_name(inner), DataType::Custom { name } => return custom_type_name(name), DataType::Geometry { .. } => "GEOMETRY", DataType::Geography { .. } => "GEOGRAPHY", diff --git a/crates/sqlquack-polyglot-check/src/comparison/_helpers/outcomes.rs b/crates/sqlquack-polyglot-check/src/comparison/_helpers/outcomes.rs index 88fd8e7fc..27baf5351 100644 --- a/crates/sqlquack-polyglot-check/src/comparison/_helpers/outcomes.rs +++ b/crates/sqlquack-polyglot-check/src/comparison/_helpers/outcomes.rs @@ -42,13 +42,38 @@ pub(crate) fn lossless_failure(sql: &str, dialect: Dialect, parsed: bool) -> Opt } } +/// `value` with each Snowflake `NOT NULL` element wrapper replaced by its inner type, which polyglot lacks. +fn without_not_null(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Object(mut object) + if object.get("data_type").and_then(serde_json::Value::as_str) == Some("not_null") => + { + object + .remove("inner") + .map(without_not_null) + .unwrap_or(serde_json::Value::Object(object)) + } + serde_json::Value::Object(object) => serde_json::Value::Object( + object + .into_iter() + .map(|(key, field)| (key, without_not_null(field))) + .collect(), + ), + serde_json::Value::Array(items) => { + serde_json::Value::Array(items.into_iter().map(without_not_null).collect()) + } + other => other, + } +} + /// polyglot's Snowflake rendering of `data_type`, cut to its upper-case base name. pub(crate) fn polyglot_base_type_name( data_type: &sqlquack_syntax::DataType, dialect: polyglot_sql::DialectType, ) -> String { - let converted = - serde_json::to_value(data_type).and_then(serde_json::from_value::); + let converted = serde_json::to_value(data_type) + .map(without_not_null) + .and_then(serde_json::from_value::); let theirs = match converted { Ok(theirs) => theirs, Err(_) => return "".to_owned(), diff --git a/crates/sqlquack-polyglot-check/tests/runs/test_exact.rs b/crates/sqlquack-polyglot-check/tests/runs/test_exact.rs index a492a1020..9d6ad6295 100644 --- a/crates/sqlquack-polyglot-check/tests/runs/test_exact.rs +++ b/crates/sqlquack-polyglot-check/tests/runs/test_exact.rs @@ -49,6 +49,8 @@ fn given_committed_corpora_when_parsed_by_both_parsers_then_results_are_identica ("snowflake_show_profile".to_owned(), 1), ("snowflake_show_groups".to_owned(), 1), ("snowflake_show_create_table".to_owned(), 1), + ("snowflake_stage_reserved_name".to_owned(), 1), + ("snowflake_stage_quoted_blank_name".to_owned(), 1), ]), differences: Vec::new(), tokens_lost: Vec::new(), @@ -87,6 +89,15 @@ fn given_committed_corpora_when_parsed_by_both_parsers_then_results_are_identica ("snowflake_describe_formatted".to_owned(), 1), ("snowflake_describe_query".to_owned(), 1), ("snowflake_describe_bare_quoted_name".to_owned(), 1), + ("snowflake_stage_string_name".to_owned(), 1), + ("snowflake_bare_interval_type_double_colon".to_owned(), 1), + ("snowflake_bare_interval_type_cast".to_owned(), 1), + ( + "snowflake_bare_interval_type_double_colon_window".to_owned(), + 1, + ), + ("snowflake_bare_interval_type_cast_window".to_owned(), 1), + ("snowflake_bare_interval_type_cast_string".to_owned(), 1), ]), differences: Vec::new(), tokens_lost: Vec::new(), @@ -157,7 +168,11 @@ fn given_committed_corpora_when_parsed_by_both_parsers_then_results_are_identica entries: load_corpus(&corpora.join("snowflake_types.json")), expected_outcome: ExactOutcome { loaded: true, - rejected: BTreeMap::new(), + rejected: BTreeMap::from([ + ("snowflake_type_bare_interval_cast".to_owned(), 1), + ("snowflake_type_bare_interval_try_cast".to_owned(), 1), + ("snowflake_type_bare_interval_double_colon".to_owned(), 1), + ]), differences: Vec::new(), tokens_lost: Vec::new(), type_names: Vec::new(), diff --git a/crates/sqlquack-semantics/src/lineage/_helpers/checks/families.rs b/crates/sqlquack-semantics/src/lineage/_helpers/checks/families.rs index 59d2a5905..f6cd4a653 100644 --- a/crates/sqlquack-semantics/src/lineage/_helpers/checks/families.rs +++ b/crates/sqlquack-semantics/src/lineage/_helpers/checks/families.rs @@ -215,7 +215,7 @@ pub(crate) fn data_type_family(data_type: &DataType) -> Family { | DataType::Union { .. } | DataType::Geometry { .. } | DataType::Geography { .. } => Family::Struct, - DataType::Nullable { inner } => data_type_family(inner), + DataType::Nullable { inner } | DataType::NotNull { inner } => data_type_family(inner), DataType::Custom { name } => type_family(name), _ => Family::Unknown, } diff --git a/crates/sqlquack-syntax/src/expressions/columns/models.rs b/crates/sqlquack-syntax/src/expressions/columns/models.rs index 18c0a4553..f4a642ffb 100644 --- a/crates/sqlquack-syntax/src/expressions/columns/models.rs +++ b/crates/sqlquack-syntax/src/expressions/columns/models.rs @@ -551,9 +551,59 @@ pub enum ColumnPosition { After(Identifier), } +/// Snowflake `{ ADD | DROP | MODIFY } DATA METRIC FUNCTION ON ( ... ) [ ], ...`. +#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DataMetricFunctionAction { + /// `ADD`, `DROP` or `MODIFY`. + pub kind: String, + pub items: Vec, +} + +/// One ` ON ( , ... )` of a data metric function action, with its clauses. +#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DataMetricFunctionItem { + pub name: TableRef, + pub arguments: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub clauses: Vec, +} + +/// A clause after a data metric function's arguments. +#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum DataMetricFunctionClause { + /// `SENSITIVITY = ` or `ANOMALY_DETECTION = `. + Property { + name: String, + value: Expression, + }, + /// `[ ADD | DROP | MODIFY ] EXPECTATION , ...`. + Expectations { + action: Option, + expectations: Vec, + }, + /// `WITHIN GROUP ( , ... ) [ GROUP LIMIT ]`. + WithinGroup { + columns: Vec, + limit: Option, + }, + ExecuteAsRole(Identifier), + /// `FILTER ( [ ] )`. + Filter(Option), + /// `SUSPEND` or `RESUME`. + State(String), +} + +/// A data metric function expectation: its name and, except under DROP, its condition. +#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DataMetricExpectation { + pub name: Identifier, + pub condition: Option, +} + /// Actions for ALTER TABLE #[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum AlterTableAction { + DataMetricFunction(DataMetricFunctionAction), AddColumn { column: ColumnDef, if_not_exists: bool, diff --git a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs index bb4745e3e..1498546aa 100644 --- a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs +++ b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs @@ -215,6 +215,7 @@ pub struct AlterView { pub enum AlterViewAction { /// Rename the view Rename(Box), + DataMetricFunction(crate::expressions::columns::models::DataMetricFunctionAction), /// Change owner OwnerTo(Identifier), /// Set schema diff --git a/crates/sqlquack-syntax/src/expressions/statements/models.rs b/crates/sqlquack-syntax/src/expressions/statements/models.rs index 130c6e502..c830aec53 100644 --- a/crates/sqlquack-syntax/src/expressions/statements/models.rs +++ b/crates/sqlquack-syntax/src/expressions/statements/models.rs @@ -352,6 +352,14 @@ pub struct ExecuteStatement { pub suffix: Option, } +/// Snowflake `EXECUTE USING POLICY_CONTEXT ( => , ... ) AS `. +#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ExecuteUsingPolicyContext { + /// The named arguments of POLICY_CONTEXT. + pub arguments: Vec, + pub query: Expression, +} + /// Snowflake `EXECUTE ALERT | TASK [ GRAPH ] | DCM PROJECT ` with its action. #[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ExecuteObject { @@ -1603,6 +1611,7 @@ pub enum ScriptingDeclaration { Cursor(ScriptingCursor), Resultset(ScriptingResultset), Exception(ScriptingException), + Procedure(Box), } /// `name [type] [{DEFAULT | :=} expression]`. @@ -1678,6 +1687,19 @@ pub struct ScriptingException { pub message: Option, } +/// `name PROCEDURE ( [ , ...] ) RETURNS { | TABLE ( ... ) } AS `. +#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ScriptingProcedure { + #[serde(default, skip_serializing_if = "SourceSpan::is_absent")] + #[ast(skip)] + pub span: SourceSpan, + pub name: Identifier, + pub parameters: Vec, + pub return_type: Option, + pub returns_table: Option>, + pub body: ScriptingBlock, +} + /// One body member, terminated by its own semicolon. #[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] diff --git a/crates/sqlquack-syntax/src/expressions/tree/_helpers/accessors.rs b/crates/sqlquack-syntax/src/expressions/tree/_helpers/accessors.rs index ba44b4728..e6e9d5297 100644 --- a/crates/sqlquack-syntax/src/expressions/tree/_helpers/accessors.rs +++ b/crates/sqlquack-syntax/src/expressions/tree/_helpers/accessors.rs @@ -133,6 +133,7 @@ impl Expression { | Expression::Prepare(_) | Expression::Execute(_) | Expression::ExecuteObject(_) + | Expression::ExecuteUsingPolicyContext(_) | Expression::Declare(_) | Expression::Refresh(_) | Expression::AlterSession(_) diff --git a/crates/sqlquack-syntax/src/expressions/tree/_helpers/api.rs b/crates/sqlquack-syntax/src/expressions/tree/_helpers/api.rs index 7e97818e9..92113e03f 100644 --- a/crates/sqlquack-syntax/src/expressions/tree/_helpers/api.rs +++ b/crates/sqlquack-syntax/src/expressions/tree/_helpers/api.rs @@ -446,6 +446,7 @@ impl Expression { Expression::Prepare(_) => "prepare", Expression::Execute(_) => "execute", Expression::ExecuteObject(_) => "execute_object", + Expression::ExecuteUsingPolicyContext(_) => "execute_using_policy_context", Expression::Raw(_) => "raw", Expression::CreateTask(_) => "create_task", Expression::Paren(_) => "paren", diff --git a/crates/sqlquack-syntax/src/expressions/tree/models.rs b/crates/sqlquack-syntax/src/expressions/tree/models.rs index beed444ea..250eb74d7 100644 --- a/crates/sqlquack-syntax/src/expressions/tree/models.rs +++ b/crates/sqlquack-syntax/src/expressions/tree/models.rs @@ -387,6 +387,7 @@ use crate::expressions::statements::models::Detach; use crate::expressions::statements::models::DropPartition; use crate::expressions::statements::models::ExecuteObject; use crate::expressions::statements::models::ExecuteStatement; +use crate::expressions::statements::models::ExecuteUsingPolicyContext; use crate::expressions::statements::models::Get; use crate::expressions::statements::models::Grant; use crate::expressions::statements::models::GrantPrincipal; @@ -1222,6 +1223,7 @@ pub enum Expression { /// EXEC/EXECUTE statement (TSQL stored procedure call) Execute(Box), ExecuteObject(Box), + ExecuteUsingPolicyContext(Box), /// Snowflake CREATE TASK statement CreateTask(Box), diff --git a/crates/sqlquack-syntax/src/expressions/types/models.rs b/crates/sqlquack-syntax/src/expressions/types/models.rs index b98872572..bd94ad8bc 100644 --- a/crates/sqlquack-syntax/src/expressions/types/models.rs +++ b/crates/sqlquack-syntax/src/expressions/types/models.rs @@ -299,6 +299,11 @@ pub enum DataType { inner: Box, }, + /// An element or value type declared NOT NULL, as in Snowflake's `ARRAY ( INT NOT NULL )`. + NotNull { + inner: Box, + }, + Custom { name: String, }, @@ -334,7 +339,7 @@ impl DataType { DataType::Array { element_type, .. } | DataType::List { element_type } => { element_type.contains_unknown() } - DataType::Nullable { inner } => inner.contains_unknown(), + DataType::Nullable { inner } | DataType::NotNull { inner } => inner.contains_unknown(), DataType::Map { key_type, value_type, @@ -362,8 +367,12 @@ impl DataType { } match (self, refined) { (DataType::Unknown, _) => true, - (DataType::Nullable { inner }, other) => inner.matches_shape_of(other), - (this, DataType::Nullable { inner }) => this.matches_shape_of(inner), + (DataType::Nullable { inner } | DataType::NotNull { inner }, other) => { + inner.matches_shape_of(other) + } + (this, DataType::Nullable { inner } | DataType::NotNull { inner }) => { + this.matches_shape_of(inner) + } ( DataType::Array { element_type, .. } | DataType::List { element_type }, DataType::Array { diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/alter.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/alter.rs index dae43e905..90e6a6e1d 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/alter.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/alter.rs @@ -39,7 +39,9 @@ use crate::tokens::types::Result; impl Parser { /// Parse ALTER TABLE action pub(crate) fn parse_alter_action(&mut self) -> Result { - if self.match_token(TokenType::Add) { + if let Some(action) = self.parse_data_metric_function_action()? { + Ok(AlterTableAction::DataMetricFunction(action)) + } else if self.match_token(TokenType::Add) { self.parse_alter_add_action() } else if self.match_token(TokenType::Drop) { self.parse_alter_drop_action() diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs index 9c5b288da..8c7ebd5be 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/commands.rs @@ -22,7 +22,6 @@ use crate::expressions::schema_objects::models::FunctionUsingResource; use crate::expressions::schema_objects::models::SqlDataAccess; use crate::expressions::statements::models::Command; use crate::expressions::statements::models::Comment; -use crate::expressions::statements::models::ExecuteObject; use crate::expressions::statements::models::ExecuteParameter; use crate::expressions::statements::models::ExecuteStatement; use crate::expressions::statements::models::Grant; @@ -39,7 +38,6 @@ use crate::expressions::tables::models::CollateProperty; use crate::expressions::tables::models::Property; use crate::expressions::tree::models::Expression; use crate::expressions::types::models::DataType; -use crate::parser::constants::EXECUTE_DCM_PROJECT; use crate::parser::constants::FUNCTION; use crate::parser::constants::PROCEDURE; use crate::parser::constants::QUALIFIED_NAME_PARTS; @@ -93,6 +91,9 @@ impl Parser { if let Some(call) = self.parse_snowflake_method_call()? { return Ok(call); } + if self.applies_snowflake_context_rules() { + return self.parse_call_target(None); + } let mut tokens = vec![("CALL".to_string(), call_token.token_type)]; while !self.is_at_end() && !self.check(TokenType::Semicolon) { @@ -159,58 +160,6 @@ impl Parser { ) } - /// Snowflake `EXECUTE ALERT | TASK [ GRAPH ] | DCM PROJECT ` with its action, or none. - fn parse_snowflake_execute_object(&mut self) -> Result> { - let start = self.current; - let kind = if self.match_text_seq(&["ALERT"]) { - "ALERT" - } else if self.match_text_seq(&["DCM", "PROJECT"]) { - EXECUTE_DCM_PROJECT - } else if self.match_text_seq(&["TASK", "GRAPH"]) { - "TASK GRAPH" - } else if self.match_text_seq(&["TASK"]) { - "TASK" - } else { - return Ok(None); - }; - let name = self.parse_object_name_ref()?; - let mut node = ExecuteObject { - kind: kind.to_owned(), - name, - action: None, - group: None, - alias: None, - config: None, - }; - if kind.starts_with("TASK") { - if self.match_text_seq(&["RETRY", "LAST"]) { - node.action = Some("RETRY LAST".to_owned()); - } else if self.match_text_seq(&["RETRY", "GRAPH", "RUN", "GROUP"]) { - node.action = Some("RETRY GRAPH RUN".to_owned()); - if !self.check(TokenType::String) { - return Err(self.parse_error("Expected the graph run group as a string")); - } - node.group = Some(self.parse_primary()?); - } else if self.match_text_seq(&["USING", "CONFIG"]) { - self.expect_token(TokenType::Eq)?; - node.config = Some(self.parse_primary()?); - } - } else if kind == EXECUTE_DCM_PROJECT { - if self.match_text_seq(&["PURGE"]) { - node.action = Some("PURGE".to_owned()); - if self.match_token(TokenType::As) { - node.alias = Some(self.expect_identifier_or_keyword_with_quoted()?); - } - } else if self.match_text_seq(&["TEST", "ALL"]) { - node.action = Some("TEST ALL".to_owned()); - } else { - self.current = start; - return Ok(None); - } - } - Ok(Some(Expression::ExecuteObject(Box::new(node)))) - } - /// Parses TSQL EXEC/EXECUTE [schema.]procedure [@param = value, ...]. pub(crate) fn parse_execute(&mut self) -> Result { self.expect_token(TokenType::Execute)?; @@ -221,6 +170,11 @@ impl Parser { if let Some(statement) = self.parse_snowflake_execute_object()? { return Ok(statement); } + if self.applies_snowflake_context_rules() + && self.match_text_seq(&["USING", "POLICY_CONTEXT"]) + { + return self.parse_execute_using_policy_context(); + } if self.current + 1 >= self.tokens.len() || self.check_next(TokenType::Semicolon) { return Err(self.parse_error("Expected an object kind and name after EXECUTE")); } diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs index 382133520..0ecfb8c3c 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs @@ -15,6 +15,7 @@ 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_data_metric; pub(crate) mod snowflake_show; pub(crate) mod snowflake_statements; pub(crate) mod table_options; diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_commands.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_commands.rs index d5ae29475..326320716 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_commands.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_commands.rs @@ -1,7 +1,11 @@ //! Snowflake's rules on COMMENT ON names and values, and its class instance method calls. +use crate::expressions::names::models::Identifier; use crate::expressions::operators::models::Call; +use crate::expressions::statements::models::ExecuteObject; +use crate::expressions::statements::models::ExecuteUsingPolicyContext; use crate::expressions::tree::models::Expression; +use crate::parser::constants::EXECUTE_DCM_PROJECT; use crate::parser::constants::SNOWFLAKE_RESERVED_KEYWORDS; use crate::parser::models::Parser; use crate::tokens::models::TokenType; @@ -30,6 +34,21 @@ impl Parser { Ok(()) } + /// A called object's dotted name; an empty middle part (`..`) is kept as an empty part. + pub(crate) fn parse_call_name(&mut self) -> Result> { + let mut name = vec![self.expect_identifier_or_keyword_with_quoted()?]; + while self.match_token(TokenType::Dot) { + if name.len() == 1 && self.match_token(TokenType::Dot) { + name.push(Identifier::new("")); + } + name.push(self.expect_identifier_or_keyword_with_quoted()?); + } + for part in &name { + self.reject_reserved_name_part(part)?; + } + Ok(name) + } + /// Parse a Snowflake class instance method call, `!( [ , ... ] )`, after CALL; None for any other CALL. pub(super) fn parse_snowflake_method_call(&mut self) -> Result> { if !self.is_snowflake() { @@ -38,30 +57,23 @@ impl Parser { if self.check(TokenType::Exclamation) { return Err(self.parse_error("Expected an instance name before !")); } - let mut offset = 0; - while self - .tokens - .get(self.current + offset + 1) - .map(|token| token.token_type) - == Some(TokenType::Dot) - { - offset += 2; + let token_type = |offset: usize| { + self.tokens + .get(self.current + offset) + .map(|token| token.token_type) + }; + let mut offset = 1; + while token_type(offset) == Some(TokenType::Dot) { + offset += if token_type(offset + 1) == Some(TokenType::Dot) { + 3 + } else { + 2 + }; } - if self - .tokens - .get(self.current + offset + 1) - .map(|token| token.token_type) - != Some(TokenType::Exclamation) - { + if token_type(offset) != Some(TokenType::Exclamation) { return Ok(None); } - let mut name = vec![self.expect_identifier_or_keyword_with_quoted()?]; - while self.match_token(TokenType::Dot) { - name.push(self.expect_identifier_or_keyword_with_quoted()?); - } - for part in &name { - self.reject_reserved_name_part(part)?; - } + let name = self.parse_call_name()?; self.expect_token(TokenType::Exclamation)?; if !self.is_identifier_token() { return Err(self.parse_error("Expected a method name after !")); @@ -85,4 +97,80 @@ impl Parser { into: None, })))) } + + /// The rest of Snowflake `EXECUTE USING POLICY_CONTEXT ( => , ... ) AS `. + pub(super) fn parse_execute_using_policy_context(&mut self) -> Result { + self.expect_token(TokenType::LParen)?; + let arguments = self.parse_function_arguments()?; + if arguments + .iter() + .any(|argument| !matches!(argument, Expression::NamedArgument(_))) + { + return Err(self.parse_error("POLICY_CONTEXT takes only named arguments")); + } + self.expect_token(TokenType::RParen)?; + self.expect_token(TokenType::As)?; + if !(self.check(TokenType::Select) + || self.check(TokenType::With) + || self.check(TokenType::LParen)) + { + return Err(self.parse_error("Expected a query after POLICY_CONTEXT ( ... ) AS")); + } + let query = self.parse_statement()?; + Ok(Expression::ExecuteUsingPolicyContext(Box::new( + ExecuteUsingPolicyContext { arguments, query }, + ))) + } + + /// Snowflake `EXECUTE ALERT | TASK [ GRAPH ] | DCM PROJECT ` with its action, or none. + pub(super) fn parse_snowflake_execute_object(&mut self) -> Result> { + let start = self.current; + let kind = if self.match_text_seq(&["ALERT"]) { + "ALERT" + } else if self.match_text_seq(&["DCM", "PROJECT"]) { + EXECUTE_DCM_PROJECT + } else if self.match_text_seq(&["TASK", "GRAPH"]) { + "TASK GRAPH" + } else if self.match_text_seq(&["TASK"]) { + "TASK" + } else { + return Ok(None); + }; + let name = self.parse_object_name_ref()?; + let mut node = ExecuteObject { + kind: kind.to_owned(), + name, + action: None, + group: None, + alias: None, + config: None, + }; + if kind.starts_with("TASK") { + if self.match_text_seq(&["RETRY", "LAST"]) { + node.action = Some("RETRY LAST".to_owned()); + } else if self.match_text_seq(&["RETRY", "GRAPH", "RUN", "GROUP"]) { + node.action = Some("RETRY GRAPH RUN".to_owned()); + if !self.check(TokenType::String) { + return Err(self.parse_error("Expected the graph run group as a string")); + } + node.group = Some(self.parse_primary()?); + } else if self.match_text_seq(&["USING", "CONFIG"]) { + self.expect_token(TokenType::Eq)?; + node.config = Some(self.parse_primary()?); + } + } else if kind == EXECUTE_DCM_PROJECT { + if self.match_text_seq(&["PURGE"]) { + node.action = Some("PURGE".to_owned()); + if self.match_token(TokenType::As) { + node.alias = Some(self.expect_identifier_or_keyword_with_quoted()?); + } + } else if self.match_text_seq(&["TEST", "ALL"]) { + node.action = Some("TEST ALL".to_owned()); + } else { + self.current = start; + return Ok(None); + } + } + Ok(Some(Expression::ExecuteObject(Box::new(node)))) + } } diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_data_metric.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_data_metric.rs new file mode 100644 index 000000000..8a6953dfa --- /dev/null +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_data_metric.rs @@ -0,0 +1,234 @@ +//! Snowflake data metric function actions in ALTER TABLE and ALTER VIEW. + +use crate::expressions::columns::models::DataMetricExpectation; +use crate::expressions::columns::models::DataMetricFunctionAction; +use crate::expressions::columns::models::DataMetricFunctionClause; +use crate::expressions::columns::models::DataMetricFunctionItem; +use crate::expressions::tree::models::Expression; +use crate::parser::models::Parser; +use crate::tokens::models::TokenType; +use crate::tokens::types::Result; + +impl Parser { + /// `{ ADD | DROP | MODIFY } DATA METRIC FUNCTION , ...` in a Snowflake ALTER TABLE or VIEW, or none. + pub(crate) fn parse_data_metric_function_action( + &mut self, + ) -> Result> { + if !self.applies_snowflake_context_rules() { + return Ok(None); + } + let kind = if self.check(TokenType::Add) { + "ADD" + } else if self.check(TokenType::Drop) { + "DROP" + } else if self.check_identifier("MODIFY") { + "MODIFY" + } else { + return Ok(None); + }; + let start = self.current; + self.skip(); + if !self.match_text_seq(&["DATA", "METRIC", "FUNCTION"]) { + self.current = start; + return Ok(None); + } + let mut items = vec![self.parse_data_metric_function_item(kind)?]; + while self.match_token(TokenType::Comma) { + items.push(self.parse_data_metric_function_item(kind)?); + } + Ok(Some(DataMetricFunctionAction { + kind: kind.to_owned(), + items, + })) + } + + /// ` ON ( [ , ... ] )` with the clauses its action allows. + fn parse_data_metric_function_item(&mut self, kind: &str) -> Result { + let name = self.parse_object_name_ref()?; + if !self.match_token(TokenType::On) { + return Err(self.parse_error("Expected ON after a data metric function name")); + } + let arguments = self.parse_data_metric_arguments()?; + let mut clauses: Vec = Vec::new(); + match kind { + "ADD" => { + while let Some(clause) = self.parse_data_metric_add_clause()? { + clauses.push(clause); + } + } + "MODIFY" => clauses.push(self.parse_data_metric_modify_clause()?), + _ => {} + } + Ok(DataMetricFunctionItem { + name, + arguments, + clauses, + }) + } + + /// `( [ , ... ] [ , TABLE ( ( , ... ) ) | , ] )`; the column list may be empty. + fn parse_data_metric_arguments(&mut self) -> Result> { + self.expect_token(TokenType::LParen)?; + let leading_comma = self.match_token(TokenType::Comma); + let arguments = if self.check(TokenType::RParen) && !leading_comma { + Vec::new() + } else { + self.parse_function_arguments()? + }; + self.expect_token(TokenType::RParen)?; + let column = |argument: &Expression| matches!(argument, Expression::Column(_)); + let last_allowed = |argument: &Expression| match argument { + Expression::Lambda(_) => true, + Expression::Function(table) if table.name.eq_ignore_ascii_case("TABLE") => { + matches!(table.args.as_slice(), [Expression::Function(source)] + if !source.args.is_empty() && source.args.iter().all(column)) + } + _ => false, + }; + let valid = match arguments.split_last() { + None => true, + Some((last, columns)) if leading_comma => columns.is_empty() && last_allowed(last), + Some((last, columns)) => { + columns.iter().all(column) && (column(last) || last_allowed(last)) + } + }; + if !valid { + return Err(self.parse_error( + "A data metric function takes columns, then optionally TABLE ( ( , ... ) ) or a lambda", + )); + } + Ok(arguments) + } + + /// A clause of ADD DATA METRIC FUNCTION, or none. + fn parse_data_metric_add_clause(&mut self) -> Result> { + if self.check_identifier("SENSITIVITY") || self.check_identifier("ANOMALY_DETECTION") { + let name = self.advance_text()?.to_ascii_uppercase(); + self.expect_token(TokenType::Eq)?; + let value = self.parse_primary()?; + return Ok(Some(DataMetricFunctionClause::Property { name, value })); + } + if self.match_identifier("EXPECTATION") { + return Ok(Some(DataMetricFunctionClause::Expectations { + action: None, + expectations: self.parse_data_metric_expectations(true)?, + })); + } + if self.match_text_seq(&["WITHIN", "GROUP"]) { + let columns = self.parse_wrapped_csv_expressions()?; + if columns.is_empty() + || !columns + .iter() + .all(|column| matches!(column, Expression::Column(_))) + { + return Err(self.parse_error("WITHIN GROUP takes columns")); + } + let limit = if self.match_text_seq(&["GROUP", "LIMIT"]) { + Some(self.parse_primary()?) + } else { + None + }; + return Ok(Some(DataMetricFunctionClause::WithinGroup { + columns, + limit, + })); + } + if self.match_text_seq(&["EXECUTE", "AS", "ROLE"]) { + let role = self.expect_identifier_or_keyword_with_quoted()?; + self.reject_reserved_name_part(&role)?; + return Ok(Some(DataMetricFunctionClause::ExecuteAsRole(role))); + } + if self.check(TokenType::Filter) { + let filter = self.parse_data_metric_filter()?; + if filter == DataMetricFunctionClause::Filter(None) { + return Err( + self.parse_error("ADD DATA METRIC FUNCTION ... FILTER needs a condition") + ); + } + return Ok(Some(filter)); + } + Ok(None) + } + + /// The one clause of a MODIFY DATA METRIC FUNCTION item. + fn parse_data_metric_modify_clause(&mut self) -> Result { + if self.check_identifier("SUSPEND") || self.check_identifier("RESUME") { + return Ok(DataMetricFunctionClause::State( + self.advance_text()?.to_ascii_uppercase(), + )); + } + if self.check(TokenType::Filter) { + return self.parse_data_metric_filter(); + } + let (action, with_condition) = if self.match_token(TokenType::Add) { + ("ADD", true) + } else if self.match_token(TokenType::Drop) { + ("DROP", false) + } else if self.match_identifier("MODIFY") { + ("MODIFY", true) + } else { + return Err(self.parse_error( + "Expected SUSPEND, RESUME, FILTER or an EXPECTATION change after the arguments", + )); + }; + if !self.match_identifier("EXPECTATION") { + return Err(self.parse_error("Expected EXPECTATION")); + } + Ok(DataMetricFunctionClause::Expectations { + action: Some(action.to_owned()), + expectations: self.parse_data_metric_expectations(with_condition)?, + }) + } + + /// `FILTER ( [ ] )`. + fn parse_data_metric_filter(&mut self) -> Result { + self.skip(); + self.expect_token(TokenType::LParen)?; + let condition = if self.check(TokenType::RParen) { + None + } else { + Some(self.parse_expression()?) + }; + self.expect_token(TokenType::RParen)?; + Ok(DataMetricFunctionClause::Filter(condition)) + } + + /// ` ( ), ...`, or bare names under DROP; a comma before ` ON` ends the list. + fn parse_data_metric_expectations( + &mut self, + with_condition: bool, + ) -> Result> { + let mut expectations: Vec = Vec::new(); + loop { + let name = self.expect_identifier_or_keyword_with_quoted()?; + self.reject_reserved_name_part(&name)?; + let condition = if with_condition { + self.expect_token(TokenType::LParen)?; + let condition = self.parse_expression()?; + self.expect_token(TokenType::RParen)?; + Some(condition) + } else { + None + }; + expectations.push(DataMetricExpectation { name, condition }); + let next = self + .tokens + .get(self.current + 1) + .map(|token| token.token_type); + let after = self + .tokens + .get(self.current + 2) + .map(|token| token.token_type); + let continues = if with_condition { + after == Some(TokenType::LParen) + } else { + !matches!(after, Some(TokenType::On) | Some(TokenType::Dot)) + && next != Some(TokenType::Semicolon) + }; + if !(self.check(TokenType::Comma) && continues) { + return Ok(expectations); + } + self.skip(); + } + } +} diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_statements.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_statements.rs index 59f099ce8..6f69a2790 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_statements.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_statements.rs @@ -1,5 +1,7 @@ //! Snowflake's rules on session, transaction, EXPLAIN and SHOW statements. +use crate::expressions::functions::models::Operator; +use crate::expressions::names::models::Identifier; use crate::expressions::statements::models::Describe; use crate::expressions::tree::models::Expression; use crate::parser::constants::{SHARED_DESCRIBE_KINDS, SNOWFLAKE_DESCRIBE_KINDS}; @@ -13,6 +15,21 @@ impl Parser { self.config.dialect == Some(crate::tokens::models::DialectType::Snowflake) } + /// Chains Snowflake statements with the flow operator, as in `SELECT 1 ->> SELECT * FROM $1`. + pub(crate) fn parse_snowflake_flow(&mut self, first: Expression) -> Result { + let mut flow = first; + while self.applies_snowflake_context_rules() && self.match_token(TokenType::DArrow) { + let next = self.parse_statement()?; + flow = Expression::Operator(Box::new(Operator { + this: Box::new(flow), + operator: Some(Box::new(Expression::Identifier(Identifier::new("->>")))), + expression: Box::new(next), + comments: Vec::new(), + })); + } + Ok(flow) + } + /// Refuses Snowflake's `START TRANSACTION NAME` without a name after it. pub(crate) fn refuse_snowflake_nameless_transaction(&self) -> Result<()> { let named = self.peek_nth(1).is_some_and(|token| { diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/transactions.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/transactions.rs index e91ffacd6..9ac56d0e6 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/transactions.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/transactions.rs @@ -210,6 +210,22 @@ impl Parser { || self.check_identifier("IMMEDIATE") || self.check_identifier("EXCLUSIVE"); + let kind_then_name = (self.check(TokenType::Transaction) || self.check_identifier("WORK")) + && self + .peek_nth(1) + .is_some_and(|token| token.text.eq_ignore_ascii_case("NAME")); + if self.is_snowflake() && kind_then_name { + self.skip(); + self.skip(); + let name = self.expect_identifier_or_safe_keyword_with_quoted()?; + return Ok(Expression::Transaction(Box::new(Transaction { + this: Some(Box::new(Expression::Identifier(name))), + modes: None, + mark: Some(Box::new(Expression::Identifier(Identifier::new( + "TRANSACTION".to_string(), + )))), + }))); + } if self.is_snowflake() && self.check_identifier("NAME") { self.refuse_snowflake_nameless_transaction()?; self.skip(); diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/comparisons.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/comparisons.rs index 420197bce..674b6c3ae 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/comparisons.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/comparisons.rs @@ -395,7 +395,7 @@ impl Parser { None }; let right = self.parse_bitwise_or()?; - let escape = if self.match_token(TokenType::Escape) { + let escape = if self.match_like_escape() { Some(self.parse_primary()?) } else { None @@ -424,7 +424,7 @@ impl Parser { None }; let right = self.parse_bitwise_or()?; - let escape = if self.match_token(TokenType::Escape) { + let escape = if self.match_like_escape() { Some(self.parse_primary()?) } else { None @@ -446,7 +446,7 @@ impl Parser { self.skip(); self.skip(); let pattern = self.parse_bitwise_or()?; - let escape = if self.match_token(TokenType::Escape) { + let escape = if self.match_like_escape() { Some(self.parse_primary()?) } else { None @@ -479,7 +479,7 @@ impl Parser { self.skip(); self.skip(); let pattern = self.parse_bitwise_or()?; - let escape = if self.match_token(TokenType::Escape) { + let escape = if self.match_like_escape() { Some(self.parse_primary()?) } else { None @@ -496,7 +496,7 @@ impl Parser { fn parse_not_like_comparison(&mut self, left: Expression) -> Result { Ok({ let right = self.parse_bitwise_or()?; - let escape = if self.match_token(TokenType::Escape) { + let escape = if self.match_like_escape() { Some(self.parse_primary()?) } else { None @@ -517,7 +517,7 @@ impl Parser { fn parse_not_ilike_comparison(&mut self, left: Expression) -> Result { Ok({ let right = self.parse_bitwise_or()?; - let escape = if self.match_token(TokenType::Escape) { + let escape = if self.match_like_escape() { Some(self.parse_primary()?) } else { None @@ -720,7 +720,7 @@ impl Parser { None }; let right = self.parse_bitwise_or()?; - let escape = if self.match_token(TokenType::Escape) { + let escape = if self.match_like_escape() { Some(self.parse_primary()?) } else { None @@ -750,7 +750,7 @@ impl Parser { None }; let right = self.parse_bitwise_or()?; - let escape = if self.match_token(TokenType::Escape) { + let escape = if self.match_like_escape() { Some(self.parse_primary()?) } else { None @@ -909,7 +909,7 @@ impl Parser { if self.match_token(TokenType::Any) || self.match_token(TokenType::Some) { let was_any = self.previous_token_type() == Some(TokenType::Any); self.expect_token(TokenType::LParen)?; - let inner = self.parse_statement()?; + let inner = self.parse_quantified_operand()?; self.expect_token(TokenType::RParen)?; let subquery = if was_any { self.maybe_wrap_in_subquery(inner) @@ -923,7 +923,7 @@ impl Parser { })) } else if self.match_token(TokenType::All) { self.expect_token(TokenType::LParen)?; - let inner = self.parse_statement()?; + let inner = self.parse_quantified_operand()?; self.expect_token(TokenType::RParen)?; let subquery = self.maybe_wrap_in_subquery(inner); Expression::All(Box::new(QuantifiedExpr { @@ -1445,7 +1445,9 @@ impl Parser { inferred_type: None, span: SourceSpan::default(), })); - } else if self.check(TokenType::Arrow) || self.check(TokenType::DArrow) { + } else if self.check(TokenType::Arrow) + || (self.check(TokenType::DArrow) && !self.applies_snowflake_context_rules()) + { let scalar = self.match_token(TokenType::DArrow); if !scalar { self.skip(); @@ -1611,7 +1613,9 @@ impl Parser { | "DATERANGE" ) || self.config.dialect == Some(crate::tokens::models::DialectType::DuckDB) - && DataType::from_unsigned_name(&type_name).is_some(); + && DataType::from_unsigned_name(&type_name).is_some() + || self.is_snowflake() + && crate::parser::constants::SNOWFLAKE_LITERAL_TYPES.contains(&type_name.as_str()); if !is_type_literal_type { return Ok(None); @@ -1935,7 +1939,9 @@ impl Parser { } } - while self.match_token(TokenType::Exclamation) { + while self.check(TokenType::Exclamation) { + self.refuse_snowflake_bang_call(&expr)?; + self.skip(); let attr = self.parse_primary()?; expr = Expression::ModelAttribute(Box::new(ModelAttribute { this: Box::new(expr), diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/operators.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/operators.rs index 42401f1b5..73bdd10a7 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/operators.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/operators.rs @@ -1377,7 +1377,9 @@ impl Parser { None }; - if self.match_token(TokenType::Rename) { + if let Some(action) = self.parse_data_metric_function_action()? { + actions.push(AlterViewAction::DataMetricFunction(action)); + } else if self.match_token(TokenType::Rename) { self.expect_token(TokenType::To)?; actions.push(AlterViewAction::Rename(Box::new(self.parse_table_ref()?))); } else if self.match_identifier("OWNER") { diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/paths.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/paths.rs index 6bd9eec38..fb24f6c76 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/paths.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/paths.rs @@ -89,6 +89,8 @@ impl Parser { had_initial_component = true; } else if self.check(TokenType::LBracket) { had_initial_component = true; + } else { + self.refuse_snowflake_string_path()?; } if !had_initial_component { @@ -647,6 +649,9 @@ impl Parser { fn parse_empty_parentheses(&mut self) -> Result { self.skip(); + if self.config.dialect == Some(crate::tokens::models::DialectType::Snowflake) { + return Err(self.parse_error("Expected an expression in the parentheses")); + } if self.config.dialect != Some(crate::tokens::models::DialectType::DuckDB) && (self.match_token(TokenType::Arrow) || self.match_token(TokenType::FArrow)) { @@ -658,9 +663,6 @@ impl Parser { parameter_types: Vec::new(), }))); } - if self.config.dialect == Some(crate::tokens::models::DialectType::Snowflake) { - return Err(self.parse_error("Expected an expression in the parentheses")); - } self.maybe_parse_subscript(Expression::Tuple(Box::new(Tuple { expressions: Vec::new(), }))) @@ -1088,6 +1090,10 @@ impl Parser { } if self.match_token(TokenType::Dot) { + if let Some(call) = self.parse_snowflake_default_schema_call(ident.clone())? { + return self.maybe_parse_subscript(call); + } + self.refuse_snowflake_reserved_part()?; if self.match_token(TokenType::Star) { let star = self.parse_star_modifiers(Some(ident))?; let star_expr = Expression::Star(star); @@ -1220,6 +1226,7 @@ impl Parser { if self.tokens.is_empty() { return Err(self.end_of_input_error()); } + self.refuse_snowflake_stage_reference()?; if self.check(TokenType::If) && !self.if_expr_ruled_out.contains(&self.current) diff --git a/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs b/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs index 73fa84e38..4940fbea1 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/expressions/primary.rs @@ -32,7 +32,6 @@ use crate::parser::constants::ODBC_TIME_ESCAPE; use crate::parser::constants::PYFORMAT_PARAMETER_SUFFIX; use crate::parser::constants::SECOND_PREVIOUS_OFFSET; use crate::parser::constants::STRUCT; -use crate::parser::constants::VARIADIC_KEYWORD; use crate::parser::models::Parser; use crate::tokens::models::SourceSpan; use crate::tokens::models::TokenType; @@ -51,19 +50,8 @@ impl Parser { return Ok(connect_by_root); } - if matches!( - self.config.dialect, - Some(crate::tokens::models::DialectType::PostgreSQL) - ) && self.check(TokenType::Var) - && self.peek_text().eq_ignore_ascii_case(VARIADIC_KEYWORD) - { - self.skip(); - let this = self.parse_bitwise_or()?; - return Ok(Expression::Variadic(Box::new( - crate::expressions::statements::models::Variadic { - this: Box::new(this), - }, - ))); + if let Some(variadic) = self.parse_variadic_argument()? { + return Ok(variadic); } if self.match_token(TokenType::LBracket) { @@ -1547,6 +1535,12 @@ impl Parser { } if self.match_token(TokenType::Dot) { + if let Some(call) = self.parse_snowflake_default_schema_call( + Identifier::new(name.clone()).with_span(token.span), + )? { + return self.maybe_parse_subscript(call); + } + self.refuse_snowflake_reserved_part()?; if self.match_token(TokenType::Star) { let ident = Identifier::new(name).with_span(token.span); let star = self.parse_star_modifiers(Some(ident))?; @@ -1666,6 +1660,10 @@ impl Parser { } else if self.check(TokenType::Number) || self.peek().token_type.is_keyword() { let token = self.advance()?; (token.text, false, false) + } else if self.is_snowflake() + && (self.check(TokenType::Comma) || self.check(TokenType::RParen)) + { + return Ok(Expression::column("@")); } else { return Err(self.parse_error("Expected variable name after @")); }; @@ -1694,7 +1692,14 @@ impl Parser { } return Err(self.parse_error(format!("Invalid colon parameter: :{}", num_token.text))); } - if self.is_identifier_token() || self.is_safe_keyword_as_identifier() { + let snowflake = self.is_snowflake(); + if snowflake && self.check(TokenType::QuotedIdentifier) { + return Err(self.parse_error("Expected an unquoted bind variable name after :")); + } + if self.is_identifier_token() + || self.is_safe_keyword_as_identifier() + || (snowflake && self.peek().token_type.is_keyword()) + { let name = self.advance_text()?; Ok(Expression::Parameter(Box::new(Parameter { name: Some(name), diff --git a/crates/sqlquack-syntax/src/parser/_helpers/functions/generic.rs b/crates/sqlquack-syntax/src/parser/_helpers/functions/generic.rs index a72f3ad51..5429b2b9a 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/functions/generic.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/functions/generic.rs @@ -437,6 +437,9 @@ impl Parser { } pub(crate) fn parse_method_arguments_for(&mut self, method: &str) -> Result> { + if let Some(query) = self.parse_snowflake_query_argument()? { + return Ok(vec![query]); + } self.parse_function_arguments_in_context(Some(method), true) } @@ -1007,7 +1010,8 @@ impl Parser { self.config.dialect, Some(crate::tokens::models::DialectType::DuckDB) | Some(crate::tokens::models::DialectType::SQLite) - ) && self.match_token(TokenType::DArrow) + ) && !self.applies_snowflake_context_rules() + && self.match_token(TokenType::DArrow) { let path = self.parse_json_path_operand()?; expr = Expression::JsonExtractScalar(Box::new(JsonExtractFunc { @@ -1492,6 +1496,11 @@ impl Parser { } } else { let expr = self.parse_addition()?; + if self.applies_snowflake_context_rules() + && matches!(&expr, Expression::Literal(literal) if matches!(**literal, Literal::String(_))) + { + return Err(self.parse_error("A Snowflake frame bound value is not a string")); + } if self.match_token(TokenType::Preceding) { let text = self.tokens[self.current - 1].text.to_string(); Ok((WindowFrameBound::Preceding(Box::new(expr)), Some(text))) @@ -1588,6 +1597,7 @@ impl Parser { return Ok(None); } + self.refuse_snowflake_interval_name(value.as_ref())?; let unit = self.try_parse_interval_unit()?; let is_generic = self.config.dialect.is_none() diff --git a/crates/sqlquack-syntax/src/parser/_helpers/functions/mod.rs b/crates/sqlquack-syntax/src/parser/_helpers/functions/mod.rs index 7b14c7e93..9068476ef 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/functions/mod.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/functions/mod.rs @@ -3,5 +3,6 @@ pub(crate) mod generic; pub(crate) mod registry; pub(crate) mod registry_functions; +pub(crate) mod snowflake_calls; pub(crate) mod token_sets; pub(crate) mod typed; diff --git a/crates/sqlquack-syntax/src/parser/_helpers/functions/snowflake_calls.rs b/crates/sqlquack-syntax/src/parser/_helpers/functions/snowflake_calls.rs new file mode 100644 index 000000000..222000361 --- /dev/null +++ b/crates/sqlquack-syntax/src/parser/_helpers/functions/snowflake_calls.rs @@ -0,0 +1,228 @@ +//! Snowflake call arguments the shared argument list does not read. + +use crate::expressions::names::models::Column; +use crate::expressions::names::models::Identifier; +use crate::expressions::queries::models::Subquery; +use crate::expressions::tree::models::Expression; +use crate::parser::constants::VARIADIC_KEYWORD; +use crate::parser::models::Parser; +use crate::tokens::models::SourceSpan; +use crate::tokens::models::TokenType; +use crate::tokens::types::Result; + +impl Parser { + /// Refuses a string after a Snowflake path colon, as in `v1 :'v1'`. + pub(crate) fn refuse_snowflake_string_path(&self) -> Result<()> { + match self.applies_snowflake_context_rules() && self.check(TokenType::String) { + true => Err(self.parse_error("Expected a path element, not a string, after :")), + false => Ok(()), + } + } + + /// Refuses a name as a Snowflake interval's value, as in `INTERVAL m1 YEAR`; the value is a string. + pub(crate) fn refuse_snowflake_interval_name(&self, value: Option<&Expression>) -> Result<()> { + let name = matches!( + value, + Some(Expression::Column(_) | Expression::Identifier(_)) + ); + match self.applies_snowflake_context_rules() && name { + true => Err(self.parse_error("Expected a string as the interval's value")), + false => Ok(()), + } + } + + /// An array spread into a call's arguments: PostgreSQL's `VARIADIC ` or Snowflake's `** `. + pub(crate) fn parse_variadic_argument(&mut self) -> Result> { + let postgres_variadic = self.config.dialect + == Some(crate::tokens::models::DialectType::PostgreSQL) + && self.check(TokenType::Var) + && self.peek_text().eq_ignore_ascii_case(VARIADIC_KEYWORD); + let argument = matches!( + self.previous_token_type(), + Some(TokenType::LParen | TokenType::Comma) + ); + let snowflake_spread = self.is_snowflake() && argument && self.check(TokenType::DStar); + if !postgres_variadic && !snowflake_spread { + return Ok(None); + } + self.skip(); + let this = self.parse_bitwise_or()?; + Ok(Some(Expression::Variadic(Box::new( + crate::expressions::statements::models::Variadic { + this: Box::new(this), + }, + )))) + } + + /// The operand of `ANY`/`ALL`, which Snowflake also reads as a list of values, as in `1 = ALL ( 1 )`. + pub(crate) fn parse_quantified_operand(&mut self) -> Result { + let query = self.check(TokenType::Select) + || self.check(TokenType::With) + || self.check(TokenType::LParen); + if !self.is_snowflake() || query { + return self.parse_statement(); + } + let mut values = self.parse_expression_list()?; + match values.len() { + 1 => Ok(values.remove(0)), + _ => Ok(Expression::Tuple(Box::new( + crate::expressions::types::models::Tuple { + expressions: values, + }, + ))), + } + } + + /// Consumes a pattern's `ESCAPE`, which Snowflake reads as an alias when nothing follows it. + pub(crate) fn match_like_escape(&mut self) -> bool { + let alias = self.is_snowflake() + && matches!( + self.peek_nth(1).map(|token| token.token_type), + None | Some( + TokenType::Eof + | TokenType::Semicolon + | TokenType::Comma + | TokenType::RParen + | TokenType::From + | TokenType::Where + | TokenType::Group + | TokenType::GroupBy + | TokenType::Order + | TokenType::OrderBy + | TokenType::Having + | TokenType::Qualify + | TokenType::Limit + | TokenType::Union + ) + ); + self.check(TokenType::Escape) && !alias && self.match_token(TokenType::Escape) + } + + /// A qualified Snowflake call's lone query argument, as in `SNOWFLAKE.CORE.NULL_COUNT(SELECT c FROM t)`. + pub(crate) fn parse_snowflake_query_argument(&mut self) -> Result> { + let query_start = self.check(TokenType::Select) || self.check(TokenType::With); + let column = matches!( + self.peek_nth(1).map(|token| token.token_type), + Some(TokenType::Comma | TokenType::RParen | TokenType::Dot) + ); + if !self.is_snowflake() || !query_start || column { + return Ok(None); + } + let query = self.parse_statement()?; + Ok(Some(Expression::Subquery(Box::new(Subquery { + this: query, + alias: None, + column_aliases: Vec::new(), + alias_explicit_as: false, + alias_keyword: None, + order_by: None, + limit: None, + offset: None, + distribute_by: None, + sort_by: None, + cluster_by: None, + lateral: false, + modifiers_inside: false, + trailing_comments: Vec::new(), + inferred_type: None, + span: SourceSpan::default(), + })))) + } + + /// Refuses a Snowflake `!( )` without a name or `MODEL( )` receiver, or a method. + pub(crate) fn refuse_snowflake_bang_call(&self, receiver: &Expression) -> Result<()> { + if !self.is_snowflake() { + return Ok(()); + } + let model_args = match receiver { + Expression::Function(call) if call.name.eq_ignore_ascii_case("MODEL") => { + Some(&call.args) + } + _ => None, + }; + let bad_model = model_args.is_some_and(|args| { + args.is_empty() || args.iter().any(|arg| !matches!(arg, Expression::Column(_))) + }); + let bad_receiver = matches!( + receiver, + Expression::Literal(_) | Expression::Paren(_) | Expression::Tuple(_) + ); + let no_method = self.peek_nth(1).map(|token| token.token_type) == Some(TokenType::LParen); + match bad_model || bad_receiver || no_method { + true => { + Err(self.parse_error("Expected a name or MODEL( ) before ! and a method after it")) + } + false => Ok(()), + } + } + + /// Refuses a Snowflake stage reference `@'x'`, `@"x y"` with blanks in its quotes, or `@`. + pub(crate) fn refuse_snowflake_stage_reference(&self) -> Result<()> { + if !self.is_snowflake() { + return Ok(()); + } + let next = self.peek_nth(1); + let split = self.check(TokenType::DAt) + && next.is_some_and(|token| match token.token_type { + TokenType::String => true, + TokenType::QuotedIdentifier => token.text.contains(char::is_whitespace), + _ => false, + }); + let text = self.peek_text(); + let word = text + .strip_prefix('@') + .and_then(|rest| rest.split(['.', '/']).next()) + .unwrap_or_default() + .to_ascii_uppercase(); + let reserved = self.check(TokenType::Var) + && crate::parser::constants::SNOWFLAKE_RESERVED_KEYWORDS.contains(&word.as_str()); + match split || reserved { + true => Err(self.parse_error("Expected a stage name after @")), + false => Ok(()), + } + } + + /// Refuses an unquoted Snowflake reserved word as a later part of a dotted name, as in `a . SELECT`. + pub(crate) fn refuse_snowflake_reserved_part(&self) -> Result<()> { + let word = self.peek_text().to_ascii_uppercase(); + let reserved = !self.check(TokenType::QuotedIdentifier) + && crate::parser::constants::SNOWFLAKE_REFUSED_NAME_PARTS.contains(&word.as_str()); + match self.is_snowflake() && reserved { + true => Err(self.parse_error(format!( + "Reserved word {word} cannot be an unquoted name part" + ))), + false => Ok(()), + } + } + + /// Snowflake's `..( ... )`, the schema left out; it is kept as an empty part. + pub(crate) fn parse_snowflake_default_schema_call( + &mut self, + database: Identifier, + ) -> Result> { + let call = self.check(TokenType::Dot) + && self.peek_nth(2).map(|token| token.token_type) == Some(TokenType::LParen); + if !self.is_snowflake() || !call { + return Ok(None); + } + self.skip(); + let method = self.expect_identifier_or_keyword_with_quoted()?; + self.expect_token(TokenType::LParen)?; + let args = match self.check(TokenType::RParen) { + true => Vec::new(), + false => self.parse_method_arguments_for(&method.name)?, + }; + self.expect_token(TokenType::RParen)?; + let schema = Expression::Column(Box::new(Column { + name: Identifier::new(String::new()), + table: Some(database), + join_mark: false, + trailing_comments: Vec::new(), + span: None, + inferred_type: None, + })); + Ok(Some( + self.parse_method_call_expression(schema, method, args), + )) + } +} diff --git a/crates/sqlquack-syntax/src/parser/_helpers/statements/entry.rs b/crates/sqlquack-syntax/src/parser/_helpers/statements/entry.rs index b35b61b27..a1f9f46d3 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/statements/entry.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/statements/entry.rs @@ -269,7 +269,8 @@ impl Parser { while !self.is_at_end() { let start = self.current; - let mut stmt = self.parse_statement()?; + let first = self.parse_statement()?; + let mut stmt = self.parse_snowflake_flow(first)?; stmt = self.spanned(start, stmt); if self.check(TokenType::Semicolon) { diff --git a/crates/sqlquack-syntax/src/parser/_helpers/statements/procedures.rs b/crates/sqlquack-syntax/src/parser/_helpers/statements/procedures.rs index 28a604189..223defc3d 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/statements/procedures.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/statements/procedures.rs @@ -8,6 +8,7 @@ use crate::expressions::schema_objects::models::CreateProcedure; use crate::expressions::schema_objects::models::FunctionBody; use crate::expressions::schema_objects::models::FunctionParameter; use crate::expressions::tree::models::Expression; +use crate::expressions::types::models::DataType; use crate::expressions::types::models::StructField; use crate::parser::models::Dialect; use crate::parser::models::Parser; @@ -30,18 +31,7 @@ impl Parser { pub(crate) fn parse_anonymous_procedure(&mut self, name: &Identifier) -> Result { self.expect_token(TokenType::Procedure)?; let parameters = self.parse_anonymous_procedure_parameters()?; - if !self.match_token(TokenType::Returns) { - return Err(self.parse_error("Expected RETURNS after the procedure parameters")); - } - let (return_type, returns_table) = if self.match_token(TokenType::Table) { - (None, Some(self.parse_procedure_table_columns()?)) - } else { - let data_type = self.parse_data_type()?; - if !self.match_keywords(&[TokenType::Not, TokenType::Null]) { - self.match_token(TokenType::Null); - } - (Some(data_type), None) - }; + let (return_type, returns_table) = self.parse_procedure_returns()?; if !self.match_token(TokenType::Language) { return Err(self.parse_error("Expected LANGUAGE after the procedure return type")); } @@ -60,8 +50,27 @@ impl Parser { Ok(Expression::CreateProcedure(Box::new(procedure))) } + /// `RETURNS { [[NOT] NULL] | TABLE ( ... ) }` after the parameters. + pub(crate) fn parse_procedure_returns( + &mut self, + ) -> Result<(Option, Option>)> { + if !self.match_token(TokenType::Returns) { + return Err(self.parse_error("Expected RETURNS after the procedure parameters")); + } + if self.match_token(TokenType::Table) { + return Ok((None, Some(self.parse_procedure_table_columns()?))); + } + let data_type = self.parse_data_type()?; + if !self.match_keywords(&[TokenType::Not, TokenType::Null]) { + self.match_token(TokenType::Null); + } + Ok((Some(data_type), None)) + } + /// `( [ [DEFAULT ]] [, ...] )` with no empty or trailing entries. - fn parse_anonymous_procedure_parameters(&mut self) -> Result> { + pub(crate) fn parse_anonymous_procedure_parameters( + &mut self, + ) -> Result> { self.expect_token(TokenType::LParen)?; let mut parameters: Vec = Vec::new(); while !self.check(TokenType::RParen) { @@ -197,19 +206,18 @@ impl Parser { Ok(FunctionBody::Statements(statements)) } - /// `CALL ( [[ =>] , ...] ) [INTO :]` after a WITH clause. + /// `CALL ( ... ) [INTO :]` after a WITH clause. pub(crate) fn parse_call_with(&mut self, with: With) -> Result { if !self.check_identifier("CALL") { return Err(self.parse_error("Expected CALL after an anonymous procedure")); } self.skip(); - let mut name = vec![self.expect_identifier_or_keyword_with_quoted()?]; - while self.match_token(TokenType::Dot) { - name.push(self.expect_identifier_or_keyword_with_quoted()?); - } - for part in &name { - self.reject_reserved_name_part(part)?; - } + self.parse_call_target(Some(with)) + } + + /// ` ( [[ =>] , ...] ) [INTO :]` after CALL. + pub(crate) fn parse_call_target(&mut self, with: Option) -> Result { + let name = self.parse_call_name()?; self.expect_token(TokenType::LParen)?; let arguments = if self.check(TokenType::RParen) { Vec::new() @@ -232,7 +240,7 @@ impl Parser { arguments, asynchronous: false, hints: Vec::new(), - with: Some(with), + with, into, }))) } diff --git a/crates/sqlquack-syntax/src/parser/_helpers/statements/scripting.rs b/crates/sqlquack-syntax/src/parser/_helpers/statements/scripting.rs index b3a48bbcd..f11f7d804 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/statements/scripting.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/statements/scripting.rs @@ -8,9 +8,9 @@ use crate::expressions::statements::models::{ ScriptingExceptionHandler, ScriptingFetch, ScriptingFor, ScriptingForIteration, ScriptingHandledExceptions, ScriptingHandlerAction, ScriptingIf, ScriptingJump, ScriptingJumpKeyword, ScriptingLet, ScriptingLoop, ScriptingLoopKeyword, ScriptingName, - ScriptingNull, ScriptingOpen, ScriptingRaise, ScriptingRange, ScriptingRepeat, - ScriptingResultset, ScriptingResultsetDefault, ScriptingReturn, ScriptingReturnValue, - ScriptingSql, ScriptingStatement, ScriptingVariable, ScriptingWhile, + ScriptingNull, ScriptingOpen, ScriptingProcedure, ScriptingRaise, ScriptingRange, + ScriptingRepeat, ScriptingResultset, ScriptingResultsetDefault, ScriptingReturn, + ScriptingReturnValue, ScriptingSql, ScriptingStatement, ScriptingVariable, ScriptingWhile, }; use crate::expressions::tree::models::Expression; use crate::parser::models::Parser; @@ -204,10 +204,24 @@ impl Parser { if !is_let && self.match_text_seq(&["EXCEPTION"]) { return self.parse_scripting_exception(start, name); } + if !is_let && self.match_text_seq(&["PROCEDURE"]) { + let parameters = self.parse_anonymous_procedure_parameters()?; + let (return_type, returns_table) = self.parse_procedure_returns()?; + self.expect_token(TokenType::As)?; + let body = self.with_parser_depth(Self::parse_scripting_block_members)?; + return Ok(ScriptingDeclaration::Procedure(Box::new( + ScriptingProcedure { + span: self.source_range(start), + name, + parameters, + return_type, + returns_table, + body, + }, + ))); + } if self.check_text_seq(&["PROCEDURE"]) { - return Err(self.parse_error( - "SCRIPTING_PENDING: nested procedure declarations are not parsed yet", - )); + return Err(self.parse_error("Expected DECLARE before a nested procedure")); } let data_type = if self.check(TokenType::ColonEq) || self.check_text_seq(&["DEFAULT"]) @@ -740,7 +754,10 @@ impl Parser { /// An optional label, written directly before the statement's semicolon. fn parse_scripting_label(&mut self) -> Result> { - if self.is_at_end() || self.check(TokenType::Semicolon) { + if self.is_at_end() + || self.check(TokenType::Semicolon) + || (self.check(TokenType::DArrow) && self.applies_snowflake_context_rules()) + { return Ok(None); } self.expect_scripting_name().map(Some) diff --git a/crates/sqlquack-syntax/src/parser/_helpers/types/cast_base_types.rs b/crates/sqlquack-syntax/src/parser/_helpers/types/cast_base_types.rs index 625fdbf36..dcf40fd0d 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/types/cast_base_types.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/types/cast_base_types.rs @@ -458,6 +458,7 @@ impl Parser { } else { None }; + self.refuse_snowflake_bare_interval(&unit, &to)?; DataType::Interval { unit, to } }) } diff --git a/crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs b/crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs index 6d03c529e..f12821c52 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/types/casts.rs @@ -148,7 +148,7 @@ impl Parser { fmt_expr }; Some(Box::new(fmt_with_tz)) - } else if self.match_token(TokenType::Comma) { + } else if !self.applies_snowflake_context_rules() && self.match_token(TokenType::Comma) { Some(Box::new(self.parse_expression()?)) } else { None @@ -232,9 +232,50 @@ impl Parser { name.eq_ignore_ascii_case("INT128") || name.eq_ignore_ascii_case("HUGEINT") } - /// Vertica interval qualifiers may carry a seconds precision: `SECOND(3)`. + /// Reads Snowflake's `( n )` after an interval field; Vertica's `SECOND(3)` is read elsewhere. pub(crate) fn parse_interval_field_precision(&mut self, unit: String) -> Result { - Ok(unit) + use crate::parser::constants::{ + SECOND_PREVIOUS_OFFSET, SNOWFLAKE_INTERVAL_FRACTION, SNOWFLAKE_INTERVAL_LEADING, + SNOWFLAKE_INTERVAL_SECOND, + }; + if !self.is_snowflake() || !self.check(TokenType::LParen) { + return Ok(unit); + } + let after_to = self.current >= SECOND_PREVIOUS_OFFSET + && self.tokens[self.current - SECOND_PREVIOUS_OFFSET].token_type == TokenType::To; + let range = match after_to && unit == SNOWFLAKE_INTERVAL_SECOND { + true => SNOWFLAKE_INTERVAL_FRACTION, + false => SNOWFLAKE_INTERVAL_LEADING, + }; + self.skip(); + let number = self.check(TokenType::Number); + match self.peek_text().parse::() { + Ok(digits) if number && range.contains(&digits) => { + self.skip(); + self.expect_token(TokenType::RParen)?; + Ok(format!("{unit}({digits})")) + } + _ => Err(self.parse_error(format!( + "Expected an interval precision from {} to {}", + range.start(), + range.end() + ))), + } + } + + /// Refuses Snowflake's INTERVAL type without a field, or with TO and no field after it. + pub(crate) fn refuse_snowflake_bare_interval( + &self, + unit: &Option, + to: &Option, + ) -> Result<()> { + let dangling_to = to.is_none() && self.previous().token_type == TokenType::To; + match self.is_snowflake() && (unit.is_none() || dangling_to) { + true => { + Err(self.parse_error("Expected an interval field such as DAY or YEAR TO MONTH")) + } + false => Ok(()), + } } /// Parse a data type. @@ -362,6 +403,13 @@ impl Parser { } } "STRING" => { + let empty = self.is_snowflake() + && self.check(TokenType::LParen) + && self.check_next(TokenType::RParen); + if empty { + self.skip(); + self.skip(); + } let length = if self.match_token(TokenType::LParen) { let n = self.expect_number()? as u32; self.expect_token(TokenType::RParen)?; @@ -867,6 +915,7 @@ impl Parser { } else { None }; + self.refuse_snowflake_bare_interval(&unit, &to)?; Ok(DataType::Interval { unit, to }) } @@ -1183,6 +1232,7 @@ impl Parser { }); } let mut element = self.parse_data_type()?; + element = self.parse_snowflake_not_null(element); while let Some(close) = closes.pop() { if close == TokenType::Gt { self.expect_gt()?; @@ -1201,6 +1251,21 @@ impl Parser { Ok(element) } + /// `inner` with a trailing Snowflake `NOT NULL` on a structured type's element or value. + fn parse_snowflake_not_null(&mut self, inner: DataType) -> DataType { + let not_null = self.applies_snowflake_context_rules() + && self.check(TokenType::Not) + && self.check_next(TokenType::Null); + if !not_null { + return inner; + } + self.skip(); + self.skip(); + DataType::NotNull { + inner: Box::new(inner), + } + } + #[inline(never)] pub(crate) fn parse_map_data_type(&mut self) -> Result { if self.match_token(TokenType::Lt) { @@ -1224,7 +1289,8 @@ impl Parser { } else if self.match_token(TokenType::LParen) { let key_type = self.parse_data_type()?; self.expect_token(TokenType::Comma)?; - let value_type = self.parse_data_type()?; + let parsed_value = self.parse_data_type()?; + let value_type = self.parse_snowflake_not_null(parsed_value); self.expect_token(TokenType::RParen)?; Ok(DataType::Map { key_type: Box::new(key_type), diff --git a/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs b/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs index 68976472e..b5d5761df 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/types/data_types.rs @@ -471,6 +471,9 @@ impl Parser { DataType::Nullable { inner } => { format!("Nullable({})", self.data_type_to_string(inner)) } + DataType::NotNull { inner } => { + format!("{} NOT NULL", self.data_type_to_string(inner)) + } DataType::Custom { name } => name.clone(), _ => format!("{:?}", dt), } diff --git a/crates/sqlquack-syntax/src/parser/constants.rs b/crates/sqlquack-syntax/src/parser/constants.rs index 591fd9ed9..4e6405a94 100644 --- a/crates/sqlquack-syntax/src/parser/constants.rs +++ b/crates/sqlquack-syntax/src/parser/constants.rs @@ -227,6 +227,12 @@ pub(crate) const SQLITE_RANGE_FOLLOWERS: [crate::tokens::models::TokenType; 9] = crate::tokens::models::TokenType::Match, ]; +/// Snowflake types written before a string as a typed literal, beyond the shared ones. +pub(crate) const SNOWFLAKE_LITERAL_TYPES: &[&str] = &["IPADDRESS"]; + +/// Reserved words Snowflake is recorded refusing as an unquoted later part of a dotted name. +pub(crate) const SNOWFLAKE_REFUSED_NAME_PARTS: &[&str] = &["SELECT"]; + /// Snowflake's fully reserved keywords; its position-limited keywords are not listed. pub(crate) const SNOWFLAKE_RESERVED_KEYWORDS: &[&str] = &[ "ALL", @@ -875,3 +881,12 @@ pub(crate) const SNOWFLAKE_SHOW_REQUIRED_CONTAINERS: &[(&str, &[&str])] = &[ ], ), ]; + +/// The interval field whose precision after TO counts fractional digits. +pub(crate) const SNOWFLAKE_INTERVAL_SECOND: &str = "SECOND"; + +/// Snowflake's recorded leading-field interval precisions, as in `INTERVAL YEAR ( 9 )`. +pub(crate) const SNOWFLAKE_INTERVAL_LEADING: std::ops::RangeInclusive = 1..=9; + +/// Snowflake's recorded fractional-second precisions after TO, as in `DAY TO SECOND ( 0 )`. +pub(crate) const SNOWFLAKE_INTERVAL_FRACTION: std::ops::RangeInclusive = 0..=9; diff --git a/crates/sqlquack-syntax/tests/parser/test_scripting.rs b/crates/sqlquack-syntax/tests/parser/test_scripting.rs index 01581a8a8..395bf4314 100644 --- a/crates/sqlquack-syntax/tests/parser/test_scripting.rs +++ b/crates/sqlquack-syntax/tests/parser/test_scripting.rs @@ -250,7 +250,7 @@ fn given_recorded_block_verdicts_when_parsing_then_acceptance_matches_snowflake( ScriptingVerdictTestCase { description: "recorded rejected: FETCH without INTO", sql: "DECLARE\n first_amount NUMBER;\n orders_cursor CURSOR FOR SELECT 17;\nBEGIN\n OPEN orders_cursor;\n FETCH orders_cursor first_amount;\n RETURN first_amount;\nEND;", expected_error: Some("Expected Into") }, ScriptingVerdictTestCase { description: "recorded rejected: LOOP closed by END WHILE", sql: "BEGIN\n LOOP\n BREAK;\n END WHILE;\nEND;", expected_error: Some("Expected END LOOP") }, ScriptingVerdictTestCase { description: "recorded rejected: statement without semicolon before END", sql: "BEGIN\n RETURN 17\nEND;", expected_error: Some("Expected Semicolon") }, - ScriptingVerdictTestCase { description: "recorded accepted but not parsed yet: nested procedure stays an explicit gap", sql: "DECLARE\n adjust PROCEDURE (amount NUMBER) RETURNS NUMBER AS BEGIN RETURN amount + 17; END;\nBEGIN\n RETURN 31;\nEND;", expected_error: Some("SCRIPTING_PENDING") }, + ScriptingVerdictTestCase { description: "recorded accepted: a nested procedure declaration", sql: "DECLARE\n adjust PROCEDURE (amount NUMBER) RETURNS NUMBER AS BEGIN RETURN amount + 17; END;\nBEGIN\n RETURN 31;\nEND;", expected_error: None }, ScriptingVerdictTestCase { description: "recorded accepted: block closed by END IF", sql: "BEGIN\n RETURN 17;\nEND IF;", expected_error: None }, ScriptingVerdictTestCase { description: "recorded accepted: block closed by END CASE", sql: "BEGIN\n RETURN 17;\nEND CASE;", expected_error: None }, ScriptingVerdictTestCase { description: "recorded accepted: keyword label after END LOOP", sql: "BEGIN\n LOOP\n BREAK;\n END LOOP WHILE;\n RETURN 17;\nEND;", expected_error: None }, diff --git a/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs b/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs index 70ae687f8..f8bbc7010 100644 --- a/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs +++ b/crates/sqlquack-syntax/tests/parser/test_snowflake_recorded.rs @@ -140,6 +140,312 @@ fn given_recorded_snowflake_cases_when_parsing_then_acceptance_matches_snowflake dialect: Dialect::Snowflake, expected_accepted: false, }, + AcceptanceTestCase { + description: "a data metric function takes a bare query (recorded semantic error)", + sql: "SELECT SNOWFLAKE . CORE . NULL_COUNT ( SELECT c1 FROM t1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an unqualified call takes no bare query", + sql: "SELECT COUNT ( SELECT 1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a model method needs a name before ! (recorded errno 1003)", + sql: "SELECT ( m1 )!predict ( * ) FROM t1 ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "MODEL( ) takes names, not strings (recorded errno 1003)", + sql: "SELECT MODEL ( 'v1' )!predict ( 1 ) FROM t1 ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a method call needs a method after ! (recorded errno 1003)", + sql: "SELECT svc1! ( v1 , v1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "MODEL( )!method parses (recorded success)", + sql: "SELECT MODEL ( m1 , v1 )!predict ( 1 ) FROM t1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a stage reference is no string (recorded errno 1003)", + sql: "SELECT GET_STAGE_LOCATION ( @'v1' ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a quoted stage name has no blanks (recorded errno 1003)", + sql: "SELECT GET_STAGE_LOCATION ( @\"v1 Q\" ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a stage name is no reserved word (recorded errno 1003)", + sql: "SELECT GET_STAGE_LOCATION ( @SELECT ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a quoted reserved stage name parses (recorded semantic error)", + sql: "SELECT GET_STAGE_LOCATION ( @\"SELECT\" ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an interval type takes a precision (recorded success)", + sql: "SELECT CAST ( NULL AS INTERVAL DAY TO SECOND ( 3 ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an interval leading precision is 1 to 9 (recorded errno 1003)", + sql: "SELECT CAST ( NULL AS INTERVAL YEAR ( 0 ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "seconds after TO take precision 0 (recorded success)", + sql: "SELECT CAST ( NULL AS INTERVAL DAY TO SECOND ( 0 ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an interval type needs a field (recorded errno 1003)", + sql: "SELECT CAST ( NULL AS INTERVAL ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "TO needs a field after it (recorded errno 1003)", + sql: "SELECT CAST ( NULL AS INTERVAL DAY TO ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a block declares a nested procedure (recorded success)", + sql: "DECLARE p1 PROCEDURE ( v1 INT ) RETURNS TABLE ( c1 INT ) AS BEGIN RETURN 1 ; END ; BEGIN RETURN 1 ; END ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a reserved word is no later name part (recorded errno 1003)", + sql: "SELECT SYSTEM$GET_SERVICE_STATUS ( v1 . SELECT . svc1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a function call may leave out the schema (recorded semantic error)", + sql: "SELECT SNOWFLAKE . . AGENT_RUN ( v1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a name without a call keeps its schema (recorded errno 1003)", + sql: "SELECT SYSTEM$GET_SERVICE_STATUS ( v1 . . svc1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "BEGIN WORK NAME names the transaction (recorded success)", + sql: "BEGIN WORK NAME v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a trailing ESCAPE is an alias (recorded semantic error)", + sql: "SELECT v1 LIKE ANY ( 'm' ) ESCAPE ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "ESCAPE takes a string (recorded success)", + sql: "SELECT v1 ILIKE 'm' ESCAPE '!' ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a lambda needs a parameter (recorded errno 1003)", + sql: "SELECT ACCUMULATE ( 1 , ( ) -> 0 , ( s , x ) -> s + x , ( s1 , s2 ) -> s1 + s2 , s -> s ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a bind variable is no quoted name (recorded errno 1003)", + sql: "SELECT :\"v1 Q\" ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a bind variable may be a keyword (recorded semantic error)", + sql: "SELECT :SELECT ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a bare @ is a stage argument (recorded semantic error)", + sql: "SELECT GET_STAGE_LOCATION ( @) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "ALL takes a list of values (recorded semantic error)", + sql: "SELECT 1 = ALL ( 1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "STRING takes empty parentheses (recorded success)", + sql: "SELECT CAST ( NULL AS STRING ( ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "statements chain with the flow operator (recorded success)", + sql: "SELECT 1 ->> SELECT 2 ->> SELECT 3 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a block chains with the flow operator (recorded success)", + sql: "BEGIN BEGIN NULL ; END ; END ->> SELECT 2 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "the flow operator needs a statement after it (recorded errno 1003)", + sql: "SELECT 1 ->> 1 ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "** spreads an array into arguments (recorded success)", + sql: "SELECT GREATEST ( ** [ 1 , 2 ] ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "** needs an array after it (recorded errno 1003)", + sql: "SELECT GREATEST ( ** ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "IPADDRESS types a string literal (recorded success)", + sql: "SELECT IPADDRESS '192.0.2.1' ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an interval's value is a string, not a name (recorded errno 1003)", + sql: "SELECT INTERVAL m1 YEAR ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a path colon takes no string (recorded errno 1003)", + sql: "SELECT v1 :'v1' ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "CAST takes nothing after its type (recorded errno 1003)", + sql: "SELECT CAST ( NULL AS INTERVAL SECOND ( 9 ) , ( 3 ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a structured ARRAY's element may be NOT NULL (recorded success)", + sql: "SELECT CAST ( NULL AS ARRAY ( INT NOT NULL ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a structured MAP's value may be NOT NULL (recorded success)", + sql: "SELECT CAST ( NULL AS MAP ( VARCHAR , INT NOT NULL ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "CALL needs its argument list closed (recorded errno 1003)", + sql: "CALL SET_SYS_CONTEXT ( 'm' , 'm' , 'm' ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "CALL names a class method with the schema left out (recorded semantic error)", + sql: "CALL SNOWFLAKE . . ANOMALY_INSIGHTS!DROP_MONITOR ( 'm' ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "CALL takes no empty argument (recorded errno 1003)", + sql: "CALL SET_SYS_CONTEXT ( 'm' , , 'm' ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a frame bound is not a string (recorded errno 1003)", + sql: "SELECT SUM ( ) OVER ( ROWS 'x' PRECEDING ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "EXECUTE USING POLICY_CONTEXT runs a query (recorded runtime error)", + sql: "EXECUTE USING POLICY_CONTEXT ( v1 => 'm' ) AS SELECT 1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "POLICY_CONTEXT ( ... ) AS needs a query (recorded errno 1003)", + sql: "EXECUTE USING POLICY_CONTEXT ( v1 => 'm' ) AS 1 ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "MODIFY DATA METRIC FUNCTION adds expectations (recorded semantic error)", + sql: "ALTER TABLE v1 MODIFY DATA METRIC FUNCTION v1 ON ( v1 ) ADD EXPECTATION v1 ( 1 ) , v1 ( 1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a data metric function's column list may be empty before TABLE (recorded semantic error)", + sql: "ALTER TABLE v1 ADD DATA METRIC FUNCTION SNOWFLAKE . CORE . ROW_COUNT_DIFFERENCE ON ( , TABLE ( v1 ( v1 ) ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a data metric function's TABLE source names columns (recorded errno 1003)", + sql: "ALTER TABLE v1 ADD DATA METRIC FUNCTION SNOWFLAKE . CORE . ROW_COUNT_DIFFERENCE ON ( v1 , TABLE ( v1 ( ) ) ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "MODIFY DATA METRIC FUNCTION may clear its filter (recorded semantic error)", + sql: "ALTER TABLE v1 MODIFY DATA METRIC FUNCTION v1 ON ( v1 ) FILTER ( ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "ADD DATA METRIC FUNCTION needs a filter condition (recorded errno 1003)", + sql: "ALTER TABLE v1 ADD DATA METRIC FUNCTION v1 ON ( v1 ) FILTER ( ) ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "ALTER VIEW adds data metric functions (recorded semantic error)", + sql: "ALTER VIEW v1 ADD DATA METRIC FUNCTION v1 ON ( v1 ) , \"SELECT\" ON ( v1 ) ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, ]; for test_case in test_cases { @@ -173,6 +479,48 @@ fn given_snowflake_create_and_begin_name_when_parsing_then_the_tree_keeps_the_el pointer: "/0/select/expressions/0/window_function/this/function/args", expected_value: json!([]), }, + ProjectionTestCase { + description: "BEGIN WORK NAME keeps the name", + sql: "BEGIN WORK NAME \"v1 Q\"", + pointer: "/0/transaction/this/identifier/quoted", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "an interval type keeps its precision", + sql: "SELECT CAST ( NULL AS INTERVAL DAY TO SECOND ( 3 ) )", + pointer: "/0/select/expressions/0/cast/to/to", + expected_value: json!("SECOND(3)"), + }, + ProjectionTestCase { + description: "a nested procedure keeps its return columns", + sql: "DECLARE p1 PROCEDURE ( ) RETURNS TABLE ( c1 INT ) AS BEGIN RETURN 1 ; END ; BEGIN RETURN 1 ; END ;", + pointer: "/0/scripting_block/declarations/0/procedure/returns_table/0/name", + expected_value: json!("c1"), + }, + ProjectionTestCase { + description: "a call without a schema keeps the empty part", + sql: "SELECT d1 . . f1 ( 1 )", + pointer: "/0/select/expressions/0/method_call/this/column/name/name", + expected_value: json!(""), + }, + ProjectionTestCase { + description: "a data metric function keeps its query argument", + sql: "SELECT SNOWFLAKE . CORE . NULL_COUNT ( SELECT c1 FROM t1 )", + pointer: "/0/select/expressions/0/method_call/args/0/subquery/this/select/from/expressions/0/table/name/name", + expected_value: json!("t1"), + }, + ProjectionTestCase { + description: "the flow operator keeps the statement after it", + sql: "SELECT 1 ->> SELECT 2", + pointer: "/0/operator/expression/select/expressions/0/literal/value", + expected_value: json!("2"), + }, + ProjectionTestCase { + description: "** keeps the array it spreads", + sql: "SELECT GREATEST ( ** [ 1 , 2 ] )", + pointer: "/0/select/expressions/0/function/args/0/variadic/this/array_func/expressions/1/literal/value", + expected_value: json!("2"), + }, ]; for test_case in test_cases {