diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c87646f85..29dd293f4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -1356,6 +1356,61 @@ pub enum AlterColumnOperation { /// Optional sequence options for identity generation. sequence_options: Option>, }, + + /// `SET MASKING POLICY [USING (, ...)] [FORCE]` + /// + /// Snowflake: attach a masking policy to the column + /// (`ALTER TABLE t MODIFY COLUMN c SET MASKING POLICY p`). + SetMaskingPolicy { + /// The policy to attach. + policy_name: ObjectName, + /// Optional `USING (, ...)` conditional-masking column list. + using_columns: Option>, + /// Whether the `FORCE` keyword was present. + force: bool, + }, + + /// `UNSET MASKING POLICY` + /// + /// Snowflake: detach the masking policy from the column. + UnsetMaskingPolicy, +} + +/// An operation on a masking policy in an `ALTER MASKING POLICY` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterMaskingPolicyOperation { + /// `SET BODY -> ` + SetBody { + /// The replacement body expression. + body: Expr, + }, + /// `RENAME TO ` + RenameTo { + /// The new policy name. + new_name: ObjectName, + }, + /// `SET COMMENT = ''` + SetComment { + /// The replacement comment text. + comment: String, + }, + /// `UNSET COMMENT` + UnsetComment, +} + +impl fmt::Display for AlterMaskingPolicyOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterMaskingPolicyOperation::SetBody { body } => write!(f, "SET BODY -> {body}"), + AlterMaskingPolicyOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterMaskingPolicyOperation::SetComment { comment } => { + write!(f, "SET COMMENT = '{}'", escape_single_quote_string(comment)) + } + AlterMaskingPolicyOperation::UnsetComment => write!(f, "UNSET COMMENT"), + } + } } impl fmt::Display for AlterColumnOperation { @@ -1408,6 +1463,21 @@ impl fmt::Display for AlterColumnOperation { } Ok(()) } + AlterColumnOperation::SetMaskingPolicy { + policy_name, + using_columns, + force, + } => { + write!(f, "SET MASKING POLICY {policy_name}")?; + if let Some(columns) = using_columns { + write!(f, " USING ({})", display_comma_separated(columns))?; + } + if *force { + write!(f, " FORCE")?; + } + Ok(()) + } + AlterColumnOperation::UnsetMaskingPolicy => write!(f, "UNSET MASKING POLICY"), } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 66b3a3631..0523deb69 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -64,6 +64,7 @@ pub use self::dcl::{ pub use self::ddl::{ Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner, AlterFunction, AlterFunctionAction, AlterFunctionKind, AlterFunctionOperation, + AlterMaskingPolicyOperation, AlterIndexOperation, AlterProcedure, AlterProcedureOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, @@ -4148,6 +4149,20 @@ pub enum Statement { with_options: Vec, }, /// ```sql + /// ALTER VIEW { MODIFY | ALTER } COLUMN + /// { SET MASKING POLICY

[USING (, ...)] [FORCE] | UNSET MASKING POLICY } + /// ``` + /// Snowflake column-level masking-policy operation on a view. + AlterViewColumn { + /// View name. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + name: ObjectName, + /// Target column. + column_name: Ident, + /// The column operation to apply. + op: AlterColumnOperation, + }, + /// ```sql /// ALTER FUNCTION /// ALTER AGGREGATE /// ``` @@ -5381,6 +5396,63 @@ pub enum Statement { filter: Option, }, /// ```sql + /// CREATE [OR REPLACE] MASKING POLICY [IF NOT EXISTS] + /// AS ( [, ...]) RETURNS -> [COMMENT = ''] + /// ``` + /// See + CreateMaskingPolicy { + /// `OR REPLACE` flag. + or_replace: bool, + /// `IF NOT EXISTS` flag. + if_not_exists: bool, + /// Policy name. + name: ObjectName, + /// Signature arguments (name + type), in declaration order. + args: Vec, + /// The declared return type. + return_type: DataType, + /// The policy body expression after `->`. + policy_expr: Expr, + /// Optional `COMMENT = ''`. + comment: Option, + }, + /// ```sql + /// ALTER MASKING POLICY [IF EXISTS] + /// { SET BODY -> | RENAME TO | SET COMMENT = '' | UNSET COMMENT } + /// ``` + /// See + AlterMaskingPolicy { + /// `IF EXISTS` flag. + if_exists: bool, + /// Policy name. + name: ObjectName, + /// The operation to apply. + operation: AlterMaskingPolicyOperation, + }, + /// ```sql + /// DROP MASKING POLICY [IF EXISTS] + /// ``` + DropMaskingPolicy { + /// `IF EXISTS` flag. + if_exists: bool, + /// Policy name. + name: ObjectName, + }, + /// ```sql + /// DESC[RIBE] MASKING POLICY + /// ``` + DescribeMaskingPolicy { + /// Policy name. + name: ObjectName, + }, + /// ```sql + /// SHOW MASKING POLICIES [ LIKE '' ] [ IN ] + /// ``` + ShowMaskingPolicies { + /// Options controlling the SHOW output (filter, `IN `, etc.). + show_options: ShowStatementOptions, + }, + /// ```sql /// SHOW PROCEDURES [ LIKE '' ] [ IN ] /// ``` ShowProcedures { @@ -6746,6 +6818,13 @@ impl fmt::Display for Statement { } write!(f, " AS {query}") } + Statement::AlterViewColumn { + name, + column_name, + op, + } => { + write!(f, "ALTER VIEW {name} ALTER COLUMN {column_name} {op}") + } Statement::AlterFunction(alter_function) => write!(f, "{alter_function}"), Statement::AlterType(AlterType { name, operation }) => { write!(f, "ALTER TYPE {name} {operation}") @@ -7835,6 +7914,55 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateMaskingPolicy { + or_replace, + if_not_exists, + name, + args, + return_type, + policy_expr, + comment, + } => { + write!( + f, + "CREATE {or_replace}MASKING POLICY {if_not_exists}{name} AS ({args}) RETURNS {return_type} -> {policy_expr}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + args = display_comma_separated(args), + )?; + if let Some(comment) = comment { + write!( + f, + " COMMENT = '{}'", + value::escape_single_quote_string(comment) + )?; + } + Ok(()) + } + Statement::AlterMaskingPolicy { + if_exists, + name, + operation, + } => { + write!( + f, + "ALTER MASKING POLICY {if_exists}{name} {operation}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::DropMaskingPolicy { if_exists, name } => { + write!( + f, + "DROP MASKING POLICY {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + ) + } + Statement::DescribeMaskingPolicy { name } => { + write!(f, "DESCRIBE MASKING POLICY {name}") + } + Statement::ShowMaskingPolicies { show_options } => { + write!(f, "SHOW MASKING POLICIES{show_options}") + } Statement::ShowProcedures { show_options } => { write!(f, "SHOW PROCEDURES{show_options}")?; Ok(()) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index bb59c18eb..a25c784f0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -422,6 +422,13 @@ impl Spanned for Statement { .chain(core::iter::once(query.span())) .chain(with_options.iter().map(|i| i.span())), ), + Statement::AlterViewColumn { + name, + column_name, + op, + } => union_spans( + [name.span(), column_name.span, op.span()].into_iter(), + ), // These statements need to be implemented Statement::AlterFunction { .. } => Span::empty(), Statement::AlterType { .. } => Span::empty(), @@ -570,6 +577,11 @@ impl Spanned for Statement { Statement::DropRowAccessPolicy { .. } => Span::empty(), Statement::DescribeRowAccessPolicy { .. } => Span::empty(), Statement::ShowRowAccessPolicies { .. } => Span::empty(), + Statement::CreateMaskingPolicy { .. } => Span::empty(), + Statement::AlterMaskingPolicy { .. } => Span::empty(), + Statement::DropMaskingPolicy { .. } => Span::empty(), + Statement::DescribeMaskingPolicy { .. } => Span::empty(), + Statement::ShowMaskingPolicies { .. } => Span::empty(), Statement::ShowProcedures { .. } => Span::empty(), Statement::ShowConnections { .. } => Span::empty(), Statement::ShowShares { .. } => Span::empty(), @@ -1024,6 +1036,8 @@ impl Spanned for AlterColumnOperation { } => using.as_ref().map_or(Span::empty(), |u| u.span()), AlterColumnOperation::Comment { .. } => Span::empty(), AlterColumnOperation::AddGenerated { .. } => Span::empty(), + AlterColumnOperation::SetMaskingPolicy { .. } => Span::empty(), + AlterColumnOperation::UnsetMaskingPolicy => Span::empty(), } } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index a95dffa1b..930475089 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -27,8 +27,8 @@ use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ - AlterExternalVolumeOperation, AlterFileFormatOperation, AlterProcedure, - AlterProcedureOperation, AlterStageOperation, AlterTable, + AlterExternalVolumeOperation, AlterFileFormatOperation, AlterMaskingPolicyOperation, + AlterProcedure, AlterProcedureOperation, AlterStageOperation, AlterTable, AlterTableOperation, AlterTableType, CatalogRestAuthentication, CatalogRestConfig, CatalogSource, CatalogSyncNamespaceMode, CatalogTableFormat, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTable, CreateTableLikeKind, @@ -387,6 +387,11 @@ impl Dialect for SnowflakeDialect { return Some(parse_alter_row_access_policy(parser)); } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::MASKING, Keyword::POLICY]) { + // ALTER MASKING POLICY + return Some(parse_alter_masking_policy(parser)); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { @@ -432,6 +437,11 @@ impl Dialect for SnowflakeDialect { return Some(parse_drop_row_access_policy(parser)); } + if parser.parse_keywords(&[Keyword::DROP, Keyword::MASKING, Keyword::POLICY]) { + // DROP MASKING POLICY + return Some(parse_drop_masking_policy(parser)); + } + if parser .parse_one_of_keywords(&[Keyword::DESC, Keyword::DESCRIBE]) .is_some() @@ -456,6 +466,10 @@ impl Dialect for SnowflakeDialect { // DESC[RIBE] ROW ACCESS POLICY return Some(parse_describe_row_access_policy(parser)); } + if parser.parse_keywords(&[Keyword::MASKING, Keyword::POLICY]) { + // DESC[RIBE] MASKING POLICY + return Some(parse_describe_masking_policy(parser)); + } // not handled — put back DESC/DESCRIBE parser.prev_token(); } @@ -485,6 +499,11 @@ impl Dialect for SnowflakeDialect { return Some(parse_create_row_access_policy(or_replace, parser)); } + // CREATE [OR REPLACE] MASKING POLICY + if parser.parse_keywords(&[Keyword::MASKING, Keyword::POLICY]) { + return Some(parse_create_masking_policy(or_replace, parser)); + } + // LOCAL | GLOBAL let global = match parser.parse_one_of_keywords(&[Keyword::LOCAL, Keyword::GLOBAL]) { Some(Keyword::LOCAL) => Some(false), @@ -644,6 +663,9 @@ impl Dialect for SnowflakeDialect { if parser.parse_keywords(&[Keyword::ROW, Keyword::ACCESS, Keyword::POLICIES]) { return Some(parse_show_row_access_policies(parser)); } + if parser.parse_keywords(&[Keyword::MASKING, Keyword::POLICIES]) { + return Some(parse_show_masking_policies(parser)); + } if parser.parse_keyword(Keyword::PROCEDURES) { return Some(parse_show_procedures(parser)); } @@ -3399,6 +3421,101 @@ fn parse_show_row_access_policies(parser: &mut Parser) -> Result +/// AS ( [, ...]) RETURNS -> [COMMENT = '']` +fn parse_create_masking_policy( + or_replace: bool, + parser: &mut Parser, +) -> Result { + let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + parser.expect_keyword_is(Keyword::AS)?; + parser.expect_token(&Token::LParen)?; + let args = parser.parse_comma_separated(|p| { + let arg_name = p.parse_identifier()?; + let data_type = p.parse_data_type()?; + Ok(OperateFunctionArg { + mode: None, + name: Some(arg_name), + data_type, + default_expr: None, + }) + })?; + parser.expect_token(&Token::RParen)?; + parser.expect_keyword_is(Keyword::RETURNS)?; + let return_type = parser.parse_data_type()?; + parser.expect_token(&Token::Arrow)?; + let policy_expr = parser.parse_expr()?; + let comment = if parser.parse_keyword(Keyword::COMMENT) { + parser.expect_token(&Token::Eq)?; + Some(parser.parse_comment_value()?) + } else { + None + }; + Ok(Statement::CreateMaskingPolicy { + or_replace, + if_not_exists, + name, + args, + return_type, + policy_expr, + comment, + }) +} + +/// Parse `ALTER MASKING POLICY [IF EXISTS] +/// { SET BODY -> | RENAME TO | SET COMMENT = '' | UNSET COMMENT }` +fn parse_alter_masking_policy(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + let operation = if parser.parse_keywords(&[Keyword::SET, Keyword::BODY]) { + parser.expect_token(&Token::Arrow)?; + AlterMaskingPolicyOperation::SetBody { + body: parser.parse_expr()?, + } + } else if parser.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterMaskingPolicyOperation::RenameTo { + new_name: parser.parse_object_name(false)?, + } + } else if parser.parse_keywords(&[Keyword::SET, Keyword::COMMENT]) { + parser.expect_token(&Token::Eq)?; + AlterMaskingPolicyOperation::SetComment { + comment: parser.parse_comment_value()?, + } + } else if parser.parse_keywords(&[Keyword::UNSET, Keyword::COMMENT]) { + AlterMaskingPolicyOperation::UnsetComment + } else { + return parser.expected_ref( + "SET BODY, RENAME TO, SET COMMENT, or UNSET COMMENT", + parser.peek_token_ref(), + ); + }; + Ok(Statement::AlterMaskingPolicy { + if_exists, + name, + operation, + }) +} + +/// Parse `DROP MASKING POLICY [IF EXISTS] ` +fn parse_drop_masking_policy(parser: &mut Parser) -> Result { + let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let name = parser.parse_object_name(false)?; + Ok(Statement::DropMaskingPolicy { if_exists, name }) +} + +/// Parse `DESC[RIBE] MASKING POLICY ` +fn parse_describe_masking_policy(parser: &mut Parser) -> Result { + let name = parser.parse_object_name(false)?; + Ok(Statement::DescribeMaskingPolicy { name }) +} + +/// Parse `SHOW MASKING POLICIES [LIKE ''] [IN ]` +fn parse_show_masking_policies(parser: &mut Parser) -> Result { + let show_options = parser.parse_show_stmt_options()?; + Ok(Statement::ShowMaskingPolicies { show_options }) +} + /// Parse `SHOW PROCEDURES [LIKE ''] [IN ]` fn parse_show_procedures(parser: &mut Parser) -> Result { let show_options = parser.parse_show_stmt_options()?; diff --git a/src/keywords.rs b/src/keywords.rs index 9b97eedcd..a8c51e223 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -179,6 +179,7 @@ define_keywords!( BLOCK, BLOOM, BLOOMFILTER, + BODY, BOOL, BOOLEAN, BOOST, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 50d8f7615..7ff5ac290 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11275,19 +11275,26 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::MODIFY) { let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] let col_name = self.parse_identifier()?; - let data_type = self.parse_data_type()?; - let mut options = vec![]; - while let Some(option) = self.parse_optional_column_option()? { - options.push(option); - } + if let Some(op) = self.maybe_parse_column_masking_policy()? { + AlterTableOperation::AlterColumn { + column_name: col_name, + op, + } + } else { + let data_type = self.parse_data_type()?; + let mut options = vec![]; + while let Some(option) = self.parse_optional_column_option()? { + options.push(option); + } - let column_position = self.parse_column_position()?; + let column_position = self.parse_column_position()?; - AlterTableOperation::ModifyColumn { - col_name, - data_type, - options, - column_position, + AlterTableOperation::ModifyColumn { + col_name, + data_type, + options, + column_position, + } } } else if self.dialect.supports_alter_column_comment() && (self.parse_keyword(Keyword::COLUMN) @@ -11363,6 +11370,8 @@ impl<'a> Parser<'a> { generated_as, sequence_options, } + } else if let Some(op) = self.maybe_parse_column_masking_policy()? { + op } else { let message = if is_postgresql { "SET/DROP NOT NULL, SET DEFAULT, SET DATA TYPE, or ADD GENERATED after ALTER COLUMN" @@ -11513,6 +11522,35 @@ impl<'a> Parser<'a> { Ok(operation) } + /// Try to parse a Snowflake column masking-policy operation + /// (`SET MASKING POLICY

