Skip to content

Commit 8c19978

Browse files
committed
fix(snowflake): fall through to generic select item on cast in COPY INTO transformation
1 parent 9833c03 commit 8c19978

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

src/dialect/snowflake.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,14 @@ fn parse_select_item_for_data_load(
15451545
}
15461546
}
15471547

1548+
// A trailing `::` means this is a cast expression (e.g.
1549+
// `$1:"col"::NUMBER(38,0)`), not a stage-load-select-item. Bail so
1550+
// `maybe_parse` rewinds and the caller falls through to
1551+
// `parse_select_item`, which handles the cast correctly.
1552+
if matches!(parser.peek_token_ref().token, Token::DoubleColon) {
1553+
return parser.expected("stage load select item", parser.peek_token());
1554+
}
1555+
15481556
// as
15491557
if parser.parse_keyword(Keyword::AS) {
15501558
item_as = Some(match parser.next_token().token {

tests/sqlparser_snowflake.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2447,6 +2447,49 @@ fn test_copy_into_with_transformations() {
24472447
snowflake().parse_sql_statements(sql1).unwrap();
24482448
}
24492449

2450+
#[test]
2451+
fn test_copy_into_with_cast_transformation() {
2452+
// Snowflake `COPY INTO` transformation lists support casts like
2453+
// `$1:"col"::TYPE` and `$1::TYPE`. These are distinct from the bare
2454+
// `$<n>[.<col>][:<elem>]` stage-load-select-item shape, so the parser
2455+
// must fall through to the generic select-item parser to handle the
2456+
// `::` cast operator.
2457+
//
2458+
// Regression: the Snowflake-specific parser used to successfully
2459+
// consume `$1:"col"` as a stage-load-select-item, leaving `::TYPE`
2460+
// behind and causing "Expected: FROM, found: ::" once the COPY INTO
2461+
// body tried to expect `FROM`.
2462+
let variants = [
2463+
concat!(
2464+
"COPY INTO my_company.emp_basic (a) FROM ",
2465+
r#"(SELECT $1:"A"::NUMBER(38, 0) FROM @stg)"#,
2466+
),
2467+
concat!(
2468+
"COPY INTO my_company.emp_basic (a) FROM ",
2469+
"(SELECT $1::NUMBER(38, 0) FROM @stg)",
2470+
),
2471+
concat!(
2472+
"COPY INTO my_company.emp_basic (a) FROM ",
2473+
"(SELECT $1:SEQUENCE::NUMBER(38, 0) FROM @stg)",
2474+
),
2475+
concat!(
2476+
"COPY INTO my_company.emp_basic (a, b) FROM ",
2477+
r#"(SELECT $1:"A"::VARIANT, $1:"B"::TEXT FROM @stg)"#,
2478+
),
2479+
// Mix with an ordinary stage-load-select-item in the same list,
2480+
// so we don't over-correct and break the existing shape.
2481+
concat!(
2482+
"COPY INTO my_company.emp_basic (a, b) FROM ",
2483+
r#"(SELECT t.$1:plain AS plain, $1:"B"::TEXT FROM @stg AS t)"#,
2484+
),
2485+
];
2486+
for sql in variants {
2487+
snowflake().parse_sql_statements(sql).unwrap_or_else(|e| {
2488+
panic!("expected {sql:?} to parse, got {e}");
2489+
});
2490+
}
2491+
}
2492+
24502493
#[test]
24512494
fn test_copy_into_file_format() {
24522495
let sql = concat!(

0 commit comments

Comments
 (0)