From d5b2f709f95de829d1202111af8d300cb06ed3b2 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Fri, 13 Nov 2026 17:11:00 +0300 Subject: [PATCH] Support Snowflake ALTER SCHEMA property syntax --- src/ast/ddl.rs | 16 ++ src/ast/spans.rs | 6 +- src/dialect/mod.rs | 9 + src/dialect/snowflake.rs | 4 + src/parser/mod.rs | 70 ++++++ tests/sqlparser_snowflake.rs | 447 +++++++++++++++++++++++++++++++++++ 6 files changed, 551 insertions(+), 1 deletion(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index ffa59e3b0a..c6804b76ca 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3824,6 +3824,16 @@ pub enum AlterSchemaOperation { /// The options to set. options: Vec, }, + /// Set primitive schema properties without an `OPTIONS (...)` wrapper (Snowflake). + SetOptions { + /// Ordered assignments, including duplicates; values are not semantically validated. + options: Vec, + }, + /// Reset schema properties to their defaults (Snowflake). + UnsetOptions { + /// Ordered property names, including duplicates. + options: Vec, + }, /// Rename the schema. Rename { /// The new name for the schema. @@ -3853,6 +3863,12 @@ impl fmt::Display for AlterSchemaOperation { AlterSchemaOperation::SetOptionsParens { options } => { write!(f, "SET OPTIONS ({})", display_comma_separated(options)) } + AlterSchemaOperation::SetOptions { options } => { + write!(f, "SET {}", display_separated(options, " ")) + } + AlterSchemaOperation::UnsetOptions { options } => { + write!(f, "UNSET {}", display_comma_separated(options)) + } AlterSchemaOperation::Rename { name } => write!(f, "RENAME TO {name}"), AlterSchemaOperation::OwnerTo { owner } => write!(f, "OWNER TO {owner}"), } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 83b0fe4a71..4842d17a33 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2483,9 +2483,13 @@ impl Spanned for AlterSchemaOperation { .chain(options.iter().flat_map(|i| i.iter().map(|i| i.span()))), ), AlterSchemaOperation::DropReplica { replica } => replica.span, - AlterSchemaOperation::SetOptionsParens { options } => { + AlterSchemaOperation::SetOptionsParens { options } + | AlterSchemaOperation::SetOptions { options } => { union_spans(options.iter().map(|i| i.span())) } + AlterSchemaOperation::UnsetOptions { options } => { + union_spans(options.iter().map(|i| i.span)) + } AlterSchemaOperation::Rename { name } => name.span(), AlterSchemaOperation::OwnerTo { owner } => { if let Owner::Ident(ident) = owner { diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 87780fd3dd..80f544c3af 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1509,6 +1509,15 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if `ALTER SCHEMA` supports native `SET key = ` + /// assignments separated by whitespace or commas, and `UNSET key, ...`. + /// This does not enable structured properties such as `TAG` or `CONTACT`. + /// + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/alter-schema) + fn supports_alter_schema_options(&self) -> bool { + false + } + /// Returns true if the dialect supports the `USING` clause in an `ALTER COLUMN` statement. /// Example: /// ```sql diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index db9bee85b9..e8a7270656 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -749,6 +749,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_alter_schema_options(&self) -> bool { + true + } + fn supports_comma_separated_drop_column_list(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8c40775e27..69fd740046 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11574,6 +11574,20 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { let owner = self.parse_owner()?; AlterSchemaOperation::OwnerTo { owner } + } else if self.dialect.supports_alter_schema_options() && self.parse_keyword(Keyword::SET) { + let options = self.parse_alter_schema_set_options()?; + AlterSchemaOperation::SetOptions { options } + } else if self.dialect.supports_alter_schema_options() && self.parse_keyword(Keyword::UNSET) + { + let options = self.parse_comma_separated_with_trailing_commas( + Self::parse_alter_schema_option_name, + false, + Self::is_reserved_for_column_alias, + )?; + if !matches!(self.peek_token_ref().token, Token::EOF | Token::SemiColon) { + return self.expected_ref("comma or statement end", self.peek_token_ref()); + } + AlterSchemaOperation::UnsetOptions { options } } else { return self.expected_ref("ALTER SCHEMA operation", self.peek_token_ref()); }; @@ -11584,6 +11598,62 @@ impl<'a> Parser<'a> { })) } + fn parse_alter_schema_option_name(&mut self) -> Result { + if !matches!(self.peek_token_ref().token, Token::Word(_)) + || self + .peek_one_of_keywords(&[Keyword::TAG, Keyword::CONTACT]) + .is_some() + { + return self.expected_ref("primitive schema property name", self.peek_token_ref()); + } + self.parse_identifier() + } + + fn parse_alter_schema_set_options(&mut self) -> Result, ParserError> { + let mut options = Vec::new(); + loop { + let key = self.parse_alter_schema_option_name()?; + self.expect_token(&Token::Eq)?; + let value = match self.peek_token_ref().token { + Token::Plus | Token::Minus + if !matches!(self.peek_nth_token_ref(1).token, Token::Number(..)) => + { + return self.expected_ref("signed numeric literal", self.peek_token_ref()); + } + Token::Number(..) | Token::Plus | Token::Minus => self.parse_number()?, + Token::EOF + | Token::SemiColon + | Token::Placeholder(_) + | Token::Colon + | Token::AtSign => { + return self + .expected_ref("primitive schema property literal", self.peek_token_ref()); + } + _ => Expr::Value(self.parse_value()?), + }; + options.push(SqlOption::KeyValue { key, value }); + + let space_separated = matches!( + self.peek_nth_token_no_skip_ref(0).token, + Token::Whitespace(_) + ); + if self.consume_token(&Token::Comma) { + continue; + } + match self.peek_token_ref().token { + Token::EOF | Token::SemiColon => break, + Token::Word(_) if space_separated => {} + _ => { + return self.expected_ref( + "schema property separator or statement end", + self.peek_token_ref(), + ) + } + } + } + Ok(options) + } + /// Parse a `CALL procedure_name(arg1, arg2, ...)` /// or `CALL procedure_name` statement pub fn parse_call(&mut self) -> Result { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 733e63b847..e6642f29a9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -7088,3 +7088,450 @@ fn snowflake_create_schema_options_expose_literal_expressions_to_visitors() { assert_eq!(result, ControlFlow::Continue(())); assert_eq!(expressions, vec!["'kept'", "7"]); } + +#[test] +fn snowflake_alter_schema_options_set_and_unset() { + for sql in [ + "ALTER SCHEMA demo SET COMMENT = 'description'", + "ALTER SCHEMA IF EXISTS db.demo SET DATA_RETENTION_TIME_IN_DAYS = 7 COMMENT = ''", + "ALTER SCHEMA demo SET COMMENT = NULL DATA_RETENTION_TIME_IN_DAYS = -1 COMMENT = 42", + "ALTER SCHEMA demo SET FUTURE_PROPERTY = false DATA_RETENTION_TIME_IN_DAYS = 'invalid'", + r#"ALTER SCHEMA demo SET "COMMENT" = 'x' "FLAG" = true"#, + "ALTER SCHEMA demo UNSET COMMENT", + "ALTER SCHEMA IF EXISTS db.demo UNSET COMMENT, DATA_RETENTION_TIME_IN_DAYS, COMMENT", + ] { + snowflake().verified_stmt(sql); + } + snowflake().one_statement_parses_to( + r#" + ALTER /* schema */ SCHEMA IF EXISTS "db"."demo" SET + comment /* key */ = /* value */ 'owner''s schema', + data_retention_time_in_days = -- retention + +7 /* trailing comment */; + "#, + r#"ALTER SCHEMA IF EXISTS "db"."demo" SET comment = 'owner''s schema' data_retention_time_in_days = +7"#, + ); + snowflake().one_statement_parses_to( + "ALTER SCHEMA demo SET COMMENT = '', DATA_RETENTION_TIME_IN_DAYS = 1, COMMENT = NULL", + "ALTER SCHEMA demo SET COMMENT = '' DATA_RETENTION_TIME_IN_DAYS = 1 COMMENT = NULL", + ); + snowflake().one_statement_parses_to( + "ALTER SCHEMA demo SET COMMENT='x'/* separator */COMMENT='y'", + "ALTER SCHEMA demo SET COMMENT = 'x' COMMENT = 'y'", + ); + snowflake().one_statement_parses_to( + r#"ALTER SCHEMA demo UNSET comment /* comma */, -- name + data_retention_time_in_days, comment;"#, + "ALTER SCHEMA demo UNSET comment, data_retention_time_in_days, comment", + ); +} + +#[test] +fn snowflake_alter_schema_options_literal_round_trip() { + for value in [ + "-1", + "+1", + "1.5", + "999999999999999999999999999999", + "'1'", + "true", + "false", + "NULL", + "$$opaque; -- /* text */$$", + ] { + snowflake().verified_stmt(&format!("ALTER SCHEMA demo SET COMMENT = {value}")); + } + snowflake().one_statement_parses_to( + "ALTER SCHEMA demo SET COMMENT = TRUE DATA_RETENTION_TIME_IN_DAYS = 1e3", + &format!( + "ALTER SCHEMA demo SET COMMENT = true DATA_RETENTION_TIME_IN_DAYS = {}", + number("1e3") + ), + ); +} + +#[test] +fn snowflake_alter_schema_options_keep_statement_boundaries() { + for sql in [ + r#"ALTER SCHEMA "db;DROP DATABASE prod"."demo" SET COMMENT = 'x''; DROP SCHEMA prod; -- /* */ DATA_RETENTION_TIME_IN_DAYS = 0' DATA_RETENTION_TIME_IN_DAYS = 7; /* ; ignored */ SELECT 1;"#, + r#"ALTER SCHEMA demo SET COMMENT = $$x'; DROP SCHEMA prod; -- /* */$$, DATA_RETENTION_TIME_IN_DAYS = 7; SELECT 1;"#, + "ALTER SCHEMA demo SET COMMENT = 'kept'; SELECT 1", + "ALTER SCHEMA demo SET DATA_RETENTION_TIME_IN_DAYS = -1; SELECT 1", + "ALTER SCHEMA demo UNSET COMMENT, DATA_RETENTION_TIME_IN_DAYS; SELECT 1", + r#"ALTER SCHEMA "db;DROP DATABASE prod"."demo" UNSET "COMMENT; --", COMMENT; SELECT 1"#, + ] { + let statements = snowflake().parse_sql_statements(sql).unwrap(); + assert_eq!(statements.len(), 2, "{sql}"); + assert_eq!(statements[1].to_string(), "SELECT 1"); + assert_eq!( + snowflake().verified_stmt(&statements[0].to_string()), + statements[0] + ); + } +} + +#[test] +fn snowflake_alter_schema_options_reject_malformed_and_complex_forms() { + use sqlparser::parser::Parser; + + for suffix in [ + "SET", + "SET; SELECT 1", + "SET ()", + "SET (COMMENT = 'x')", + "SET , COMMENT = 'x'", + "SET COMMENT", + "SET COMMENT 'missing equals'", + "SET COMMENT =", + "SET COMMENT =; SELECT 1", + "SET COMMENT = = 'extra equals'", + "SET COMMENT = 'unterminated", + "SET COMMENT = 'x',", + "SET COMMENT = 'x',; SELECT 1", + "SET COMMENT = 'x',, COMMENT = 'y'", + "SET COMMENT = 'x' COMMENT =", + "SET COMMENT = DATA_RETENTION_TIME_IN_DAYS = 1", + "SET COMMENT = 'x'COMMENT = 'y'", + "SET COMMENT = 1COMMENT = 'y'", + "SET COMMENT = 'x' SELECT 1", + "SET COMMENT = 'x' CLONE other", + "SET COMMENT = 'x' CREATE SCHEMA other", + "SET COMMENT = 'x', UNSET COMMENT", + "SET COMMENT = ?", + "SET COMMENT = :value", + "SET COMMENT = @value", + "SET COMMENT = +?", + "SET 'COMMENT' = 'x'", + "SET COMMENT = DEFAULT", + "SET COMMENT = NOT true", + "SET COMMENT = [1]", + "SET COMMENT = {'a': 1}", + "SET COMMENT = CAST(1 AS INT)", + "SET DATA_RETENTION_TIME_IN_DAYS = +", + "SET DATA_RETENTION_TIME_IN_DAYS = ++1", + "SET DATA_RETENTION_TIME_IN_DAYS = -NULL", + "SET DATA_RETENTION_TIME_IN_DAYS = -'1'", + "SET DATA_RETENTION_TIME_IN_DAYS = 1.5.2", + "SET DATA_RETENTION_TIME_IN_DAYS = (1)", + "SET DATA_RETENTION_TIME_IN_DAYS = 1 + 1", + "SET COMMENT = 'a' || 'b'", + "SET COMMENT = concat('a', 'b')", + "SET COMMENT = (SELECT 'x')", + "SET TAG t = 'v'", + "SET TAG = 'v'", + "SET COMMENT = 'x', TAG t = 'v'", + "SET CONTACT STEWARD = person", + "SET CONTACT = 'person'", + "SET COMMENT = 'x' CONTACT STEWARD = person", + "UNSET", + "UNSET; SELECT 1", + "UNSET ()", + "UNSET (COMMENT)", + "UNSET , COMMENT", + "UNSET COMMENT,", + "UNSET COMMENT,; SELECT 1", + "UNSET COMMENT,, DATA_RETENTION_TIME_IN_DAYS", + "UNSET COMMENT DATA_RETENTION_TIME_IN_DAYS", + "UNSET COMMENT = 'x'", + "UNSET COMMENT, DATA_RETENTION_TIME_IN_DAYS = 1", + "UNSET COMMENT SET COMMENT = 'x'", + "UNSET 'COMMENT'", + "UNSET 1", + "UNSET TAG", + "UNSET TAG t", + "UNSET COMMENT, TAG", + "UNSET COMMENT, TAG t", + "UNSET CONTACT", + "UNSET COMMENT, CONTACT", + "UNSET CONTACT STEWARD", + "UNSET db.demo.COMMENT", + "SWAP WITH other", + "ENABLE MANAGED ACCESS", + "DISABLE MANAGED ACCESS", + "CLONE other", + ] { + let sql = format!("ALTER SCHEMA demo {suffix}"); + for trailing_commas in [false, true] { + let result = Parser::new(&SnowflakeDialect) + .with_options(ParserOptions::new().with_trailing_commas(trailing_commas)) + .try_with_sql(&sql) + .and_then(|mut parser| parser.parse_statements()); + assert!(result.is_err(), "trailing_commas={trailing_commas}: {sql}"); + } + } +} + +#[test] +fn snowflake_alter_schema_options_are_dialect_gated_and_preserve_existing_forms() { + use sqlparser::parser::Parser; + + for dialect in all_dialects().dialects { + assert_eq!( + dialect.supports_alter_schema_options(), + dialect.is::(), + "{dialect:?}" + ); + for sql in [ + "ALTER SCHEMA demo SET COMMENT = 'x'", + "ALTER SCHEMA demo SET DATA_RETENTION_TIME_IN_DAYS = 1", + "ALTER SCHEMA demo UNSET COMMENT", + ] { + let result = Parser::parse_sql(&*dialect, sql); + assert_eq!( + result.is_ok(), + dialect.is::(), + "{dialect:?}: {sql}" + ); + } + } + for sql in [ + "ALTER SCHEMA demo SET OPTIONS (description = 'kept')", + "ALTER SCHEMA IF EXISTS db.demo SET DEFAULT COLLATE 'und:ci'", + "ALTER SCHEMA demo ADD REPLICA replica OPTIONS (location = 'US')", + "ALTER SCHEMA demo DROP REPLICA replica", + "ALTER SCHEMA demo RENAME TO other", + "ALTER SCHEMA demo OWNER TO owner", + ] { + all_dialects().verified_stmt(sql); + } +} + +#[test] +fn snowflake_alter_schema_options_preserve_typed_ast_and_clone() { + let statement = snowflake().verified_stmt( + "ALTER SCHEMA IF EXISTS db.demo SET COMMENT = NULL DATA_RETENTION_TIME_IN_DAYS = -1 COMMENT = 42 FLAG = true COMMENT = 'invalid'", + ); + assert_eq!( + statement, + Statement::AlterSchema(AlterSchema { + name: ObjectName::from(vec![Ident::new("db"), Ident::new("demo")]), + if_exists: true, + operations: vec![AlterSchemaOperation::SetOptions { + options: vec![ + SqlOption::KeyValue { + key: Ident::new("COMMENT"), + value: Expr::Value(Value::Null.into()), + }, + SqlOption::KeyValue { + key: Ident::new("DATA_RETENTION_TIME_IN_DAYS"), + value: Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(number("1").into())), + }, + }, + SqlOption::KeyValue { + key: Ident::new("COMMENT"), + value: Expr::Value(number("42").into()), + }, + SqlOption::KeyValue { + key: Ident::new("FLAG"), + value: Expr::Value(Value::Boolean(true).into()), + }, + SqlOption::KeyValue { + key: Ident::new("COMMENT"), + value: Expr::Value(Value::SingleQuotedString("invalid".into()).into()), + }, + ], + }], + }) + ); + assert_eq!(statement.clone(), statement); + + assert_eq!( + snowflake() + .verified_stmt("ALTER SCHEMA demo UNSET COMMENT, COMMENT, DATA_RETENTION_TIME_IN_DAYS"), + Statement::AlterSchema(AlterSchema { + name: ObjectName::from(vec![Ident::new("demo")]), + if_exists: false, + operations: vec![AlterSchemaOperation::UnsetOptions { + options: vec![ + Ident::new("COMMENT"), + Ident::new("COMMENT"), + Ident::new("DATA_RETENTION_TIME_IN_DAYS"), + ], + }], + }) + ); +} + +#[test] +fn snowflake_alter_schema_options_capability_can_be_enabled_independently() { + use sqlparser::parser::Parser; + + #[derive(Debug)] + struct SchemaOptionsDialect; + impl Dialect for SchemaOptionsDialect { + fn is_identifier_start(&self, ch: char) -> bool { + ch.is_ascii_alphabetic() || ch == '_' + } + + fn is_identifier_part(&self, ch: char) -> bool { + self.is_identifier_start(ch) || ch.is_ascii_digit() + } + + fn supports_alter_schema_options(&self) -> bool { + true + } + } + + for sql in [ + "ALTER SCHEMA demo SET COMMENT = 'x', DATA_RETENTION_TIME_IN_DAYS = -1", + "ALTER SCHEMA demo UNSET COMMENT, DATA_RETENTION_TIME_IN_DAYS", + ] { + assert_eq!( + Parser::parse_sql(&SchemaOptionsDialect, sql).unwrap(), + Parser::parse_sql(&SnowflakeDialect, sql).unwrap() + ); + } +} + +#[test] +fn snowflake_alter_schema_options_spans_and_unconsumed_delimiters() { + use sqlparser::parser::Parser; + + for (sql, operation_span, statement_span) in [ + ( + r#"ALTER SCHEMA db.demo SET + COMMENT = 'kept', + FLAG = true"#, + Span::new(Location::new(2, 3), Location::new(3, 14)), + Span::new(Location::new(1, 14), Location::new(3, 14)), + ), + ( + r#"ALTER SCHEMA db.demo UNSET + COMMENT, + FLAG"#, + Span::new(Location::new(2, 2), Location::new(3, 6)), + Span::new(Location::new(1, 14), Location::new(3, 6)), + ), + ] { + let statements = Parser::parse_sql(&SnowflakeDialect, sql).unwrap(); + let Statement::AlterSchema(schema) = &statements[0] else { + panic!("expected ALTER SCHEMA"); + }; + assert_eq!(schema.operations[0].span(), operation_span); + assert_eq!(schema.span(), statement_span); + } + + for suffix in [ + "SET COMMENT = 'x'", + "UNSET COMMENT", + "SET", + "SET COMMENT", + "SET COMMENT =", + "SET COMMENT = 'x',", + "SET COMMENT = 'x' COMMENT", + "UNSET", + "UNSET COMMENT,", + ] { + let sql = format!("ALTER SCHEMA demo {suffix}; SELECT 1"); + let mut parser = Parser::new(&SnowflakeDialect).try_with_sql(&sql).unwrap(); + let result = parser.parse_alter_schema(); + assert_eq!( + result.is_ok(), + matches!(suffix, "SET COMMENT = 'x'" | "UNSET COMMENT") + ); + assert_eq!(parser.peek_token_ref().token, Token::SemiColon, "{sql}"); + } +} + +#[test] +fn snowflake_alter_schema_options_injection_text_is_literal_data() { + let sql = r#"ALTER SCHEMA "db;DROP DATABASE prod"."demo" SET COMMENT = 'x''; DROP SCHEMA prod; -- /* */', COMMENT = $$x'; DROP SCHEMA prod; -- /* */$$; SELECT 1"#; + let statements = snowflake().parse_sql_statements(sql).unwrap(); + assert_eq!(statements.len(), 2); + let Statement::AlterSchema(schema) = &statements[0] else { + panic!("expected ALTER SCHEMA"); + }; + assert_eq!( + schema.name, + ObjectName::from(vec![ + Ident::with_quote('"', "db;DROP DATABASE prod"), + Ident::with_quote('"', "demo"), + ]) + ); + let [AlterSchemaOperation::SetOptions { options }] = schema.operations.as_slice() else { + panic!("expected native SET"); + }; + let [SqlOption::KeyValue { + value: Expr::Value(first), + .. + }, SqlOption::KeyValue { + value: Expr::Value(second), + .. + }] = options.as_slice() + else { + panic!("expected two literal assignments"); + }; + assert_eq!( + first.value, + Value::SingleQuotedString("x'; DROP SCHEMA prod; -- /* */".into()) + ); + let Value::DollarQuotedString(value) = &second.value else { + panic!("expected dollar-quoted literal"); + }; + assert_eq!(value.value, "x'; DROP SCHEMA prod; -- /* */"); + assert_eq!(statements[1].to_string(), "SELECT 1"); +} + +#[cfg(all(feature = "serde", feature = "serde_json"))] +#[test] +fn snowflake_alter_schema_options_serde_round_trip() { + for (sql, variant) in [ + ( + "ALTER SCHEMA demo SET COMMENT = NULL DATA_RETENTION_TIME_IN_DAYS = -1 COMMENT = 'kept'", + "SetOptions", + ), + ("ALTER SCHEMA demo UNSET COMMENT, COMMENT", "UnsetOptions"), + ( + "ALTER SCHEMA demo SET OPTIONS (description = 'kept')", + "SetOptionsParens", + ), + ] { + let statement = snowflake().verified_stmt(sql); + let encoded = serde_json::to_value(&statement).unwrap(); + assert!(encoded["AlterSchema"]["operations"][0][variant]["options"].is_array()); + assert_eq!(serde_json::from_value::(encoded).unwrap(), statement); + } +} + +#[cfg(feature = "visitor")] +#[test] +fn snowflake_alter_schema_options_visitors_do_not_treat_names_as_relations() { + use core::ops::ControlFlow; + use sqlparser::ast::{visit_expressions, visit_expressions_mut, visit_relations}; + + let mut statement = snowflake().verified_stmt( + "ALTER SCHEMA db.demo SET COMMENT = 'kept' DATA_RETENTION_TIME_IN_DAYS = -1", + ); + let mut expressions = Vec::new(); + let result = visit_expressions(&statement, |expr| { + expressions.push(expr.to_string()); + ControlFlow::<()>::Continue(()) + }); + assert_eq!(result, ControlFlow::Continue(())); + assert_eq!(expressions, vec!["'kept'", "-1", "1"]); + let result = visit_expressions_mut(&mut statement, |expr| { + if let Expr::Value(ValueWithSpan { + value: Value::SingleQuotedString(value), + .. + }) = expr + { + *value = "changed".into(); + } + ControlFlow::<()>::Continue(()) + }); + assert_eq!(result, ControlFlow::Continue(())); + assert_eq!( + statement.to_string(), + "ALTER SCHEMA db.demo SET COMMENT = 'changed' DATA_RETENTION_TIME_IN_DAYS = -1" + ); + + let unset = snowflake().verified_stmt("ALTER SCHEMA db.demo UNSET COMMENT, COMMENT"); + let result = visit_expressions(&unset, |_| ControlFlow::Break("unexpected expression")); + assert_eq!(result, ControlFlow::Continue(())); + for statement in [statement, unset] { + let result = visit_relations(&statement, |_| ControlFlow::Break("unexpected relation")); + assert_eq!(result, ControlFlow::Continue(())); + } +}