From d3cfa33fb565d5c6b5add8e4053744cf33f2e9d9 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 02:48:03 +0000 Subject: [PATCH 01/22] fix(snowflake): keep a data metric function's left-out column ON ( , ) parses with the column before the first comma left out. The tree now records that with column_omitted instead of dropping the slot. --- .../_helpers/ddl/snowflake_data_metric.rs | 9 +++++---- .../tests/parser/test_snowflake_recorded.rs | 18 ++++++++++++++++++ .../src/expressions/columns/models.rs | 3 +++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_data_metric.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_data_metric.rs index 76f0e8d21..b2b3b284d 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_data_metric.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_data_metric.rs @@ -146,7 +146,7 @@ impl Parser { 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 (arguments, column_omitted) = self.parse_data_metric_arguments()?; let mut clauses: Vec = Vec::new(); match kind { "ADD" => { @@ -160,12 +160,13 @@ impl Parser { Ok(DataMetricFunctionItem { name, arguments, + column_omitted, clauses, }) } - /// `( [ , ... ] [ , TABLE ( ( , ... ) ) | , ] )`; the column list may be empty. - fn parse_data_metric_arguments(&mut self) -> Result> { + /// `( [ , ... ] [ , TABLE ( ( , ... ) ) | , ] )`, whose column list may be empty; returns the arguments and whether a leading comma left the column out. + fn parse_data_metric_arguments(&mut self) -> Result<(Vec, bool)> { self.expect_token(TokenType::LParen)?; let leading_comma = self.match_token(TokenType::Comma); let arguments = if self.check(TokenType::RParen) && !leading_comma { @@ -195,7 +196,7 @@ impl Parser { "A data metric function takes columns, then optionally TABLE ( ( , ... ) ) or a lambda", )); } - Ok(arguments) + Ok((arguments, leading_comma)) } /// A clause of ADD DATA METRIC FUNCTION, or none. diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs index e6ca1b556..70c8fb178 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs @@ -941,6 +941,24 @@ fn given_snowflake_create_and_begin_name_when_parsing_then_the_tree_keeps_the_el pointer: "/0/create_data_metric_function/nullable", expected_value: json!(false), }, + ProjectionTestCase { + description: "a data metric function's arguments keep a left-out column (recorded semantic error)", + sql: "ALTER TABLE t1 ADD DATA METRIC FUNCTION SNOWFLAKE . CORE . ACCEPTED_VALUES ON ( , x -> x > 0 ) ;", + pointer: "/0/alter_table/actions/0/DataMetricFunction/items/0/column_omitted", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "a left-out column before TABLE is kept", + sql: "ALTER TABLE v1 ADD DATA METRIC FUNCTION SNOWFLAKE . CORE . ROW_COUNT_DIFFERENCE ON ( , TABLE ( v1 ( v1 ) ) ) ;", + pointer: "/0/alter_table/actions/0/DataMetricFunction/items/0/column_omitted", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "a data metric function's written column leaves nothing out", + sql: "ALTER TABLE t1 ADD DATA METRIC FUNCTION SNOWFLAKE . CORE . ACCEPTED_VALUES ON ( v1 , x -> x > 0 ) ;", + pointer: "/0/alter_table/actions/0/DataMetricFunction/items/0/column_omitted", + expected_value: json!(null), + }, ProjectionTestCase { description: "a typed literal keeps its type's name and parameters", sql: "SELECT PERIOD ( INT ) 'x'", diff --git a/crates/sqlquack-syntax/src/expressions/columns/models.rs b/crates/sqlquack-syntax/src/expressions/columns/models.rs index 5cb125d84..b4191da24 100644 --- a/crates/sqlquack-syntax/src/expressions/columns/models.rs +++ b/crates/sqlquack-syntax/src/expressions/columns/models.rs @@ -618,6 +618,9 @@ pub struct DataMetricColumn { pub struct DataMetricFunctionItem { pub name: TableRef, pub arguments: Vec, + /// `ON ( , )`: the column before the first comma was left out. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub column_omitted: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub clauses: Vec, } From 7d8056c716487d7ab8388ef4a2e657ca90537fbd Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 02:52:16 +0000 Subject: [PATCH 02/22] fix(snowflake): keep a data movement rule's row-limit return type MAX_ROWS AS ( ) RETURNS -> read the type and dropped it. The MAX_ROWS property now keeps it in returns, in CREATE and ALTER DATA MOVEMENT RULE alike. --- .../_helpers/ddl/snowflake_alter_resources.rs | 7 +++++-- .../tests/parser/test_snowflake_create_alert.rs | 14 +++++++++++++- .../src/expressions/schema_objects/models.rs | 3 +++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs index 0f019e28f..dfe535428 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs @@ -228,9 +228,12 @@ impl Parser { self.expect_token(TokenType::LParen)?; self.expect_token(TokenType::RParen)?; self.expect_token(TokenType::Returns)?; - self.parse_data_type()?; + let returns = self.parse_data_type()?; self.expect_token(TokenType::Arrow)?; - Ok(property("MAX_ROWS", None, Some(self.parse_expression()?))) + Ok(ObjectProperty { + returns: Some(returns), + ..property("MAX_ROWS", None, Some(self.parse_expression()?)) + }) } /// Whether `MODEL MONITOR` here starts `ALTER MODEL ` with a name or schema called monitor: a dot or a whole ALTER MODEL action follows. diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_alert.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_alert.rs index f9f31fc7b..ee282d50d 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_alert.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_alert.rs @@ -117,7 +117,7 @@ fn given_a_snowflake_alert_or_data_movement_rule_when_parsing_then_only_what_sno } #[test] -fn given_a_snowflake_alert_create_when_parsing_then_the_tree_keeps_its_parts() { +fn given_a_snowflake_alert_or_data_movement_rule_when_parsing_then_the_tree_keeps_its_parts() { let test_cases = [ ProjectionTestCase { description: "the condition is the first operand", @@ -131,6 +131,18 @@ fn given_a_snowflake_alert_create_when_parsing_then_the_tree_keeps_its_parts() { pointer: "/0/create_object/properties/0/kind", expected_value: json!("SUSPENDED"), }, + ProjectionTestCase { + description: "a data movement rule's row limit keeps its return type", + sql: "CREATE DATA MOVEMENT RULE v1 TYPE = 'm' MAX_ROWS AS ( ) RETURNS INTEGER -> ( 1 ) ;", + pointer: "/0/create_object/properties/1/returns/data_type", + expected_value: json!("int"), + }, + ProjectionTestCase { + description: "an altered row limit keeps its return type", + sql: "ALTER DATA MOVEMENT RULE IF EXISTS v1 SET MAX_ROWS AS ( ) RETURNS VARCHAR -> ( 1 ) ;", + pointer: "/0/alter_object/properties/0/returns/data_type", + expected_value: json!("var_char"), + }, ]; for test_case in test_cases { assert_eq!( diff --git a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs index cf335bfc9..e87204545 100644 --- a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs +++ b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs @@ -1352,6 +1352,9 @@ pub struct ObjectProperty { /// Whether the optional WITH was written before it, as in Snowflake's `WITH TAG ( ... )` #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub with: bool, + /// The type after RETURNS when the value is a body, as in `MAX_ROWS AS ( ) RETURNS -> ` + #[serde(default, skip_serializing_if = "Option::is_none")] + pub returns: Option, } /// Snowflake `EXECUTE ALERT | TASK [ GRAPH ] | DCM PROJECT ` with its action. From c306f0c22780341eacffe10747ef832ac10005db Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 02:58:44 +0000 Subject: [PATCH 03/22] fix(snowflake): keep a sequence's OR ALTER and optional words CREATE OR ALTER SEQUENCE gave the same tree as CREATE SEQUENCE, and the optional WITH, START WITH, INCREMENT BY and = were read and dropped. The tree now records or_alter, with and the spelling of START and INCREMENT. --- .../parser/_helpers/expressions/operators.rs | 31 +++------- .../src/parser/_helpers/statements/dml.rs | 2 +- .../tests/parser/test_snowflake_sequences.rs | 61 ++++++++++++++++++- .../src/expressions/schema_objects/models.rs | 35 +++++++++++ 4 files changed, 102 insertions(+), 27 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/expressions/operators.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/expressions/operators.rs index 9aabadb61..4bab44b87 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/expressions/operators.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/expressions/operators.rs @@ -519,6 +519,7 @@ impl Parser { &mut self, temporary: bool, or_replace: bool, + or_alter: bool, ) -> Result { self.expect_token(TokenType::Sequence)?; @@ -535,31 +536,15 @@ impl Parser { if_not_exists, temporary, or_replace, - as_type: None, - increment: None, - minvalue: None, - maxvalue: None, - start: None, - cache: None, - cycle: false, - owned_by: None, - owned_by_none: false, - order: None, - comment: None, - sharing: None, - scale_modifier: None, - shard_modifier: None, - property_order: Vec::new(), - define: false, - comment_name: None, - start_text: None, + or_alter, + ..CreateSequence::new(String::new()) }; if self.match_token(TokenType::As) { seq.as_type = Some(self.parse_data_type()?); } - self.match_token(TokenType::With); + seq.with = self.match_token(TokenType::With); loop { self.match_token(TokenType::Comma); @@ -656,8 +641,8 @@ impl Parser { } fn parse_sequence_start_clause(&mut self, mut seq: CreateSequence) -> Result { - self.match_token(TokenType::With); - self.match_token(TokenType::Eq); + seq.spelling.start_with = self.match_token(TokenType::With); + seq.spelling.start_eq = self.match_token(TokenType::Eq); match self.parse_sequence_start()? { Ok(start) => seq.start = Some(start), Err(text) => seq.start_text = Some(text), @@ -683,8 +668,8 @@ impl Parser { } fn parse_sequence_increment(&mut self, mut seq: CreateSequence) -> Result { - self.match_token(TokenType::By); - self.match_token(TokenType::Eq); + seq.spelling.increment_by = self.match_token(TokenType::By); + seq.spelling.increment_eq = self.match_token(TokenType::Eq); seq.increment = Some(self.parse_signed_integer()?); seq.property_order.push(SeqPropKind::Increment); Ok(seq) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/statements/dml.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/statements/dml.rs index 4f62c025b..21b15bf80 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/statements/dml.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/statements/dml.rs @@ -541,7 +541,7 @@ impl Parser { .parse_create_procedure(or_replace, or_alter) .map(|procedure| with_routine_modifiers(procedure, secure, temporary, false)), }, - TokenType::Sequence => self.parse_create_sequence(temporary, or_replace), + TokenType::Sequence => self.parse_create_sequence(temporary, or_replace, or_alter), TokenType::Trigger => { self.parse_create_trigger(or_replace, or_alter, false, create_pos) } diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_sequences.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_sequences.rs index 01c4c65e8..fa46c340b 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_sequences.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_sequences.rs @@ -1,7 +1,9 @@ -use sqlquack_syntax::Dialect; +use serde_json::json; -use crate::helpers::{accepted, snowflake_kind}; -use crate::test_types::{AcceptanceTestCase, SnowflakeVerdictTestCase}; +use sqlquack_syntax::{CallMode, Dialect}; + +use crate::helpers::{accepted, snowflake_kind, statements}; +use crate::test_types::{AcceptanceTestCase, ProjectionTestCase, SnowflakeVerdictTestCase}; #[test] fn given_a_snowflake_sequence_when_parsing_then_recorded_verdicts_hold() { @@ -83,3 +85,56 @@ fn given_another_dialect_when_altering_a_sequence_increment_then_only_by_is_acce ); } } + +#[test] +fn given_a_snowflake_sequence_create_when_parsing_then_the_tree_keeps_its_written_words() { + let test_cases = [ + ProjectionTestCase { + description: "OR ALTER is kept (recorded success)", + sql: "CREATE OR ALTER SEQUENCE v1 ;", + pointer: "/0/create_sequence/or_alter", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "a plain create is no OR ALTER", + sql: "CREATE SEQUENCE v1 ;", + pointer: "/0/create_sequence/or_alter", + expected_value: json!(null), + }, + ProjectionTestCase { + description: "the leading WITH is kept", + sql: "CREATE SEQUENCE v1 WITH START WITH = 1 INCREMENT BY = 1 ;", + pointer: "/0/create_sequence/with", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "START WITH = and INCREMENT BY = keep their optional words", + sql: "CREATE SEQUENCE v1 WITH START WITH = 1 INCREMENT BY = 1 ;", + pointer: "/0/create_sequence/spelling", + expected_value: json!({"start_with": true, "start_eq": true, "increment_by": true, "increment_eq": true}), + }, + ProjectionTestCase { + description: "START and INCREMENT alone leave the optional words out", + sql: "CREATE SEQUENCE v1 START 1 INCREMENT 1 ;", + pointer: "/0/create_sequence/spelling", + expected_value: json!(null), + }, + ProjectionTestCase { + description: "START = keeps its = without WITH", + sql: "CREATE SEQUENCE v1 START = 1 INCREMENT BY 1 ;", + pointer: "/0/create_sequence/spelling", + expected_value: json!({"start_eq": true, "increment_by": true}), + }, + ]; + for test_case in test_cases { + assert_eq!( + statements(test_case.sql, Dialect::Snowflake, CallMode::Faithful) + .pointer(test_case.pointer) + .cloned() + .unwrap_or_default(), + test_case.expected_value, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs index e87204545..c49a8e67f 100644 --- a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs +++ b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs @@ -927,6 +927,38 @@ pub struct CreateSequence { /// Snowflake: a START value outside the 64-bit range, as written; `start` is then None #[serde(default, skip_serializing_if = "Option::is_none")] pub start_text: Option, + /// Snowflake: written as `CREATE OR ALTER SEQUENCE` + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub or_alter: bool, + /// The optional WITH written before the properties + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub with: bool, + /// The optional words written in START [ WITH ] [ = ] and INCREMENT [ BY ] [ = ] + #[serde(default, skip_serializing_if = "SequenceSpelling::is_empty")] + pub spelling: SequenceSpelling, +} + +/// The optional words a sequence's START and INCREMENT were written with. +#[derive(sqlquack_derive::AstNode, Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct SequenceSpelling { + /// `START WITH` + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub start_with: bool, + /// `START [ WITH ] =` + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub start_eq: bool, + /// `INCREMENT BY` + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub increment_by: bool, + /// `INCREMENT [ BY ] =` + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub increment_eq: bool, +} + +impl SequenceSpelling { + pub fn is_empty(&self) -> bool { + *self == Self::default() + } } /// Sequence bound (value or NO MINVALUE/NO MAXVALUE) @@ -961,6 +993,9 @@ impl CreateSequence { define: false, comment_name: None, start_text: None, + or_alter: false, + with: false, + spelling: SequenceSpelling::default(), } } } From 2b6bb5fe98f2cdb0713739d63a33cb97644616a8 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:02:31 +0000 Subject: [PATCH 04/22] fix(snowflake): keep quoting and AS in MATCH_RECOGNIZE names AFTER MATCH SKIP TO [ FIRST | LAST ] , DEFINE names and measure aliases written after AS lost their quoting, and a measure alias did not record its AS. They now keep both. --- .../src/parser/_helpers/queries/clauses.rs | 22 ++++++------ .../tests/parser/test_snowflake_tables.rs | 36 +++++++++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/queries/clauses.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/queries/clauses.rs index f4e6e0758..58662268e 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/queries/clauses.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/queries/clauses.rs @@ -1449,8 +1449,10 @@ impl Parser { let mut expr = self.parse_expression()?; if self.match_token(TokenType::As) { - let alias = self.parse_unquoted_identifier(Self::expect_identifier)?; - expr = Expression::Alias(Box::new(Alias::new(expr, alias))); + let alias = self.parse_quoting_identifier(Self::expect_identifier)?; + let mut alias = Alias::new(expr, alias); + alias.alias_explicit_as = true; + expr = Expression::Alias(Box::new(alias)); } else if self.is_snowflake() { let at_clause = self.is_at_end() || self.check(TokenType::Comma) @@ -1595,22 +1597,22 @@ impl Parser { } if self.match_token(TokenType::First) { - let name = self.expect_identifier()?; - return Ok(Some(MatchRecognizeAfter::ToFirst(Identifier::new(name)))); + let name = self.parse_quoting_identifier(Self::expect_identifier)?; + return Ok(Some(MatchRecognizeAfter::ToFirst(name))); } if self.match_token(TokenType::Last) { - let name = self.expect_identifier()?; - return Ok(Some(MatchRecognizeAfter::ToLast(Identifier::new(name)))); + let name = self.parse_quoting_identifier(Self::expect_identifier)?; + return Ok(Some(MatchRecognizeAfter::ToLast(name))); } if self.is_snowflake() { let name = if next { - self.advance_text()? + self.parse_quoting_identifier(Self::advance_text)? } else { - self.expect_identifier()? + self.parse_quoting_identifier(Self::expect_identifier)? }; - return Ok(Some(MatchRecognizeAfter::To(Identifier::new(name)))); + return Ok(Some(MatchRecognizeAfter::To(name))); } return Err(self.parse_error("Expected NEXT ROW, FIRST x, or LAST x after TO")); @@ -1686,7 +1688,7 @@ impl Parser { let mut semantics: Vec<(usize, MatchRecognizeSemantics)> = Vec::new(); loop { - let name = self.parse_unquoted_identifier(Self::expect_identifier)?; + let name = self.parse_quoting_identifier(Self::expect_identifier)?; self.expect_token(TokenType::As)?; if self.is_snowflake() { semantics.extend( diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_tables.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_tables.rs index 645948872..51c8a3a57 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_tables.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_tables.rs @@ -1128,6 +1128,42 @@ fn given_a_snowflake_clause_accepted_on_its_receipt_when_parsing_then_the_tree_k pointer: "/0/select/from/expressions/0/match_recognize/define_semantics", expected_value: json!([[1, "Final"]]), }, + StatementTestCase { + description: "SKIP TO keeps a quoted pattern variable's quoting", + sql: "SELECT * FROM t1 MATCH_RECOGNIZE ( ORDER BY c1 MEASURES COUNT ( * ) AS \"m Q\" , COUNT ( * ) m2 AFTER MATCH SKIP TO \"a Q\" PATTERN ( \"a Q\" ) DEFINE \"a Q\" AS TRUE ) ;", + pointer: "/0/select/from/expressions/0/match_recognize/after/To/quoted", + expected_value: json!(true), + }, + StatementTestCase { + description: "SKIP TO LAST keeps a quoted pattern variable's quoting", + sql: "SELECT * FROM t1 MATCH_RECOGNIZE ( ORDER BY c1 AFTER MATCH SKIP TO LAST \"SELECT\" PATTERN ( \"SELECT\" ) DEFINE \"SELECT\" AS TRUE ) ;", + pointer: "/0/select/from/expressions/0/match_recognize/after/ToLast/quoted", + expected_value: json!(true), + }, + StatementTestCase { + description: "a DEFINE name keeps its quoting", + sql: "SELECT * FROM t1 MATCH_RECOGNIZE ( ORDER BY c1 MEASURES COUNT ( * ) AS \"m Q\" , COUNT ( * ) m2 AFTER MATCH SKIP TO \"a Q\" PATTERN ( \"a Q\" ) DEFINE \"a Q\" AS TRUE ) ;", + pointer: "/0/select/from/expressions/0/match_recognize/define/0/0/quoted", + expected_value: json!(true), + }, + StatementTestCase { + description: "a measure alias after AS keeps its quoting", + sql: "SELECT * FROM t1 MATCH_RECOGNIZE ( ORDER BY c1 MEASURES COUNT ( * ) AS \"m Q\" , COUNT ( * ) m2 AFTER MATCH SKIP TO \"a Q\" PATTERN ( \"a Q\" ) DEFINE \"a Q\" AS TRUE ) ;", + pointer: "/0/select/from/expressions/0/match_recognize/measures/0/this/alias/alias/quoted", + expected_value: json!(true), + }, + StatementTestCase { + description: "a measure alias records its written AS", + sql: "SELECT * FROM t1 MATCH_RECOGNIZE ( ORDER BY c1 MEASURES COUNT ( * ) AS \"m Q\" , COUNT ( * ) m2 AFTER MATCH SKIP TO \"a Q\" PATTERN ( \"a Q\" ) DEFINE \"a Q\" AS TRUE ) ;", + pointer: "/0/select/from/expressions/0/match_recognize/measures/0/this/alias/alias_explicit_as", + expected_value: json!(true), + }, + StatementTestCase { + description: "a measure alias without AS records none", + sql: "SELECT * FROM t1 MATCH_RECOGNIZE ( ORDER BY c1 MEASURES COUNT ( * ) AS \"m Q\" , COUNT ( * ) m2 AFTER MATCH SKIP TO \"a Q\" PATTERN ( \"a Q\" ) DEFINE \"a Q\" AS TRUE ) ;", + pointer: "/0/select/from/expressions/0/match_recognize/measures/1/this/alias/alias_explicit_as", + expected_value: json!(false), + }, StatementTestCase { description: "SHOW PARAMETERS IN USER keeps USER as the scope kind", sql: "SHOW PARAMETERS IN USER \"v1 Q\" ;", From f1f2f6b1a485bcfd5f374454ca5c4f57974edaec Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:08:23 +0000 Subject: [PATCH 05/22] fix(snowflake): keep whether RUNTIME_VERSION and PACKAGES were written as strings RUNTIME_VERSION = m1 and PACKAGES = ( m1 ) gave the same tree as the quoted spellings. A function now records runtime_version_bare, and packages_bare by position, when the value was written as a name or a number. --- .../parser/_helpers/ddl/create_function.rs | 16 +++++++++++++ .../tests/parser/test_snowflake_recorded.rs | 24 +++++++++++++++++++ .../src/expressions/schema_objects/models.rs | 15 ++++++++++++ 3 files changed, 55 insertions(+) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_function.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_function.rs index 6ec1853d3..fe961a0b9 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_function.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_function.rs @@ -4,6 +4,7 @@ use crate::parser::_helpers::ddl::snowflake_routines::nullability_property; use crate::parser::models::Parser; use sqlquack_syntax::expressions::columns::models::ColumnDef; use sqlquack_syntax::expressions::schema_objects::main::record_function_property::record_function_property; +use sqlquack_syntax::expressions::schema_objects::models::BareValue; use sqlquack_syntax::expressions::schema_objects::models::CreateFunction; use sqlquack_syntax::expressions::schema_objects::models::FunctionBody; use sqlquack_syntax::expressions::schema_objects::models::FunctionPropertyKind; @@ -90,6 +91,8 @@ impl Parser { handler_uses_eq: false, runtime_version: None, packages: None, + runtime_version_bare: None, + packages_bare: Vec::new(), parameter_style: None, snowflake_properties: return_nullability .into_iter() @@ -440,12 +443,22 @@ impl Parser { || self.check(TokenType::Number) || self.is_identifier_or_keyword_token() { + function.runtime_version_bare = self.bare_value(); function.runtime_version = Some(self.advance_text()?); } function = record_function_property(function, FunctionPropertyKind::RuntimeVersion); Ok(function) } + /// How the current token writes a value that is usually a string: none for a string. + fn bare_value(&self) -> Option { + match self.peek().token_type { + TokenType::String => None, + TokenType::Number => Some(BareValue::Number), + _ => Some(BareValue::Name), + } + } + fn parse_create_function_packages( &mut self, mut function: CreateFunction, @@ -460,6 +473,9 @@ impl Parser { || self.check(TokenType::Number) || self.check_keyword() { + if let Some(bare) = self.bare_value() { + function.packages_bare.push((parsed_packages.len(), bare)); + } parsed_packages.push(self.advance_text()?); } else { break; diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs index 70c8fb178..c35a1b8aa 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs @@ -959,6 +959,30 @@ fn given_snowflake_create_and_begin_name_when_parsing_then_the_tree_keeps_the_el pointer: "/0/alter_table/actions/0/DataMetricFunction/items/0/column_omitted", expected_value: json!(null), }, + ProjectionTestCase { + description: "a runtime version written as a name says so (recorded semantic error)", + sql: "CREATE FUNCTION v1 ( ) RETURNS INT LANGUAGE PYTHON RUNTIME_VERSION = m1 PACKAGES = ( 'm' ) HANDLER = 'm' AS 'm'", + pointer: "/0/create_function/runtime_version_bare", + expected_value: json!("Name"), + }, + ProjectionTestCase { + description: "a runtime version written as a number says so", + sql: "CREATE FUNCTION v1 ( ) RETURNS INT LANGUAGE PYTHON RUNTIME_VERSION = 3 PACKAGES = ( 'm' ) HANDLER = 'm' AS 'm'", + pointer: "/0/create_function/runtime_version_bare", + expected_value: json!("Number"), + }, + ProjectionTestCase { + description: "a runtime version written as a string is no bare value", + sql: "CREATE FUNCTION v1 ( ) RETURNS INT LANGUAGE PYTHON RUNTIME_VERSION = '3.11' PACKAGES = ( 'm' ) HANDLER = 'm' AS 'm'", + pointer: "/0/create_function/runtime_version_bare", + expected_value: json!(null), + }, + ProjectionTestCase { + description: "a package written as a name says so by position (recorded semantic error)", + sql: "CREATE FUNCTION v1 ( ) RETURNS INT LANGUAGE PYTHON RUNTIME_VERSION = '3.11' PACKAGES = ( 'm' , m1 ) HANDLER = 'm' AS 'm'", + pointer: "/0/create_function/packages_bare", + expected_value: json!([[1, "Name"]]), + }, ProjectionTestCase { description: "a typed literal keeps its type's name and parameters", sql: "SELECT PERIOD ( INT ) 'x'", diff --git a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs index c49a8e67f..a58df467d 100644 --- a/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs +++ b/crates/sqlquack-syntax/src/expressions/schema_objects/models.rs @@ -531,6 +531,12 @@ pub struct CreateFunction { /// Snowflake: PACKAGES=('pkg1', 'pkg2') #[serde(default, skip_serializing_if = "Option::is_none")] pub packages: Option>, + /// Snowflake: how RUNTIME_VERSION was written when not as a string + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime_version_bare: Option, + /// Snowflake: how each PACKAGES entry not written as a string was written, by its position in `packages` + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub packages_bare: Vec<(usize, BareValue)>, /// PARAMETER STYLE clause (e.g., PANDAS for Databricks) #[serde(default, skip_serializing_if = "Option::is_none")] pub parameter_style: Option, @@ -708,6 +714,8 @@ impl CreateFunction { handler_uses_eq: false, runtime_version: None, packages: None, + runtime_version_bare: None, + packages_bare: Vec::new(), parameter_style: None, snowflake_properties: Vec::new(), overloads: Vec::new(), @@ -717,6 +725,13 @@ impl CreateFunction { } } +/// A value Snowflake usually takes as a string, written instead as a name or a number. +#[derive(sqlquack_derive::AstNode, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum BareValue { + Name, + Number, +} + /// DROP FUNCTION statement #[derive(sqlquack_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DropFunction { From d7399d45185a64a9ead84e9ce8ebdfe9ac9b0f91 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:17:01 +0000 Subject: [PATCH 06/22] fix(snowflake): keep KEY = value pairs in ALTER lists keyed Parenthesised ALTER values such as WORKLOAD_IDENTITY = ( ANOMALY_DETECTION = TRUE ), SECRETS = ( 'm' = v1 ) and STORAGE_LOCATION = ( NAME = 'm' ... ) kept each pair as an unlabelled two-item tuple, the shape of a row. A KEY = value pair is now a property_e_q, as in the create paths. A => pair, as in USING, is a named argument. --- .../parser/_helpers/ddl/snowflake_alter.rs | 26 +++++++++++++--- .../_helpers/ddl/snowflake_alter_account.rs | 5 ++-- .../_helpers/ddl/snowflake_alter_objects.rs | 3 +- .../_helpers/ddl/snowflake_alter_resources.rs | 16 +++++----- .../_helpers/ddl/snowflake_execute_jobs.rs | 16 +++++----- .../tests/parser/test_snowflake_alter.rs | 30 +++++++++++++++++++ .../tests/parser/test_snowflake_execute.rs | 2 +- 7 files changed, 73 insertions(+), 25 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs index af73c3dba..ba79e399b 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs @@ -9,6 +9,9 @@ use sqlquack_syntax::expressions::functions::models::Function; use sqlquack_syntax::expressions::names::main::identifier_from_token::identifier_from_token; use sqlquack_syntax::expressions::names::models::Identifier; use sqlquack_syntax::expressions::names::models::TableRef; +use sqlquack_syntax::expressions::operators::models::BinaryOp; +use sqlquack_syntax::expressions::operators::models::NamedArgSeparator; +use sqlquack_syntax::expressions::operators::models::NamedArgument; use sqlquack_syntax::expressions::schema_objects::models::AlterObject; use sqlquack_syntax::expressions::schema_objects::models::ObjectProperty; use sqlquack_syntax::expressions::tree::models::Expression; @@ -833,7 +836,7 @@ impl Parser { } } - /// Parse `( [ [ , ... ] ] )`, each item a value or a `'' = ` pair. + /// Parse `( [ [ , ... ] ] )`, each item a value or a ` = ` pair. pub(super) fn parse_routine_value_list(&mut self) -> Result { self.expect_token(TokenType::LParen)?; let mut items: Vec = Vec::new(); @@ -841,9 +844,7 @@ impl Parser { loop { let item = self.parse_object_value()?; items.push(if self.match_token(TokenType::Eq) { - Expression::Tuple(Box::new(Tuple { - expressions: vec![item, self.parse_object_value()?], - })) + keyed_pair(item, self.parse_object_value()?, false) } else { item }); @@ -895,6 +896,23 @@ pub(super) fn parameter_property(name: &Identifier, value: Option) - } } +/// A ` = ` pair, or ` => ` (`arrow`) as a named argument when the key is a plain name. +pub(super) fn keyed_pair(key: Expression, value: Expression, arrow: bool) -> Expression { + match key { + Expression::Table(table) if arrow && table.schema.is_none() && table.catalog.is_none() => { + Expression::NamedArgument(Box::new(NamedArgument { + name: table.name, + value, + separator: NamedArgSeparator::DArrow, + })) + } + key if arrow => Expression::Tuple(Box::new(Tuple { + expressions: vec![key, value], + })), + key => Expression::PropertyEQ(Box::new(BinaryOp::new(key, value))), + } +} + /// One property of kind `kind`. pub(super) fn property( kind: &str, diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs index 66ef2719f..581864d46 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs @@ -1,6 +1,7 @@ //! Parser ddl: Snowflake ALTER of account-level objects: failover and replication groups, connections, integrations and organization objects. use crate::parser::_helpers::ddl::snowflake_alter::alter_object; +use crate::parser::_helpers::ddl::snowflake_alter::keyed_pair; use crate::parser::_helpers::ddl::snowflake_alter::named_property; use crate::parser::_helpers::ddl::snowflake_alter::parameter_property; use crate::parser::_helpers::ddl::snowflake_alter::property; @@ -458,9 +459,7 @@ impl Parser { }; self.expect_token(TokenType::Eq)?; let value = self.parse_object_value()?; - items.push(Expression::Tuple(Box::new(Tuple { - expressions: vec![key, value], - }))); + items.push(keyed_pair(key, value, false)); } else { items.push(self.parse_object_value()?); } diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_objects.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_objects.rs index 428920514..c3eabb508 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_objects.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_objects.rs @@ -1,6 +1,7 @@ //! Snowflake ALTER of APPLICATION, APPLICATION PACKAGE, APPLICATION SERVICE, BACKUP SET, CORTEX SEARCH SERVICE, DATABASE, SEMANTIC VIEW and STREAMLIT. use crate::parser::_helpers::ddl::snowflake_alter::alter_object; +use crate::parser::_helpers::ddl::snowflake_alter::keyed_pair; use crate::parser::_helpers::ddl::snowflake_alter::named_property; use crate::parser::_helpers::ddl::snowflake_alter::parameter_property; use crate::parser::_helpers::ddl::snowflake_alter::property; @@ -167,7 +168,7 @@ impl Parser { loop { let item = self.parse_object_value()?; if self.match_token(TokenType::Eq) { - items.push(tuple(vec![item, self.parse_object_value()?])); + items.push(keyed_pair(item, self.parse_object_value()?, false)); } else { items.push(item); } diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs index dfe535428..6f46af9d7 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs @@ -1,6 +1,7 @@ //! Parser ddl: Snowflake ALTER of schemas, stages, file formats, routines, services and project objects. use crate::parser::_helpers::ddl::snowflake_alter::alter_object; +use crate::parser::_helpers::ddl::snowflake_alter::keyed_pair; use crate::parser::_helpers::ddl::snowflake_alter::named_property; use crate::parser::_helpers::ddl::snowflake_alter::parameter_property; use crate::parser::_helpers::ddl::snowflake_alter::property; @@ -875,15 +876,12 @@ impl Parser { let mut items: Vec = Vec::new(); while !self.match_token(TokenType::RParen) { let item = self.parse_resource_value()?; - items.push( - if self.match_token(TokenType::Eq) || self.match_token(TokenType::FArrow) { - Expression::Tuple(Box::new(Tuple { - expressions: vec![item, self.parse_resource_value()?], - })) - } else { - item - }, - ); + let arrow = self.match_token(TokenType::FArrow); + items.push(if arrow || self.match_token(TokenType::Eq) { + keyed_pair(item, self.parse_resource_value()?, arrow) + } else { + item + }); if self.match_token(TokenType::Comma) && self.check(TokenType::RParen) { return Err(self.parse_error("Expected a value after the comma")); } diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_execute_jobs.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_execute_jobs.rs index 7adba5049..5dc511776 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_execute_jobs.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_execute_jobs.rs @@ -1,7 +1,10 @@ //! Snowflake `EXECUTE JOB SERVICE`, `INFERENCE JOB SERVICE`, `NOTEBOOK [ PROJECT ]`, `DBT PROJECT` and `CODE BUNDLE`. +use crate::parser::_helpers::ddl::snowflake_alter::keyed_pair; use crate::parser::_helpers::ddl::snowflake_alter::property; use crate::parser::models::Parser; +use sqlquack_syntax::expressions::operators::models::NamedArgSeparator; +use sqlquack_syntax::expressions::operators::models::NamedArgument; use sqlquack_syntax::expressions::schema_objects::models::ExecuteObject; use sqlquack_syntax::expressions::schema_objects::models::ObjectProperty; use sqlquack_syntax::expressions::tree::models::Expression; @@ -311,9 +314,7 @@ impl Parser { let key = self.parse_execute_value()?; self.expect_token(TokenType::Eq)?; let value = self.parse_execute_value()?; - Ok(Expression::Tuple(Box::new(Tuple { - expressions: vec![key, value], - }))) + Ok(keyed_pair(key, value, false)) } /// An IMPORTS entry: a location or a system function call, with an optional `AS ''`. @@ -336,11 +337,12 @@ impl Parser { self.expect_token(TokenType::LParen)?; let mut items: Vec = Vec::new(); loop { - let name = Expression::Identifier(self.parse_simple_name()?); + let name = self.parse_simple_name()?; self.expect_token(TokenType::FArrow)?; - let value = self.parse_expression()?; - items.push(Expression::Tuple(Box::new(Tuple { - expressions: vec![name, value], + items.push(Expression::NamedArgument(Box::new(NamedArgument { + name, + value: self.parse_expression()?, + separator: NamedArgSeparator::DArrow, }))); if !self.match_token(TokenType::Comma) { break; diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs index 023d59078..131bbd68b 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs @@ -567,6 +567,36 @@ fn given_a_snowflake_alter_of_an_object_kind_when_parsing_then_the_tree_names_ki pointer: "/0/alter_object/kind", expected_value: json!("COMPUTE POOL"), }, + ProjectionTestCase { + description: "a parenthesised KEY = value keeps the pair keyed (recorded semantic error)", + sql: "ALTER USER v1 SET WORKLOAD_IDENTITY = ( ANOMALY_DETECTION = TRUE ) ;", + pointer: "/0/alter_object/properties/0/value/tuple/expressions/0/property_e_q/left/table/name/name", + expected_value: json!("ANOMALY_DETECTION"), + }, + ProjectionTestCase { + description: "a keyed pair's value is its right side", + sql: "ALTER USER v1 SET WORKLOAD_IDENTITY = ( ANOMALY_DETECTION = TRUE ) ;", + pointer: "/0/alter_object/properties/0/value/tuple/expressions/0/property_e_q/right/boolean/value", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "a string key keeps its pair keyed", + sql: "ALTER STREAMLIT v1 SET SECRETS = ( 'm' = v1 , 'n' = v2 ) ;", + pointer: "/0/alter_object/properties/0/value/tuple/expressions/1/property_e_q/left/literal/value", + expected_value: json!("n"), + }, + ProjectionTestCase { + description: "a nested option list keeps its pairs keyed", + sql: "ALTER EXTERNAL VOLUME v1 ADD STORAGE_LOCATION = ( NAME = 'm' STORAGE_PROVIDER = 'S3' ENCRYPTION = ( TYPE = 'AWS_SSE_S3' ) ) ;", + pointer: "/0/alter_object/operands/0/tuple/expressions/2/property_e_q/right/tuple/expressions/0/property_e_q/left/table/name/name", + expected_value: json!("TYPE"), + }, + ProjectionTestCase { + description: "=> keeps its separator as a named argument", + sql: "ALTER SERVICE v1 FROM @s1 SPECIFICATION_TEMPLATE_FILE = 'm' USING ( v1 => 1 ) ;", + pointer: "/0/alter_object/properties/1/value/tuple/expressions/0/named_argument/separator", + expected_value: json!("DArrow"), + }, ProjectionTestCase { description: "IF EXISTS is kept", sql: "ALTER TAG IF EXISTS v1 RENAME TO v2", diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_execute.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_execute.rs index c55aac4aa..0eb36654f 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_execute.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_execute.rs @@ -83,7 +83,7 @@ fn given_a_snowflake_execute_of_a_job_service_when_parsing_then_the_tree_keeps_i ProjectionTestCase { description: "the USING arguments keep their name", sql: SQL, - pointer: "/0/execute_object/properties/3/value/tuple/expressions/0/tuple/expressions/0/identifier/name", + pointer: "/0/execute_object/properties/3/value/tuple/expressions/0/named_argument/name/name", expected_value: json!("k"), }, ProjectionTestCase { From 39ad9f625876779ed5ade007d67ab601b480e111 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:18:55 +0000 Subject: [PATCH 07/22] fix(snowflake): keep an ALTER action's own IF [ NOT ] EXISTS as a flag ADD SCORING PROFILE IF NOT EXISTS, ADD VERSION IF NOT EXISTS, CREATE VERSION_TAG IF NOT EXISTS and their DROP ... IF EXISTS counterparts kept the words only in the action string. They now set operand_if_exists or operand_if_not_exists, as ALTER USER's token actions already did, and the action keeps only its keywords. --- .../parser/_helpers/ddl/snowflake_alter.rs | 16 ++++++++-- .../tests/parser/test_snowflake_alter_gaps.rs | 29 +++++++++++++++++-- .../tests/parser/test_snowflake_execute.rs | 3 +- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs index ba79e399b..35796aa95 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs @@ -866,20 +866,32 @@ pub(super) fn alter_object( (operands, properties): (Vec, Vec), ) -> Expression { let named = !name.name.name.is_empty() || name.name.quoted; + let (action, operand_if_exists, operand_if_not_exists) = operand_existence(action); Expression::AlterObject(Box::new(AlterObject { kind: kind.to_owned(), name: named.then_some(name), arguments: Vec::new(), if_exists, action, - operand_if_exists: false, - operand_if_not_exists: false, + operand_if_exists, + operand_if_not_exists, operands, properties, ..Default::default() })) } +/// The action without the IF [ NOT ] EXISTS an action parser wrote into it for its own object, such as `ADD SCORING PROFILE IF NOT EXISTS`, with whether it said IF EXISTS and whether it said IF NOT EXISTS. +fn operand_existence(mut action: String) -> (String, bool, bool) { + for (words, not) in [(" IF NOT EXISTS", true), (" IF EXISTS", false)] { + if let Some(at) = action.find(words) { + action.replace_range(at..at + words.len(), ""); + return (action, !not, not); + } + } + (action, false, false) +} + /// A property's kind from its name: upper-cased when unquoted, as written when quoted. pub(super) fn property_kind(name: &Identifier) -> String { match name.quoted { diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs index 27c0d73a8..56e155f66 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs @@ -223,10 +223,35 @@ fn given_snowflake_table_actions_and_method_calls_when_parsing_then_the_tree_kee expected_value: json!("ICEBERG TABLE"), }, ProjectionTestCase { - description: "IF NOT EXISTS is part of the version tag action", + description: "IF NOT EXISTS on the version tag is a flag, not part of the action", sql: "ALTER ICEBERG TABLE v1 CREATE VERSION_TAG IF NOT EXISTS t1", pointer: "/0/alter_object/action", - expected_value: json!("CREATE VERSION_TAG IF NOT EXISTS"), + expected_value: json!("CREATE VERSION_TAG"), + }, + ProjectionTestCase { + description: "IF NOT EXISTS on the version tag is kept", + sql: "ALTER ICEBERG TABLE v1 CREATE VERSION_TAG IF NOT EXISTS t1", + pointer: "/0/alter_object/operand_if_not_exists", + expected_value: json!(true), + }, + ProjectionTestCase { + description: + "IF NOT EXISTS on a scoring profile is kept as a flag (recorded semantic error)", + sql: "ALTER CORTEX SEARCH SERVICE v1 ADD SCORING PROFILE IF NOT EXISTS v1 '{}' ;", + pointer: "/0/alter_object/operand_if_not_exists", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "IF EXISTS on a dropped scoring profile is kept as a flag", + sql: "ALTER CORTEX SEARCH SERVICE v1 DROP SCORING PROFILE IF EXISTS v1 ;", + pointer: "/0/alter_object/operand_if_exists", + expected_value: json!(true), + }, + ProjectionTestCase { + description: "IF NOT EXISTS on an organization profile version is kept as a flag", + sql: "ALTER ORGANIZATION PROFILE v1 ADD VERSION IF NOT EXISTS v1 FROM @d1.s1.st1/p1 ;", + pointer: "/0/alter_object/action", + expected_value: json!("ADD VERSION FROM"), }, ProjectionTestCase { description: "RETAIN keeps its unit", diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_execute.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_execute.rs index 0eb36654f..e2a4d76e2 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_execute.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_execute.rs @@ -83,7 +83,8 @@ fn given_a_snowflake_execute_of_a_job_service_when_parsing_then_the_tree_keeps_i ProjectionTestCase { description: "the USING arguments keep their name", sql: SQL, - pointer: "/0/execute_object/properties/3/value/tuple/expressions/0/named_argument/name/name", + pointer: + "/0/execute_object/properties/3/value/tuple/expressions/0/named_argument/name/name", expected_value: json!("k"), }, ProjectionTestCase { From a8825afd5b46275d6c483e06d20eb818d0ef2005 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:19:52 +0000 Subject: [PATCH 08/22] fix(snowflake): keep an account policy's FOR ALL scope as a property SET and UNSET of an account policy kept FOR ALL PERSON USERS and its siblings only in the action string, while FORCE was its own property. The scope is now a property too, in written order with FORCE, and the action names only the policy. --- .../_helpers/ddl/snowflake_alter_account.rs | 14 ++++++----- .../tests/parser/test_snowflake_alter.rs | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs index 581864d46..acc53d5a2 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs @@ -269,15 +269,17 @@ impl Parser { true => vec![self.parse_group_name()?], false => vec![], }; + let mut properties: Vec = Vec::new(); let force = set && self.match_identifier("FORCE"); + if force { + properties.push(property("FORCE", None, None)); + } if let Some(scope) = self.match_policy_scope() { - action.push_str(&format!(" FOR ALL {scope}")); + properties.push(property(&format!("FOR ALL {scope}"), None, None)); + } + if set && !force && self.match_identifier("FORCE") { + properties.push(property("FORCE", None, None)); } - let forced = set && (force || self.match_identifier("FORCE")); - let properties = match forced { - true => vec![property("FORCE", None, None)], - false => vec![], - }; (operands, properties) } else if self.check_identifier("TAG") && !self.check_next(TokenType::Eq) diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs index 131bbd68b..8b9c34d8a 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs @@ -597,6 +597,30 @@ fn given_a_snowflake_alter_of_an_object_kind_when_parsing_then_the_tree_names_ki pointer: "/0/alter_object/properties/1/value/tuple/expressions/0/named_argument/separator", expected_value: json!("DArrow"), }, + ProjectionTestCase { + description: "a policy's FOR ALL scope is its own property (recorded success)", + sql: "ALTER ACCOUNT SET AUTHENTICATION POLICY v1 FORCE FOR ALL PERSON USERS ;", + pointer: "/0/alter_object/properties/1/kind", + expected_value: json!("FOR ALL PERSON USERS"), + }, + ProjectionTestCase { + description: "the policy's action names only the policy", + sql: "ALTER ACCOUNT SET AUTHENTICATION POLICY v1 FORCE FOR ALL PERSON USERS ;", + pointer: "/0/alter_object/action", + expected_value: json!("SET AUTHENTICATION POLICY"), + }, + ProjectionTestCase { + description: "FORCE after the scope follows it", + sql: "ALTER ACCOUNT SET AUTHENTICATION POLICY v1 FOR ALL SERVICE USERS FORCE ;", + pointer: "/0/alter_object/properties/1/kind", + expected_value: json!("FORCE"), + }, + ProjectionTestCase { + description: "an unset policy keeps its scope", + sql: "ALTER ACCOUNT UNSET AUTHENTICATION POLICY FOR ALL PERSON USERS ;", + pointer: "/0/alter_object/properties/0/kind", + expected_value: json!("FOR ALL PERSON USERS"), + }, ProjectionTestCase { description: "IF EXISTS is kept", sql: "ALTER TAG IF EXISTS v1 RENAME TO v2", From 74556259147f6f219fb586557b8c51b200ef22bc Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:21:23 +0000 Subject: [PATCH 09/22] fix(snowflake): keep PAT as written in token actions ALTER USER and ALTER EXTERNAL CONSUMER wrote PAT as PROGRAMMATIC ACCESS TOKEN in the action, so the spelling taken could not be told apart. The action now keeps the spelling that was written. --- .../_helpers/ddl/snowflake_alter_resources.rs | 16 ++++++------- .../_helpers/ddl/snowflake_alter_user.rs | 6 +---- .../tests/parser/test_snowflake_alter.rs | 24 +++++++++++++++++++ .../tests/parser/test_snowflake_alter_gaps.rs | 4 ++-- .../test_snowflake_receipted_objects.rs | 2 +- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs index 6f46af9d7..b2d778433 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs @@ -478,22 +478,20 @@ impl Parser { /// An external consumer's token action after its keyword: `{ PROGRAMMATIC ACCESS TOKEN | PAT } `, then MODIFY's `SET ...` or ADD and ROTATE's properties, with no IF [ NOT ] EXISTS. fn parse_consumer_token(&mut self, mut action: String) -> Result<(String, ActionParts)> { - if !self.match_text_seq(&["PROGRAMMATIC", "ACCESS", "TOKEN"]) - && !self.match_identifier("PAT") - { - return Err(self.parse_error("Expected PROGRAMMATIC ACCESS TOKEN or PAT")); - } - action.push_str(" PROGRAMMATIC ACCESS TOKEN"); + let verb = action.clone(); + let kind = self.expect_alter_words(&[&["PROGRAMMATIC", "ACCESS", "TOKEN"], &["PAT"]])?; + action.push(' '); + action.push_str(&kind); let operands = vec![self.parse_name_operand()?]; - let properties = match action.as_str() { - "MODIFY PROGRAMMATIC ACCESS TOKEN" => { + let properties = match verb.as_str() { + "MODIFY" => { if !self.match_token(TokenType::Set) { return Err(self.parse_error("Expected SET")); } action.push_str(" SET"); self.parse_resource_set()? } - "REMOVE PROGRAMMATIC ACCESS TOKEN" => Vec::new(), + "REMOVE" => Vec::new(), _ => self.parse_resource_properties()?, }; Ok((action, (operands, properties))) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_user.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_user.rs index 212a7d350..98de40e4e 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_user.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_user.rs @@ -112,13 +112,9 @@ impl Parser { return Ok((self.parse_user_delegation(&verb, action)?, existence)); } let kind = self.expect_alter_words(CREDENTIALS)?; - let kind = match kind.as_str() { - "PAT" => "PROGRAMMATIC ACCESS TOKEN".to_string(), - _ => kind, - }; action.push(' '); action.push_str(&kind); - let token = matches!(kind.as_str(), "PROGRAMMATIC ACCESS TOKEN"); + let token = matches!(kind.as_str(), "PROGRAMMATIC ACCESS TOKEN" | "PAT"); if token && matches!(verb.as_str(), "ADD") && self.check(TokenType::If) { self.expect_alter_words(&[&["IF", "NOT", "EXISTS"]])?; existence.1 = true; diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs index 8b9c34d8a..7f8cdb404 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs @@ -621,6 +621,30 @@ fn given_a_snowflake_alter_of_an_object_kind_when_parsing_then_the_tree_names_ki pointer: "/0/alter_object/properties/0/kind", expected_value: json!("FOR ALL PERSON USERS"), }, + ProjectionTestCase { + description: "a user's PAT keeps its short spelling (recorded success)", + sql: "ALTER USER v1 MODIFY PAT v1 UNSET DISABLED ;", + pointer: "/0/alter_object/action", + expected_value: json!("MODIFY PAT UNSET"), + }, + ProjectionTestCase { + description: "a user's PROGRAMMATIC ACCESS TOKEN keeps its long spelling", + sql: "ALTER USER v1 MODIFY PROGRAMMATIC ACCESS TOKEN v1 UNSET DISABLED ;", + pointer: "/0/alter_object/action", + expected_value: json!("MODIFY PROGRAMMATIC ACCESS TOKEN UNSET"), + }, + ProjectionTestCase { + description: "a consumer's PAT keeps its short spelling", + sql: "ALTER EXTERNAL CONSUMER v1 REMOVE PAT v1 ;", + pointer: "/0/alter_object/action", + expected_value: json!("REMOVE PAT"), + }, + ProjectionTestCase { + description: "a consumer's modified PAT still takes SET", + sql: "ALTER EXTERNAL CONSUMER v1 MODIFY PAT v1 SET COMMENT = 'm' ;", + pointer: "/0/alter_object/action", + expected_value: json!("MODIFY PAT SET"), + }, ProjectionTestCase { description: "IF EXISTS is kept", sql: "ALTER TAG IF EXISTS v1 RENAME TO v2", diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs index 56e155f66..9492dfca4 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs @@ -184,7 +184,7 @@ fn given_snowflake_table_actions_and_method_calls_when_parsing_then_the_tree_kee description: "ALTER USER without a name acts on the current user", sql: "ALTER USER ADD PAT t1 DAYS_TO_EXPIRY = 1", pointer: "/0/alter_object/action", - expected_value: json!("ADD PROGRAMMATIC ACCESS TOKEN"), + expected_value: json!("ADD PAT"), }, ProjectionTestCase { description: "a token's IF NOT EXISTS is a flag, not part of the action", @@ -196,7 +196,7 @@ fn given_snowflake_table_actions_and_method_calls_when_parsing_then_the_tree_kee description: "a token's IF EXISTS is a flag apart from the user's", sql: "ALTER USER IF EXISTS u1 MODIFY PAT IF EXISTS t1 RENAME TO t2", pointer: "/0/alter_object/action", - expected_value: json!("MODIFY PROGRAMMATIC ACCESS TOKEN RENAME TO"), + expected_value: json!("MODIFY PAT RENAME TO"), }, ProjectionTestCase { description: "the user's IF EXISTS stays on the user", diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_receipted_objects.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_receipted_objects.rs index 2d8f66916..f376c88b7 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_receipted_objects.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_receipted_objects.rs @@ -144,7 +144,7 @@ fn given_a_snowflake_external_consumer_when_parsing_then_the_tree_keeps_its_kind description: "a token action keeps its words", sql: "ALTER EXTERNAL CONSUMER v1 ROTATE PAT v1 ;", pointer: "/0/alter_object/action", - expected_value: json!("ROTATE PROGRAMMATIC ACCESS TOKEN"), + expected_value: json!("ROTATE PAT"), }, ]; for test_case in test_cases { From fcf20d6ffaf4f6de06560e8ac60f9b1250cc9cb3 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:22:06 +0000 Subject: [PATCH 10/22] fix(snowflake): keep a compute pool's STOP ALL OF TYPE as a property STOP ALL OF TYPE kept OF TYPE in the action string and the workload types as unlabelled operands. The types are now an OF TYPE property, and the action is STOP ALL. --- .../_helpers/ddl/snowflake_alter_resources.rs | 10 ++++++++-- .../tests/parser/test_snowflake_alter.rs | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs index b2d778433..4bc903a91 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_resources.rs @@ -618,8 +618,14 @@ impl Parser { let mut properties: Vec = Vec::new(); match action.as_str() { "STOP ALL" if self.match_text_seq(&["OF", "TYPE"]) => { - action.push_str(" OF TYPE"); - operands = self.parse_object_name_list()?; + let types = Tuple { + expressions: self.parse_object_name_list()?, + }; + properties.push(property( + "OF TYPE", + None, + Some(Expression::Tuple(Box::new(types))), + )); } "EXECUTE AS" => { let caller = diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs index 7f8cdb404..f8b6d691c 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs @@ -645,6 +645,24 @@ fn given_a_snowflake_alter_of_an_object_kind_when_parsing_then_the_tree_names_ki pointer: "/0/alter_object/action", expected_value: json!("MODIFY PAT SET"), }, + ProjectionTestCase { + description: "STOP ALL keeps OF TYPE as a property (recorded semantic error)", + sql: "ALTER COMPUTE POOL v1 STOP ALL OF TYPE USER , USER ;", + pointer: "/0/alter_object/properties/0/kind", + expected_value: json!("OF TYPE"), + }, + ProjectionTestCase { + description: "OF TYPE keeps each workload type", + sql: "ALTER COMPUTE POOL v1 STOP ALL OF TYPE USER , SYSTEM ;", + pointer: "/0/alter_object/properties/0/value/tuple/expressions/1/table/name/name", + expected_value: json!("SYSTEM"), + }, + ProjectionTestCase { + description: "STOP ALL OF TYPE names only STOP ALL as its action", + sql: "ALTER COMPUTE POOL v1 STOP ALL OF TYPE USER ;", + pointer: "/0/alter_object/action", + expected_value: json!("STOP ALL"), + }, ProjectionTestCase { description: "IF EXISTS is kept", sql: "ALTER TAG IF EXISTS v1 RENAME TO v2", From e5e6bf0f294a2f6c768387b7e0230497a0f6c00c Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:22:44 +0000 Subject: [PATCH 11/22] fix(snowflake): keep an application upgrade's PATCH as a property UPGRADE USING VERSION PATCH kept PATCH only in the action string and the patch number as an unlabelled second operand. The patch is now a PATCH property, as CREATE APPLICATION ... USING VERSION already records it. --- .../parser/_helpers/ddl/snowflake_alter_objects.rs | 8 ++++---- .../tests/parser/test_snowflake_alter.rs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_objects.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_objects.rs index c3eabb508..7e46d1dec 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_objects.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_objects.rs @@ -492,12 +492,12 @@ impl Parser { "UPGRADE" => { if self.match_text_seq(&["USING", "VERSION"]) { action.push_str(" USING VERSION"); - let mut operands = vec![self.object_operand()?]; + let operands = vec![self.object_operand()?]; + let mut properties: Vec = Vec::new(); if self.match_text_seq(&["PATCH"]) { - action.push_str(" PATCH"); - operands.push(self.number_operand()?); + properties.push(property("PATCH", None, Some(self.number_operand()?))); } - (operands, vec![]) + (operands, properties) } else if self.match_text_seq(&["USING"]) { action.push_str(" USING"); (vec![self.parse_string_value()?], vec![]) diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs index f8b6d691c..22a2a6e70 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs @@ -663,6 +663,18 @@ fn given_a_snowflake_alter_of_an_object_kind_when_parsing_then_the_tree_names_ki pointer: "/0/alter_object/action", expected_value: json!("STOP ALL"), }, + ProjectionTestCase { + description: "UPGRADE USING VERSION keeps PATCH as a property (recorded semantic error)", + sql: "ALTER APPLICATION v1 UPGRADE USING VERSION v1 PATCH 1 ;", + pointer: "/0/alter_object/properties/0/value/literal/value", + expected_value: json!("1"), + }, + ProjectionTestCase { + description: "UPGRADE USING VERSION with PATCH names only USING VERSION as its action", + sql: "ALTER APPLICATION v1 UPGRADE USING VERSION v1 PATCH 1 ;", + pointer: "/0/alter_object/action", + expected_value: json!("UPGRADE USING VERSION"), + }, ProjectionTestCase { description: "IF EXISTS is kept", sql: "ALTER TAG IF EXISTS v1 RENAME TO v2", From 3028cd579af48c6585f1f3c2c6c67392d46bfc51 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:25:04 +0000 Subject: [PATCH 12/22] fix(snowflake): keep FROM in model and specification create sources CREATE MODEL ... FROM MODEL and CREATE { EXPERIMENT | GATEWAY } ... FROM SPECIFICATION recorded their source as MODEL and SPECIFICATION, while other creates record FROM SPECIFICATION, FROM SERVICE and FROM EXPERIMENT. Both now keep FROM. --- .../src/parser/_helpers/ddl/snowflake_create_from.rs | 4 ++-- .../src/parser/_helpers/ddl/snowflake_opaque.rs | 2 +- .../tests/parser/test_snowflake_create_properties.rs | 10 ++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_from.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_from.rs index 72284e369..bd810e100 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_from.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_from.rs @@ -138,7 +138,7 @@ impl Parser { let version = Expression::Identifier(self.parse_simple_name()?); properties.push(property("VERSION", None, Some(version))); } - "MODEL" + "FROM MODEL" } "GATEWAY" if !self.check(TokenType::From) => { return Err(self.parse_error("Expected FROM SPECIFICATION")); @@ -150,7 +150,7 @@ impl Parser { return Err(self.parse_error("Expected the specification text")); } operands.push(self.parse_primary()?); - "SPECIFICATION" + "FROM SPECIFICATION" } _ if self.check(TokenType::Type) || self.check_identifier("TYPE") => return Ok(None), _ => "", diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_opaque.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_opaque.rs index a5bcc7154..8790ee8fe 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_opaque.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_opaque.rs @@ -863,7 +863,7 @@ impl Parser { ) } else if create.kind == MODEL_KIND && self.match_text_seq(&[MODEL_KIND]) { ( - "MODEL", + "FROM MODEL", Expression::Table(Box::new(self.parse_object_name()?)), ) } else if self.is_stage_path() || self.check(TokenType::String) { diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs index 23e97339d..109b38d8b 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs @@ -583,10 +583,16 @@ fn given_a_snowflake_property_or_policy_create_when_parsing_then_the_tree_keeps_ expected_value: json!("PIPE"), }, ProjectionTestCase { - description: "a model named monitor copied from a model keeps its MODEL source", + description: "a model named monitor copied from a model keeps its FROM MODEL source", sql: "CREATE MODEL monitor FROM MODEL m2", pointer: "/0/create_object/source", - expected_value: json!("MODEL"), + expected_value: json!("FROM MODEL"), + }, + ProjectionTestCase { + description: "an experiment keeps FROM in its specification source (recorded runtime error)", + sql: "CREATE EXPERIMENT v1 FROM SPECIFICATION $$m1 $$ ;", + pointer: "/0/create_object/source", + expected_value: json!("FROM SPECIFICATION"), }, ProjectionTestCase { description: "a dropped account keeps its grace period", From c57ac4ff65dff874ac29d80daa8f6429f4416c0f Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:26:12 +0000 Subject: [PATCH 13/22] fix(snowflake): label an Openflow connector's runtime and a runtime's deployment CREATE OPENFLOW RUNTIME ... IN DEPLOYMENT and CREATE OPENFLOW CONNECTOR ... IN RUNTIME kept the name as an unlabelled operand and dropped IN DEPLOYMENT and IN RUNTIME. Each is now a property named by its words. --- .../_helpers/ddl/snowflake_create_openflow.rs | 8 +++--- .../tests/parser/test_snowflake_openflow.rs | 26 ++++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_openflow.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_openflow.rs index af37e43be..470d435dc 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_openflow.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_openflow.rs @@ -75,11 +75,13 @@ impl Parser { } let name = self.parse_object_name()?; let mut operands: Vec = Vec::new(); + let mut properties: Vec = Vec::new(); let mut source = String::new(); match kind { "CONNECTOR" => { self.expect_openflow_words(&["IN", "RUNTIME"])?; - operands.push(Expression::Table(Box::new(self.parse_object_name()?))); + let runtime = Expression::Table(Box::new(self.parse_object_name()?)); + properties.push(property("IN RUNTIME", None, Some(runtime))); self.expect_token(TokenType::From)?; if self.match_text_seq(&["DEFINITION"]) { source = "FROM DEFINITION".to_owned(); @@ -91,11 +93,11 @@ impl Parser { } "RUNTIME" => { self.expect_openflow_words(&["IN", "DEPLOYMENT"])?; - operands.push(Expression::Table(Box::new(self.parse_object_name()?))); + let deployment = Expression::Table(Box::new(self.parse_object_name()?)); + properties.push(property("IN DEPLOYMENT", None, Some(deployment))); } _ => {} } - let mut properties: Vec = Vec::new(); while !self.is_statement_end_at(0) { let key = self.peek_text().to_ascii_uppercase(); let Some(&(_, value)) = keys.iter().find(|(name, _)| *name == key) else { diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_openflow.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_openflow.rs index 84ff5a412..d99a9d946 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_openflow.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_openflow.rs @@ -176,13 +176,13 @@ fn given_a_snowflake_create_openflow_when_parsing_then_the_tree_keeps_its_parts( ProjectionTestCase { description: "a connector keeps its runtime", sql: "CREATE OPENFLOW CONNECTOR c1 IN RUNTIME r1 FROM DEFINITION d1", - pointer: "/0/create_object/operands/0/table/name/name", + pointer: "/0/create_object/properties/0/value/table/name/name", expected_value: json!("r1"), }, ProjectionTestCase { description: "a connector keeps its definition", sql: "CREATE OPENFLOW CONNECTOR c1 IN RUNTIME r1 FROM DEFINITION d1", - pointer: "/0/create_object/operands/1/table/name/name", + pointer: "/0/create_object/operands/0/table/name/name", expected_value: json!("d1"), }, ProjectionTestCase { @@ -194,33 +194,45 @@ fn given_a_snowflake_create_openflow_when_parsing_then_the_tree_keeps_its_parts( ProjectionTestCase { description: "a connector keeps its stage location as a string", sql: "CREATE OPENFLOW CONNECTOR c1 IN RUNTIME r1 FROM '@s1/p1'", - pointer: "/0/create_object/operands/1/literal/value", + pointer: "/0/create_object/operands/0/literal/value", expected_value: json!("@s1/p1"), }, ProjectionTestCase { description: "a runtime keeps its deployment", sql: "CREATE OPENFLOW RUNTIME r1 IN DEPLOYMENT p1 NODE_TYPE = SMALL MIN_NODES = 1 MAX_NODES = 2 EXECUTE_AS_ROLE = o1", - pointer: "/0/create_object/operands/0/table/name/name", + pointer: "/0/create_object/properties/0/value/table/name/name", expected_value: json!("p1"), }, ProjectionTestCase { description: "a node type is a keyword, not a name", sql: "CREATE OPENFLOW RUNTIME r1 IN DEPLOYMENT p1 NODE_TYPE = small MIN_NODES = 1 MAX_NODES = 2 EXECUTE_AS_ROLE = o1", - pointer: "/0/create_object/properties/0/value/var/this", + pointer: "/0/create_object/properties/1/value/var/this", expected_value: json!("SMALL"), }, ProjectionTestCase { description: "a runtime keeps its role as a name", sql: "CREATE OPENFLOW RUNTIME r1 IN DEPLOYMENT p1 NODE_TYPE = SMALL MIN_NODES = 1 MAX_NODES = 2 EXECUTE_AS_ROLE = o1", - pointer: "/0/create_object/properties/3/value/table/name/name", + pointer: "/0/create_object/properties/4/value/table/name/name", expected_value: json!("o1"), }, ProjectionTestCase { description: "a runtime's integrations stay one list", sql: "CREATE OPENFLOW RUNTIME r1 IN DEPLOYMENT p1 NODE_TYPE = SMALL MIN_NODES = 1 MAX_NODES = 2 EXECUTE_AS_ROLE = o1 EXTERNAL_ACCESS_INTEGRATIONS = ( i1 , i2 )", - pointer: "/0/create_object/properties/4/value/tuple/expressions/1/table/name/name", + pointer: "/0/create_object/properties/5/value/tuple/expressions/1/table/name/name", expected_value: json!("i2"), }, + ProjectionTestCase { + description: "a runtime labels its deployment with IN DEPLOYMENT (recorded semantic error)", + sql: "CREATE OPENFLOW RUNTIME r1 IN DEPLOYMENT p1 NODE_TYPE = SMALL MIN_NODES = 1 MAX_NODES = 2 EXECUTE_AS_ROLE = o1", + pointer: "/0/create_object/properties/0/kind", + expected_value: json!("IN DEPLOYMENT"), + }, + ProjectionTestCase { + description: "a connector labels its runtime with IN RUNTIME", + sql: "CREATE OPENFLOW CONNECTOR c1 IN RUNTIME r1 FROM DEFINITION d1", + pointer: "/0/create_object/properties/0/kind", + expected_value: json!("IN RUNTIME"), + }, ProjectionTestCase { description: "a deployment type is a keyword", sql: "CREATE OPENFLOW DEPLOYMENT p1 DEPLOYMENT_TYPE = byoc", From 5539960739597ea03fd718c8b5df529c45be6c78 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:28:01 +0000 Subject: [PATCH 14/22] fix(snowflake): read a multi-word property value as a keyword phrase ALLOWED_INTEGRATION_TYPES = SECURITY INTEGRATIONS gave one unquoted name with a blank in it and the span of its first word. A value of several plain words is now a keyword phrase, as Openflow keyword values are, and a one-word value stays a name. --- .../parser/_helpers/ddl/snowflake_alter.rs | 18 +++++++++++ .../_helpers/ddl/snowflake_alter_account.rs | 9 +++--- .../ddl/snowflake_create_properties.rs | 31 ++++++++++--------- .../test_snowflake_create_properties.rs | 18 +++++++++-- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs index 35796aa95..0592e5e9a 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter.rs @@ -14,6 +14,7 @@ use sqlquack_syntax::expressions::operators::models::NamedArgSeparator; use sqlquack_syntax::expressions::operators::models::NamedArgument; use sqlquack_syntax::expressions::schema_objects::models::AlterObject; use sqlquack_syntax::expressions::schema_objects::models::ObjectProperty; +use sqlquack_syntax::expressions::statements::models::Var; use sqlquack_syntax::expressions::tree::models::Expression; use sqlquack_syntax::expressions::types::models::Tuple; use sqlquack_syntax::tokens::models::TokenType; @@ -925,6 +926,23 @@ pub(super) fn keyed_pair(key: Expression, value: Expression, arrow: bool) -> Exp } } +/// A value written as a name, or as a keyword phrase such as `SECURITY INTEGRATIONS` when `more` words followed a plain one-part name; after a quoted or qualified name the words stay part of its last part. +pub(super) fn value_phrase(mut first: Box, more: Vec) -> Expression { + if more.is_empty() { + return Expression::Table(first); + } + if first.name.quoted || first.schema.is_some() || first.catalog.is_some() { + first.name.name = std::iter::once(first.name.name) + .chain(more) + .collect::>() + .join(" "); + return Expression::Table(first); + } + let words = std::iter::once(first.name.name).chain(more); + let this = words.collect::>().join(" ").to_ascii_uppercase(); + Expression::Var(Box::new(Var { this })) +} + /// One property of kind `kind`. pub(super) fn property( kind: &str, diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs index acc53d5a2..d1f129bb2 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs @@ -6,6 +6,7 @@ use crate::parser::_helpers::ddl::snowflake_alter::named_property; use crate::parser::_helpers::ddl::snowflake_alter::parameter_property; use crate::parser::_helpers::ddl::snowflake_alter::property; use crate::parser::_helpers::ddl::snowflake_alter::property_kind; +use crate::parser::_helpers::ddl::snowflake_alter::value_phrase; use crate::parser::constants::SNOWFLAKE_GROUP_NAME_PROPERTIES; use crate::parser::constants::SNOWFLAKE_STRING_KEY_MAP_PROPERTIES; use crate::parser::models::Parser; @@ -147,19 +148,19 @@ impl Parser { return Err(self.parse_error(format!("Expected a name in the list of {key}"))); } let value = self.parse_object_value()?; - let Expression::Table(mut name) = value else { + let Expression::Table(name) = value else { values.push(value); if !self.match_token(TokenType::Comma) { break; } continue; }; + let mut more: Vec = Vec::new(); while !self.is_at_end() && self.check(TokenType::Var) && !self.check_next(TokenType::Eq) { - name.name.name = format!("{} {}", name.name.name, self.peek_text()); - self.skip(); + more.push(self.advance_text()?); } - values.push(Expression::Table(name)); + values.push(value_phrase(name, more)); if !self.match_token(TokenType::Comma) { break; } diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_properties.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_properties.rs index 1edfa0e6f..e41e49a6c 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_properties.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_properties.rs @@ -1,6 +1,7 @@ //! Snowflake CREATE for kinds defined only by ` = ` lists: users, policies, integrations and groups. use crate::parser::_helpers::ddl::snowflake_alter::property; +use crate::parser::_helpers::ddl::snowflake_alter::value_phrase; use crate::parser::constants::{ SNOWFLAKE_BODY_POLICY_KINDS, SNOWFLAKE_GROUP_NAME_PROPERTIES, SNOWFLAKE_KIND_WORDS_ALONE, SNOWFLAKE_PROPERTY_KINDS, SNOWFLAKE_STRING_KEY_MAP_PROPERTIES, SNOWFLAKE_TAGS_LAST_KINDS, @@ -554,31 +555,31 @@ impl Parser { let word = self.advance_text()?.to_ascii_uppercase(); return Ok(Expression::Identifier(Identifier::new(word))); } - let mut first = self.parse_object_value()?; - if let Expression::Table(table) = &mut first { - while self.continues_value_words() { - let word = self.advance_text()?; - table.name.name = format!("{} {}", table.name.name, word); - } - } + let first = self.parse_value_phrase()?; if in_list || !self.continues_name_list() { return Ok(first); } let mut expressions = vec![first]; while self.continues_name_list() { self.skip(); - let mut next = self.parse_object_value()?; - if let Expression::Table(table) = &mut next { - while self.continues_value_words() { - let word = self.advance_text()?; - table.name.name = format!("{} {}", table.name.name, word); - } - } - expressions.push(next); + expressions.push(self.parse_value_phrase()?); } Ok(Expression::Tuple(Box::new(Tuple { expressions }))) } + /// A value, or a keyword phrase such as SECURITY INTEGRATIONS when plain words follow a name. + fn parse_value_phrase(&mut self) -> Result { + let value = self.parse_object_value()?; + let Expression::Table(name) = value else { + return Ok(value); + }; + let mut more: Vec = Vec::new(); + while self.continues_value_words() { + more.push(self.advance_text()?); + } + Ok(value_phrase(name, more)) + } + /// Whether the next word continues a multi-word value, such as SECURITY INTEGRATIONS: a plain word that starts no ` =` or clause. fn continues_value_words(&self) -> bool { self.is_identifier_token() diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs index 109b38d8b..c309ad6ea 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs @@ -367,9 +367,9 @@ fn given_a_snowflake_property_or_policy_create_when_parsing_then_the_tree_keeps_ expected_value: json!("DEFAULT_ROLE"), }, ProjectionTestCase { - description: "a multi-word value stays one value", - sql: "CREATE FAILOVER GROUP g1 OBJECT_TYPES = INTEGRATIONS ALLOWED_INTEGRATION_TYPES = SECURITY INTEGRATIONS ALLOWED_ACCOUNTS = o1.a1", - pointer: "/0/create_object/properties/1/value/table/name/name", + description: "a multi-word value stays one keyword phrase, not a name (recorded success)", + sql: "CREATE FAILOVER GROUP g1 OBJECT_TYPES = INTEGRATIONS ALLOWED_INTEGRATION_TYPES = security INTEGRATIONS ALLOWED_ACCOUNTS = o1.a1", + pointer: "/0/create_object/properties/1/value/var/this", expected_value: json!("SECURITY INTEGRATIONS"), }, ProjectionTestCase { @@ -390,6 +390,18 @@ fn given_a_snowflake_property_or_policy_create_when_parsing_then_the_tree_keeps_ pointer: "/0/create_object/source", expected_value: json!("SHARE"), }, + ProjectionTestCase { + description: "a one-word value stays a name", + sql: "CREATE FAILOVER GROUP g1 OBJECT_TYPES = INTEGRATIONS ALLOWED_INTEGRATION_TYPES = SECURITY INTEGRATIONS ALLOWED_ACCOUNTS = o1.a1", + pointer: "/0/create_object/properties/0/value/table/name/name", + expected_value: json!("INTEGRATIONS"), + }, + ProjectionTestCase { + description: "an altered group's multi-word value is a keyword phrase too", + sql: "ALTER FAILOVER GROUP g1 SET ALLOWED_INTEGRATION_TYPES = SECURITY INTEGRATIONS , API INTEGRATIONS", + pointer: "/0/alter_object/properties/0/value/tuple/expressions/1/var/this", + expected_value: json!("API INTEGRATIONS"), + }, ProjectionTestCase { description: "a warehouse keeps OR ALTER", sql: "CREATE OR ALTER WAREHOUSE v1 WAREHOUSE_SIZE = MEDIUM", From 90d735c88f94befc2ce7137d7c87c21a12df4784 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:32:34 +0000 Subject: [PATCH 15/22] fix(snowflake): keep how a temporary table was spelled CREATE { TEMP | VOLATILE | LOCAL TEMP | GLOBAL TEMPORARY } TABLE gave the same tree as CREATE TEMPORARY TABLE, and a read-only temporary clone kept READ ONLY inside its modifier string. A table now records temporary_spelling when the words were not TEMPORARY, and a read-only clone keeps its temporary words as the modifier and READ ONLY as a property. --- .../src/parser/_helpers/ddl/create_table.rs | 1 + .../parser/_helpers/ddl/create_table_forms.rs | 9 +++++ .../_helpers/ddl/snowflake_create_clone.rs | 7 ++-- .../_helpers/ddl/snowflake_create_from.rs | 20 +++++++++-- .../src/parser/_helpers/statements/dml.rs | 3 +- .../src/parser/constants.rs | 3 ++ .../test_snowflake_create_properties.rs | 34 +++++++++++++++++-- .../src/expressions/tables/models.rs | 4 +++ 8 files changed, 73 insertions(+), 8 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_table.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_table.rs index 1c0f279e8..fbc3d6ea2 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_table.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_table.rs @@ -475,6 +475,7 @@ impl Parser { uuid, with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_table_forms.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_table_forms.rs index bfd1baced..368fac77d 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_table_forms.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/create_table_forms.rs @@ -151,6 +151,7 @@ impl Parser { uuid: None, with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } @@ -318,6 +319,7 @@ impl Parser { uuid: uuid.clone(), with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } @@ -384,6 +386,7 @@ impl Parser { uuid: uuid.clone(), with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } @@ -489,6 +492,7 @@ impl Parser { uuid: uuid.clone(), with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } @@ -584,6 +588,7 @@ impl Parser { uuid: uuid.clone(), with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } @@ -654,6 +659,7 @@ impl Parser { uuid: uuid.clone(), with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } @@ -720,6 +726,7 @@ impl Parser { uuid: uuid.clone(), with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } @@ -793,6 +800,7 @@ impl Parser { uuid: uuid.clone(), with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } @@ -869,6 +877,7 @@ impl Parser { uuid: uuid.clone(), with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, }))) } } diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_clone.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_clone.rs index 79202aa7b..97a6dffcb 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_clone.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_clone.rs @@ -339,7 +339,7 @@ impl Parser { })))) } - /// `[ LOCAL | GLOBAL ] { TEMP | TEMPORARY } READ ONLY TABLE [ IF NOT EXISTS ] CLONE ...` after its temporary words, which stay in the modifier as written; None for anything else. + /// `[ LOCAL | GLOBAL ] { TEMP | TEMPORARY } READ ONLY TABLE [ IF NOT EXISTS ] CLONE ...` after its temporary words, which stay in the modifier as written, with READ ONLY as its first property; None for anything else. pub(crate) fn parse_snowflake_read_only_clone( &mut self, or_replace: bool, @@ -370,10 +370,11 @@ impl Parser { if let Some(time_travel) = self.parse_clone_time_travel()? { operands.push(time_travel); } - let properties = self.parse_clone_options(CloneOptions::Copy)?; + let mut properties = vec![property("READ ONLY", None, None)]; + properties.extend(self.parse_clone_options(CloneOptions::Copy)?); Ok(Some(Expression::CreateObject(Box::new(CreateObject { kind: "TABLE".to_owned(), - modifier: Some(format!("{} READ ONLY", self.written_temporary(start))), + modifier: Some(self.written_temporary(start)), or_replace, if_not_exists, name, diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_from.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_from.rs index bd810e100..fd866fc5f 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_from.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_from.rs @@ -2,6 +2,7 @@ use crate::parser::_helpers::ddl::snowflake_alter::property; use crate::parser::_helpers::ddl::snowflake_create_properties::with_written; +use crate::parser::constants::PLAIN_TEMPORARY; use crate::parser::models::Parser; use sqlquack_syntax::expressions::names::models::TableRef; use sqlquack_syntax::expressions::operators::models::BinaryOp; @@ -172,8 +173,23 @@ impl Parser { })))) } + /// Record on a Snowflake temporary table's tree how TEMPORARY was spelled, when not plainly, from the words before `at`. + pub(crate) fn keep_temporary_spelling( + &self, + parsed: &mut Result, + temporary: bool, + at: usize, + ) { + if let (true, Ok(Expression::CreateTable(table))) = + (temporary && self.is_snowflake(), parsed) + { + let written = self.written_temporary(at); + table.temporary_spelling = (written != PLAIN_TEMPORARY).then_some(written); + } + } + /// The words before the token at `at` that made a CREATE temporary, as written and upper-cased: `[ LOCAL | GLOBAL ] { TEMP | TEMPORARY | VOLATILE }`. - pub(super) fn written_temporary(&self, at: usize) -> String { + pub(crate) fn written_temporary(&self, at: usize) -> String { let word = |offset: usize| { at.checked_sub(offset) .and_then(|index| self.tokens.get(index)) @@ -182,7 +198,7 @@ impl Parser { }; let temporary = word(1); if !matches!(temporary.as_str(), "TEMP" | "TEMPORARY" | "VOLATILE") { - return "TEMPORARY".to_owned(); + return PLAIN_TEMPORARY.to_owned(); } match word(2).as_str() { scope @ ("LOCAL" | "GLOBAL") => format!("{scope} {temporary}"), diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/statements/dml.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/statements/dml.rs index 21b15bf80..56ab0b08e 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/statements/dml.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/statements/dml.rs @@ -476,8 +476,9 @@ impl Parser { return table; } } - let parsed = + let mut parsed = self.parse_create_table(or_replace, temporary, leading_comments, modifier); + self.keep_temporary_spelling(&mut parsed, temporary, table_start); let checkable = !define && !secure && matches!(modifier, None | Some("TRANSIENT" | "HYBRID")); self.keep_snowflake_create_table( diff --git a/crates/sqlquack-syntax-snowflake/src/parser/constants.rs b/crates/sqlquack-syntax-snowflake/src/parser/constants.rs index 1f4be2437..5a3a1ad60 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/constants.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/constants.rs @@ -584,6 +584,9 @@ pub(crate) const MODEL_KIND: &str = "MODEL"; /// The opaque CREATE kinds that take their manifest only after AS, never FROM a location: a bare LISTING, unlike an EXTERNAL or ORGANIZATION LISTING. pub(crate) const SNOWFLAKE_MANIFEST_ONLY_KINDS: &[&str] = &["LISTING"]; +/// The plain spelling of a temporary CREATE, which a tree records as its flag alone. +pub(crate) const PLAIN_TEMPORARY: &str = "TEMPORARY"; + /// The modifiers an opaque CREATE may write before its kind, each kept as written. pub(crate) const SNOWFLAKE_OPAQUE_MODIFIERS: &[&[&str]] = &[ &["LOCAL", "TEMPORARY"], diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs index c309ad6ea..729233a08 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs @@ -535,10 +535,40 @@ fn given_a_snowflake_property_or_policy_create_when_parsing_then_the_tree_keeps_ expected_value: json!("SCOPED TEMP"), }, ProjectionTestCase { - description: "a read-only temporary clone keeps its words", + description: "a read-only temporary clone keeps its temporary words as its modifier", sql: "CREATE OR REPLACE LOCAL TEMP READ ONLY TABLE v1 CLONE v2 COPY GRANTS", pointer: "/0/create_object/modifier", - expected_value: json!("LOCAL TEMP READ ONLY"), + expected_value: json!("LOCAL TEMP"), + }, + ProjectionTestCase { + description: "a read-only temporary clone keeps READ ONLY as a property", + sql: "CREATE OR REPLACE LOCAL TEMP READ ONLY TABLE v1 CLONE v2 COPY GRANTS", + pointer: "/0/create_object/properties/0/kind", + expected_value: json!("READ ONLY"), + }, + ProjectionTestCase { + description: "a volatile table clone keeps VOLATILE (recorded success)", + sql: "CREATE VOLATILE TABLE v1 CLONE v1 ;", + pointer: "/0/create_table/temporary_spelling", + expected_value: json!("VOLATILE"), + }, + ProjectionTestCase { + description: "a global temporary table keeps GLOBAL TEMPORARY", + sql: "CREATE GLOBAL TEMPORARY TABLE t1 ( c1 INT ) ;", + pointer: "/0/create_table/temporary_spelling", + expected_value: json!("GLOBAL TEMPORARY"), + }, + ProjectionTestCase { + description: "a TEMP table keeps TEMP", + sql: "CREATE TEMP TABLE v1 CLONE v1 ;", + pointer: "/0/create_table/temporary_spelling", + expected_value: json!("TEMP"), + }, + ProjectionTestCase { + description: "a TEMPORARY table needs no spelling", + sql: "CREATE TEMPORARY TABLE t1 ( c1 INT ) ;", + pointer: "/0/create_table/temporary_spelling", + expected_value: json!(null), }, ProjectionTestCase { description: "a class instance keeps its class's omitted schema as null", diff --git a/crates/sqlquack-syntax/src/expressions/tables/models.rs b/crates/sqlquack-syntax/src/expressions/tables/models.rs index 86a497f26..fb110f508 100644 --- a/crates/sqlquack-syntax/src/expressions/tables/models.rs +++ b/crates/sqlquack-syntax/src/expressions/tables/models.rs @@ -176,6 +176,9 @@ pub struct CreateTable { /// WITH CONNECTION `project.region.connection`, as BigQuery external tables use. #[serde(default, skip_serializing_if = "Option::is_none")] pub with_connection: Option, + /// Snowflake: the words that made the table temporary when they were not TEMPORARY, e.g. `TEMP`, `VOLATILE` or `LOCAL TEMP` + #[serde(default, skip_serializing_if = "Option::is_none")] + pub temporary_spelling: Option, } /// Teradata index specification for CREATE TABLE @@ -246,6 +249,7 @@ impl CreateTable { uuid: None, with_partition_columns: Vec::new(), with_connection: None, + temporary_spelling: None, } } } From fe519431bc594207a92ad23cc700493468e238ce Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:37:53 +0000 Subject: [PATCH 16/22] fix(snowflake): refuse FORCE before an account policy's FOR ALL scope Snowflake refuses ALTER ACCOUNT SET MAINTENANCE POLICY FORCE FOR ALL APPLICATIONS with a syntax error, although the page prints [ FORCE ] before the scope. It parses FORCE after the scope and FORCE alone, for every policy kind recorded. The parser now accepts only those orders. --- .../_helpers/ddl/snowflake_alter_account.rs | 9 +++---- .../tests/parser/test_snowflake_alter.rs | 26 ++++++++++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs index d1f129bb2..5091cc654 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs @@ -271,14 +271,13 @@ impl Parser { false => vec![], }; let mut properties: Vec = Vec::new(); - let force = set && self.match_identifier("FORCE"); - if force { - properties.push(property("FORCE", None, None)); - } if let Some(scope) = self.match_policy_scope() { properties.push(property(&format!("FOR ALL {scope}"), None, None)); } - if set && !force && self.match_identifier("FORCE") { + if set && self.match_identifier("FORCE") { + if properties.is_empty() && self.check_text_seq(&["FOR", "ALL"]) { + return Err(self.parse_error("FORCE comes after FOR ALL ")); + } properties.push(property("FORCE", None, None)); } (operands, properties) diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs index 22a2a6e70..36a86eda1 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter.rs @@ -293,6 +293,24 @@ fn given_a_snowflake_alter_of_an_object_kind_when_parsing_then_recorded_rejectio dialect: Dialect::Snowflake, expected_accepted: false, }, + AcceptanceTestCase { + description: "FORCE before an account policy's FOR ALL scope is refused (recorded errno 1003)", + sql: "ALTER ACCOUNT SET MAINTENANCE POLICY v1 FORCE FOR ALL APPLICATIONS ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "FORCE after an account policy's FOR ALL scope is accepted (recorded semantic error)", + sql: "ALTER ACCOUNT SET FEATURE POLICY v1 FOR ALL APPLICATIONS FORCE ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "FORCE without a scope is accepted (recorded semantic error)", + sql: "ALTER ACCOUNT SET PACKAGES POLICY v1 FORCE ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, AcceptanceTestCase { description: "only external tables add partitions (recorded errno 1003)", sql: "ALTER TABLE v1 ADD PARTITION ( v1 = 'm' ) LOCATION 'm' ;", @@ -598,14 +616,14 @@ fn given_a_snowflake_alter_of_an_object_kind_when_parsing_then_the_tree_names_ki expected_value: json!("DArrow"), }, ProjectionTestCase { - description: "a policy's FOR ALL scope is its own property (recorded success)", - sql: "ALTER ACCOUNT SET AUTHENTICATION POLICY v1 FORCE FOR ALL PERSON USERS ;", - pointer: "/0/alter_object/properties/1/kind", + description: "a policy's FOR ALL scope is its own property (recorded semantic error)", + sql: "ALTER ACCOUNT SET AUTHENTICATION POLICY v1 FOR ALL PERSON USERS FORCE ;", + pointer: "/0/alter_object/properties/0/kind", expected_value: json!("FOR ALL PERSON USERS"), }, ProjectionTestCase { description: "the policy's action names only the policy", - sql: "ALTER ACCOUNT SET AUTHENTICATION POLICY v1 FORCE FOR ALL PERSON USERS ;", + sql: "ALTER ACCOUNT SET AUTHENTICATION POLICY v1 FOR ALL PERSON USERS FORCE ;", pointer: "/0/alter_object/action", expected_value: json!("SET AUTHENTICATION POLICY"), }, From 1240a351ce7fa678c924486e707c56a36080e3de Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:45:52 +0000 Subject: [PATCH 17/22] fix(snowflake): accept SET and UNSET on a named organization --- .../_helpers/ddl/snowflake_alter_account.rs | 24 ++++++++++++------- .../tests/parser/test_snowflake_alter_gaps.rs | 17 +++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs index 5091cc654..311ad91dd 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_alter_account.rs @@ -504,7 +504,7 @@ impl Parser { Ok(alter_object("CONNECTION", head, action, parts)) } - /// Parse ALTER ORGANIZATION after its first word: the organization itself, or its ACCOUNT, PROFILE, USER or USER GROUP. + /// Parse ALTER ORGANIZATION after its first word: a named organization's RENAME TO, SET or UNSET, the current one's SET or UNSET, or its ACCOUNT, PROFILE, USER or USER GROUP. fn parse_alter_organization(&mut self) -> Result { if self.match_text_seq(&["USER", "GROUP"]) { return self.parse_alter_organization_user_group(); @@ -540,14 +540,22 @@ impl Parser { (vec![], properties), )); } - let renames = self - .tokens - .get(self.current + 1) - .is_some_and(|token| token.text.eq_ignore_ascii_case("RENAME")); - if renames { + let named = match self.text_at(1).as_deref() { + Some("RENAME") => true, + Some("SET" | "UNSET") => !matches!( + self.text_at(0).as_deref(), + Some("ACCOUNT" | "PROFILE" | "SET" | "UNSET") + ), + _ => false, + }; + if named { let head = self.parse_alter_object_head()?; - let action = self.expect_alter_words(&[&["RENAME", "TO"]])?; - let parts: ActionParts = (vec![self.parse_group_name()?], vec![]); + let action = self.expect_alter_words(&[&["RENAME", "TO"], &["SET"], &["UNSET"]])?; + let parts: ActionParts = match action.as_str() { + "RENAME TO" => (vec![self.parse_group_name()?], vec![]), + "SET" => (vec![], self.parse_organization_set()?), + _ => (vec![], self.parse_group_unset_names()?), + }; return Ok(alter_object("ORGANIZATION", head, action, parts)); } if self.match_identifier("PROFILE") { diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs index 9492dfca4..f376e5832 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_alter_gaps.rs @@ -334,6 +334,11 @@ fn given_snowflake_policy_and_integration_alters_without_a_name_when_parsing_the AcceptanceTestCase { description: "an integration named refresh refreshes its secret", sql: "ALTER INTEGRATION refresh REFRESH OAUTH_CLIENT_SECRET ;", dialect: Dialect::Snowflake, expected_accepted: true }, AcceptanceTestCase { description: "an organization user's visibility may be another value", sql: "ALTER ORGANIZATION USER v1 SET VISIBILITY = 'x' ;", dialect: Dialect::Snowflake, expected_accepted: true }, AcceptanceTestCase { description: "VISIBILITY = ALL is followed by other properties", sql: "ALTER ORGANIZATION USER v1 SET VISIBILITY = ALL , COMMENT = 'm' ;", dialect: Dialect::Snowflake, expected_accepted: true }, + AcceptanceTestCase { description: "a named organization sets a property (recorded semantic error)", sql: "ALTER ORGANIZATION v1 SET COMMENT = 'm' ;", dialect: Dialect::Snowflake, expected_accepted: true }, + AcceptanceTestCase { description: "a named organization unsets a property (recorded semantic error)", sql: "ALTER ORGANIZATION v1 UNSET EMAIL ;", dialect: Dialect::Snowflake, expected_accepted: true }, + AcceptanceTestCase { description: "a named organization's SET names a property (recorded syntax error)", sql: "ALTER ORGANIZATION v1 SET ;", dialect: Dialect::Snowflake, expected_accepted: false }, + AcceptanceTestCase { description: "a named organization's UNSET names a property (recorded syntax error)", sql: "ALTER ORGANIZATION v1 UNSET ;", dialect: Dialect::Snowflake, expected_accepted: false }, + AcceptanceTestCase { description: "a named organization takes no COMMIT (recorded syntax error)", sql: "ALTER ORGANIZATION v1 COMMIT ;", dialect: Dialect::Snowflake, expected_accepted: false }, ]; for test_case in test_cases { @@ -392,6 +397,18 @@ fn given_snowflake_policy_and_integration_alters_without_a_name_when_parsing_the pointer: "/0/alter_object/properties/0/kind", expected_value: json!("VISIBILITY"), }, + ProjectionTestCase { + description: "a named organization keeps its name", + sql: "ALTER ORGANIZATION v1 UNSET EMAIL", + pointer: "/0/alter_object/name/name/name", + expected_value: json!("v1"), + }, + ProjectionTestCase { + description: "a named organization's SET keeps its property", + sql: "ALTER ORGANIZATION v1 SET COMMENT = 'm'", + pointer: "/0/alter_object/properties/0/kind", + expected_value: json!("COMMENT"), + }, ]; for test_case in test_cases { From 7a8a63ef50b5d9585f1f2b6fb330d349e3ce09c4 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:49:39 +0000 Subject: [PATCH 18/22] fix(snowflake): read CREATE ORGANIZATION as an organization with properties --- .../ddl/snowflake_create_properties.rs | 24 +++++- .../test_snowflake_create_properties.rs | 78 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_properties.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_properties.rs index e41e49a6c..6757740ef 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_properties.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_properties.rs @@ -39,12 +39,14 @@ impl Parser { return Err(self.parse_error("Expected an object kind after CREATE")); } let database_role = or_alter && self.names_a_database_role(); + let organization = self.names_an_organization(); let Some(parts) = SNOWFLAKE_PROPERTY_KINDS .iter() .map(|kind| kind.split(' ').collect::>()) - .filter(|parts| self.check_text_seq(parts)) + .filter(|parts| !organization && self.check_text_seq(parts)) .max_by_key(Vec::len) .or_else(|| database_role.then(|| vec!["DATABASE", "ROLE"])) + .or_else(|| organization.then(|| vec!["ORGANIZATION"])) else { let body = SNOWFLAKE_BODY_POLICY_KINDS .iter() @@ -91,6 +93,9 @@ impl Parser { with = true; continue; } + if parts == ["ORGANIZATION"] && !self.check_next(TokenType::Eq) { + return Err(self.parse_error("Expected = ")); + } let tags = self.check_identifier("TAG") && self.check_next(TokenType::LParen); if tags && or_alter && parts == ["SECRET"] { return Err(self.parse_error("CREATE OR ALTER and DEFINE SECRET take no tags")); @@ -330,6 +335,23 @@ impl Parser { })))) } + /// Whether CREATE names an organization itself: `ORGANIZATION `, where a kind word such as ACCOUNT or USER is the name when the statement ends after it or a property follows it. + fn names_an_organization(&self) -> bool { + if self.peek().token_type == TokenType::QuotedIdentifier + || !self.check_identifier("ORGANIZATION") + { + return false; + } + if !matches!( + self.text_at(1).as_deref(), + Some("ACCOUNT" | "LISTING" | "PROFILE" | "USER") + ) { + return true; + } + let at = |offset: usize| self.tokens.get(self.current + offset).map(|t| t.token_type); + matches!(at(2), None | Some(TokenType::Semicolon)) || at(3) == Some(TokenType::Eq) + } + /// Whether the words at the cursor are part of a property or body kind, not all of it, before a name and then `;`, AS, WITH, FROM or ` =`. fn names_a_partial_kind(&self) -> bool { let kinds = SNOWFLAKE_PROPERTY_KINDS diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs index 729233a08..8df62eeed 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_properties.rs @@ -390,6 +390,24 @@ fn given_a_snowflake_property_or_policy_create_when_parsing_then_the_tree_keeps_ pointer: "/0/create_object/source", expected_value: json!("SHARE"), }, + ProjectionTestCase { + description: "an organization named ACCOUNT is an organization, not an account", + sql: "CREATE ORGANIZATION ACCOUNT ADMIN_NAME = 'm' EMAIL = 'm'", + pointer: "/0/create_object/kind", + expected_value: json!("ORGANIZATION"), + }, + ProjectionTestCase { + description: "an organization named ACCOUNT keeps the word as its name", + sql: "CREATE ORGANIZATION ACCOUNT ADMIN_NAME = 'm' EMAIL = 'm'", + pointer: "/0/create_object/name/name/name", + expected_value: json!("ACCOUNT"), + }, + ProjectionTestCase { + description: "an organization account keeps its own kind", + sql: "CREATE ORGANIZATION ACCOUNT v1 ADMIN_NAME = 'm' EMAIL = 'm'", + pointer: "/0/create_object/kind", + expected_value: json!("ORGANIZATION ACCOUNT"), + }, ProjectionTestCase { description: "a one-word value stays a name", sql: "CREATE FAILOVER GROUP g1 OBJECT_TYPES = INTEGRATIONS ALLOWED_INTEGRATION_TYPES = SECURITY INTEGRATIONS ALLOWED_ACCOUNTS = o1.a1", @@ -780,6 +798,66 @@ fn given_a_snowflake_create_without_a_complete_kind_when_parsing_then_it_is_refu dialect: Dialect::Snowflake, expected_accepted: true, }, + AcceptanceTestCase { + description: "an organization named v1 takes properties (recorded semantic error)", + sql: "CREATE ORGANIZATION v1 ADMIN_NAME = 'm' ADMIN_PASSWORD = 'm' EMAIL = 'm' EDITION = ENTERPRISE ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an organization named ACCOUNT takes properties (recorded semantic error)", + sql: "CREATE ORGANIZATION ACCOUNT ADMIN_NAME = 'm' ADMIN_PASSWORD = 'm' EMAIL = 'm' EDITION = ENTERPRISE ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an organization named ACCOUNT reads v1 as a property (recorded semantic error)", + sql: "CREATE ORGANIZATION ACCOUNT v1 = 'm' ADMIN_PASSWORD = 'm' EMAIL = 'm' EDITION = ENTERPRISE ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an organization named v1 alone (recorded semantic error)", + sql: "CREATE ORGANIZATION v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an organization named PROFILE alone (recorded semantic error)", + sql: "CREATE ORGANIZATION PROFILE ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "an organization named USER alone (recorded semantic error)", + sql: "CREATE ORGANIZATION USER ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a reserved word is no organization profile name (recorded syntax error)", + sql: "CREATE ORGANIZATION PROFILE SELECT ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a reserved word is no organization account name (recorded syntax error)", + sql: "CREATE ORGANIZATION ACCOUNT SELECT ADMIN_NAME = 'm' ADMIN_PASSWORD = 'm' EMAIL = 'm' EDITION = ENTERPRISE ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a nameless organization profile has no AS (recorded syntax error)", + sql: "CREATE ORGANIZATION PROFILE AS 'm' ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "an organization named v1 takes no AS (recorded syntax error)", + sql: "CREATE ORGANIZATION v1 AS 'm' ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, AcceptanceTestCase { description: "a packages policy is kept", sql: "CREATE PACKAGES POLICY v1 LANGUAGE PYTHON ;", From 2bf4a06af58b37fe176b2b78101d1f2e18168ecf Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:51:12 +0000 Subject: [PATCH 19/22] fix(snowflake): accept a replica source that leaves out its account --- .../_helpers/ddl/snowflake_create_clone.rs | 11 +++--- .../parser/test_snowflake_create_clone.rs | 38 ++++++++++++++++++- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_clone.rs b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_clone.rs index 97a6dffcb..734bb1915 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_clone.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/_helpers/ddl/snowflake_create_clone.rs @@ -434,12 +434,11 @@ impl Parser { self.expect_token(TokenType::As)?; self.expect_alter_words(&[&["REPLICA", "OF"]])?; let primary = self.parse_object_name()?; - if primary.catalog.is_none() - || primary - .schema - .as_ref() - .is_none_or(|part| part.name.is_empty()) - { + let account_missing = primary + .schema + .as_ref() + .is_none_or(|part| part.name.is_empty()); + if primary.catalog.is_none() || (account_missing && !primary.schema_omitted) { return Err(self.parse_error("Expected ..")); } let mut properties: Vec = Vec::new(); diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_clone.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_clone.rs index a0821e21f..dff8c2409 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_clone.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_create_clone.rs @@ -153,9 +153,33 @@ fn given_a_snowflake_clone_or_replica_when_parsing_then_only_documented_forms_ar expected_accepted: true, }, AcceptanceTestCase { - description: "a replica's source has all three parts", + description: "a replica's source may leave out its account (recorded semantic error)", sql: "CREATE FAILOVER GROUP v1 AS REPLICA OF v1 . . v1 ;", dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a connection replica may leave out its account (recorded unsupported feature)", + sql: "CREATE CONNECTION v1 AS REPLICA OF v1 . . v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "a replica's source needs its organization (recorded syntax error)", + sql: "CREATE REPLICATION GROUP v1 AS REPLICA OF . v1 . v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a replica's source needs its name (recorded syntax error)", + sql: "CREATE FAILOVER GROUP v1 AS REPLICA OF v1 . v1 . ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, + AcceptanceTestCase { + description: "a group replica's source has three parts", + sql: "CREATE REPLICATION GROUP v1 AS REPLICA OF v1 . v1 ;", + dialect: Dialect::Snowflake, expected_accepted: false, }, AcceptanceTestCase { @@ -233,6 +257,18 @@ fn given_a_snowflake_clone_or_replica_when_parsing_then_the_tree_keeps_its_parts pointer: "/0/create_object/operands/0/table/catalog/name", expected_value: json!("o1"), }, + ProjectionTestCase { + description: "a replica's left-out account is null, not an empty name", + sql: "CREATE REPLICATION GROUP g1 AS REPLICA OF o1 . . g2", + pointer: "/0/create_object/operands/0/table/schema", + expected_value: json!(null), + }, + ProjectionTestCase { + description: "a replica records that its account was left out", + sql: "CREATE REPLICATION GROUP g1 AS REPLICA OF o1 . . g2", + pointer: "/0/create_object/operands/0/table/schema_omitted", + expected_value: json!(true), + }, ProjectionTestCase { description: "a failover group replica names its source", sql: "CREATE FAILOVER GROUP g1 AS REPLICA OF o1.a1.g2", From bbe93da70dd6d08a6c0c747386aa02c96338aa2e Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 03:52:37 +0000 Subject: [PATCH 20/22] fix(snowflake): accept UNDROP VOLUME --- .../src/parser/constants.rs | 1 + .../tests/parser/test_snowflake_recorded.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/crates/sqlquack-syntax-snowflake/src/parser/constants.rs b/crates/sqlquack-syntax-snowflake/src/parser/constants.rs index 5a3a1ad60..6e6486f70 100644 --- a/crates/sqlquack-syntax-snowflake/src/parser/constants.rs +++ b/crates/sqlquack-syntax-snowflake/src/parser/constants.rs @@ -1074,6 +1074,7 @@ pub(crate) const SNOWFLAKE_UNDROP_KINDS: &[&str] = &[ "TAG", "TYPE", "VARIABLE", + "VOLUME", ]; /// The UNDROP kinds that take a system-generated id in `IDENTIFIER( )`, as Snowflake's UNDROP pages name them. diff --git a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs index c35a1b8aa..0df8fed1c 100644 --- a/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs +++ b/crates/sqlquack-syntax-snowflake/tests/parser/test_snowflake_recorded.rs @@ -32,6 +32,18 @@ fn given_recorded_snowflake_cases_when_parsing_then_acceptance_matches_snowflake dialect: Dialect::Snowflake, expected_accepted: true, }, + AcceptanceTestCase { + description: "UNDROP takes VOLUME without EXTERNAL (recorded semantic error)", + sql: "UNDROP VOLUME v1 ;", + dialect: Dialect::Snowflake, + expected_accepted: true, + }, + AcceptanceTestCase { + description: "UNDROP EXTERNAL VOLUME needs its name (recorded syntax error)", + sql: "UNDROP EXTERNAL VOLUME ;", + dialect: Dialect::Snowflake, + expected_accepted: false, + }, AcceptanceTestCase { description: "UNDROP DATABASE's IDENTIFIER( ) takes no bare word (recorded errno 1003)", sql: "UNDROP DATABASE IDENTIFIER ( m1 ) ;", From 0643ab3f2bb3cd49706e32dcf15f5057a447252b Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 04:08:20 +0000 Subject: [PATCH 21/22] chore(syntax-coverage): drop the receipt disagreements the organization, replica and UNDROP fixes resolve --- coverage/syntax/receipt-disagreements.toml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/coverage/syntax/receipt-disagreements.toml b/coverage/syntax/receipt-disagreements.toml index 284a30efd..880ee96e9 100644 --- a/coverage/syntax/receipt-disagreements.toml +++ b/coverage/syntax/receipt-disagreements.toml @@ -371,7 +371,6 @@ other_receipts_contradicted = [ "k52f4b25a2004", "kdfe11763c33e", ] -"sf.security_admin.alter-account.set-maintenance-policy-for" = ["kd79fe523c612"] "sf.transaction_session.show-parameters" = ["k087afefdac13"] "sf.utility.execute-inference-job-service" = [ "kd484841cc87f", @@ -380,19 +379,3 @@ other_receipts_contradicted = [ [parsed_cases_refused] "sf.ddl.alter-stage.set.syntax" = ["kd30e04e36906"] -"sf.ddl.undrop-external-volume" = ["kb557e7ff4586"] -"sf.security_admin.alter-organization-user.set" = ["k4614b5f6b6ce"] -"sf.security_admin.alter-organization-user.unset" = ["k996ab4d41cd1"] -"sf.security_admin.create-connection.connection-as-replica-of" = ["kee0d007198dd"] -"sf.security_admin.create-failover-group.as-replica-of" = ["k5e081373c209"] -"sf.security_admin.create-organization-account" = [ - "k839222785fe7", - "kcf702494458a", - "kff1992709592", -] -"sf.security_admin.create-organization-profile.profile" = [ - "k0c40743be9b8", - "kf3cbe549deb2", -] -"sf.security_admin.create-organization-user" = ["kf135125eeecb"] -"sf.security_admin.create-replication-group.as-replica-of" = ["k28cad1926c65"] From f0809d3f464aaf649af3562592a08c9c1fe49067 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 15 Nov 2026 04:19:54 +0000 Subject: [PATCH 22/22] chore(syntax-coverage): record the re-reviews of the forms the fidelity fixes changed --- coverage/syntax/baseline.json | 125 ++++++++++---------- coverage/syntax/reviews/ddl.toml | 109 +++++++++++++++++ coverage/syntax/reviews/functions.toml | 49 ++++++++ coverage/syntax/reviews/security-admin.toml | 112 ++++++++++++++++++ coverage/syntax/reviews/utility.toml | 17 +++ 5 files changed, 351 insertions(+), 61 deletions(-) diff --git a/coverage/syntax/baseline.json b/coverage/syntax/baseline.json index c6a063756..921d2fa47 100644 --- a/coverage/syntax/baseline.json +++ b/coverage/syntax/baseline.json @@ -15,7 +15,7 @@ "catalogue/utility.toml": "55b30dcf06bf8182232a0da6b1da75462ae171e015cc8c9b8b95b36a722b44c5", "sources.toml": "47dbcf41cb7aa0fb2981d2fe9e0c33578a3eb53d80863685f2711c551ff0591e" }, - "parser_tree": "3e89af5774ad48e90b91c6416fca02a0517c4cb7", + "parser_tree": "0cca295bf9de498b61adfbc95c00f2d066d23251", "forms": { "sf.classes.anomaly-detection.commands.create-anomaly-detection": "verified", "sf.classes.anomaly-detection.commands.drop-anomaly-detection": "verified", @@ -230,7 +230,7 @@ "sf.ddl.alter-application.unset-tag": "verified", "sf.ddl.alter-application.upgrade": "verified", "sf.ddl.alter-application.upgrade-using": "verified", - "sf.ddl.alter-application.upgrade-using-version": "unverified", + "sf.ddl.alter-application.upgrade-using-version": "verified", "sf.ddl.alter-artifact-repository.set": "verified", "sf.ddl.alter-artifact-repository.set-tag": "verified", "sf.ddl.alter-artifact-repository.unset-comment-authentication-secret-package-retention": "verified", @@ -249,12 +249,12 @@ "sf.ddl.alter-backup-set.unset-comment": "verified", "sf.ddl.alter-backup-set.unset-tag": "verified", "sf.ddl.alter-compute-pool.set": "unverified", - "sf.ddl.alter-compute-pool.stop-all": "unverified", + "sf.ddl.alter-compute-pool.stop-all": "verified", "sf.ddl.alter-compute-pool.suspend-resume": "verified", "sf.ddl.alter-compute-pool.unset-auto-suspend-secs-auto-resume-placement-group": "verified", "sf.ddl.alter-contact.rename-to": "verified", "sf.ddl.alter-contact.set": "verified", - "sf.ddl.alter-cortex-search.add-scoring-profile": "unverified", + "sf.ddl.alter-cortex-search.add-scoring-profile": "verified", "sf.ddl.alter-cortex-search.drop-scoring-profile": "verified", "sf.ddl.alter-cortex-search.refresh": "verified", "sf.ddl.alter-cortex-search.set": "unverified", @@ -552,7 +552,7 @@ "sf.ddl.alter-streamlit.pull": "verified", "sf.ddl.alter-streamlit.push": "unverified", "sf.ddl.alter-streamlit.rename-to": "verified", - "sf.ddl.alter-streamlit.set": "unverified", + "sf.ddl.alter-streamlit.set": "verified", "sf.ddl.alter-streamlit.set.syntax": "verified", "sf.ddl.alter-streamlit.unset-secrets-external-access-integrations-query-warehouse": "unverified", "sf.ddl.alter-table-column.datagovnpolicytagaction": "unverified", @@ -681,7 +681,7 @@ "sf.ddl.create-data-metric-function.create-data-metric-function": "verified", "sf.ddl.create-data-metric-function.create-data-metric-function.create-or-alter-data-metric-function": "verified", "sf.ddl.create-data-metric-function.define-data-metric-function": "unverified", - "sf.ddl.create-data-movement-rule": "unverified", + "sf.ddl.create-data-movement-rule": "verified", "sf.ddl.create-database-catalog-linked": "unverified", "sf.ddl.create-database.create-database": "unverified", "sf.ddl.create-database.create-database-as-replica": "verified", @@ -777,7 +777,7 @@ "sf.ddl.create-openflow-connector.from": "verified", "sf.ddl.create-openflow-connector.from-definition": "verified", "sf.ddl.create-openflow-deployment": "verified", - "sf.ddl.create-openflow-runtime": "unverified", + "sf.ddl.create-openflow-runtime": "verified", "sf.ddl.create-pipe.create-or-alter-pipe": "verified", "sf.ddl.create-pipe.create-pipe-as": "verified", "sf.ddl.create-pipe.define-pipe": "verified", @@ -805,9 +805,9 @@ "sf.ddl.create-semantic-view.create-or-alter-semantic": "unverified", "sf.ddl.create-semantic-view.create-semantic-view-tables": "unverified", "sf.ddl.create-semantic-view.define-semantic-view": "unverified", - "sf.ddl.create-sequence.create-or-alter-sequence": "unverified", - "sf.ddl.create-sequence.create-sequence": "unverified", - "sf.ddl.create-sequence.define-sequence": "unverified", + "sf.ddl.create-sequence.create-or-alter-sequence": "verified", + "sf.ddl.create-sequence.create-sequence": "verified", + "sf.ddl.create-sequence.define-sequence": "verified", "sf.ddl.create-service": "unverified", "sf.ddl.create-snapshot": "verified", "sf.ddl.create-snapshot-set.database": "verified", @@ -846,7 +846,7 @@ "sf.ddl.create-table.create-or-alter-table": "unverified", "sf.ddl.create-table.create-table": "unverified", "sf.ddl.create-table.create-table-as": "verified", - "sf.ddl.create-table.create-table-clone": "unverified", + "sf.ddl.create-table.create-table-clone": "verified", "sf.ddl.create-table.create-table-from-archive": "verified", "sf.ddl.create-table.create-table-from-backup": "unverified", "sf.ddl.create-table.create-table-like": "verified", @@ -995,7 +995,7 @@ "sf.ddl.drop-view": "verified", "sf.ddl.drop-warehouse": "verified", "sf.ddl.experiment.alter-experiment-abort": "verified", - "sf.ddl.experiment.create-experiment": "unverified", + "sf.ddl.experiment.create-experiment": "verified", "sf.ddl.experiment.describe-desc-experiment": "verified", "sf.ddl.experiment.drop-experiment": "verified", "sf.ddl.experiment.execute-experiment": "verified", @@ -1143,7 +1143,7 @@ "sf.ddl.truncate-materialized-view": "unverified", "sf.ddl.undrop-database": "verified", "sf.ddl.undrop-dynamic-table": "verified", - "sf.ddl.undrop-external-volume": "unverified", + "sf.ddl.undrop-external-volume": "verified", "sf.ddl.undrop-iceberg-table": "verified", "sf.ddl.undrop-notebook": "verified", "sf.ddl.undrop-schema": "unverified", @@ -1272,7 +1272,7 @@ "sf.expressions.sql-format-models": "absent", "sf.expressions.virtual-columns.alter-table-add-as": "absent", "sf.expressions.virtual-columns.create-or-replace-table": "absent", - "sf.functions.abs": "unverified", + "sf.functions.abs": "verified", "sf.functions.accumulate": "unverified", "sf.functions.acos": "verified", "sf.functions.acosh": "verified", @@ -1449,7 +1449,7 @@ "sf.functions.check-xml.check-xml-str": "verified", "sf.functions.chr": "verified", "sf.functions.classify-text-snowflake-cortex": "verified", - "sf.functions.coalesce": "unverified", + "sf.functions.coalesce": "verified", "sf.functions.collate.collate": "verified", "sf.functions.collate.collate.syntax": "verified", "sf.functions.collation": "verified", @@ -1551,8 +1551,8 @@ "sf.functions.deregister-extension": "verified", "sf.functions.div0": "verified", "sf.functions.div0null": "verified", - "sf.functions.dmf-accepted-values": "unverified", - "sf.functions.dmf-accepted-values-percent": "unverified", + "sf.functions.dmf-accepted-values": "verified", + "sf.functions.dmf-accepted-values-percent": "verified", "sf.functions.dmf-accuracy": "verified", "sf.functions.dmf-all-space-count": "verified", "sf.functions.dmf-all-space-percent": "verified", @@ -1703,7 +1703,7 @@ "sf.functions.dmf-outlier-zscore-count": "verified", "sf.functions.dmf-outlier-zscore-percent": "verified", "sf.functions.dmf-r-squared": "verified", - "sf.functions.dmf-referential-integrity-count.table": "unverified", + "sf.functions.dmf-referential-integrity-count.table": "verified", "sf.functions.dmf-referential-integrity-count.table.syntax": "unverified", "sf.functions.dmf-rmse": "verified", "sf.functions.dmf-row-count-difference": "verified", @@ -1719,7 +1719,7 @@ "sf.functions.dmf-rows-in-past-year-percent": "verified", "sf.functions.dmf-seconds-since-row-count-change": "verified", "sf.functions.dmf-skew": "verified", - "sf.functions.dmf-source-target-lag.alter-table-add-data": "unverified", + "sf.functions.dmf-source-target-lag.alter-table-add-data": "verified", "sf.functions.dmf-source-target-lag.snowflake-core-source-target-lag": "verified", "sf.functions.dmf-special-character-count": "verified", "sf.functions.dmf-special-character-percent": "verified", @@ -2806,7 +2806,7 @@ "sf.security_admin.alter-account.remove-organization-user-group": "unverified", "sf.security_admin.alter-account.rename-to": "verified", "sf.security_admin.alter-account.set": "unverified", - "sf.security_admin.alter-account.set-authentication-session-policy": "unverified", + "sf.security_admin.alter-account.set-authentication-session-policy": "verified", "sf.security_admin.alter-account.set-contact": "verified", "sf.security_admin.alter-account.set-data-movement-policy": "verified", "sf.security_admin.alter-account.set-domain-names": "verified", @@ -2821,7 +2821,7 @@ "sf.security_admin.alter-account.set-tag": "verified", "sf.security_admin.alter-account.set-tenant-type-internal-external": "verified", "sf.security_admin.alter-account.unset": "verified", - "sf.security_admin.alter-account.unset-authentication-session-policy": "unverified", + "sf.security_admin.alter-account.unset-authentication-session-policy": "verified", "sf.security_admin.alter-account.unset-contact": "verified", "sf.security_admin.alter-account.unset-data-movement-policy": "verified", "sf.security_admin.alter-account.unset-domain-names": "verified", @@ -2976,7 +2976,7 @@ "sf.security_admin.alter-organization-account.unset-tag": "unverified", "sf.security_admin.alter-organization-profile.abort": "verified", "sf.security_admin.alter-organization-profile.add-live-version-from": "verified", - "sf.security_admin.alter-organization-profile.add-version-from": "unverified", + "sf.security_admin.alter-organization-profile.add-version-from": "verified", "sf.security_admin.alter-organization-profile.as": "unverified", "sf.security_admin.alter-organization-profile.commit": "verified", "sf.security_admin.alter-organization-profile.publish": "unverified", @@ -2987,8 +2987,8 @@ "sf.security_admin.alter-organization-user-group.remove-organization-users": "verified", "sf.security_admin.alter-organization-user-group.set-is-grantable-true-false": "verified", "sf.security_admin.alter-organization-user-group.set-visibility-all-accounts": "verified", - "sf.security_admin.alter-organization-user.set": "unverified", - "sf.security_admin.alter-organization-user.unset": "unverified", + "sf.security_admin.alter-organization-user.set": "verified", + "sf.security_admin.alter-organization-user.unset": "verified", "sf.security_admin.alter-organization.set-default-tenant-type-internal-external": "verified", "sf.security_admin.alter-organization.set-domain-names": "verified", "sf.security_admin.alter-organization.unset-domain-names": "verified", @@ -3122,7 +3122,7 @@ "sf.security_admin.alter-user-modify-key-pair.unset": "verified", "sf.security_admin.alter-user-modify-programmatic-access-token.rename-to": "verified", "sf.security_admin.alter-user-modify-programmatic-access-token.set": "verified", - "sf.security_admin.alter-user-modify-programmatic-access-token.unset": "unverified", + "sf.security_admin.alter-user-modify-programmatic-access-token.unset": "verified", "sf.security_admin.alter-user-modify-workload-identity.rename-to": "verified", "sf.security_admin.alter-user-modify-workload-identity.set": "verified", "sf.security_admin.alter-user-modify-workload-identity.unset": "unverified", @@ -3140,7 +3140,7 @@ "sf.security_admin.alter-user.remove-delegated-authorizations-of": "verified", "sf.security_admin.alter-user.rename-to": "unverified", "sf.security_admin.alter-user.reset-password": "verified", - "sf.security_admin.alter-user.set": "unverified", + "sf.security_admin.alter-user.set": "verified", "sf.security_admin.alter-user.set-authentication-password-session": "unverified", "sf.security_admin.alter-user.set-tag": "verified", "sf.security_admin.alter-user.unset": "verified", @@ -3179,7 +3179,7 @@ "sf.security_admin.create-database-role.create-or-alter-database": "verified", "sf.security_admin.create-database-role.define-database-role": "unverified", "sf.security_admin.create-external-access-integration": "verified", - "sf.security_admin.create-failover-group.as-replica-of": "unverified", + "sf.security_admin.create-failover-group.as-replica-of": "verified", "sf.security_admin.create-failover-group.object-types-allowed-accounts": "unverified", "sf.security_admin.create-feature-policy": "verified", "sf.security_admin.create-ingress-policy": "verified", @@ -3204,20 +3204,20 @@ "sf.security_admin.create-notification-integration-queue-outbound-azure": "verified", "sf.security_admin.create-notification-integration-queue-outbound-gcp": "verified", "sf.security_admin.create-notification-integration-webhooks": "verified", - "sf.security_admin.create-organization-account": "unverified", + "sf.security_admin.create-organization-account": "verified", "sf.security_admin.create-organization-listing.as": "verified", "sf.security_admin.create-organization-listing.from": "verified", - "sf.security_admin.create-organization-profile.profile": "unverified", + "sf.security_admin.create-organization-profile.profile": "verified", "sf.security_admin.create-organization-profile.profile-as": "unverified", "sf.security_admin.create-organization-profile.profile-from": "verified", - "sf.security_admin.create-organization-user": "unverified", + "sf.security_admin.create-organization-user": "verified", "sf.security_admin.create-organization-user-group": "verified", "sf.security_admin.create-packages-policy": "verified", "sf.security_admin.create-password-policy": "verified", "sf.security_admin.create-privacy-policy": "verified", "sf.security_admin.create-projection-policy": "verified", - "sf.security_admin.create-replication-group.as-replica-of": "unverified", - "sf.security_admin.create-replication-group.object-types-allowed-accounts": "unverified", + "sf.security_admin.create-replication-group.as-replica-of": "verified", + "sf.security_admin.create-replication-group.object-types-allowed-accounts": "verified", "sf.security_admin.create-resource-monitor": "unverified", "sf.security_admin.create-role.create-or-alter-role": "verified", "sf.security_admin.create-role.create-role": "verified", @@ -53183,6 +53183,7 @@ "sf.ddl.undrop-external-volume.o002", "sf.ddl.undrop-external-volume.o003", "sf.ddl.undrop-external-volume.o004", + "sf.ddl.undrop-external-volume.o005", "sf.ddl.undrop-external-volume.o006", "sf.ddl.undrop-external-volume.o007", "sf.ddl.undrop-external-volume.o008", @@ -56691,6 +56692,7 @@ "sf.functions.abs.o005", "sf.functions.abs.o006", "sf.functions.abs.o007", + "sf.functions.abs.o008", "sf.functions.accumulate.o002", "sf.functions.accumulate.o004", "sf.functions.accumulate.o006", @@ -58456,6 +58458,7 @@ "sf.functions.coalesce.o008", "sf.functions.coalesce.o009", "sf.functions.coalesce.o010", + "sf.functions.coalesce.o011", "sf.functions.coalesce.o012", "sf.functions.coalesce.o013", "sf.functions.collate.collate.o001", @@ -73376,6 +73379,7 @@ "sf.security_admin.alter-account.set-maintenance-policy-for.o002", "sf.security_admin.alter-account.set-maintenance-policy-for.o003", "sf.security_admin.alter-account.set-maintenance-policy-for.o004", + "sf.security_admin.alter-account.set-maintenance-policy-for.o005", "sf.security_admin.alter-account.set-maintenance-policy-for.o006", "sf.security_admin.alter-account.set-maintenance-policy-for.o007", "sf.security_admin.alter-account.set-maintenance-policy-for.o008", @@ -76786,6 +76790,7 @@ "sf.security_admin.alter-organization-user.set.o040", "sf.security_admin.alter-organization-user.set.o041", "sf.security_admin.alter-organization-user.set.o042", + "sf.security_admin.alter-organization-user.set.o044", "sf.security_admin.alter-organization-user.set.o045", "sf.security_admin.alter-organization-user.set.o046", "sf.security_admin.alter-organization-user.set.o047", @@ -76798,6 +76803,7 @@ "sf.security_admin.alter-organization-user.unset.o002", "sf.security_admin.alter-organization-user.unset.o003", "sf.security_admin.alter-organization-user.unset.o004", + "sf.security_admin.alter-organization-user.unset.o042", "sf.security_admin.alter-organization-user.unset.o043", "sf.security_admin.alter-organization-user.unset.o045", "sf.security_admin.alter-organization-user.unset.o046", @@ -83199,6 +83205,7 @@ "sf.security_admin.create-connection.connection-as-replica-of.o007", "sf.security_admin.create-connection.connection-as-replica-of.o008", "sf.security_admin.create-connection.connection-as-replica-of.o009", + "sf.security_admin.create-connection.connection-as-replica-of.o010", "sf.security_admin.create-connection.connection-as-replica-of.o011", "sf.security_admin.create-connection.connection-as-replica-of.o012", "sf.security_admin.create-connection.connection-as-replica-of.o013", @@ -83403,6 +83410,7 @@ "sf.security_admin.create-failover-group.as-replica-of.o007", "sf.security_admin.create-failover-group.as-replica-of.o008", "sf.security_admin.create-failover-group.as-replica-of.o009", + "sf.security_admin.create-failover-group.as-replica-of.o010", "sf.security_admin.create-failover-group.as-replica-of.o011", "sf.security_admin.create-failover-group.as-replica-of.o012", "sf.security_admin.create-failover-group.as-replica-of.o013", @@ -84529,6 +84537,7 @@ "sf.security_admin.create-notification-integration-webhooks.o060", "sf.security_admin.create-notification-integration-webhooks.o061", "sf.security_admin.create-organization-account.o001", + "sf.security_admin.create-organization-account.o002", "sf.security_admin.create-organization-account.o003", "sf.security_admin.create-organization-account.o004", "sf.security_admin.create-organization-account.o005", @@ -84574,6 +84583,8 @@ "sf.security_admin.create-organization-account.o045", "sf.security_admin.create-organization-account.o046", "sf.security_admin.create-organization-account.o047", + "sf.security_admin.create-organization-account.o048", + "sf.security_admin.create-organization-account.o049", "sf.security_admin.create-organization-account.o051", "sf.security_admin.create-organization-account.o052", "sf.security_admin.create-organization-account.o053", @@ -84665,9 +84676,11 @@ "sf.security_admin.create-organization-profile.profile.o001", "sf.security_admin.create-organization-profile.profile.o002", "sf.security_admin.create-organization-profile.profile.o003", + "sf.security_admin.create-organization-profile.profile.o004", "sf.security_admin.create-organization-profile.profile.o005", "sf.security_admin.create-organization-profile.profile.o006", "sf.security_admin.create-organization-profile.profile.o007", + "sf.security_admin.create-organization-profile.profile.o008", "sf.security_admin.create-organization-profile.profile.o009", "sf.security_admin.create-organization-profile.profile.o010", "sf.security_admin.create-organization-profile.profile.o011", @@ -84732,6 +84745,7 @@ "sf.security_admin.create-organization-user.o001", "sf.security_admin.create-organization-user.o002", "sf.security_admin.create-organization-user.o003", + "sf.security_admin.create-organization-user.o004", "sf.security_admin.create-organization-user.o005", "sf.security_admin.create-organization-user.o006", "sf.security_admin.create-organization-user.o007", @@ -84775,6 +84789,7 @@ "sf.security_admin.create-organization-user.o045", "sf.security_admin.create-organization-user.o046", "sf.security_admin.create-organization-user.o047", + "sf.security_admin.create-organization-user.o048", "sf.security_admin.create-organization-user.o049", "sf.security_admin.create-organization-user.o050", "sf.security_admin.create-organization-user.o051", @@ -85067,6 +85082,7 @@ "sf.security_admin.create-replication-group.as-replica-of.o007", "sf.security_admin.create-replication-group.as-replica-of.o008", "sf.security_admin.create-replication-group.as-replica-of.o009", + "sf.security_admin.create-replication-group.as-replica-of.o010", "sf.security_admin.create-replication-group.as-replica-of.o011", "sf.security_admin.create-replication-group.as-replica-of.o012", "sf.security_admin.create-replication-group.as-replica-of.o013", @@ -113274,6 +113290,7 @@ "sf.ddl.undrop-dynamic-table.k2b72e781a9bd", "sf.ddl.undrop-dynamic-table.ka077c76c3131", "sf.ddl.undrop-external-volume.k1c72fbeb15e0", + "sf.ddl.undrop-external-volume.kb557e7ff4586", "sf.ddl.undrop-external-volume.kb77fe649b342", "sf.ddl.undrop-external-volume.ke8393e3618ed", "sf.ddl.undrop-iceberg-table.k01f54a38ec58", @@ -114707,6 +114724,7 @@ "sf.expressions.snowflake-db-classes.kf09e47361402", "sf.expressions.snowflake-db-classes.kf7884278b104", "sf.functions.abs.k2c81ac0cf981", + "sf.functions.abs.k93f76764032f", "sf.functions.abs.k9da3f129e3df", "sf.functions.acos.k01bd50c84d39", "sf.functions.acosh.k642e930348a4", @@ -115079,6 +115097,7 @@ "sf.functions.classify-text-snowflake-cortex.k37dc905636c4", "sf.functions.classify-text-snowflake-cortex.kbad57ff198dc", "sf.functions.coalesce.k119d9c3e7187", + "sf.functions.coalesce.k25882bf706ba", "sf.functions.coalesce.k66abc4034420", "sf.functions.coalesce.ka4ce739e47da", "sf.functions.collate.collate.k9f4919da118f", @@ -120097,6 +120116,7 @@ "sf.security_admin.alter-organization-user-group.set-visibility-all-accounts.kde7c41a3c93d", "sf.security_admin.alter-organization-user-group.set-visibility-all-accounts.kfb24a514c8d4", "sf.security_admin.alter-organization-user.set.k208a549656e7", + "sf.security_admin.alter-organization-user.set.k4614b5f6b6ce", "sf.security_admin.alter-organization-user.set.k4d0ca810d66e", "sf.security_admin.alter-organization-user.set.k5c35f72dfd7b", "sf.security_admin.alter-organization-user.set.k5d12dd267060", @@ -120114,6 +120134,7 @@ "sf.security_admin.alter-organization-user.unset.k4698b9020f70", "sf.security_admin.alter-organization-user.unset.k46a7447b71bd", "sf.security_admin.alter-organization-user.unset.k993043dcfecf", + "sf.security_admin.alter-organization-user.unset.k996ab4d41cd1", "sf.security_admin.alter-organization-user.unset.k9b7a20e662a7", "sf.security_admin.alter-organization-user.unset.kab5bd9bf7dfd", "sf.security_admin.alter-organization-user.unset.kb035f670ea1e", @@ -121966,6 +121987,7 @@ "sf.security_admin.create-connection.connection-as-replica-of.kc33cfcefc61a", "sf.security_admin.create-connection.connection-as-replica-of.kd9485b076672", "sf.security_admin.create-connection.connection-as-replica-of.ke7d6a46981e7", + "sf.security_admin.create-connection.connection-as-replica-of.kee0d007198dd", "sf.security_admin.create-connection.connection.k13a3463a9f4b", "sf.security_admin.create-connection.connection.k83787192dc8c", "sf.security_admin.create-connection.connection.k8ad769b60ace", @@ -122053,6 +122075,7 @@ "sf.security_admin.create-failover-group.as-replica-of.k4788ebe125ee", "sf.security_admin.create-failover-group.as-replica-of.k50ded129e55e", "sf.security_admin.create-failover-group.as-replica-of.k5591ba2fb41a", + "sf.security_admin.create-failover-group.as-replica-of.k5e081373c209", "sf.security_admin.create-failover-group.as-replica-of.kc5f0c572b695", "sf.security_admin.create-failover-group.as-replica-of.ke778f6e49235", "sf.security_admin.create-failover-group.object-types-allowed-accounts.k0da25cbb5fe7", @@ -122390,7 +122413,10 @@ "sf.security_admin.create-organization-account.k6cc79630610d", "sf.security_admin.create-organization-account.k7874d968a227", "sf.security_admin.create-organization-account.k78766b20857a", + "sf.security_admin.create-organization-account.k839222785fe7", "sf.security_admin.create-organization-account.kbfe07d4685ab", + "sf.security_admin.create-organization-account.kcf702494458a", + "sf.security_admin.create-organization-account.kff1992709592", "sf.security_admin.create-organization-listing.as.k0edf866f1e3c", "sf.security_admin.create-organization-listing.as.k7de93b82036b", "sf.security_admin.create-organization-listing.as.kc57d8b5d2353", @@ -122422,10 +122448,12 @@ "sf.security_admin.create-organization-profile.profile-from.kd6a275ea65db", "sf.security_admin.create-organization-profile.profile-from.kde9075a65c07", "sf.security_admin.create-organization-profile.profile-from.kf4175fc8b162", + "sf.security_admin.create-organization-profile.profile.k0c40743be9b8", "sf.security_admin.create-organization-profile.profile.k88b9f8902687", "sf.security_admin.create-organization-profile.profile.ka442895af050", "sf.security_admin.create-organization-profile.profile.kb43d848e38c5", "sf.security_admin.create-organization-profile.profile.kbda58681b938", + "sf.security_admin.create-organization-profile.profile.kf3cbe549deb2", "sf.security_admin.create-organization-user-group.k00f80439b95c", "sf.security_admin.create-organization-user-group.k024ff599bce5", "sf.security_admin.create-organization-user-group.k4318aab765b7", @@ -122447,6 +122475,7 @@ "sf.security_admin.create-organization-user.kb3c237cc130f", "sf.security_admin.create-organization-user.kbc51cd74a1c4", "sf.security_admin.create-organization-user.kf01ae16152cf", + "sf.security_admin.create-organization-user.kf135125eeecb", "sf.security_admin.create-packages-policy.k06b7ddc63f4b", "sf.security_admin.create-packages-policy.k190e27aa911d", "sf.security_admin.create-packages-policy.k4e062169c83a", @@ -122547,6 +122576,7 @@ "sf.security_admin.create-projection-policy.kc9aef1f4312b", "sf.security_admin.create-replication-group.as-replica-of.k03b25bc0a669", "sf.security_admin.create-replication-group.as-replica-of.k2672e8c2258f", + "sf.security_admin.create-replication-group.as-replica-of.k28cad1926c65", "sf.security_admin.create-replication-group.as-replica-of.k3bd263ec38ea", "sf.security_admin.create-replication-group.as-replica-of.k42e066792f56", "sf.security_admin.create-replication-group.as-replica-of.k50021556c9a6", @@ -127225,19 +127255,7 @@ "sf.utility.explain.kf562011f3a6d" ], "refused_cases": [ - "sf.ddl.alter-stage.set.syntax.kd30e04e36906", - "sf.ddl.undrop-external-volume.kb557e7ff4586", - "sf.security_admin.alter-organization-user.set.k4614b5f6b6ce", - "sf.security_admin.alter-organization-user.unset.k996ab4d41cd1", - "sf.security_admin.create-connection.connection-as-replica-of.kee0d007198dd", - "sf.security_admin.create-failover-group.as-replica-of.k5e081373c209", - "sf.security_admin.create-organization-account.k839222785fe7", - "sf.security_admin.create-organization-account.kcf702494458a", - "sf.security_admin.create-organization-account.kff1992709592", - "sf.security_admin.create-organization-profile.profile.k0c40743be9b8", - "sf.security_admin.create-organization-profile.profile.kf3cbe549deb2", - "sf.security_admin.create-organization-user.kf135125eeecb", - "sf.security_admin.create-replication-group.as-replica-of.k28cad1926c65" + "sf.ddl.alter-stage.set.syntax.kd30e04e36906" ], "syntax_error_cases_rejected": [ "b5-p3-04-counter_for_loop_closed_by_end_for", @@ -154312,6 +154330,7 @@ "sf.security_admin.alter-account.set-maintenance-policy-for.k7d68d7442373", "sf.security_admin.alter-account.set-maintenance-policy-for.k8e3d3ccbfe57", "sf.security_admin.alter-account.set-maintenance-policy-for.kacf1e1da2285", + "sf.security_admin.alter-account.set-maintenance-policy-for.kd79fe523c612", "sf.security_admin.alter-account.set-maintenance-policy-for.kea12a80554ef", "sf.security_admin.alter-account.set-packages-password-multi.k06d7b8080b42", "sf.security_admin.alter-account.set-packages-password-multi.k535ea9b891a2", @@ -163690,7 +163709,6 @@ "sf.functions.system-explain-plan-json.k34b4a27a76b5", "sf.functions.to-query.k52f4b25a2004", "sf.functions.to-query.kdfe11763c33e", - "sf.security_admin.alter-account.set-maintenance-policy-for.kd79fe523c612", "sf.transaction_session.show-parameters.k087afefdac13", "sf.utility.execute-inference-job-service.kd484841cc87f", "sf.utility.execute-inference-job-service.kfa70a352f258" @@ -164563,10 +164581,8 @@ "sf.ddl.create-table.create-table.k294b45271663", "sf.ddl.create-table.create-table.k81e6795ae05a", "sf.ddl.drop-iceberg-table.k0c6d88e813a7", - "sf.ddl.undrop-external-volume.kb557e7ff4586", "sf.expressions.snowflake-db-classes.k9eccc2186fa2", "sf.functions.abs.k6169455e23b9", - "sf.functions.abs.k93f76764032f", "sf.functions.acos.k9277749f1514", "sf.functions.acosh.k2bfcf40e4874", "sf.functions.ai-summarize-agg.k96152ee79950", @@ -164603,7 +164619,6 @@ "sf.functions.ceil.kadb89c6ce35e", "sf.functions.check-xml.check-xml.kebab6f35e2a9", "sf.functions.chr.ke91db843fd30", - "sf.functions.coalesce.k25882bf706ba", "sf.functions.collate.collate.syntax.kd9832a5c1f19", "sf.functions.cos.k5ffbc006170e", "sf.functions.cosh.k26b88d9dac0b", @@ -164724,18 +164739,6 @@ "sf.query.operators-query.except.k5a8b5eec3643", "sf.query.select.select.selecting-specific-columns.k2883fa022666", "sf.scripting.begin.k0fb78add2575", - "sf.security_admin.alter-account.set-maintenance-policy-for.kd79fe523c612", - "sf.security_admin.alter-organization-user.set.k4614b5f6b6ce", - "sf.security_admin.alter-organization-user.unset.k996ab4d41cd1", - "sf.security_admin.create-connection.connection-as-replica-of.kee0d007198dd", - "sf.security_admin.create-failover-group.as-replica-of.k5e081373c209", - "sf.security_admin.create-organization-account.k839222785fe7", - "sf.security_admin.create-organization-account.kcf702494458a", - "sf.security_admin.create-organization-account.kff1992709592", - "sf.security_admin.create-organization-profile.profile.k0c40743be9b8", - "sf.security_admin.create-organization-profile.profile.kf3cbe549deb2", - "sf.security_admin.create-organization-user.kf135125eeecb", - "sf.security_admin.create-replication-group.as-replica-of.k28cad1926c65", "sf.security_admin.create-resource-monitor.k1a411efdd4d8", "sf.security_admin.create-resource-monitor.k1da4b22723a7", "sf.security_admin.create-resource-monitor.k1dc94f7a8149", diff --git a/coverage/syntax/reviews/ddl.toml b/coverage/syntax/reviews/ddl.toml index f1511e12e..114c92e03 100644 --- a/coverage/syntax/reviews/ddl.toml +++ b/coverage/syntax/reviews/ddl.toml @@ -4237,3 +4237,112 @@ note = "Approved at its last review with the same definition, obligations and ca "sf.ddl.create-tag.create-tag" = "da99d3b7e73ff860170b43d19e1c172a97300f20efc627c4fbb0b2c786cb3925" "sf.ddl.create-tag.define-tag" = "788f08aa9ca96d9d8f54777d086bdf8f17b0ee629381486171f23b3f82ddb127" "sf.ddl.create-versioned-schema" = "6383231c1aed459dbe09d220aab7116af32a1d4b152ab52cb1427846cd02b166" + +[[review]] +id = "ddl-043" +family = "ddl" +parser_tree = "0cca295bf9de498b61adfbc95c00f2d066d23251" +reviewed_utc = "2026-11-15T04:17:54Z" +verdict = "approved" +forms = [ + "sf.ddl.alter-cortex-search.drop-scoring-profile", + "sf.ddl.alter-data-movement-rule.set-max-rows-as-returns", + "sf.ddl.alter-external-volume.update-storage-location-credentials-aws-key-id", + "sf.ddl.alter-function.set", + "sf.ddl.alter-function.set-headers", + "sf.ddl.alter-iceberg-table-version-tag.drop-version-tag", + "sf.ddl.alter-listing.listing-add-version-from", + "sf.ddl.alter-notebook.set", + "sf.ddl.alter-service.fromspecification-fromspecificationtemplate", + "sf.ddl.alter-stage.set", + "sf.ddl.alter-stage.set-directory-enable-true", + "sf.ddl.create-gateway", + "sf.ddl.create-model.model", + "sf.ddl.create-openflow-connector.from", + "sf.ddl.create-openflow-connector.from-definition", +] +note = "Approved at its last review with the same definition, obligations and cases; the cases whose parse trees changed with the parser at 548fe5bda, which keeps keywords, key-value pairs and omitted name parts in their own fields, were read again under review brief version 1, and every scored case agrees with its Snowflake receipt." + +[review.fingerprints] +"sf.ddl.alter-cortex-search.drop-scoring-profile" = "8e4036eea485f7e8a5d3c17f9d6b02371c99e45b0c12c9642a29618615d1ea87" +"sf.ddl.alter-data-movement-rule.set-max-rows-as-returns" = "c16b657daf0c84d5e7ac9bdf505df9666445e2f35c157572064109486a0e998f" +"sf.ddl.alter-external-volume.update-storage-location-credentials-aws-key-id" = "e655f23ab3bcfa58499bb64287b7da5219cd5c12834e92881f11751af9aa0955" +"sf.ddl.alter-function.set" = "50feca5997a051fe819b69273e3fcd7344729e4ace3df817215bb1d5282208c0" +"sf.ddl.alter-function.set-headers" = "645259d8ff9ac96209f3a82731b9d5a1af0ec596d4d648b5dc0ec54b4a640a70" +"sf.ddl.alter-iceberg-table-version-tag.drop-version-tag" = "97118b84ec912ec4e312bf4a74022e89d5bb033929f37802ff64a1f7f229ad18" +"sf.ddl.alter-listing.listing-add-version-from" = "4ba1e7c871ff282c7935e1a5c98bbb6e2edb6fbb6feec380d69b1ea36e53e874" +"sf.ddl.alter-notebook.set" = "bfbb6bbb29a9b6e6cd75d835e135759ae49a97a512cdc6318761c7cce42a1b5d" +"sf.ddl.alter-service.fromspecification-fromspecificationtemplate" = "7e6d5f258c96121ccde71517ae2aacf8455bd9968c9cbb5b806706e9f2310266" +"sf.ddl.alter-stage.set" = "dcc135b27019640eed6b6f903f7dc7973f6e6d09606dbb5f88ff0a6ef5cafb31" +"sf.ddl.alter-stage.set-directory-enable-true" = "3589bd15fcb85c051168b600e77d46cebabb471fab26dec4735dfd991f1ca45c" +"sf.ddl.create-gateway" = "369343cf2a9f860c71a361be9b5b165560103945e12168809e6adb81a4af91e6" +"sf.ddl.create-model.model" = "f27c46af0becf410b8a98371d52982f3468bd8507deabb7c599c95cf5498c00f" +"sf.ddl.create-openflow-connector.from" = "e7736caa107e7aadb561c79120cf1106f0f41a668d76762351ed1cbf7c436400" +"sf.ddl.create-openflow-connector.from-definition" = "77b1a50729af558fa851ea834ca263964a90e377235fb4a2d8009f0918c8d4be" + +[review.definitions] +"sf.ddl.alter-cortex-search.drop-scoring-profile" = "4f962619d813f2ac79ac64b53dad353b63b901c021266cd77926333cbbaab80e" +"sf.ddl.alter-data-movement-rule.set-max-rows-as-returns" = "8572fe2a90171f3be739044a7b05360259a67c530871a982d1ef44ae5cf7e5db" +"sf.ddl.alter-external-volume.update-storage-location-credentials-aws-key-id" = "c0280728d0f4de8036fe0dccbc6ae78df8db99b0b1e8219ba87ffd8da86d8b49" +"sf.ddl.alter-function.set" = "d714479280ab160fa10a59c8723c6514d8e718295db1d87d27d8b8cd8bf2bd9d" +"sf.ddl.alter-function.set-headers" = "38995b57b10cdd3eb7510950e16fe5484ba90ab156b9a8220f8b76dd51e22b2f" +"sf.ddl.alter-iceberg-table-version-tag.drop-version-tag" = "c0686669412efdf5bc737a60fdd0c81074f11b61f3c52cdd300b08e4f7704187" +"sf.ddl.alter-listing.listing-add-version-from" = "d2b6b9d6850402fbf3cb027e387305cef8a9e1f08f883561b58bdf7cbca3d380" +"sf.ddl.alter-notebook.set" = "faba8e8fc6371ca2cf22841e6e9d64bd521ea114a3128286e0459ed4deb8a9c4" +"sf.ddl.alter-service.fromspecification-fromspecificationtemplate" = "e056da280443d8ffe942530dda82de2c17f85e62e5c8481f057f72e7b122d27a" +"sf.ddl.alter-stage.set" = "4036d1cf417bcef8312e0799e586abd3db1b78b8921662a0dfd1026589239025" +"sf.ddl.alter-stage.set-directory-enable-true" = "df151ec6045eef01ae04d23f0acf73e699613df1040941b2251b47ca318d9311" +"sf.ddl.create-gateway" = "b2449339ed642e417a8051f44099f278e5346544136fe70aa03f74c1bdf93bed" +"sf.ddl.create-model.model" = "6f4b59e3f853f43628a158f218bfc2dcd72176a108ccbab4cae309ae2922e029" +"sf.ddl.create-openflow-connector.from" = "024f89daa1eae75e6d70157aed21ce242bcabf8f2a59944d9443d64356ba7e64" +"sf.ddl.create-openflow-connector.from-definition" = "3d95b8e7437b458b789d4593613d6bedc939bc51e9fcdeed390b03da13663011" + +[[review]] +id = "ddl-044" +family = "ddl" +parser_tree = "0cca295bf9de498b61adfbc95c00f2d066d23251" +reviewed_utc = "2026-11-15T04:17:56Z" +verdict = "approved" +forms = [ + "sf.ddl.alter-application.upgrade-using-version", + "sf.ddl.alter-compute-pool.stop-all", + "sf.ddl.alter-cortex-search.add-scoring-profile", + "sf.ddl.alter-streamlit.set", + "sf.ddl.create-data-movement-rule", + "sf.ddl.create-openflow-runtime", + "sf.ddl.create-sequence.create-or-alter-sequence", + "sf.ddl.create-sequence.create-sequence", + "sf.ddl.create-sequence.define-sequence", + "sf.ddl.create-table.create-table-clone", + "sf.ddl.experiment.create-experiment", + "sf.ddl.undrop-external-volume", +] +note = "Read in full under review brief version 1 against the documentation page, the obligations, the cases and the parse trees of the parser at 548fe5bda; every scored case agrees with its Snowflake receipt." + +[review.fingerprints] +"sf.ddl.alter-application.upgrade-using-version" = "efb0fea29df73f1c00cd6b20f1120023fbd4f7864ee420df15953ab82acffdf1" +"sf.ddl.alter-compute-pool.stop-all" = "0981e9f37a159e94778c32e29c888173342beb95b13defdec93c968c936060be" +"sf.ddl.alter-cortex-search.add-scoring-profile" = "d0b9c6fd6b3fc6292fb82e02b10bb837a477042c66e51d22f3700a46190037ec" +"sf.ddl.alter-streamlit.set" = "054e874b930b0241e73558bcc3b0b6c15f48ad2a038daef6512156feb1760ba3" +"sf.ddl.create-data-movement-rule" = "1a498b7d6b40be26c94c222f87668c39a10d99e8b075fbe16bb3606419997d90" +"sf.ddl.create-openflow-runtime" = "395e75ef8681034fbdcab6b3d25c57389bbe622da1db260df04fdafac99aec30" +"sf.ddl.create-sequence.create-or-alter-sequence" = "ae9c5d2f072229d901f11ca8fc29a239937e8e7f172820cb5b1849a0b9bef8ce" +"sf.ddl.create-sequence.create-sequence" = "9b7e1344c7b84fe501e50f141284462cc3042bdd7d5d7b0414d9bec35b97265d" +"sf.ddl.create-sequence.define-sequence" = "bb1c636d9ac7a56b47613c33dd63900a7c41ed6f117cdbb493fee783025d75af" +"sf.ddl.create-table.create-table-clone" = "582bb6dab3f3b4f4b05af44596b55ef78f3e9f01e70bf2e06e81c8ee1860b9aa" +"sf.ddl.experiment.create-experiment" = "b5ddb6a953d937db4ede0f7ac0aac778cfa7bc617c73a34d86ea61416f7f2797" +"sf.ddl.undrop-external-volume" = "eb8f669ec3eea211aad6cfa7487228e36139672babc4fa1350bf009fd9fcfeaf" + +[review.definitions] +"sf.ddl.alter-application.upgrade-using-version" = "77d8d92fbec092f23770862ceee5146bd7ccf2d38cd4c8242327dedb9a3dd934" +"sf.ddl.alter-compute-pool.stop-all" = "de4cb12516489444f5acdb5537a53ecab9577a18e3bf9b9913bbcc3e28f90ba5" +"sf.ddl.alter-cortex-search.add-scoring-profile" = "bf9c593fe9af17594f55cfdeada19c115f56f17f8e7ab9498f75fe28a2f3fe2e" +"sf.ddl.alter-streamlit.set" = "b1c640342348018c6b7c810dc46bc5b82a382837cb6d739baa6aabe202bcd038" +"sf.ddl.create-data-movement-rule" = "700ab60144013c2a34310036d0165ea00415d401c1695402cd1a1d1f83e4f2b2" +"sf.ddl.create-openflow-runtime" = "43a9931584a407d6ef079c21342d496156277ba62fe920354e8c3d88d205108b" +"sf.ddl.create-sequence.create-or-alter-sequence" = "02d5df7ce84571ef03286dc957f79b9c631f9e769bfcdbfb1763d654cc200b47" +"sf.ddl.create-sequence.create-sequence" = "2140ce342defcdff90c93602f7b8654b64db3819e6d06c24c4af55ecb292ca3c" +"sf.ddl.create-sequence.define-sequence" = "cde1f9045a1afd7322eaa5ac2212747b4a0aaa0a7e22864d9474a12b2cff3c99" +"sf.ddl.create-table.create-table-clone" = "fc141db0872a5504e4750af33aa4b51c07665287ed8fbf0aedac0ec66cd11a50" +"sf.ddl.experiment.create-experiment" = "c45d7e4d3c3690b2d1d97937f8d33d8ef52900d9adabe76c3e64f2ab7c00edc9" +"sf.ddl.undrop-external-volume" = "a9ea5bd96c8fd93cec48dfa8af88a70a050c296b7d54d985426a23cb37644ece" diff --git a/coverage/syntax/reviews/functions.toml b/coverage/syntax/reviews/functions.toml index 6d3844b94..9b2fa7d6e 100644 --- a/coverage/syntax/reviews/functions.toml +++ b/coverage/syntax/reviews/functions.toml @@ -6466,3 +6466,52 @@ note = "Read in full under review brief version 1 against the documentation page "sf.functions.get-ai-observability-events-snowflake-local" = "cbe0d75551bde8512d6d420461141aba335cf5756b894b121e13d60e879a3d7b" "sf.functions.get-ai-observability-logs-snowflake-local" = "2ae530230cdfe96a6c26d9199bae708ddf6f67f504b2499e6fe9ae69278a0de9" "sf.functions.get-ai-record-trace-snowflake-local" = "9540cdeb323e5cb881562f6d0a298154ceeac305c3d0776583d2dc4f9f10ddf7" + +[[review]] +id = "functions-032" +family = "functions" +parser_tree = "0cca295bf9de498b61adfbc95c00f2d066d23251" +reviewed_utc = "2026-11-15T04:17:54Z" +verdict = "approved" +forms = [ + "sf.functions.dmf-row-count-difference", +] +note = "Approved at its last review with the same definition, obligations and cases; the cases whose parse trees changed with the parser at 548fe5bda, which keeps keywords, key-value pairs and omitted name parts in their own fields, were read again under review brief version 1, and every scored case agrees with its Snowflake receipt." + +[review.fingerprints] +"sf.functions.dmf-row-count-difference" = "8e736fdd6a13d8d26bbd52aaa9ebeb01ae2344d1082f58353fca6f2f01e3bd6f" + +[review.definitions] +"sf.functions.dmf-row-count-difference" = "6633e608b8a64fa01efefe1b7f05518779304ec781c22423f740b5e9042a3f90" + +[[review]] +id = "functions-033" +family = "functions" +parser_tree = "0cca295bf9de498b61adfbc95c00f2d066d23251" +reviewed_utc = "2026-11-15T04:17:56Z" +verdict = "approved" +forms = [ + "sf.functions.abs", + "sf.functions.coalesce", + "sf.functions.dmf-accepted-values", + "sf.functions.dmf-accepted-values-percent", + "sf.functions.dmf-referential-integrity-count.table", + "sf.functions.dmf-source-target-lag.alter-table-add-data", +] +note = "Read in full under review brief version 1 against the documentation page, the obligations, the cases and the parse trees of the parser at 548fe5bda; every scored case agrees with its Snowflake receipt." + +[review.fingerprints] +"sf.functions.abs" = "01362a8cbd3887b0b45a5ffd9e737bd58348645afcca38d36ca7230cd829f1cc" +"sf.functions.coalesce" = "89d274afe91c10e650dc71fcd6ab4d570682bfc9864855c619dfbfc25a52f6e6" +"sf.functions.dmf-accepted-values" = "9022763e092c00d8d60ff582713bfb8a6baee88acd5dcf2ebe7a9a929bb1bdfb" +"sf.functions.dmf-accepted-values-percent" = "835afdc605a242a91f6b625ac8ff35a04c74bf1e80fb3c2e0642510a8ab1529c" +"sf.functions.dmf-referential-integrity-count.table" = "ff32a6d4143f4e1f42ce3aa6ffc14608dcd8fbac812f5463258a42be03b8a437" +"sf.functions.dmf-source-target-lag.alter-table-add-data" = "d073ee5f3ef9f2b2a64b1ff2da09aa7d8e5fa0a90cebaa74e6588c0ea1845ea3" + +[review.definitions] +"sf.functions.abs" = "be5235fa19680039066d239814f876e6941c86b4814300a90d19b1612d2c5b4e" +"sf.functions.coalesce" = "ea2354d1459479e4f06f6dfc2067e0cac6f4a6d401289860353f9687ce6d9aed" +"sf.functions.dmf-accepted-values" = "0f43871a9bf53475e95d04f9b58e8a146db0992b0c5c0209a7f5c4e5e77922f1" +"sf.functions.dmf-accepted-values-percent" = "13194a834f982364851a9aedff65d3248cb2ce29343fd8438fb7fdc775261c4c" +"sf.functions.dmf-referential-integrity-count.table" = "47c14d85203c383d248c0eef06e65fd37693ef72606d8105541250e62f1a49f9" +"sf.functions.dmf-source-target-lag.alter-table-add-data" = "820f46969fb8b72bdd6bb35fa792ed4e6a2e81073393417a907c7263befce7e0" diff --git a/coverage/syntax/reviews/security-admin.toml b/coverage/syntax/reviews/security-admin.toml index c2eb69a5a..43c5372be 100644 --- a/coverage/syntax/reviews/security-admin.toml +++ b/coverage/syntax/reviews/security-admin.toml @@ -3173,3 +3173,115 @@ note = "Approved at its last review with the same definition, obligations and ca "sf.security_admin.create-storage-integration-postgres-internal" = "76481f8cc8ceb166d463046715305012b82e4e52875fc69fb8da8b0a38ef38bb" "sf.security_admin.create-storage-lifecycle-policy" = "8665b98dca1048abdea0c35f7990f3f66bc17b725ceca8daee0b251d4ea25610" "sf.security_admin.create-user" = "80b02e9a4f791284926b9a3bed2a6c7e8462272bc8701be2658208175a0aa1b4" + +[[review]] +id = "security-admin-030" +family = "security_admin" +parser_tree = "0cca295bf9de498b61adfbc95c00f2d066d23251" +reviewed_utc = "2026-11-15T04:17:54Z" +verdict = "approved" +forms = [ + "sf.security_admin.alter-account.set-feature-policy-for", + "sf.security_admin.alter-account.set-feature-policy-for-2", + "sf.security_admin.alter-account.set-feature-policy-for.altering-the-current-account", + "sf.security_admin.alter-account.unset-feature-policy-for", + "sf.security_admin.alter-account.unset-feature-policy-for-2", + "sf.security_admin.alter-account.unset-feature-policy-for.altering-the-current-account", + "sf.security_admin.alter-account.unset-maintenance-policy-for", + "sf.security_admin.alter-failover-group.set-object-types-integrations-allowed-integration-types", + "sf.security_admin.alter-organization-profile.add-live-version-from", + "sf.security_admin.alter-replication-group.set-object-types-integrations-allowed-integration-types", + "sf.security_admin.alter-user-add-programmatic-access-token", + "sf.security_admin.alter-user-modify-programmatic-access-token.rename-to", + "sf.security_admin.alter-user-modify-programmatic-access-token.set", + "sf.security_admin.alter-user-remove-programmatic-access-token", + "sf.security_admin.alter-user-rotate-programmatic-access-token", +] +note = "Approved at its last review with the same definition, obligations and cases; the cases whose parse trees changed with the parser at 548fe5bda, which keeps keywords, key-value pairs and omitted name parts in their own fields, were read again under review brief version 1, and every scored case agrees with its Snowflake receipt." + +[review.fingerprints] +"sf.security_admin.alter-account.set-feature-policy-for" = "c66af12641714e4329a402c945b09490323eb4dbc0916fc2411b03961b39645a" +"sf.security_admin.alter-account.set-feature-policy-for-2" = "3f21fa6346c325290a514a711f18363bee2a3a01759daf6c6fd9253a5ea98ca6" +"sf.security_admin.alter-account.set-feature-policy-for.altering-the-current-account" = "4eabf5a08375162b8085b1f9d2331f769d58fac70e9a680b1674487fa697bd29" +"sf.security_admin.alter-account.unset-feature-policy-for" = "e70d7987f85e329c70cf95bf71d97527ce4c935dedd8cf2c4ea0c21b04a67d50" +"sf.security_admin.alter-account.unset-feature-policy-for-2" = "207d4753e0cafd78c62b450980a26bf7bd2f820f8fe3d0179ed8625bca1d3840" +"sf.security_admin.alter-account.unset-feature-policy-for.altering-the-current-account" = "b0c8cc45dd89cfcf3e895cbdfa562a61ac6c5bd40fd553c7bbd5d32d8e89a148" +"sf.security_admin.alter-account.unset-maintenance-policy-for" = "26b53e5e4a93a91bc15ab4234311a65684727f1d8a34dfeb7376582279e0846e" +"sf.security_admin.alter-failover-group.set-object-types-integrations-allowed-integration-types" = "e2e571cfc5d19301fb58d7663a2a050655b2db43d84d8d45a1410e3cd9e354b9" +"sf.security_admin.alter-organization-profile.add-live-version-from" = "cf0b124697416739eac89e097498af26f2c2ec7eacc44f86bf84ab19ee55d5d1" +"sf.security_admin.alter-replication-group.set-object-types-integrations-allowed-integration-types" = "530bb6fb5da8732f184c5cf7b8d06658cd84d6a00a3e8f0e82597d726003fc04" +"sf.security_admin.alter-user-add-programmatic-access-token" = "7e7af9b367a9b37f7a99377c22c3fb5448bc6af773a436c18220bdd2a5d9fea1" +"sf.security_admin.alter-user-modify-programmatic-access-token.rename-to" = "aabff3eec475f43f2243f963dc21fc9c2fdb62cccbd528296f5aecc0640e8306" +"sf.security_admin.alter-user-modify-programmatic-access-token.set" = "caedc806b09eeafb510762ef7cd2b24acf7f42989c19725e54f5b84e74e17b60" +"sf.security_admin.alter-user-remove-programmatic-access-token" = "d2d3f5be6dcc113a9b08961d39e83a7e83733322fcf3ddbbc4388e86f4a9268d" +"sf.security_admin.alter-user-rotate-programmatic-access-token" = "2fd3b4e05f6c9ae804686878a1c06781f1aad9d46da802ab982dba30552777a2" + +[review.definitions] +"sf.security_admin.alter-account.set-feature-policy-for" = "808878a0491177a939d0fb15b198ec0f7b4e18b5fea92b198516a302bb1a13d4" +"sf.security_admin.alter-account.set-feature-policy-for-2" = "d8a88b1132a3d51f67508a1aee93f1723b5be246293c869cb834dc436a350b75" +"sf.security_admin.alter-account.set-feature-policy-for.altering-the-current-account" = "9c9f6a6753f6e7448346023ed98e3e625c879b1569f6050eb745758e7c3d28bd" +"sf.security_admin.alter-account.unset-feature-policy-for" = "8453d8ca4ab2500dcd916b0f205fbb82210c77303a4de02e1e9712233d42686a" +"sf.security_admin.alter-account.unset-feature-policy-for-2" = "6a81cc0253920b5286cbe6572ed2b6de462800d11e9c6bbd1129838dfdca0f05" +"sf.security_admin.alter-account.unset-feature-policy-for.altering-the-current-account" = "c3ddf7c6507748f340d772a6ef2fb72fe6133cfdeaa805ad6e385d09e8a5206d" +"sf.security_admin.alter-account.unset-maintenance-policy-for" = "994d6a5da1aa135911012915782c5fd1b8e863e4c7a9eb7a4d191c6bd5126e81" +"sf.security_admin.alter-failover-group.set-object-types-integrations-allowed-integration-types" = "a2bba8661f4227b8d26e65c348fde817e9a705b2cbc7e1007057b542259bd0ba" +"sf.security_admin.alter-organization-profile.add-live-version-from" = "6a7749b99110a0a0dd5688adfabc262526d38b0139dcc185b4030560ac44be62" +"sf.security_admin.alter-replication-group.set-object-types-integrations-allowed-integration-types" = "97712b285f83607ad848369368df68b00fe33bdcd124accd1770ea16853aa204" +"sf.security_admin.alter-user-add-programmatic-access-token" = "950416cefc3db0faf416ef071dcc708a696b230d6211053efe7c6f99ba7bd065" +"sf.security_admin.alter-user-modify-programmatic-access-token.rename-to" = "cd64f608ce8631449eeecd44b34e70ea6ac94e692ebc60f8b29ab969b41e41a0" +"sf.security_admin.alter-user-modify-programmatic-access-token.set" = "78968d0e01a9d477ce82b06c5b00d895c96e60b87e61acbd385001dc88108ac1" +"sf.security_admin.alter-user-remove-programmatic-access-token" = "90ef6b5ef46d82197fc786377ba3054f38796488e7d80bf6b7c129af30429e15" +"sf.security_admin.alter-user-rotate-programmatic-access-token" = "76da2ed28f7265dc0b60e5c12693d38733db7233ee5ca49baa29748fa5fa553b" + +[[review]] +id = "security-admin-031" +family = "security_admin" +parser_tree = "0cca295bf9de498b61adfbc95c00f2d066d23251" +reviewed_utc = "2026-11-15T04:17:56Z" +verdict = "approved" +forms = [ + "sf.security_admin.alter-account.set-authentication-session-policy", + "sf.security_admin.alter-account.unset-authentication-session-policy", + "sf.security_admin.alter-organization-profile.add-version-from", + "sf.security_admin.alter-organization-user.set", + "sf.security_admin.alter-organization-user.unset", + "sf.security_admin.alter-user-modify-programmatic-access-token.unset", + "sf.security_admin.alter-user.set", + "sf.security_admin.create-failover-group.as-replica-of", + "sf.security_admin.create-organization-account", + "sf.security_admin.create-organization-profile.profile", + "sf.security_admin.create-organization-user", + "sf.security_admin.create-replication-group.as-replica-of", + "sf.security_admin.create-replication-group.object-types-allowed-accounts", +] +note = "Read in full under review brief version 1 against the documentation page, the obligations, the cases and the parse trees of the parser at 548fe5bda; every scored case agrees with its Snowflake receipt." + +[review.fingerprints] +"sf.security_admin.alter-account.set-authentication-session-policy" = "9afef0f505d7e236b51fd7879af660dab85d743901f2dee0cf0d884558759e2e" +"sf.security_admin.alter-account.unset-authentication-session-policy" = "5d6199ed6f6cd8daa78f354e36b6d79eeddb844eb7a8a181981fcf262bc4755a" +"sf.security_admin.alter-organization-profile.add-version-from" = "5ec5717fd4f2a698876d4763464fbb203a7b85ad566bee99d96034f1e5fa97fb" +"sf.security_admin.alter-organization-user.set" = "effaed837c24b49e548b3516175cbfe1334ba4a32053f87f8448a077f8375b78" +"sf.security_admin.alter-organization-user.unset" = "22a56a56fdbc7284dd1c75b195c30b7a92b9baa681793ddbcc4e39295e108d60" +"sf.security_admin.alter-user-modify-programmatic-access-token.unset" = "aa2e6df08ffea6249bd4b73873927547c9369f0b3aeefaf8d4a800398af1ffb4" +"sf.security_admin.alter-user.set" = "2bc598caf966e0f7dcc0bb56793da8d6aa1822b614d298fc15b6146ab261595c" +"sf.security_admin.create-failover-group.as-replica-of" = "55aeef335ea8d187efca3437f877db53e534a96d3a47b9188dd3c125ee3a22bd" +"sf.security_admin.create-organization-account" = "1ebf869d2c93c6418755a676c507c28418128b06168b01b6a76d582c2d9fad73" +"sf.security_admin.create-organization-profile.profile" = "e1f2401f84280d87d855c083104a0b2d8ff1c652021e8a22d578d9c86fba7bc0" +"sf.security_admin.create-organization-user" = "e97550a715f6cd54d369a4a9c34070c6ee8bf523d32cccd4ac3ae3a6b993c0ed" +"sf.security_admin.create-replication-group.as-replica-of" = "3d30d1e38f8d2fd92701f5b2db8a4bf31126a0b29bcf82ffee48a1176f376705" +"sf.security_admin.create-replication-group.object-types-allowed-accounts" = "c387a592ec922f1b86944553898dc080620495c3f8151ea76e2a7502026b5779" + +[review.definitions] +"sf.security_admin.alter-account.set-authentication-session-policy" = "a392562e7ceb3710c8427f52caa13fd367365a95e54d1c211094ae9342fe8e12" +"sf.security_admin.alter-account.unset-authentication-session-policy" = "5c659ec1234be8ffc1deb28c0867706b3b48bfb880920492fca925581bd634d2" +"sf.security_admin.alter-organization-profile.add-version-from" = "2b73fd4e8df4cb80fe575cf1669b45cff2cea5b75a5d2a92551f446bd4e8be67" +"sf.security_admin.alter-organization-user.set" = "48e629da8bbc812ff8fadf5573e0488f9a9734155c7539f2f9c6190903da8905" +"sf.security_admin.alter-organization-user.unset" = "f28349e92cb55a0095ca6dfa340230e7082380216cb4ce425e6c13679958fe4c" +"sf.security_admin.alter-user-modify-programmatic-access-token.unset" = "551eaecaee197460f0834fbb80ce76d4f62f00425ecd9cd674e32dbb08bbff4e" +"sf.security_admin.alter-user.set" = "c43fd1a03cd2d4d09e5a44e2a4063b90c663d01524f0bbe5237d40af2b065965" +"sf.security_admin.create-failover-group.as-replica-of" = "a1fcecb52304ef2690b591c06754e58e926cc1e4d0ff374ec63e543ce0027729" +"sf.security_admin.create-organization-account" = "cc4b2c9b87c5a4116904813c400e26e9f6c3d1732bf8554e15d77c0ee7afbed7" +"sf.security_admin.create-organization-profile.profile" = "bd9a88ac7b956bb7e7f212873ecf2236308d2f12b1af203fe98547351e031922" +"sf.security_admin.create-organization-user" = "66bea5bc0f65e7c955dc9a715de69e79a6986baefb36ead5b6203cdf4f0f6588" +"sf.security_admin.create-replication-group.as-replica-of" = "934cd8d5b644b257cc91e50fef6c48fc8c82a909f69b3a321a90cf8bf4d9d0a0" +"sf.security_admin.create-replication-group.object-types-allowed-accounts" = "04ac21af3aad40eeb81c032996c2f537a470451620b5929494dcec1c036d84e2" diff --git a/coverage/syntax/reviews/utility.toml b/coverage/syntax/reviews/utility.toml index db5d8e11f..682465fc9 100644 --- a/coverage/syntax/reviews/utility.toml +++ b/coverage/syntax/reviews/utility.toml @@ -279,3 +279,20 @@ note = "Read in full under review brief version 1 against the documentation page [review.definitions] "sf.utility.execute-job-service" = "698e7e3c000c21f8956c0bc257ba54e2af95a14b450e96a23ccdf4f61c906605" + +[[review]] +id = "utility-014" +family = "utility" +parser_tree = "0cca295bf9de498b61adfbc95c00f2d066d23251" +reviewed_utc = "2026-11-15T04:17:54Z" +verdict = "approved" +forms = [ + "sf.utility.execute-job-service", +] +note = "Approved at its last review with the same definition, obligations and cases; the cases whose parse trees changed with the parser at 548fe5bda, which keeps keywords, key-value pairs and omitted name parts in their own fields, were read again under review brief version 1, and every scored case agrees with its Snowflake receipt." + +[review.fingerprints] +"sf.utility.execute-job-service" = "b34d7de74b3d1054325ee6bc4931066e932c24a9d341aff337cf1dfcab1413bc" + +[review.definitions] +"sf.utility.execute-job-service" = "698e7e3c000c21f8956c0bc257ba54e2af95a14b450e96a23ccdf4f61c906605"