diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index 56e91dd4b..26bbacc5d 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -85,6 +85,26 @@ pub enum AlterStageOperation { }, } +/// An operation supported by Snowflake's `ALTER FILE FORMAT` statement. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterFileFormatOperation { + /// Rename a named file format. + RenameTo { + /// New file format name. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + new_name: ObjectName, + }, + /// Set one or more format-specific properties or the comment. + Set { + /// Format-specific options. + options: KeyValueOptions, + /// Optional comment replacement. + comment: Option, + }, +} + /// This enum enables support for both standard SQL select item expressions /// and Snowflake-specific ones for data loading. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 15ca6a5cb..df6d95c41 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -26,7 +26,9 @@ use alloc::{ }; use helpers::{ attached_token::AttachedToken, - stmt_data_loading::{AlterStageOperation, FileStagingCommand, StageLoadSelectItemKind}, + stmt_data_loading::{ + AlterFileFormatOperation, AlterStageOperation, FileStagingCommand, StageLoadSelectItemKind, + }, }; use core::cmp::Ordering; @@ -4244,6 +4246,15 @@ pub enum Statement { show_options: ShowStatementOptions, }, /// ```sql + /// SHOW FILE FORMATS [ LIKE '' ] [ IN { ACCOUNT | DATABASE | SCHEMA } ] + /// ``` + /// Snowflake-specific statement. + /// + ShowFileFormats { + /// Additional options for filtering and scoping the file format listing. + show_options: ShowStatementOptions, + }, + /// ```sql /// SHOW VIEWS /// ``` ShowViews { @@ -4547,6 +4558,18 @@ pub enum Statement { operation: AlterStageOperation, }, /// ```sql + /// ALTER FILE FORMAT + /// ``` + /// See + AlterFileFormat { + /// `IF EXISTS` flag. + if_exists: bool, + /// File format name. + name: ObjectName, + /// File format alteration to perform. + operation: AlterFileFormatOperation, + }, + /// ```sql /// CREATE FILE FORMAT /// ``` /// See @@ -4685,6 +4708,18 @@ pub enum Statement { stage_name: ObjectName, }, /// ```sql + /// DESC[RIBE] FILE FORMAT + /// ``` + /// Snowflake-specific statement. + /// + DescribeFileFormat { + /// `DESC | DESCRIBE` spelling used by the input statement. + describe_alias: DescribeAlias, + /// File format name. + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + name: ObjectName, + }, + /// ```sql /// [EXPLAIN | DESC | DESCRIBE] /// ``` Explain { @@ -5185,6 +5220,10 @@ impl fmt::Display for Statement { describe_alias, stage_name, } => write!(f, "{describe_alias} STAGE {stage_name}"), + Statement::DescribeFileFormat { + describe_alias, + name, + } => write!(f, "{describe_alias} FILE FORMAT {name}"), Statement::Explain { describe_alias, verbose, @@ -5999,6 +6038,9 @@ impl fmt::Display for Statement { Statement::ShowStages { show_options } => { write!(f, "SHOW STAGES{show_options}") } + Statement::ShowFileFormats { show_options } => { + write!(f, "SHOW FILE FORMATS{show_options}") + } Statement::ShowViews { terse, materialized, @@ -6356,6 +6398,32 @@ impl fmt::Display for Statement { } } } + Statement::AlterFileFormat { + if_exists, + name, + operation, + } => { + write!( + f, + "ALTER FILE FORMAT {if_exists}{name}", + if_exists = if *if_exists { "IF EXISTS " } else { "" }, + )?; + match operation { + AlterFileFormatOperation::RenameTo { new_name } => { + write!(f, " RENAME TO {new_name}") + } + AlterFileFormatOperation::Set { options, comment } => { + write!(f, " SET")?; + if !options.options.is_empty() { + write!(f, " {options}")?; + } + if let Some(comment) = comment { + write!(f, " COMMENT='{comment}'")?; + } + Ok(()) + } + } + } Statement::CreateFileFormat { or_replace, temporary, @@ -8686,6 +8754,8 @@ pub enum ObjectType { Sequence, /// A stage. Stage, + /// A named file format. + FileFormat, /// A type definition. Type, /// A user. @@ -8707,6 +8777,7 @@ impl fmt::Display for ObjectType { ObjectType::Role => "ROLE", ObjectType::Sequence => "SEQUENCE", ObjectType::Stage => "STAGE", + ObjectType::FileFormat => "FILE FORMAT", ObjectType::Type => "TYPE", ObjectType::User => "USER", ObjectType::Stream => "STREAM", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 1223420a1..07c93b364 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -285,6 +285,7 @@ impl Spanned for Values { /// - [Statement::ShowColumns] /// - [Statement::ShowTables] /// - [Statement::ShowStages] +/// - [Statement::ShowFileFormats] /// - [Statement::ShowCollation] /// - [Statement::StartTransaction] /// - [Statement::Comment] @@ -300,6 +301,7 @@ impl Spanned for Values { /// - [Statement::CreateMacro] /// - [Statement::CreateStage] /// - [Statement::AlterStage] +/// - [Statement::AlterFileFormat] /// - [Statement::Assert] /// - [Statement::Grant] /// - [Statement::Revoke] @@ -309,6 +311,7 @@ impl Spanned for Values { /// - [Statement::Kill] /// - [Statement::ExplainTable] /// - [Statement::DescribeStage] +/// - [Statement::DescribeFileFormat] /// - [Statement::Explain] /// - [Statement::Savepoint] /// - [Statement::ReleaseSavepoint] @@ -447,6 +450,7 @@ impl Spanned for Statement { Statement::ShowColumns { .. } => Span::empty(), Statement::ShowTables { .. } => Span::empty(), Statement::ShowStages { .. } => Span::empty(), + Statement::ShowFileFormats { .. } => Span::empty(), Statement::ShowCollation { .. } => Span::empty(), Statement::ShowCharset { .. } => Span::empty(), Statement::Use(u) => u.span(), @@ -465,6 +469,7 @@ impl Spanned for Statement { Statement::CreateMacro { .. } => Span::empty(), Statement::CreateStage { .. } => Span::empty(), Statement::AlterStage { .. } => Span::empty(), + Statement::AlterFileFormat { .. } => Span::empty(), Statement::CreateFileFormat { .. } => Span::empty(), Statement::Assert { .. } => Span::empty(), Statement::Grant { .. } => Span::empty(), @@ -476,6 +481,7 @@ impl Spanned for Statement { Statement::Kill { .. } => Span::empty(), Statement::ExplainTable { .. } => Span::empty(), Statement::DescribeStage { .. } => Span::empty(), + Statement::DescribeFileFormat { .. } => Span::empty(), Statement::Explain { .. } => Span::empty(), Statement::Savepoint { .. } => Span::empty(), Statement::ReleaseSavepoint { .. } => Span::empty(), diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6aceb191a..32e0bf927 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1257,6 +1257,12 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect supports Snowflake-style named file format + /// lifecycle commands (`ALTER`, `DROP`, `SHOW`, and `DESCRIBE`). + fn supports_file_format_commands(&self) -> bool { + false + } + /// Returns true if this dialect supports the `COMMENT` statement fn supports_comment_on(&self) -> bool { false diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 4ec0f9655..9dd51c483 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -24,8 +24,8 @@ use crate::ast::helpers::key_value_options::{ use crate::ast::helpers::stmt_create_database::CreateDatabaseBuilder; use crate::ast::helpers::stmt_create_table::CreateTableBuilder; use crate::ast::helpers::stmt_data_loading::{ - AlterStageOperation, FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, - StageParamsObject, + AlterFileFormatOperation, AlterStageOperation, FileStagingCommand, StageLoadSelectItem, + StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ AlterTable, AlterTableOperation, AlterTableType, CatalogSyncNamespaceMode, CloudProviderParams, @@ -303,6 +303,10 @@ impl Dialect for SnowflakeDialect { return Some(parse_alter_stage(parser)); } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::FILE, Keyword::FORMAT]) { + return Some(parse_alter_file_format(parser)); + } + if parser.parse_keyword(Keyword::CREATE) { // possibly CREATE STAGE //[ OR REPLACE ] @@ -492,6 +496,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_file_format_commands(&self) -> bool { + true + } + fn supports_left_associative_joins_without_parens(&self) -> bool { false } @@ -1459,6 +1467,37 @@ fn parse_alter_stage(parser: &mut Parser) -> Result { }) } +fn parse_alter_file_format(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_keyword(Keyword::RENAME) { + parser.expect_keyword(Keyword::TO)?; + AlterFileFormatOperation::RenameTo { + new_name: parser.parse_object_name(false)?, + } + } else if parser.parse_keyword(Keyword::SET) { + let options = parser.parse_key_value_options(false, &[Keyword::COMMENT])?; + let comment = if parser.parse_keyword(Keyword::COMMENT) { + parser.expect_token(&Token::Eq)?; + Some(parser.parse_comment_value()?) + } else { + None + }; + if options.options.is_empty() && comment.is_none() { + return parser.expected_ref("a file format property", parser.peek_token_ref()); + } + AlterFileFormatOperation::Set { options, comment } + } else { + return parser.expected_ref("RENAME TO or SET", parser.peek_token_ref()); + }; + + Ok(Statement::AlterFileFormat { + if_exists, + name, + operation, + }) +} + pub fn parse_create_file_format( or_replace: bool, temporary: bool, diff --git a/src/keywords.rs b/src/keywords.rs index a45a8ed7e..c10530af1 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -448,6 +448,7 @@ define_keywords!( FORCE_QUOTE, FOREIGN, FORMAT, + FORMATS, FORMATTED, FORWARD, FRAME_ROW, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 19459114b..272a57040 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7394,6 +7394,10 @@ impl<'a> Parser<'a> { ObjectType::Sequence } else if self.parse_keyword(Keyword::STAGE) { ObjectType::Stage + } else if self.dialect.supports_file_format_commands() + && self.parse_keywords(&[Keyword::FILE, Keyword::FORMAT]) + { + ObjectType::FileFormat } else if self.parse_keyword(Keyword::TYPE) { ObjectType::Type } else if self.parse_keyword(Keyword::USER) { @@ -7427,7 +7431,7 @@ impl<'a> Parser<'a> { }; } else { return self.expected_ref( - "COLLATION, CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", + "COLLATION, CONNECTOR, DATABASE, EXTENSION, FILE FORMAT, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", self.peek_token_ref(), ); }; @@ -14030,6 +14034,16 @@ impl<'a> Parser<'a> { }); } + if describe_alias != DescribeAlias::Explain + && self.dialect.supports_file_format_commands() + && self.parse_keywords(&[Keyword::FILE, Keyword::FORMAT]) + { + return Ok(Statement::DescribeFileFormat { + describe_alias, + name: self.parse_object_name(false)?, + }); + } + match self.maybe_parse(|parser| parser.parse_statement())? { Some(Statement::Explain { .. }) | Some(Statement::ExplainTable { .. }) => Err( ParserError::ParserError("Explain must be root of the plan".to_string()), @@ -15540,6 +15554,18 @@ impl<'a> Parser<'a> { Ok(Statement::ShowStages { show_options: self.parse_show_stmt_options()?, }) + } else if self.dialect.supports_file_format_commands() + && self.parse_keywords(&[Keyword::FILE, Keyword::FORMATS]) + { + if terse || extended || full || session || global || external { + Err(ParserError::ParserError( + "SHOW FILE FORMATS does not support SHOW modifiers".to_string(), + )) + } else { + Ok(Statement::ShowFileFormats { + show_options: self.parse_show_stmt_options()?, + }) + } } else if self.parse_keywords(&[Keyword::MATERIALIZED, Keyword::VIEWS]) { Ok(self.parse_show_views(terse, true)?) } else if self.parse_keyword(Keyword::VIEWS) { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 49d324220..e0cea510e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -21,7 +21,7 @@ use sqlparser::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionKind}; use sqlparser::ast::helpers::stmt_data_loading::{ - AlterStageOperation, StageLoadSelectItem, StageLoadSelectItemKind, + AlterFileFormatOperation, AlterStageOperation, StageLoadSelectItem, StageLoadSelectItemKind, }; use sqlparser::ast::*; use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect}; @@ -5567,3 +5567,103 @@ fn test_structured_object_type_errors() { assert!(snowflake().parse_sql_statements(sql).is_err(), "{sql}"); } } + +#[test] +fn test_named_file_format_lifecycle() { + let rename_sql = + "ALTER FILE FORMAT IF EXISTS analytics.raw.csv_format RENAME TO archived_format"; + match snowflake().verified_stmt(rename_sql) { + Statement::AlterFileFormat { + if_exists, + name, + operation: AlterFileFormatOperation::RenameTo { new_name }, + } => { + assert!(if_exists); + assert_eq!("analytics.raw.csv_format", name.to_string()); + assert_eq!("archived_format", new_name.to_string()); + } + statement => panic!("unexpected statement: {statement:?}"), + } + + let set_sql = concat!( + "ALTER FILE FORMAT analytics.raw.csv_format SET ", + "COMPRESSION=GZIP FIELD_DELIMITER='|' SKIP_HEADER=1 COMMENT='updated'" + ); + match snowflake().verified_stmt(set_sql) { + Statement::AlterFileFormat { + if_exists, + name, + operation: AlterFileFormatOperation::Set { options, comment }, + } => { + assert!(!if_exists); + assert_eq!("analytics.raw.csv_format", name.to_string()); + assert_eq!(3, options.options.len()); + assert_eq!(Some("updated"), comment.as_deref()); + } + statement => panic!("unexpected statement: {statement:?}"), + } + + match snowflake().verified_stmt("SHOW FILE FORMATS LIKE 'csv%' IN SCHEMA analytics.raw") { + Statement::ShowFileFormats { show_options } => { + assert!(matches!( + show_options.filter_position, + Some(ShowStatementFilterPosition::Infix( + ShowStatementFilter::Like(ref pattern) + )) if pattern == "csv%" + )); + assert!(matches!( + show_options.show_in, + Some(ShowStatementIn { + parent_type: Some(ShowStatementInParentType::Schema), + parent_name: Some(ref name), + .. + }) if name.to_string() == "analytics.raw" + )); + } + statement => panic!("unexpected statement: {statement:?}"), + } + + for sql in [ + "DESCRIBE FILE FORMAT analytics.raw.csv_format", + "DESC FILE FORMAT analytics.raw.csv_format", + ] { + match snowflake().verified_stmt(sql) { + Statement::DescribeFileFormat { + describe_alias, + name, + } => { + assert!(matches!( + describe_alias, + DescribeAlias::Describe | DescribeAlias::Desc + )); + assert_eq!("analytics.raw.csv_format", name.to_string()); + } + statement => panic!("unexpected statement: {statement:?}"), + } + } + + match snowflake().verified_stmt("DROP FILE FORMAT IF EXISTS analytics.raw.csv_format") { + Statement::Drop { + object_type, + if_exists, + names, + .. + } => { + assert_eq!(ObjectType::FileFormat, object_type); + assert!(if_exists); + assert_eq!("analytics.raw.csv_format", names[0].to_string()); + } + statement => panic!("unexpected statement: {statement:?}"), + } + + for invalid in [ + "ALTER FILE FORMAT analytics.raw.csv_format SET", + "ALTER FILE FORMAT analytics.raw.csv_format REFRESH", + "SHOW TERSE FILE FORMATS", + ] { + assert!( + snowflake().parse_sql_statements(invalid).is_err(), + "{invalid} should fail" + ); + } +}