From 8c199788c67bd7433ec233f0d0cf629ae58ffda2 Mon Sep 17 00:00:00 2001 From: Corey Fritz Date: Tue, 21 Apr 2026 22:19:19 -0600 Subject: [PATCH 1/3] fix(snowflake): fall through to generic select item on cast in COPY INTO transformation --- src/dialect/snowflake.rs | 8 +++++++ tests/sqlparser_snowflake.rs | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 1ac21d007..ea295cf3e 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1545,6 +1545,14 @@ fn parse_select_item_for_data_load( } } + // A trailing `::` means this is a cast expression (e.g. + // `$1:"col"::NUMBER(38,0)`), not a stage-load-select-item. Bail so + // `maybe_parse` rewinds and the caller falls through to + // `parse_select_item`, which handles the cast correctly. + if matches!(parser.peek_token_ref().token, Token::DoubleColon) { + return parser.expected("stage load select item", parser.peek_token()); + } + // as if parser.parse_keyword(Keyword::AS) { item_as = Some(match parser.next_token().token { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 790bf1515..a2e4a9674 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2447,6 +2447,49 @@ fn test_copy_into_with_transformations() { snowflake().parse_sql_statements(sql1).unwrap(); } +#[test] +fn test_copy_into_with_cast_transformation() { + // Snowflake `COPY INTO` transformation lists support casts like + // `$1:"col"::TYPE` and `$1::TYPE`. These are distinct from the bare + // `$[.][:]` stage-load-select-item shape, so the parser + // must fall through to the generic select-item parser to handle the + // `::` cast operator. + // + // Regression: the Snowflake-specific parser used to successfully + // consume `$1:"col"` as a stage-load-select-item, leaving `::TYPE` + // behind and causing "Expected: FROM, found: ::" once the COPY INTO + // body tried to expect `FROM`. + let variants = [ + concat!( + "COPY INTO my_company.emp_basic (a) FROM ", + r#"(SELECT $1:"A"::NUMBER(38, 0) FROM @stg)"#, + ), + concat!( + "COPY INTO my_company.emp_basic (a) FROM ", + "(SELECT $1::NUMBER(38, 0) FROM @stg)", + ), + concat!( + "COPY INTO my_company.emp_basic (a) FROM ", + "(SELECT $1:SEQUENCE::NUMBER(38, 0) FROM @stg)", + ), + concat!( + "COPY INTO my_company.emp_basic (a, b) FROM ", + r#"(SELECT $1:"A"::VARIANT, $1:"B"::TEXT FROM @stg)"#, + ), + // Mix with an ordinary stage-load-select-item in the same list, + // so we don't over-correct and break the existing shape. + concat!( + "COPY INTO my_company.emp_basic (a, b) FROM ", + r#"(SELECT t.$1:plain AS plain, $1:"B"::TEXT FROM @stg AS t)"#, + ), + ]; + for sql in variants { + snowflake().parse_sql_statements(sql).unwrap_or_else(|e| { + panic!("expected {sql:?} to parse, got {e}"); + }); + } +} + #[test] fn test_copy_into_file_format() { let sql = concat!( From 32a34701f76d35f4a2fb81a648f0db49ffdb7e00 Mon Sep 17 00:00:00 2001 From: Corey Fritz Date: Wed, 29 Apr 2026 09:42:30 -0600 Subject: [PATCH 2/3] Trim parser comment for COPY INTO cast bail --- src/dialect/snowflake.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index ea295cf3e..fda5b7b97 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1546,9 +1546,7 @@ fn parse_select_item_for_data_load( } // A trailing `::` means this is a cast expression (e.g. - // `$1:"col"::NUMBER(38,0)`), not a stage-load-select-item. Bail so - // `maybe_parse` rewinds and the caller falls through to - // `parse_select_item`, which handles the cast correctly. + // `$1:"col"::NUMBER(38,0)`), not a stage-load-select-item. if matches!(parser.peek_token_ref().token, Token::DoubleColon) { return parser.expected("stage load select item", parser.peek_token()); } From d9f0b2b17464464c3571bc6ea1ab1ecf62a6b5b0 Mon Sep 17 00:00:00 2001 From: Corey Fritz Date: Wed, 29 Apr 2026 09:43:06 -0600 Subject: [PATCH 3/3] Use verified_stmt and drop comments in COPY INTO cast test --- tests/sqlparser_snowflake.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a2e4a9674..32a55d500 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2449,16 +2449,6 @@ fn test_copy_into_with_transformations() { #[test] fn test_copy_into_with_cast_transformation() { - // Snowflake `COPY INTO` transformation lists support casts like - // `$1:"col"::TYPE` and `$1::TYPE`. These are distinct from the bare - // `$[.][:]` stage-load-select-item shape, so the parser - // must fall through to the generic select-item parser to handle the - // `::` cast operator. - // - // Regression: the Snowflake-specific parser used to successfully - // consume `$1:"col"` as a stage-load-select-item, leaving `::TYPE` - // behind and causing "Expected: FROM, found: ::" once the COPY INTO - // body tried to expect `FROM`. let variants = [ concat!( "COPY INTO my_company.emp_basic (a) FROM ", @@ -2476,17 +2466,13 @@ fn test_copy_into_with_cast_transformation() { "COPY INTO my_company.emp_basic (a, b) FROM ", r#"(SELECT $1:"A"::VARIANT, $1:"B"::TEXT FROM @stg)"#, ), - // Mix with an ordinary stage-load-select-item in the same list, - // so we don't over-correct and break the existing shape. concat!( "COPY INTO my_company.emp_basic (a, b) FROM ", r#"(SELECT t.$1:plain AS plain, $1:"B"::TEXT FROM @stg AS t)"#, ), ]; for sql in variants { - snowflake().parse_sql_statements(sql).unwrap_or_else(|e| { - panic!("expected {sql:?} to parse, got {e}"); - }); + snowflake().verified_stmt(sql); } }