Skip to content

Commit c96cee9

Browse files
wojpadloWojciech Padlo
andauthored
Snowflake: FILE_FORMAT = <name> shorthand in COPY INTO (#11)
* Snowflake: FILE_FORMAT = <name> shorthand in COPY INTO * Snowflake: test FILE_FORMAT = <name> shorthand; fix pre-existing rustfmt Add a parser test covering the FILE_FORMAT = <name> COPY INTO shorthand (unquoted ident, quoted string, and the unaffected parenthesized form). Also reflow a pre-existing unformatted block in parser/mod.rs so 'cargo fmt --all -- --check' (CI codestyle) passes. --------- Co-authored-by: Wojciech Padlo <wojciech.padlo@localstack.cloud>
1 parent f15c92a commit c96cee9

3 files changed

Lines changed: 107 additions & 7 deletions

File tree

src/dialect/snowflake.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1623,7 +1623,25 @@ pub fn parse_copy_into(parser: &mut Parser) -> Result<Statement, ParserError> {
16231623
// FILE_FORMAT
16241624
if parser.parse_keyword(Keyword::FILE_FORMAT) {
16251625
parser.expect_token(&Token::Eq)?;
1626-
file_format = parser.parse_key_value_options(true, &[])?.options;
1626+
if parser.peek_token().token == Token::LParen {
1627+
file_format = parser.parse_key_value_options(true, &[])?.options;
1628+
} else {
1629+
// Shorthand `FILE_FORMAT = '<name>'` / `FILE_FORMAT = <ident>`
1630+
// is sugar for `FILE_FORMAT = (FORMAT_NAME = <name>)` —
1631+
// normalize it (mirrors CREATE STAGE).
1632+
let tok = parser.peek_token();
1633+
let value = match tok.token {
1634+
Token::Word(w) => {
1635+
parser.next_token();
1636+
Value::Placeholder(w.value.clone()).with_span(tok.span)
1637+
}
1638+
_ => parser.parse_value()?,
1639+
};
1640+
file_format = vec![KeyValueOption {
1641+
option_name: "FORMAT_NAME".to_string(),
1642+
option_value: KeyValueOptionKind::Single(value),
1643+
}];
1644+
}
16271645
// PARTITION BY
16281646
} else if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) {
16291647
partition = Some(Box::new(parser.parse_expr()?))

src/parser/mod.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,16 +1886,21 @@ impl<'a> Parser<'a> {
18861886
// callee name (an `ObjectNamePart::Function`) and the trailing
18871887
// `(args)` is the actual call — mirror the object-name grammar
18881888
// so the resulting `Expr::Function` matches the CALL path shape.
1889-
if self.dialect.is_identifier_generating_function_name(&ident, &[]) {
1889+
if self
1890+
.dialect
1891+
.is_identifier_generating_function_name(&ident, &[])
1892+
{
18901893
let checkpoint = self.index;
18911894
self.expect_token(&Token::LParen)?;
1892-
let args = self
1893-
.parse_comma_separated0(Self::parse_function_args, Token::RParen)?;
1895+
let args =
1896+
self.parse_comma_separated0(Self::parse_function_args, Token::RParen)?;
18941897
self.expect_token(&Token::RParen)?;
18951898
if self.peek_token_ref().token == Token::LParen {
1896-
let name = ObjectName(vec![ObjectNamePart::Function(
1897-
ObjectNamePartFunction { name: ident, args },
1898-
)]);
1899+
let name =
1900+
ObjectName(vec![ObjectNamePart::Function(ObjectNamePartFunction {
1901+
name: ident,
1902+
args,
1903+
})]);
18991904
return self.parse_function(name);
19001905
}
19011906
self.index = checkpoint;

tests/sqlparser_snowflake.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2675,6 +2675,83 @@ fn test_copy_into_file_format() {
26752675
}
26762676
}
26772677

2678+
#[test]
2679+
fn test_copy_into_file_format_name_shorthand() {
2680+
// Bare `FILE_FORMAT = <name>` is sugar for
2681+
// `FILE_FORMAT = (FORMAT_NAME = <name>)`, for both the location-unload and
2682+
// table-load forms. An unquoted identifier parses to a `Placeholder`.
2683+
for sql in [
2684+
"COPY INTO @my_stage/out.csv FROM (SELECT * FROM t) FILE_FORMAT = my_format",
2685+
"COPY INTO my_table FROM @my_stage FILE_FORMAT = my_format",
2686+
] {
2687+
match snowflake()
2688+
.parse_sql_statements(sql)
2689+
.unwrap()
2690+
.pop()
2691+
.unwrap()
2692+
{
2693+
Statement::CopyIntoSnowflake { file_format, .. } => {
2694+
assert_eq!(
2695+
file_format.options,
2696+
vec![KeyValueOption {
2697+
option_name: "FORMAT_NAME".to_string(),
2698+
option_value: KeyValueOptionKind::Single(
2699+
Value::Placeholder("my_format".to_string()).with_empty_span()
2700+
),
2701+
}]
2702+
);
2703+
}
2704+
_ => unreachable!(),
2705+
}
2706+
}
2707+
2708+
// A quoted name parses to a `SingleQuotedString`.
2709+
match snowflake()
2710+
.parse_sql_statements(
2711+
"COPY INTO @my_stage/out.csv FROM (SELECT * FROM t) FILE_FORMAT = 'my_format'",
2712+
)
2713+
.unwrap()
2714+
.pop()
2715+
.unwrap()
2716+
{
2717+
Statement::CopyIntoSnowflake { file_format, .. } => {
2718+
assert_eq!(
2719+
file_format.options,
2720+
vec![KeyValueOption {
2721+
option_name: "FORMAT_NAME".to_string(),
2722+
option_value: KeyValueOptionKind::Single(
2723+
Value::SingleQuotedString("my_format".to_string()).with_empty_span()
2724+
),
2725+
}]
2726+
);
2727+
}
2728+
_ => unreachable!(),
2729+
}
2730+
2731+
// The parenthesized form is unaffected.
2732+
match snowflake()
2733+
.parse_sql_statements(
2734+
"COPY INTO @my_stage/out.csv FROM (SELECT * FROM t) FILE_FORMAT = (TYPE = 'CSV')",
2735+
)
2736+
.unwrap()
2737+
.pop()
2738+
.unwrap()
2739+
{
2740+
Statement::CopyIntoSnowflake { file_format, .. } => {
2741+
assert_eq!(
2742+
file_format.options,
2743+
vec![KeyValueOption {
2744+
option_name: "TYPE".to_string(),
2745+
option_value: KeyValueOptionKind::Single(
2746+
Value::SingleQuotedString("CSV".to_string()).with_empty_span()
2747+
),
2748+
}]
2749+
);
2750+
}
2751+
_ => unreachable!(),
2752+
}
2753+
}
2754+
26782755
#[test]
26792756
fn test_copy_into_copy_options() {
26802757
let sql = concat!(

0 commit comments

Comments
 (0)