From 932a61d0efe846607bdd02e9528f21b5a1dee5dc Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Wed, 18 Feb 2026 17:24:11 -0800 Subject: [PATCH 1/3] Extended the Snowflake COPY INTO data-load select item parser to support nested semi-structured path traversal --- src/ast/helpers/stmt_data_loading.rs | 11 +++-- src/dialect/snowflake.rs | 33 +++++--------- src/parser/mod.rs | 2 +- tests/sqlparser_snowflake.rs | 68 +++++++++++++++++++++++++++- 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index dfc1f4b0bf..6c34b3e47e 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -78,8 +78,9 @@ pub struct StageLoadSelectItem { pub alias: Option, /// Column number within the staged file (1-based). pub file_col_num: i32, - /// Optional element identifier following the column reference. - pub element: Option, + /// Optional semi-structured element path following the column reference + /// (e.g. `$1:UsageMetrics:hh` produces `["UsageMetrics", "hh"]`). + pub element: Option>, /// Optional alias for the item (AS clause). pub item_as: Option, } @@ -116,8 +117,10 @@ impl fmt::Display for StageLoadSelectItem { write!(f, "{alias}.")?; } write!(f, "${}", self.file_col_num)?; - if let Some(element) = &self.element { - write!(f, ":{element}")?; + if let Some(elements) = &self.element { + for element in elements { + write!(f, ":{element}")?; + } } if let Some(item_as) = &self.item_as { write!(f, " AS {item_as}")?; diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index f756c4159e..28b4e6041e 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1470,8 +1470,7 @@ fn parse_select_item_for_data_load( ) -> Result { let mut alias: Option = None; let mut file_col_num: i32 = 0; - let mut element: Option = None; - let mut item_as: Option = None; + let mut element: Option> = None; let next_token = parser.next_token(); match next_token.token { @@ -1503,29 +1502,21 @@ fn parse_select_item_for_data_load( }?; } - // try extracting optional element - match parser.next_token().token { - Token::Colon => { - // parse element - element = Some(Ident::new(match parser.next_token().token { - Token::Word(w) => Ok(w.value), - _ => parser.expected_ref("file_col_num", parser.peek_token_ref()), - }?)); - } - _ => { - // element not present move back - parser.prev_token(); + // try extracting optional element path (e.g. :UsageMetrics:hh) + let mut elements = Vec::new(); + while parser.next_token().token == Token::Colon { + match parser.next_token().token { + Token::Word(w) => elements.push(Ident::new(w.value)), + _ => return parser.expected_ref("element name", parser.peek_token_ref()), } } - - // as - if parser.parse_keyword(Keyword::AS) { - item_as = Some(match parser.next_token().token { - Token::Word(w) => Ok(Ident::new(w.value)), - _ => parser.expected_ref("column item alias", parser.peek_token_ref()), - }?); + parser.prev_token(); + if !elements.is_empty() { + element = Some(elements); } + let item_as = parser.maybe_parse_select_item_alias()?; + Ok(StageLoadSelectItem { alias, file_col_num, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 75b5bfa762..9527d7eed7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12314,7 +12314,7 @@ impl<'a> Parser<'a> { } /// Optionally parses an alias for a select list item - fn maybe_parse_select_item_alias(&mut self) -> Result, ParserError> { + pub fn maybe_parse_select_item_alias(&mut self) -> Result, ParserError> { fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { parser.dialect.is_select_item_alias(explicit, kw, parser) } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c51cf3bdf1..b8190f1b46 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2369,7 +2369,7 @@ fn test_copy_into_with_transformations() { StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { alias: Some(Ident::new("t1")), file_col_num: 1, - element: Some(Ident::new("st")), + element: Some(vec![Ident::new("st")]), item_as: Some(Ident::new("st")) }) ); @@ -2378,7 +2378,7 @@ fn test_copy_into_with_transformations() { StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { alias: None, file_col_num: 1, - element: Some(Ident::new("index")), + element: Some(vec![Ident::new("index")]), item_as: None }) ); @@ -2640,6 +2640,70 @@ fn test_snowflake_copy_into_stage_name_ends_with_parens() { } } +#[test] +fn test_copy_into_with_nested_colon_path() { + // Nested colon path with explicit AS alias + let sql = "COPY INTO tbl (col) FROM (SELECT $1:a:b AS col FROM @stage)"; + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + from_transformations, + .. + } => { + assert_eq!( + from_transformations.as_ref().unwrap()[0], + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { + alias: None, + file_col_num: 1, + element: Some(vec![Ident::new("a"), Ident::new("b")]), + item_as: Some(Ident::new("col")) + }) + ); + } + _ => unreachable!(), + } + + // Nested colon path with implicit alias (no AS keyword) + let sql = "COPY INTO tbl (col) FROM (SELECT $1:a:b col FROM @stage)"; + let stmts = snowflake().parse_sql_statements(sql).unwrap(); + match &stmts[0] { + Statement::CopyIntoSnowflake { + from_transformations, + .. + } => { + assert_eq!( + from_transformations.as_ref().unwrap()[0], + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { + alias: None, + file_col_num: 1, + element: Some(vec![Ident::new("a"), Ident::new("b")]), + item_as: Some(Ident::new("col")) + }) + ); + } + _ => unreachable!(), + } + + // Nested colon path with no alias + let sql = "COPY INTO tbl FROM (SELECT $1:a:b FROM @stage)"; + match snowflake().verified_stmt(sql) { + Statement::CopyIntoSnowflake { + from_transformations, + .. + } => { + assert_eq!( + from_transformations.as_ref().unwrap()[0], + StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem { + alias: None, + file_col_num: 1, + element: Some(vec![Ident::new("a"), Ident::new("b")]), + item_as: None + }) + ); + } + _ => unreachable!(), + } +} + #[test] fn test_snowflake_trim() { let real_sql = r#"SELECT customer_id, TRIM(sub_items.value:item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; From e4f760fb0fc0674f62f7015ddebe6985bbec97e1 Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Thu, 19 Feb 2026 09:53:27 -0800 Subject: [PATCH 2/3] Cargo check fix --- src/ast/helpers/stmt_data_loading.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index 6c34b3e47e..5cbb0d62a1 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs @@ -21,6 +21,8 @@ #[cfg(not(feature = "std"))] use alloc::string::String; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; use core::fmt; #[cfg(feature = "serde")] From df6b53ce624091ee7daf64ee5b32b748c45a8ceb Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Wed, 4 Mar 2026 13:54:02 -0800 Subject: [PATCH 3/3] Fix based on comments --- src/dialect/snowflake.rs | 4 ++-- src/parser/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 28b4e6041e..4db5548ee6 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1504,13 +1504,13 @@ fn parse_select_item_for_data_load( // try extracting optional element path (e.g. :UsageMetrics:hh) let mut elements = Vec::new(); - while parser.next_token().token == Token::Colon { + while parser.peek_token_ref().token == Token::Colon { + parser.next_token(); // consume the colon match parser.next_token().token { Token::Word(w) => elements.push(Ident::new(w.value)), _ => return parser.expected_ref("element name", parser.peek_token_ref()), } } - parser.prev_token(); if !elements.is_empty() { element = Some(elements); } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9527d7eed7..c3968dab4b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12314,7 +12314,7 @@ impl<'a> Parser<'a> { } /// Optionally parses an alias for a select list item - pub fn maybe_parse_select_item_alias(&mut self) -> Result, ParserError> { + pub(crate) fn maybe_parse_select_item_alias(&mut self) -> Result, ParserError> { fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { parser.dialect.is_select_item_alias(explicit, kw, parser) }