Skip to content

Commit 0d5f4c0

Browse files
authored
Snowflake: Add support for CREATE FILE FORMAT (#2336)
1 parent 2705a7d commit 0d5f4c0

4 files changed

Lines changed: 215 additions & 0 deletions

File tree

src/ast/mod.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4487,6 +4487,28 @@ pub enum Statement {
44874487
comment: Option<String>,
44884488
},
44894489
/// ```sql
4490+
/// CREATE [ OR REPLACE ] [ { TEMP | TEMPORARY | VOLATILE } ] FILE FORMAT [ IF NOT EXISTS ] <name>
4491+
/// [ TYPE = { CSV | JSON | AVRO | ORC | PARQUET | XML } [ formatTypeOptions ] ]
4492+
/// [ COMMENT = '<string_literal>' ]
4493+
/// ```
4494+
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-file-format>
4495+
CreateFileFormat {
4496+
/// `OR REPLACE` flag.
4497+
or_replace: bool,
4498+
/// Whether file format is temporary.
4499+
temporary: bool,
4500+
/// Whether file format is volatile.
4501+
volatile: bool,
4502+
/// `IF NOT EXISTS` flag.
4503+
if_not_exists: bool,
4504+
/// File format name.
4505+
name: ObjectName,
4506+
/// Format type options (e.g. `TYPE`, `FIELD_DELIMITER`, `COMPRESSION`, ...).
4507+
options: KeyValueOptions,
4508+
/// Optional comment.
4509+
comment: Option<String>,
4510+
},
4511+
/// ```sql
44904512
/// ASSERT <condition> [AS <message>]
44914513
/// ```
44924514
Assert {
@@ -6185,6 +6207,31 @@ impl fmt::Display for Statement {
61856207
}
61866208
Ok(())
61876209
}
6210+
Statement::CreateFileFormat {
6211+
or_replace,
6212+
temporary,
6213+
volatile,
6214+
if_not_exists,
6215+
name,
6216+
options,
6217+
comment,
6218+
} => {
6219+
write!(
6220+
f,
6221+
"CREATE {or_replace}{temp}{volatile}FILE FORMAT {if_not_exists}{name}",
6222+
or_replace = if *or_replace { "OR REPLACE " } else { "" },
6223+
temp = if *temporary { "TEMPORARY " } else { "" },
6224+
volatile = if *volatile { "VOLATILE " } else { "" },
6225+
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
6226+
)?;
6227+
if !options.options.is_empty() {
6228+
write!(f, " {options}")?;
6229+
}
6230+
if let Some(comment) = comment {
6231+
write!(f, " COMMENT='{}'", comment)?;
6232+
}
6233+
Ok(())
6234+
}
61886235
Statement::CopyIntoSnowflake {
61896236
kind,
61906237
into,

src/ast/spans.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ impl Spanned for Values {
297297
/// - [Statement::CreateProcedure]
298298
/// - [Statement::CreateMacro]
299299
/// - [Statement::CreateStage]
300+
/// - [Statement::CreateFileFormat]
300301
/// - [Statement::Assert]
301302
/// - [Statement::Grant]
302303
/// - [Statement::Revoke]
@@ -457,6 +458,7 @@ impl Spanned for Statement {
457458
Statement::CreateProcedure { .. } => Span::empty(),
458459
Statement::CreateMacro { .. } => Span::empty(),
459460
Statement::CreateStage { .. } => Span::empty(),
461+
Statement::CreateFileFormat { .. } => Span::empty(),
460462
Statement::Assert { .. } => Span::empty(),
461463
Statement::Grant { .. } => Span::empty(),
462464
Statement::Deny { .. } => Span::empty(),

src/dialect/snowflake.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,10 @@ impl Dialect for SnowflakeDialect {
326326
);
327327
} else if parser.parse_keyword(Keyword::DATABASE) {
328328
return Some(parse_create_database(or_replace, transient, parser));
329+
} else if parser.parse_keywords(&[Keyword::FILE, Keyword::FORMAT]) {
330+
return Some(parse_create_file_format(
331+
or_replace, temporary, volatile, parser,
332+
));
329333
} else {
330334
// need to go back with the cursor
331335
let mut back = 1;
@@ -1272,6 +1276,35 @@ pub fn parse_create_stage(
12721276
})
12731277
}
12741278

1279+
/// Parse a Snowflake `CREATE FILE FORMAT` statement.
1280+
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-file-format>
1281+
pub fn parse_create_file_format(
1282+
or_replace: bool,
1283+
temporary: bool,
1284+
volatile: bool,
1285+
parser: &mut Parser,
1286+
) -> Result<Statement, ParserError> {
1287+
let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
1288+
let name = parser.parse_object_name(true)?;
1289+
let options = parser.parse_key_value_options(false, &[Keyword::COMMENT])?;
1290+
let comment = if parser.parse_keyword(Keyword::COMMENT) {
1291+
parser.expect_token(&Token::Eq)?;
1292+
Some(parser.parse_comment_value()?)
1293+
} else {
1294+
None
1295+
};
1296+
1297+
Ok(Statement::CreateFileFormat {
1298+
or_replace,
1299+
temporary,
1300+
volatile,
1301+
if_not_exists,
1302+
name,
1303+
options,
1304+
comment,
1305+
})
1306+
}
1307+
12751308
pub fn parse_stage_name_identifier(parser: &mut Parser) -> Result<Ident, ParserError> {
12761309
let mut ident = String::new();
12771310
while let Some(next_token) = parser.next_token_no_skip() {

tests/sqlparser_snowflake.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,16 @@ fn test_snowflake_create_invalid_temporal_table() {
479479
);
480480
}
481481

482+
#[test]
483+
fn test_snowflake_create_invalid_temporal_file_format() {
484+
assert_eq!(
485+
snowflake().parse_sql_statements("CREATE TEMPORARY VOLATILE FILE FORMAT my_fmt"),
486+
Err(ParserError::ParserError(
487+
"Expected: an object type after CREATE, found: FILE".to_string()
488+
))
489+
);
490+
}
491+
482492
#[test]
483493
fn test_snowflake_create_table_if_not_exists() {
484494
match snowflake().verified_stmt("CREATE TABLE IF NOT EXISTS my_table (a INT)") {
@@ -2160,6 +2170,129 @@ fn test_create_stage_with_copy_options() {
21602170
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
21612171
}
21622172

2173+
#[test]
2174+
fn test_create_file_format() {
2175+
let sql = "CREATE FILE FORMAT my_fmt";
2176+
match snowflake().verified_stmt(sql) {
2177+
Statement::CreateFileFormat {
2178+
or_replace,
2179+
temporary,
2180+
volatile,
2181+
if_not_exists,
2182+
name,
2183+
options,
2184+
comment,
2185+
} => {
2186+
assert!(!or_replace);
2187+
assert!(!temporary);
2188+
assert!(!volatile);
2189+
assert!(!if_not_exists);
2190+
assert_eq!("my_fmt", name.to_string());
2191+
assert!(options.options.is_empty());
2192+
assert!(comment.is_none());
2193+
}
2194+
_ => unreachable!(),
2195+
};
2196+
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
2197+
2198+
let extended_sql = concat!(
2199+
"CREATE OR REPLACE TEMPORARY FILE FORMAT IF NOT EXISTS my_fmt ",
2200+
"COMMENT='some-comment'"
2201+
);
2202+
match snowflake().verified_stmt(extended_sql) {
2203+
Statement::CreateFileFormat {
2204+
or_replace,
2205+
temporary,
2206+
if_not_exists,
2207+
name,
2208+
comment,
2209+
..
2210+
} => {
2211+
assert!(or_replace);
2212+
assert!(temporary);
2213+
assert!(if_not_exists);
2214+
assert_eq!("my_fmt", name.to_string());
2215+
assert_eq!("some-comment", comment.unwrap());
2216+
}
2217+
_ => unreachable!(),
2218+
};
2219+
assert_eq!(
2220+
snowflake().verified_stmt(extended_sql).to_string(),
2221+
extended_sql
2222+
);
2223+
}
2224+
2225+
#[test]
2226+
fn test_create_file_format_with_options() {
2227+
let sql = concat!(
2228+
"CREATE FILE FORMAT my_fmt ",
2229+
"TYPE=CSV FIELD_DELIMITER='|' SKIP_HEADER=1 COMPRESSION=GZIP"
2230+
);
2231+
match snowflake().verified_stmt(sql) {
2232+
Statement::CreateFileFormat { options, .. } => {
2233+
assert!(options.options.contains(&KeyValueOption {
2234+
option_name: "TYPE".to_string(),
2235+
option_value: KeyValueOptionKind::Single(
2236+
Value::Placeholder("CSV".to_string()).with_empty_span()
2237+
),
2238+
}));
2239+
assert!(options.options.contains(&KeyValueOption {
2240+
option_name: "FIELD_DELIMITER".to_string(),
2241+
option_value: KeyValueOptionKind::Single(
2242+
Value::SingleQuotedString("|".to_string()).with_empty_span()
2243+
),
2244+
}));
2245+
assert!(options.options.contains(&KeyValueOption {
2246+
option_name: "SKIP_HEADER".to_string(),
2247+
option_value: KeyValueOptionKind::Single(
2248+
Value::Number("1".parse().unwrap(), false).with_empty_span()
2249+
),
2250+
}));
2251+
assert!(options.options.contains(&KeyValueOption {
2252+
option_name: "COMPRESSION".to_string(),
2253+
option_value: KeyValueOptionKind::Single(
2254+
Value::Placeholder("GZIP".to_string()).with_empty_span()
2255+
),
2256+
}));
2257+
}
2258+
_ => unreachable!(),
2259+
};
2260+
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
2261+
}
2262+
2263+
#[test]
2264+
fn test_create_file_format_volatile() {
2265+
let sql = "CREATE VOLATILE FILE FORMAT my_fmt TYPE=JSON STRIP_OUTER_ARRAY=true";
2266+
match snowflake().verified_stmt(sql) {
2267+
Statement::CreateFileFormat {
2268+
temporary,
2269+
volatile,
2270+
options,
2271+
..
2272+
} => {
2273+
assert!(!temporary);
2274+
assert!(volatile);
2275+
assert!(options.options.contains(&KeyValueOption {
2276+
option_name: "STRIP_OUTER_ARRAY".to_string(),
2277+
option_value: KeyValueOptionKind::Single(Value::Boolean(true).with_empty_span()),
2278+
}));
2279+
}
2280+
_ => unreachable!(),
2281+
};
2282+
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
2283+
}
2284+
2285+
#[test]
2286+
fn test_create_file_format_with_identifier_function() {
2287+
// The Snowflake driver emits `CREATE TEMP FILE FORMAT identifier(?) ...` when
2288+
// uploading pandas DataFrames. `TEMP` is an alias of `TEMPORARY` and the name
2289+
// is a call to the `IDENTIFIER` function with a bind parameter.
2290+
snowflake().one_statement_parses_to(
2291+
"CREATE TEMP FILE FORMAT identifier(?) TYPE=PARQUET COMPRESSION=auto",
2292+
"CREATE TEMPORARY FILE FORMAT identifier(?) TYPE=PARQUET COMPRESSION=auto",
2293+
);
2294+
}
2295+
21632296
#[test]
21642297
fn test_copy_into() {
21652298
let sql = concat!(

0 commit comments

Comments
 (0)