From 176c57b7af45cecb6dccf63f557169592ea6d654 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Sat, 12 Sep 2026 12:07:52 +0300 Subject: [PATCH] Snowflake: parse CREATE FILE FORMAT --- src/ast/mod.rs | 41 +++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/dialect/snowflake.rs | 31 +++++++++++++++++++ tests/sqlparser_snowflake.rs | 58 ++++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0042a476a2..41f7861786 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4511,6 +4511,24 @@ pub enum Statement { comment: Option, }, /// ```sql + /// CREATE FILE FORMAT + /// ``` + /// See + CreateFileFormat { + /// `OR REPLACE` flag. + or_replace: bool, + /// Whether the file format is temporary or volatile. + temporary: bool, + /// `IF NOT EXISTS` flag. + if_not_exists: bool, + /// File format name. + name: ObjectName, + /// Format type and format-specific options. + options: KeyValueOptions, + /// Optional comment. + comment: Option, + }, + /// ```sql /// ASSERT [AS ] /// ``` Assert { @@ -6228,6 +6246,29 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::CreateFileFormat { + or_replace, + temporary, + if_not_exists, + name, + options, + comment, + } => { + write!( + f, + "CREATE {or_replace}{temporary}FILE FORMAT {if_not_exists}{name}", + or_replace = if *or_replace { "OR REPLACE " } else { "" }, + temporary = if *temporary { "TEMPORARY " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + )?; + if !options.options.is_empty() { + write!(f, " {options}")?; + } + if let Some(comment) = comment { + write!(f, " COMMENT='{comment}'")?; + } + Ok(()) + } Statement::CopyIntoSnowflake { kind, into, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 90fe079e98..b725897db2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -459,6 +459,7 @@ impl Spanned for Statement { Statement::CreateProcedure { .. } => Span::empty(), Statement::CreateMacro { .. } => Span::empty(), Statement::CreateStage { .. } => Span::empty(), + Statement::CreateFileFormat { .. } => Span::empty(), Statement::Assert { .. } => Span::empty(), Statement::Grant { .. } => Span::empty(), Statement::Deny { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 764c528299..99d7fe498d 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -328,6 +328,12 @@ impl Dialect for SnowflakeDialect { if parser.parse_keyword(Keyword::STAGE) { // OK - this is CREATE STAGE statement return Some(parse_create_stage(or_replace, temporary, parser)); + } else if parser.parse_keywords(&[Keyword::FILE, Keyword::FORMAT]) { + return Some(parse_create_file_format( + or_replace, + temporary || volatile, + parser, + )); } else if parser.parse_keyword(Keyword::TABLE) { return Some( parse_create_table( @@ -1426,6 +1432,31 @@ pub fn parse_create_stage( }) } +pub fn parse_create_file_format( + or_replace: bool, + temporary: 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)?; + 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 + }; + + Ok(Statement::CreateFileFormat { + or_replace, + temporary, + if_not_exists, + name, + options, + comment, + }) +} + pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result { let mut ident = String::new(); while let Some(next_token) = parser.next_token_no_skip() { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 920d168a42..2e537a71a1 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2149,6 +2149,64 @@ fn test_create_stage() { ); } +#[test] +fn test_create_file_format() { + let sql = "CREATE FILE FORMAT analytics.formats.parquet TYPE=PARQUET"; + match snowflake().verified_stmt(sql) { + Statement::CreateFileFormat { + or_replace, + temporary, + if_not_exists, + name, + options, + comment, + } => { + assert!(!or_replace); + assert!(!temporary); + assert!(!if_not_exists); + assert_eq!(name.to_string(), "analytics.formats.parquet"); + assert_eq!(options.options.len(), 1); + assert!(comment.is_none()); + } + _ => unreachable!(), + } + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + let sql = "CREATE FILE FORMAT IF NOT EXISTS existing_format TYPE=JSON"; + match snowflake().verified_stmt(sql) { + Statement::CreateFileFormat { if_not_exists, .. } => assert!(if_not_exists), + _ => unreachable!(), + } + + let sql = concat!( + "CREATE OR REPLACE TEMPORARY FILE FORMAT csv_format ", + "TYPE=CSV FIELD_DELIMITER='|' NULL_IF=('NULL', 'null') ", + "COMMENT='pipe-delimited input'" + ); + match snowflake().verified_stmt(sql) { + Statement::CreateFileFormat { + or_replace, + temporary, + if_not_exists, + name, + options, + comment, + } => { + assert!(or_replace); + assert!(temporary); + assert!(!if_not_exists); + assert_eq!(name.to_string(), "csv_format"); + assert_eq!(options.options.len(), 3); + assert_eq!(comment.as_deref(), Some("pipe-delimited input")); + } + _ => unreachable!(), + } + assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + + let sql = "CREATE VOLATILE FILE FORMAT json_format TYPE=JSON"; + snowflake().one_statement_parses_to(sql, "CREATE TEMPORARY FILE FORMAT json_format TYPE=JSON"); +} + #[test] fn test_create_stage_with_stage_params() { let sql = concat!(