Skip to content

Commit 56ad61e

Browse files
committed
Preserve zero-padded COPY staged placeholders
1 parent b08c229 commit 56ad61e

3 files changed

Lines changed: 60 additions & 7 deletions

File tree

‎src/ast/helpers/stmt_data_loading.rs‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ pub struct StageLoadSelectItem {
135135
pub alias: Option<Ident>,
136136
/// Column number within the staged file (1-based).
137137
pub file_col_num: i32,
138+
/// Original spelling when the staged placeholder differs from its numeric form.
139+
#[cfg_attr(
140+
feature = "serde",
141+
serde(default, skip_serializing_if = "Option::is_none")
142+
)]
143+
pub file_col_num_raw: Option<String>,
138144
/// Optional element identifier following the column reference.
139145
pub element: Option<Ident>,
140146
/// Optional alias for the item (AS clause).
@@ -172,7 +178,15 @@ impl fmt::Display for StageLoadSelectItem {
172178
if let Some(alias) = &self.alias {
173179
write!(f, "{alias}.")?;
174180
}
175-
write!(f, "${}", self.file_col_num)?;
181+
if let Some(raw) = self.file_col_num_raw.as_deref().filter(|raw| {
182+
raw.strip_prefix('$')
183+
.and_then(|digits| digits.parse::<i32>().ok())
184+
== Some(self.file_col_num)
185+
}) {
186+
write!(f, "{raw}")?;
187+
} else {
188+
write!(f, "${}", self.file_col_num)?;
189+
}
176190
if let Some(element) = &self.element {
177191
write!(f, ":{element}")?;
178192
}

‎src/dialect/snowflake.rs‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,15 +1773,14 @@ fn parse_select_item_for_data_load(
17731773
) -> Result<StageLoadSelectItem, ParserError> {
17741774
let mut alias: Option<Ident> = None;
17751775
let mut file_col_num: i32 = 0;
1776+
let mut file_col_num_raw: Option<String> = None;
17761777
let mut element: Option<Ident> = None;
17771778
let mut item_as: Option<Ident> = None;
17781779

17791780
let next_token = parser.next_token();
17801781
match next_token.token {
17811782
Token::Placeholder(w) => {
1782-
file_col_num = w.to_string().split_off(1).parse::<i32>().map_err(|e| {
1783-
ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}"))
1784-
})?;
1783+
(file_col_num, file_col_num_raw) = parse_stage_load_column_number(&w)?;
17851784
Ok(())
17861785
}
17871786
Token::Word(w) => {
@@ -1801,9 +1800,7 @@ fn parse_select_item_for_data_load(
18011800
let col_num_token = parser.next_token();
18021801
match col_num_token.token {
18031802
Token::Placeholder(w) => {
1804-
file_col_num = w.to_string().split_off(1).parse::<i32>().map_err(|e| {
1805-
ParserError::ParserError(format!("Could not parse '{w}' as i32: {e}"))
1806-
})?;
1803+
(file_col_num, file_col_num_raw) = parse_stage_load_column_number(&w)?;
18071804
Ok(())
18081805
}
18091806
_ => parser.expected("file_col_num", col_num_token),
@@ -1862,11 +1859,21 @@ fn parse_select_item_for_data_load(
18621859
Ok(StageLoadSelectItem {
18631860
alias,
18641861
file_col_num,
1862+
file_col_num_raw,
18651863
element,
18661864
item_as,
18671865
})
18681866
}
18691867

1868+
fn parse_stage_load_column_number(placeholder: &str) -> Result<(i32, Option<String>), ParserError> {
1869+
let digits = placeholder.strip_prefix('$').unwrap_or(placeholder);
1870+
let number = digits.parse::<i32>().map_err(|error| {
1871+
ParserError::ParserError(format!("Could not parse '{placeholder}' as i32: {error}"))
1872+
})?;
1873+
let raw = (placeholder != format!("${number}")).then(|| placeholder.to_owned());
1874+
Ok((number, raw))
1875+
}
1876+
18701877
fn parse_stage_params(parser: &mut Parser) -> Result<StageParamsObject, ParserError> {
18711878
let (mut url, mut storage_integration, mut endpoint) = (None, None, None);
18721879
let mut encryption: KeyValueOptions = KeyValueOptions {

‎tests/sqlparser_snowflake.rs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2703,6 +2703,7 @@ fn test_copy_into_with_transformations() {
27032703
StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem {
27042704
alias: Some(Ident::new("t1")),
27052705
file_col_num: 1,
2706+
file_col_num_raw: None,
27062707
element: Some(Ident::new("st")),
27072708
item_as: Some(Ident::new("st"))
27082709
})
@@ -2712,6 +2713,7 @@ fn test_copy_into_with_transformations() {
27122713
StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem {
27132714
alias: None,
27142715
file_col_num: 1,
2716+
file_col_num_raw: None,
27152717
element: Some(Ident::new("index")),
27162718
item_as: None
27172719
})
@@ -2721,6 +2723,7 @@ fn test_copy_into_with_transformations() {
27212723
StageLoadSelectItemKind::StageLoadSelectItem(StageLoadSelectItem {
27222724
alias: Some(Ident::new("t2")),
27232725
file_col_num: 1,
2726+
file_col_num_raw: None,
27242727
element: None,
27252728
item_as: None
27262729
})
@@ -2782,6 +2785,35 @@ fn test_copy_into_staged_identifiers_preserve_quotes_and_spans() {
27822785
assert_eq!(items[0].to_string(), r#""src".$1:"field" AS "Mixed""#);
27832786
}
27842787

2788+
#[test]
2789+
fn test_copy_into_staged_placeholders_preserve_zero_padding() {
2790+
let sql = r#"COPY INTO t FROM (SELECT $1 AS "$00", $00, src.$01 AS "One" FROM @stage AS src)"#;
2791+
let statement = snowflake().verified_stmt(sql);
2792+
let Statement::CopyIntoSnowflake {
2793+
from_transformations: Some(items),
2794+
..
2795+
} = statement
2796+
else {
2797+
panic!("expected COPY INTO with transformations");
2798+
};
2799+
for (item, number, raw) in [(&items[1], 0, "$00"), (&items[2], 1, "$01")] {
2800+
let StageLoadSelectItemKind::StageLoadSelectItem(item) = item else {
2801+
panic!("expected a staged field");
2802+
};
2803+
assert_eq!(item.file_col_num, number);
2804+
assert_eq!(item.file_col_num_raw.as_deref(), Some(raw));
2805+
}
2806+
assert_eq!(items[0].to_string(), "$1 AS \"$00\"");
2807+
assert_eq!(items[1].to_string(), "$00");
2808+
assert_eq!(items[2].to_string(), "src.$01 AS \"One\"");
2809+
2810+
let StageLoadSelectItemKind::StageLoadSelectItem(mut changed) = items[2].clone() else {
2811+
panic!("expected a staged field");
2812+
};
2813+
changed.file_col_num = 2;
2814+
assert_eq!(changed.to_string(), "src.$2 AS \"One\"");
2815+
}
2816+
27852817
#[test]
27862818
fn test_copy_into_with_nested_transformation_path() {
27872819
let sql = concat!(

0 commit comments

Comments
 (0)