From ef06ba97676face9af557d8b2de4aad39ad89818 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:24:11 +0000 Subject: [PATCH 1/3] fix(syntax): parse Snowflake ALTER of application, database and service kinds as a typed statement ALTER APPLICATION, APPLICATION PACKAGE, APPLICATION SERVICE, BACKUP SET, CORTEX SEARCH SERVICE, DATABASE, SEMANTIC VIEW and STREAMLIT parsed through the raw ALTER fallback, which accepted malformed statements. They now parse to AlterObject with the kind, IF EXISTS, the action words and its operands and properties, and reject missing operands, string or unquoted reserved names, and half of a multi-word kind (ALTER CORTEX SEARCH, ALTER SEMANTIC, ALTER BACKUP without SET). --- .../src/parser/_helpers/ddl/mod.rs | 1 + .../parser/_helpers/ddl/snowflake_alter.rs | 28 +- .../_helpers/ddl/snowflake_alter_objects.rs | 776 ++++++++++++++++++ .../tests/parser/test_fixes.rs | 179 ++++ 4 files changed, 974 insertions(+), 10 deletions(-) create mode 100644 crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs index d1ad98a35..75201228c 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/mod.rs @@ -8,4 +8,5 @@ pub(crate) mod create_table_forms; pub(crate) mod explain; pub(crate) mod scripting; pub(crate) mod snowflake_alter; +pub(crate) mod snowflake_alter_objects; pub(crate) mod table_options; diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter.rs index 338ba936a..b7fe1f2a2 100644 --- a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter.rs +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter.rs @@ -1,4 +1,5 @@ //! Snowflake ALTER of object kinds with no statement of their own: EXPERIMENT, SNAPSHOT, SNAPSHOT SET, TAG, TASK and TASK GRAPH. +//! The application, backup, Cortex Search, database, semantic view and Streamlit kinds are in `snowflake_alter_objects`. use crate::expressions::names::models::Identifier; use crate::expressions::names::models::TableRef; @@ -14,6 +15,9 @@ use crate::tokens::types::Result; impl Parser { /// Parse a Snowflake ALTER of a kind this module owns, or None when the next words name another kind. pub(crate) fn parse_alter_snowflake_object(&mut self) -> Result> { + if let Some(expr) = self.parse_alter_snowflake_objects()? { + return Ok(Some(expr)); + } if self.check_identifier("TASK") && self.check_next_identifier("GRAPH") && !self @@ -60,20 +64,20 @@ impl Parser { } /// The uppercase text of the token `offset` tokens ahead, if there is one. - fn text_at(&self, offset: usize) -> Option { + pub(crate) fn text_at(&self, offset: usize) -> Option { self.tokens .get(self.current + offset) .map(|token| token.text.to_ascii_uppercase()) } /// Parse `[ IF EXISTS ] ` after the kind. - fn parse_alter_object_head(&mut self) -> Result<(TableRef, bool)> { + pub(crate) fn parse_alter_object_head(&mut self) -> Result<(TableRef, bool)> { let if_exists = self.match_keywords(&[TokenType::If, TokenType::Exists]); Ok((self.parse_object_name()?, if_exists)) } /// Parse an object name: an identifier, never a string or an unquoted reserved keyword. - fn parse_object_name(&mut self) -> Result { + pub(crate) fn parse_object_name(&mut self) -> Result { if self.check(TokenType::String) || self.is_snowflake_reserved_keyword() || !self.is_object_name_start() @@ -91,7 +95,7 @@ impl Parser { } /// Parse a simple name, such as a contact purpose or a property: an identifier, not a reserved keyword. - fn parse_simple_name(&mut self) -> Result { + pub(crate) fn parse_simple_name(&mut self) -> Result { if self.check(TokenType::String) || self.is_snowflake_reserved_keyword() || !self.is_object_name_start() @@ -102,7 +106,7 @@ impl Parser { } /// Parse a property value: a literal, TRUE, FALSE or a name. - fn parse_object_value(&mut self) -> Result { + pub(crate) fn parse_object_value(&mut self) -> Result { if self.check(TokenType::String) || self.check(TokenType::Number) || self.check(TokenType::DollarString) @@ -126,7 +130,7 @@ impl Parser { } /// Parse a string literal value. - fn parse_string_value(&mut self) -> Result { + pub(crate) fn parse_string_value(&mut self) -> Result { if !self.check(TokenType::String) { return Err(self.parse_error("Expected a string")); } @@ -134,7 +138,7 @@ impl Parser { } /// Consume one of `choices`, each a sequence of words, and return its words joined. - fn expect_alter_words(&mut self, choices: &[&[&str]]) -> Result { + pub(crate) fn expect_alter_words(&mut self, choices: &[&[&str]]) -> Result { match choices.iter().find(|words| self.match_text_seq(words)) { Some(words) => Ok(words.join(" ")), None => Err(self.parse_error(format!( @@ -245,7 +249,7 @@ impl Parser { } /// Parse a possibly qualified name, such as a tag name, as one identifier. - fn parse_simple_qualified_name(&mut self) -> Result { + pub(crate) fn parse_simple_qualified_name(&mut self) -> Result { let mut name = self.parse_simple_name()?; while self.match_token(TokenType::Dot) { let part = self.parse_simple_name()?; @@ -496,7 +500,7 @@ impl Parser { } /// An ALTER of `kind` on the named object, with its action, operands and properties. -fn alter_object( +pub(crate) fn alter_object( kind: &str, (name, if_exists): (TableRef, bool), action: String, @@ -513,7 +517,11 @@ fn alter_object( } /// One property of kind `kind`. -fn property(kind: &str, name: Option, value: Option) -> ObjectProperty { +pub(crate) fn property( + kind: &str, + name: Option, + value: Option, +) -> ObjectProperty { ObjectProperty { kind: kind.to_owned(), name, diff --git a/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs new file mode 100644 index 000000000..ac888d7ac --- /dev/null +++ b/crates/sqlquack-syntax/src/parser/_helpers/ddl/snowflake_alter_objects.rs @@ -0,0 +1,776 @@ +//! Snowflake ALTER of APPLICATION, APPLICATION PACKAGE, APPLICATION SERVICE, BACKUP SET, CORTEX SEARCH SERVICE, DATABASE, SEMANTIC VIEW and STREAMLIT. + +use crate::expressions::names::models::Identifier; +use crate::expressions::names::models::TableRef; +use crate::expressions::schema_objects::models::ObjectProperty; +use crate::expressions::tree::models::Expression; +use crate::expressions::types::models::Tuple; +use crate::parser::_helpers::ddl::snowflake_alter::alter_object; +use crate::parser::_helpers::ddl::snowflake_alter::property; +use crate::parser::models::Parser; +use crate::tokens::models::TokenType; +use crate::tokens::types::Result; + +/// The options that may follow a release directive's VERSION and PATCH. +const UPGRADE_OPTIONS: &[&str] = &[ + "UPGRADE_AFTER", + "UPGRADE_IN_MAINTENANCE_WINDOW", + "UPGRADE_DEADLINE", +]; + +type Parts = (Vec, Vec); + +impl Parser { + /// Parse a Snowflake ALTER of a kind this module owns, or None when the next words name another kind. + pub(crate) fn parse_alter_snowflake_objects(&mut self) -> Result> { + if self.match_text_seq(&["APPLICATION", "PACKAGE"]) { + return self.parse_alter_application_package().map(Some); + } + if self.match_text_seq(&["APPLICATION", "SERVICE"]) { + return self.parse_alter_application_service().map(Some); + } + if self.match_text_seq(&["APPLICATION"]) { + return self.parse_alter_application().map(Some); + } + if self.match_text_seq(&["BACKUP", "SET"]) { + return self.parse_alter_backup_set().map(Some); + } + if self.match_text_seq(&["CORTEX", "SEARCH", "SERVICE"]) { + return self.parse_alter_cortex_search_service().map(Some); + } + if self.match_token(TokenType::Database) { + return self.parse_alter_database().map(Some); + } + if self.match_text_seq(&["SEMANTIC", "VIEW"]) { + return self.parse_alter_semantic_view().map(Some); + } + if self.match_text_seq(&["STREAMLIT"]) { + return self.parse_alter_streamlit().map(Some); + } + if self.check_text_seq(&["CORTEX"]) + || self.check_text_seq(&["SEMANTIC"]) + || self.check_text_seq(&["BACKUP"]) && !self.check_text_seq(&["BACKUP", "POLICY"]) + { + return Err(self.parse_error(format!( + "Snowflake has no ALTER {} {}", + self.peek_text().to_ascii_uppercase(), + self.text_at(1).unwrap_or_default() + ))); + } + Ok(None) + } + + /// Parse an object name as an operand. + fn object_operand(&mut self) -> Result { + Ok(Expression::Table(Box::new(self.parse_object_name()?))) + } + + /// Parse a comma-separated list of object names as one operand. + fn object_list_operand(&mut self) -> Result { + let mut names = vec![self.object_operand()?]; + while self.match_token(TokenType::Comma) { + names.push(self.object_operand()?); + } + Ok(tuple(names)) + } + + /// Parse a comma-separated list of account identifiers, each `[ . ] `. + fn account_list_operand(&mut self) -> Result { + let mut accounts = vec![Expression::Identifier(self.parse_simple_qualified_name()?)]; + while self.match_token(TokenType::Comma) { + accounts.push(Expression::Identifier(self.parse_simple_qualified_name()?)); + } + Ok(tuple(accounts)) + } + + /// Parse a parenthesized list of account identifiers. + fn parse_account_list(&mut self) -> Result { + self.expect_token(TokenType::LParen)?; + let list = self.account_list_operand()?; + self.expect_token(TokenType::RParen)?; + Ok(list) + } + + /// Parse an integer literal. + fn number_operand(&mut self) -> Result { + if !self.check(TokenType::Number) { + return Err(self.parse_error("Expected a number")); + } + self.parse_primary() + } + + /// Parse `RENAME TO ` or `SWAP WITH ` after its first word. + fn parse_alter_target(&mut self, second: &str) -> Result { + self.expect_alter_words(&[&[second]])?; + Ok((vec![self.object_operand()?], vec![])) + } + + /// Parse a property value: a parenthesized list, NULL, a literal or a name. + fn parse_alter_value(&mut self) -> Result { + if self.match_token(TokenType::LParen) { + let mut items = Vec::new(); + loop { + let item = self.parse_object_value()?; + if self.match_token(TokenType::Eq) { + items.push(tuple(vec![item, self.parse_object_value()?])); + } else { + items.push(item); + } + if !self.match_token(TokenType::Comma) { + break; + } + } + self.expect_token(TokenType::RParen)?; + return Ok(tuple(items)); + } + if self.check(TokenType::Null) { + return self.parse_primary(); + } + self.parse_object_value() + } + + /// Parse a parenthesized list of string literals. + fn parse_string_list(&mut self) -> Result { + self.expect_token(TokenType::LParen)?; + let mut items = vec![self.parse_string_value()?]; + while self.match_token(TokenType::Comma) { + items.push(self.parse_string_value()?); + } + self.expect_token(TokenType::RParen)?; + Ok(tuple(items)) + } + + /// Parse a parenthesized list of object names. + fn parse_name_list(&mut self) -> Result { + self.expect_token(TokenType::LParen)?; + let list = self.object_list_operand()?; + self.expect_token(TokenType::RParen)?; + Ok(list) + } + + /// Parse ` = ` for each name of `names` that comes next, in any order. + fn parse_named_options(&mut self, names: &[&str]) -> Result> { + let mut properties = Vec::new(); + while let Some(name) = names + .iter() + .find(|name| self.check_text_seq(&[name]) && self.check_next(TokenType::Eq)) + { + self.skip(); + self.skip(); + properties.push(property(name, None, Some(self.parse_alter_value()?))); + } + Ok(properties) + } + + /// Parse ` = ` for one required name. + fn parse_required_option(&mut self, name: &str) -> Result { + self.expect_alter_words(&[&[name]])?; + self.expect_token(TokenType::Eq)?; + Ok(property(name, None, Some(self.parse_alter_value()?))) + } + + /// Parse the properties of a SET (`set`) or UNSET: TAG and CONTACT lists, PRIMARY KEY, or named properties. + fn parse_alter_settings(&mut self, set: bool) -> Result> { + let mut properties: Vec = Vec::new(); + loop { + let named = self.check_next(TokenType::Eq) || self.check_next(TokenType::Comma) || { + let next = self.current + 1; + next >= self.tokens.len() || self.tokens[next].token_type == TokenType::Semicolon + }; + if !named && (self.check_text_seq(&["TAG"]) || self.check_text_seq(&["CONTACT"])) { + let kind = self.peek_text().to_ascii_uppercase(); + self.skip(); + loop { + let name = if kind == "TAG" { + self.parse_simple_qualified_name()? + } else { + self.parse_simple_name()? + }; + let value = if set { + self.expect_token(TokenType::Eq)?; + Some(self.parse_object_value()?) + } else { + None + }; + properties.push(property(&kind, Some(name), value)); + if !self.match_token(TokenType::Comma) { + break; + } + } + } else if self.match_text_seq(&["PRIMARY", "KEY"]) { + let value = if set { + self.expect_token(TokenType::Eq)?; + Some(self.parse_name_list()?) + } else { + None + }; + properties.push(property("PRIMARY KEY", None, value)); + } else { + let name = self.parse_simple_name()?; + let value = if set || self.check(TokenType::Eq) { + self.expect_token(TokenType::Eq)?; + Some(self.parse_alter_value()?) + } else { + None + }; + properties.push(property(&name.name.to_ascii_uppercase(), None, value)); + } + let comma = self.match_token(TokenType::Comma); + if self.is_at_end() || self.check(TokenType::Semicolon) || !set && !comma { + return Ok(properties); + } + } + } + + /// Parse `SET ...` or `UNSET ...` after the word. + fn parse_alter_set(&mut self, set: bool) -> Result { + Ok((vec![], self.parse_alter_settings(set)?)) + } + + /// Parse ` [ FORCE ]` after `{ FEATURE | MAINTENANCE | BACKUP } POLICY`. + fn parse_alter_policy(&mut self, action: &mut String) -> Result { + let policy = self.object_operand()?; + if self.match_text_seq(&["FORCE"]) { + action.push_str(" FORCE"); + } + Ok((vec![policy], vec![])) + } + + /// Parse ALTER DATABASE after its kind. + fn parse_alter_database(&mut self) -> Result { + let head = self.parse_alter_object_head()?; + let mut action = self.expect_alter_words(&[ + &["RENAME"], + &["SWAP"], + &["RESUME", "DISCOVERY"], + &["SUSPEND", "DISCOVERY"], + &["REFRESH"], + &["PRIMARY"], + &["ENABLE", "REPLICATION"], + &["ENABLE", "FAILOVER"], + &["DISABLE", "REPLICATION"], + &["DISABLE", "FAILOVER"], + &["SET", "FEATURE", "POLICY"], + &["UNSET", "FEATURE", "POLICY"], + &["UPDATE", "LINKED_CATALOG"], + &["SET"], + &["UNSET"], + ])?; + let parts = match action.as_str() { + "RENAME" => self.parse_alter_target("TO")?, + "SWAP" => self.parse_alter_target("WITH")?, + "SET FEATURE POLICY" => self.parse_alter_policy(&mut action)?, + "SET" => self.parse_alter_set(true)?, + "UNSET" => self.parse_alter_set(false)?, + "UPDATE LINKED_CATALOG" => self.parse_linked_catalog_update(&mut action)?, + _ if action.starts_with("ENABLE") || action.starts_with("DISABLE") => { + let mut operands = Vec::new(); + if action.starts_with("ENABLE") || self.check_text_seq(&["TO"]) { + self.expect_alter_words(&[&["TO", "ACCOUNTS"]])?; + operands.push(self.account_list_operand()?); + } + if action == "ENABLE REPLICATION" + && self.match_text_seq(&["IGNORE", "EDITION", "CHECK"]) + { + action.push_str(" IGNORE EDITION CHECK"); + } + (operands, vec![]) + } + _ => (vec![], vec![]), + }; + Ok(alter_object("DATABASE", head, action, parts)) + } + + /// Parse the change after `UPDATE LINKED_CATALOG`. + fn parse_linked_catalog_update(&mut self, action: &mut String) -> Result { + let change = self.expect_alter_words(&[&["ADD"], &["REMOVE"], &["SET"], &["UNSET"]])?; + action.push(' '); + action.push_str(&change); + let namespaces: &[&[&str]] = &[&["ALLOWED_NAMESPACES"], &["BLOCKED_NAMESPACES"]]; + match change.as_str() { + "ADD" | "REMOVE" => { + let list = self.parse_string_list()?; + let word = if change == "ADD" { "TO" } else { "FROM" }; + self.expect_alter_words(&[&[word]])?; + let target = self.expect_alter_words(namespaces)?; + action.push_str(&format!(" {word} {target}")); + Ok((vec![list], vec![])) + } + "SET" => { + let name = self.expect_alter_words(&[ + &["ALLOWED_WRITE_OPERATIONS"], + &["SYNC_INTERVAL_SECONDS"], + ])?; + self.expect_token(TokenType::Eq)?; + let value = if name == "SYNC_INTERVAL_SECONDS" { + self.parse_object_value()? + } else { + let mode = self.expect_alter_words(&[&["NONE"], &["ALL"]])?; + Expression::Identifier(Identifier::new(mode)) + }; + Ok((vec![], vec![property(&name, None, Some(value))])) + } + _ => { + let target = self.expect_alter_words(namespaces)?; + action.push_str(&format!(" {target}")); + Ok((vec![], vec![])) + } + } + } + + /// Parse ALTER APPLICATION after its kind. + fn parse_alter_application(&mut self) -> Result { + if let Some(action) = [ + ["SET", "SPECIFICATION"].as_slice(), + &["DROP", "SPECIFICATION"], + &["SET", "CONFIGURATION", "DEFINITION"], + &["DROP", "CONFIGURATION", "DEFINITION"], + ] + .into_iter() + .find(|words| self.check_text_seq(words)) + { + self.current += action.len(); + let name = self.object_operand()?; + let properties = if action[0] == "SET" { + self.parse_alter_settings(true)? + } else { + vec![] + }; + let head = (TableRef::new(""), false); + return Ok(alter_object( + "APPLICATION", + head, + action.join(" "), + (vec![name], properties), + )); + } + let head = self.parse_alter_object_head()?; + let mut action = self.expect_alter_words(&[ + &["RENAME"], + &["SET", "FEATURE", "POLICY"], + &["SET", "MAINTENANCE", "POLICY"], + &["SET", "SHARED", "TELEMETRY", "EVENTS"], + &["SET", "CONFIGURATION"], + &["UNSET", "FEATURE", "POLICY"], + &["UNSET", "MAINTENANCE", "POLICY"], + &["UNSET", "REFERENCES"], + &["UNSET", "CONFIGURATION"], + &["SET"], + &["UNSET"], + &["UPGRADE"], + &["APPROVE", "SPECIFICATION"], + &["DECLINE", "SPECIFICATION"], + ])?; + let parts = match action.as_str() { + "RENAME" => self.parse_alter_target("TO")?, + "SET FEATURE POLICY" | "SET MAINTENANCE POLICY" => { + self.parse_alter_policy(&mut action)? + } + "SET SHARED TELEMETRY EVENTS" => (vec![self.parse_string_list()?], vec![]), + "SET CONFIGURATION" => { + let name = self.object_operand()?; + (vec![name], vec![self.parse_required_option("VALUE")?]) + } + "UNSET CONFIGURATION" => (vec![self.object_operand()?], vec![]), + "UNSET REFERENCES" => { + if self.check(TokenType::LParen) { + (vec![self.parse_string_list()?], vec![]) + } else { + (vec![], vec![]) + } + } + "SET" => self.parse_alter_set(true)?, + "UNSET" => self.parse_alter_set(false)?, + "UPGRADE" => { + if self.match_text_seq(&["USING", "VERSION"]) { + action.push_str(" USING VERSION"); + let mut operands = vec![self.object_operand()?]; + if self.match_text_seq(&["PATCH"]) { + action.push_str(" PATCH"); + operands.push(self.number_operand()?); + } + (operands, vec![]) + } else if self.match_text_seq(&["USING"]) { + action.push_str(" USING"); + (vec![self.parse_string_value()?], vec![]) + } else { + (vec![], vec![]) + } + } + "APPROVE SPECIFICATION" | "DECLINE SPECIFICATION" => { + let name = self.object_operand()?; + ( + vec![name], + vec![self.parse_required_option("SEQUENCE_NUMBER")?], + ) + } + _ => (vec![], vec![]), + }; + Ok(alter_object("APPLICATION", head, action, parts)) + } + + /// Parse ALTER APPLICATION PACKAGE after its kind. + fn parse_alter_application_package(&mut self) -> Result { + let head = self.parse_alter_object_head()?; + let mut action = self.expect_alter_words(&[ + &["RENAME"], + &["ADD", "VERSION"], + &["REGISTER", "VERSION"], + &["DEREGISTER", "VERSION"], + &["DROP", "VERSION"], + &["ADD", "PATCH"], + &["MODIFY", "RELEASE", "CHANNEL"], + &["MODIFY", "RELEASE", "DIRECTIVE"], + &["SET", "DEFAULT", "RELEASE", "DIRECTIVE"], + &["SET", "RELEASE", "DIRECTIVE"], + &["UNSET", "RELEASE", "DIRECTIVE"], + &["SET"], + &["UNSET"], + ])?; + let parts = match action.as_str() { + "RENAME" => self.parse_alter_target("TO")?, + "ADD VERSION" | "REGISTER VERSION" => { + let mut operands = Vec::new(); + if !self.check_text_seq(&["USING"]) { + operands.push(self.object_operand()?); + } + self.parse_version_source(operands)? + } + "ADD PATCH" => { + let mut operands = Vec::new(); + if self.check(TokenType::Number) { + operands.push(self.number_operand()?); + } + self.expect_alter_words(&[&["FOR", "VERSION"]])?; + if !self.check_text_seq(&["USING"]) { + operands.push(self.object_operand()?); + } + self.parse_version_source(operands)? + } + "DEREGISTER VERSION" | "DROP VERSION" => (vec![self.object_operand()?], vec![]), + "MODIFY RELEASE CHANNEL" => { + let channel = self.object_operand()?; + let change = self.expect_alter_words(&[ + &["ADD", "VERSION"], + &["DROP", "VERSION"], + &["ADD", "ACCOUNTS"], + &["REMOVE", "ACCOUNTS"], + &["SET", "ACCOUNTS"], + &["MODIFY", "RELEASE", "DIRECTIVE"], + &["SET", "DEFAULT", "RELEASE", "DIRECTIVE"], + &["SET", "RELEASE", "DIRECTIVE"], + &["UNSET", "RELEASE", "DIRECTIVE"], + ])?; + action.push_str(&format!(" {change}")); + let (mut operands, properties) = match change.as_str() { + "ADD VERSION" | "DROP VERSION" => (vec![self.object_operand()?], vec![]), + "ADD ACCOUNTS" | "REMOVE ACCOUNTS" | "SET ACCOUNTS" => { + self.expect_token(TokenType::Eq)?; + (vec![self.parse_account_list()?], vec![]) + } + _ => self.parse_release_directive(&change, &mut action)?, + }; + operands.insert(0, channel); + (operands, properties) + } + _ if action.contains("RELEASE DIRECTIVE") => { + let change = action.clone(); + self.parse_release_directive(&change, &mut action)? + } + "SET" => self.parse_alter_set(true)?, + _ => self.parse_alter_set(false)?, + }; + Ok(alter_object("APPLICATION PACKAGE", head, action, parts)) + } + + /// Parse `USING [ LABEL = '