[USING (, ...)] [FORCE]` or + /// `UNSET MASKING POLICY`) following the column name in an `ALTER TABLE` + /// / `ALTER VIEW` `{MODIFY|ALTER} COLUMN` clause. Returns `None` if the + /// upcoming tokens are not a masking-policy operation, leaving the parser + /// position unchanged. + fn maybe_parse_column_masking_policy( + &mut self, + ) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::SET, Keyword::MASKING, Keyword::POLICY]) { + let policy_name = self.parse_object_name(false)?; + let using_columns = if self.parse_keyword(Keyword::USING) { + Some(self.parse_parenthesized_column_list(Mandatory, false)?) + } else { + None + }; + let force = self.parse_keyword(Keyword::FORCE); + Ok(Some(AlterColumnOperation::SetMaskingPolicy { + policy_name, + using_columns, + force, + })) + } else if self.parse_keywords(&[Keyword::UNSET, Keyword::MASKING, Keyword::POLICY]) { + Ok(Some(AlterColumnOperation::UnsetMaskingPolicy)) + } else { + Ok(None) + } + } + fn parse_set_data_type(&mut self, had_set: bool) -> Result { let data_type = self.parse_data_type()?; let using = if self.dialect.supports_alter_column_type_using() @@ -12021,6 +12059,28 @@ impl<'a> Parser<'a> { /// Parse an `ALTER VIEW` statement. pub fn parse_alter_view(&mut self) -> Result { let name = self.parse_object_name(false)?; + + // Snowflake: `ALTER VIEW v { MODIFY | ALTER } COLUMN c + // { SET MASKING POLICY p [USING (...)] [FORCE] | UNSET MASKING POLICY }` + if self.parse_one_of_keywords(&[Keyword::MODIFY, Keyword::ALTER]).is_some() { + let _ = self.parse_keyword(Keyword::COLUMN); // [ COLUMN ] + let column_name = self.parse_identifier()?; + let op = match self.maybe_parse_column_masking_policy()? { + Some(op) => op, + None => { + return self.expected_ref( + "SET MASKING POLICY or UNSET MASKING POLICY", + self.peek_token_ref(), + ) + } + }; + return Ok(Statement::AlterViewColumn { + name, + column_name, + op, + }); + } + let columns = self.parse_parenthesized_column_list(Optional, false)?; let with_options = self.parse_options(Keyword::WITH)?; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c79cfb0bb..ad051b498 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -8876,3 +8876,291 @@ fn test_with_as_procedure() { let stmt = snowflake().verified_stmt("WITH t AS (SELECT 1) SELECT * FROM t"); assert!(matches!(stmt, Statement::Query(_))); } + +#[test] +fn parse_snowflake_create_masking_policy() { + match snowflake().verified_stmt( + "CREATE MASKING POLICY p AS (a VARCHAR, b VARCHAR) RETURNS VARCHAR -> a", + ) { + Statement::CreateMaskingPolicy { + or_replace, + if_not_exists, + name, + args, + return_type, + policy_expr, + comment, + } => { + assert!(!or_replace); + assert!(!if_not_exists); + assert_eq!("p", name.to_string()); + assert_eq!(2, args.len()); + assert_eq!(Some(Ident::new("a")), args[0].name); + assert_eq!(DataType::Varchar(None), args[0].data_type); + assert_eq!(Some(Ident::new("b")), args[1].name); + assert_eq!(DataType::Varchar(None), return_type); + assert_eq!(Expr::Identifier(Ident::new("a")), policy_expr); + assert_eq!(None, comment); + } + other => panic!("expected CreateMaskingPolicy, got {other:?}"), + } + + // OR REPLACE, IF NOT EXISTS, and a COMMENT. + match snowflake().verified_stmt( + "CREATE OR REPLACE MASKING POLICY p AS (a VARCHAR) RETURNS VARCHAR -> a COMMENT = 'hi'", + ) { + Statement::CreateMaskingPolicy { + or_replace, + if_not_exists, + comment, + .. + } => { + assert!(or_replace); + assert!(!if_not_exists); + assert_eq!(Some("hi".to_string()), comment); + } + other => panic!("expected CreateMaskingPolicy, got {other:?}"), + } + + match snowflake().verified_stmt( + "CREATE MASKING POLICY IF NOT EXISTS p AS (a VARCHAR) RETURNS VARCHAR -> a", + ) { + Statement::CreateMaskingPolicy { if_not_exists, .. } => assert!(if_not_exists), + other => panic!("expected CreateMaskingPolicy, got {other:?}"), + } + + // A conditional-masking body with multiple arguments round-trips. + snowflake().verified_stmt( + "CREATE MASKING POLICY p AS (a VARCHAR, b VARCHAR) RETURNS VARCHAR -> \ + CASE WHEN b = 'x' THEN '***' ELSE a END", + ); +} + +#[test] +fn parse_snowflake_alter_masking_policy() { + match snowflake().verified_stmt("ALTER MASKING POLICY p SET BODY -> 'true'") { + Statement::AlterMaskingPolicy { + if_exists, + name, + operation, + } => { + assert!(!if_exists); + assert_eq!("p", name.to_string()); + assert_eq!( + AlterMaskingPolicyOperation::SetBody { + body: Expr::Value( + Value::SingleQuotedString("true".to_string()).with_empty_span() + ), + }, + operation + ); + } + other => panic!("expected AlterMaskingPolicy, got {other:?}"), + } + + match snowflake().verified_stmt("ALTER MASKING POLICY IF EXISTS p RENAME TO q") { + Statement::AlterMaskingPolicy { + if_exists, + operation, + .. + } => { + assert!(if_exists); + assert_eq!( + AlterMaskingPolicyOperation::RenameTo { + new_name: ObjectName::from(vec![Ident::new("q")]), + }, + operation + ); + } + other => panic!("expected AlterMaskingPolicy, got {other:?}"), + } + + match snowflake().verified_stmt("ALTER MASKING POLICY p SET COMMENT = 'c'") { + Statement::AlterMaskingPolicy { operation, .. } => assert_eq!( + AlterMaskingPolicyOperation::SetComment { + comment: "c".to_string(), + }, + operation + ), + other => panic!("expected AlterMaskingPolicy, got {other:?}"), + } + + match snowflake().verified_stmt("ALTER MASKING POLICY p UNSET COMMENT") { + Statement::AlterMaskingPolicy { operation, .. } => { + assert_eq!(AlterMaskingPolicyOperation::UnsetComment, operation) + } + other => panic!("expected AlterMaskingPolicy, got {other:?}"), + } +} + +#[test] +fn parse_snowflake_drop_masking_policy() { + match snowflake().verified_stmt("DROP MASKING POLICY p") { + Statement::DropMaskingPolicy { if_exists, name } => { + assert!(!if_exists); + assert_eq!("p", name.to_string()); + } + other => panic!("expected DropMaskingPolicy, got {other:?}"), + } + + match snowflake().verified_stmt("DROP MASKING POLICY IF EXISTS p") { + Statement::DropMaskingPolicy { if_exists, .. } => assert!(if_exists), + other => panic!("expected DropMaskingPolicy, got {other:?}"), + } +} + +#[test] +fn parse_snowflake_describe_masking_policy() { + for sql in [ + "DESCRIBE MASKING POLICY p", + "DESC MASKING POLICY p", + ] { + match snowflake().one_statement_parses_to(sql, "DESCRIBE MASKING POLICY p") { + Statement::DescribeMaskingPolicy { name } => assert_eq!("p", name.to_string()), + other => panic!("expected DescribeMaskingPolicy, got {other:?}"), + } + } +} + +#[test] +fn parse_snowflake_show_masking_policies() { + match snowflake().verified_stmt("SHOW MASKING POLICIES") { + Statement::ShowMaskingPolicies { show_options } => { + assert!(show_options.filter_position.is_none()); + } + other => panic!("expected ShowMaskingPolicies, got {other:?}"), + } + + match snowflake().verified_stmt("SHOW MASKING POLICIES LIKE '%p%'") { + Statement::ShowMaskingPolicies { .. } => {} + other => panic!("expected ShowMaskingPolicies, got {other:?}"), + } + + snowflake().verified_stmt("SHOW MASKING POLICIES IN SCHEMA s"); +} + +#[test] +fn parse_snowflake_alter_table_column_masking_policy() { + for keyword in ["MODIFY", "ALTER"] { + let sql = format!("ALTER TABLE t {keyword} COLUMN c SET MASKING POLICY p"); + match snowflake().one_statement_parses_to( + &sql, + "ALTER TABLE t ALTER COLUMN c SET MASKING POLICY p", + ) { + Statement::AlterTable(AlterTable { operations, .. }) => match &operations[0] { + AlterTableOperation::AlterColumn { column_name, op } => { + assert_eq!("c", column_name.to_string()); + assert_eq!( + &AlterColumnOperation::SetMaskingPolicy { + policy_name: ObjectName::from(vec![Ident::new("p")]), + using_columns: None, + force: false, + }, + op + ); + } + other => panic!("expected AlterColumn, got {other:?}"), + }, + other => panic!("expected AlterTable, got {other:?}"), + } + } + + // USING and FORCE. + match snowflake() + .verified_stmt("ALTER TABLE t ALTER COLUMN c SET MASKING POLICY p USING (c, d) FORCE") + { + Statement::AlterTable(AlterTable { operations, .. }) => match &operations[0] { + AlterTableOperation::AlterColumn { + op: + AlterColumnOperation::SetMaskingPolicy { + using_columns, + force, + .. + }, + .. + } => { + assert_eq!( + &Some(vec![Ident::new("c"), Ident::new("d")]), + using_columns + ); + assert!(force); + } + other => panic!("expected SetMaskingPolicy, got {other:?}"), + }, + other => panic!("expected AlterTable, got {other:?}"), + } + + // UNSET. + match snowflake().verified_stmt("ALTER TABLE t ALTER COLUMN c UNSET MASKING POLICY") { + Statement::AlterTable(AlterTable { operations, .. }) => assert_eq!( + AlterTableOperation::AlterColumn { + column_name: Ident::new("c"), + op: AlterColumnOperation::UnsetMaskingPolicy, + }, + operations[0] + ), + other => panic!("expected AlterTable, got {other:?}"), + } +} + +#[test] +fn parse_snowflake_alter_view_column_masking_policy() { + for keyword in ["MODIFY", "ALTER"] { + let sql = format!("ALTER VIEW v {keyword} COLUMN c SET MASKING POLICY p"); + match snowflake() + .one_statement_parses_to(&sql, "ALTER VIEW v ALTER COLUMN c SET MASKING POLICY p") + { + Statement::AlterViewColumn { + name, + column_name, + op, + } => { + assert_eq!("v", name.to_string()); + assert_eq!("c", column_name.to_string()); + assert_eq!( + AlterColumnOperation::SetMaskingPolicy { + policy_name: ObjectName::from(vec![Ident::new("p")]), + using_columns: None, + force: false, + }, + op + ); + } + other => panic!("expected AlterViewColumn, got {other:?}"), + } + } + + match snowflake().verified_stmt("ALTER VIEW v ALTER COLUMN c UNSET MASKING POLICY") { + Statement::AlterViewColumn { op, .. } => { + assert_eq!(AlterColumnOperation::UnsetMaskingPolicy, op) + } + other => panic!("expected AlterViewColumn, got {other:?}"), + } +} + +#[test] +fn parse_snowflake_create_view_column_masking_policy() { + match snowflake().verified_stmt( + "CREATE VIEW v (c WITH MASKING POLICY p USING (c, d)) AS SELECT c, d FROM t", + ) { + Statement::CreateView(CreateView { columns, .. }) => { + assert_eq!(1, columns.len()); + assert_eq!("c", columns[0].name.to_string()); + match &columns[0].options { + Some(ColumnOptions::SpaceSeparated(options)) => match &options[0] { + ColumnOption::Policy(ColumnPolicy::MaskingPolicy(property)) => { + assert!(property.with); + assert_eq!("p", property.policy_name.to_string()); + assert_eq!( + Some(vec![Ident::new("c"), Ident::new("d")]), + property.using_columns + ); + } + other => panic!("expected MaskingPolicy, got {other:?}"), + }, + other => panic!("expected column options, got {other:?}"), + } + } + other => panic!("expected CreateView, got {other:?}"), + } +}