Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9450333
Parse more recorded Snowflake functions, expressions and scripting (W…
kvlonge Nov 5, 2026
61edb80
Match Snowflake on ESCAPE aliases, bind names, bare stage arguments a…
kvlonge Nov 5, 2026
7cfca78
Merge remote-tracking branch 'github/main' into fix/parser-gaps-f
kvlonge Nov 5, 2026
832a768
Parse Snowflake's flow operator, list-valued ANY/ALL and empty STRING( )
kvlonge Nov 5, 2026
36dda6b
Parse Snowflake's ** array spread and IPADDRESS literals; keep ->> as…
kvlonge Nov 5, 2026
534f6c2
Refuse a name as a Snowflake interval's value and a string after a pa…
kvlonge Nov 5, 2026
61da191
Join every changed case to Snowflake's receipts in the coverage diff,…
kvlonge Nov 5, 2026
94ecdee
Refuse a comma after a Snowflake CAST's type
kvlonge Nov 5, 2026
6c66b27
Accept NOT NULL on a Snowflake structured ARRAY element and MAP value
kvlonge Nov 5, 2026
5c6c025
Parse Snowflake CALL, EXECUTE USING POLICY_CONTEXT and frame bounds a…
kvlonge Nov 5, 2026
5f3eced
Parse Snowflake ADD, DROP and MODIFY DATA METRIC FUNCTION in ALTER TA…
kvlonge Nov 5, 2026
7b2075b
Keep a CALL name's empty schema part, as in SNOWFLAKE..ANOMALY_INSIGH…
kvlonge Nov 5, 2026
95e0ac5
Keep the structured NOT NULL, data metric function and POLICY_CONTEXT…
kvlonge Nov 5, 2026
5ae92e0
Compare a NOT NULL structured element's base type name with polyglot's
kvlonge Nov 5, 2026
e5e3be2
Merge remote-tracking branch 'github/main' into fix/parser-gaps-f
kvlonge Nov 5, 2026
a33e9c3
Merge remote-tracking branch 'github/main' into fix/parser-gaps-f
kvlonge Nov 5, 2026
8e6fd73
Merge remote-tracking branch 'github/main' into fix/parser-gaps-f
kvlonge Nov 5, 2026
f3c75ab
Move the Snowflake EXECUTE object and POLICY_CONTEXT parsers to snowf…
kvlonge Nov 5, 2026
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
77 changes: 77 additions & 0 deletions corpora/polyglot-check/expected_differences.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ))"
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 27 additions & 2 deletions crates/sqlquack-polyglot-check/src/comparison/_helpers/outcomes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<polyglot_sql::DataType>);
let converted = serde_json::to_value(data_type)
.map(without_not_null)
.and_then(serde_json::from_value::<polyglot_sql::DataType>);
let theirs = match converted {
Ok(theirs) => theirs,
Err(_) => return "<unconvertible>".to_owned(),
Expand Down
17 changes: 16 additions & 1 deletion crates/sqlquack-polyglot-check/tests/runs/test_exact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
50 changes: 50 additions & 0 deletions crates/sqlquack-syntax/src/expressions/columns/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,9 +551,59 @@ pub enum ColumnPosition {
After(Identifier),
}

/// Snowflake `{ ADD | DROP | MODIFY } DATA METRIC FUNCTION <name> ON ( ... ) [ <clause> ], ...`.
#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataMetricFunctionAction {
/// `ADD`, `DROP` or `MODIFY`.
pub kind: String,
pub items: Vec<DataMetricFunctionItem>,
}

/// One `<name> ON ( <argument>, ... )` 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<Expression>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub clauses: Vec<DataMetricFunctionClause>,
}

/// A clause after a data metric function's arguments.
#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DataMetricFunctionClause {
/// `SENSITIVITY = <value>` or `ANOMALY_DETECTION = <value>`.
Property {
name: String,
value: Expression,
},
/// `[ ADD | DROP | MODIFY ] EXPECTATION <expectation>, ...`.
Expectations {
action: Option<String>,
expectations: Vec<DataMetricExpectation>,
},
/// `WITHIN GROUP ( <column>, ... ) [ GROUP LIMIT <n> ]`.
WithinGroup {
columns: Vec<Expression>,
limit: Option<Expression>,
},
ExecuteAsRole(Identifier),
/// `FILTER ( [ <condition> ] )`.
Filter(Option<Expression>),
/// `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<Expression>,
}

/// 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ pub struct AlterView {
pub enum AlterViewAction {
/// Rename the view
Rename(Box<TableRef>),
DataMetricFunction(crate::expressions::columns::models::DataMetricFunctionAction),
/// Change owner
OwnerTo(Identifier),
/// Set schema
Expand Down
22 changes: 22 additions & 0 deletions crates/sqlquack-syntax/src/expressions/statements/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,14 @@ pub struct ExecuteStatement {
pub suffix: Option<String>,
}

/// Snowflake `EXECUTE USING POLICY_CONTEXT ( <name> => <value> , ... ) AS <query>`.
#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExecuteUsingPolicyContext {
/// The named arguments of POLICY_CONTEXT.
pub arguments: Vec<Expression>,
pub query: Expression,
}

/// Snowflake `EXECUTE ALERT | TASK [ GRAPH ] | DCM PROJECT <name>` with its action.
#[derive(sqlquack_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExecuteObject {
Expand Down Expand Up @@ -1603,6 +1611,7 @@ pub enum ScriptingDeclaration {
Cursor(ScriptingCursor),
Resultset(ScriptingResultset),
Exception(ScriptingException),
Procedure(Box<ScriptingProcedure>),
}

/// `name [type] [{DEFAULT | :=} expression]`.
Expand Down Expand Up @@ -1678,6 +1687,19 @@ pub struct ScriptingException {
pub message: Option<Expression>,
}

/// `name PROCEDURE ( [<arg> <type>, ...] ) RETURNS { <type> | TABLE ( ... ) } AS <block>`.
#[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<crate::expressions::schema_objects::models::FunctionParameter>,
pub return_type: Option<DataType>,
pub returns_table: Option<Vec<StructField>>,
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")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ impl Expression {
| Expression::Prepare(_)
| Expression::Execute(_)
| Expression::ExecuteObject(_)
| Expression::ExecuteUsingPolicyContext(_)
| Expression::Declare(_)
| Expression::Refresh(_)
| Expression::AlterSession(_)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlquack-syntax/src/expressions/tree/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1222,6 +1223,7 @@ pub enum Expression {
/// EXEC/EXECUTE statement (TSQL stored procedure call)
Execute(Box<ExecuteStatement>),
ExecuteObject(Box<ExecuteObject>),
ExecuteUsingPolicyContext(Box<ExecuteUsingPolicyContext>),

/// Snowflake CREATE TASK statement
CreateTask(Box<CreateTask>),
Expand Down
15 changes: 12 additions & 3 deletions crates/sqlquack-syntax/src/expressions/types/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ pub enum DataType {
inner: Box<DataType>,
},

/// An element or value type declared NOT NULL, as in Snowflake's `ARRAY ( INT NOT NULL )`.
NotNull {
inner: Box<DataType>,
},

Custom {
name: String,
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion crates/sqlquack-syntax/src/parser/_helpers/ddl/alter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ use crate::tokens::types::Result;
impl Parser {
/// Parse ALTER TABLE action
pub(crate) fn parse_alter_action(&mut self) -> Result<AlterTableAction> {
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()
Expand Down
Loading