Skip to content

Commit 05428be

Browse files
authored
Snowflake: Add support for PUT (apache#2341)
1 parent 05af09c commit 05428be

5 files changed

Lines changed: 111 additions & 1 deletion

File tree

src/ast/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4852,6 +4852,20 @@ pub enum Statement {
48524852
/// Snowflake `LIST`
48534853
/// See: <https://docs.snowflake.com/en/sql-reference/sql/list>
48544854
List(FileStagingCommand),
4855+
/// Snowflake `PUT`
4856+
/// ```sql
4857+
/// PUT 'file://<path>' <internalStage> [ <option> = <value> ... ]
4858+
/// ```
4859+
/// Options include `PARALLEL`, `AUTO_COMPRESS`, `SOURCE_COMPRESSION`, `OVERWRITE`.
4860+
/// See: <https://docs.snowflake.com/en/sql-reference/sql/put>
4861+
Put {
4862+
/// Local source URI as written in the statement, e.g. `file:///tmp/data.csv`.
4863+
source: String,
4864+
/// Target internal stage (e.g. `@mystage`, `@~`, `@%table`).
4865+
stage: ObjectName,
4866+
/// Trailing options (`PARALLEL=4`, `AUTO_COMPRESS=TRUE`, ...).
4867+
options: KeyValueOptions,
4868+
},
48554869
/// Snowflake `REMOVE`
48564870
/// See: <https://docs.snowflake.com/en/sql-reference/sql/remove>
48574871
Remove(FileStagingCommand),
@@ -6372,6 +6386,17 @@ impl fmt::Display for Statement {
63726386
Statement::WaitFor(s) => write!(f, "{s}"),
63736387
Statement::Return(r) => write!(f, "{r}"),
63746388
Statement::List(command) => write!(f, "LIST {command}"),
6389+
Statement::Put {
6390+
source,
6391+
stage,
6392+
options,
6393+
} => {
6394+
write!(f, "PUT '{source}' {stage}")?;
6395+
if !options.options.is_empty() {
6396+
write!(f, " {options}")?;
6397+
}
6398+
Ok(())
6399+
}
63756400
Statement::Remove(command) => write!(f, "REMOVE {command}"),
63766401
Statement::ExportData(e) => write!(f, "{e}"),
63776402
Statement::CreateUser(s) => write!(f, "{s}"),

src/ast/spans.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ impl Spanned for Statement {
501501
Statement::Print { .. } => Span::empty(),
502502
Statement::WaitFor(_) => Span::empty(),
503503
Statement::Return { .. } => Span::empty(),
504-
Statement::List(..) | Statement::Remove(..) => Span::empty(),
504+
Statement::List(..) | Statement::Put { .. } | Statement::Remove(..) => Span::empty(),
505505
Statement::ExportData(ExportData {
506506
options,
507507
query,

src/dialect/snowflake.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,10 @@ impl Dialect for SnowflakeDialect {
354354
return Some(parse_file_staging_command(kw, parser));
355355
}
356356

357+
if parser.parse_keyword(Keyword::PUT) {
358+
return Some(parse_put(parser));
359+
}
360+
357361
if parser.parse_keyword(Keyword::SHOW) {
358362
let terse = parser.parse_keyword(Keyword::TERSE);
359363
if parser.parse_keyword(Keyword::OBJECTS) {
@@ -696,6 +700,21 @@ fn peek_for_limit_options(parser: &Parser) -> bool {
696700
}
697701
}
698702

703+
/// Parse a Snowflake `PUT <source> <stage> [ options ]` statement. The caller
704+
/// is expected to have already consumed `PUT`.
705+
///
706+
/// See <https://docs.snowflake.com/en/sql-reference/sql/put>.
707+
fn parse_put(parser: &mut Parser) -> Result<Statement, ParserError> {
708+
let source = parser.parse_literal_string()?;
709+
let stage = parse_snowflake_stage_name(parser)?;
710+
let options = parser.parse_key_value_options(false, &[])?;
711+
Ok(Statement::Put {
712+
source,
713+
stage,
714+
options,
715+
})
716+
}
717+
699718
fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> {
700719
let stage = parse_snowflake_stage_name(parser)?;
701720
let pattern = if parser.parse_keyword(Keyword::PATTERN) {

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,7 @@ define_keywords!(
822822
PUBLIC,
823823
PURCHASE,
824824
PURGE,
825+
PUT,
825826
QUALIFY,
826827
QUARTER,
827828
QUERIES,

tests/sqlparser_snowflake.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3861,6 +3861,71 @@ fn parse_ls_and_rm() {
38613861
.unwrap();
38623862
}
38633863

3864+
#[test]
3865+
fn test_put() {
3866+
let sql = "PUT 'file:///tmp/data.csv' @my_stage";
3867+
match snowflake().verified_stmt(sql) {
3868+
Statement::Put {
3869+
source,
3870+
stage,
3871+
options,
3872+
} => {
3873+
assert_eq!("file:///tmp/data.csv", source);
3874+
assert_eq!(ObjectName::from(vec!["@my_stage".into()]), stage);
3875+
assert!(options.options.is_empty());
3876+
}
3877+
_ => unreachable!(),
3878+
};
3879+
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
3880+
}
3881+
3882+
#[test]
3883+
fn test_put_with_quoted_stage() {
3884+
// Stage names can be quoted (e.g. Snowflake driver `write_pandas`)
3885+
let sql = r#"PUT 'file:///tmp/data.csv' @"my stage" PARALLEL=4"#;
3886+
match snowflake().verified_stmt(sql) {
3887+
Statement::Put { stage, .. } => {
3888+
assert_eq!(ObjectName::from(vec![r#"@"my stage""#.into()]), stage);
3889+
}
3890+
_ => unreachable!(),
3891+
};
3892+
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
3893+
}
3894+
3895+
#[test]
3896+
fn test_put_with_options() {
3897+
let sql = concat!(
3898+
"PUT 'file:///tmp/data.csv' @my_stage ",
3899+
"PARALLEL=8 AUTO_COMPRESS=true SOURCE_COMPRESSION=GZIP OVERWRITE=false"
3900+
);
3901+
match snowflake().verified_stmt(sql) {
3902+
Statement::Put { options, .. } => {
3903+
assert!(options.options.contains(&KeyValueOption {
3904+
option_name: "PARALLEL".to_string(),
3905+
option_value: KeyValueOptionKind::Single(
3906+
Value::Number("8".parse().unwrap(), false).with_empty_span()
3907+
),
3908+
}));
3909+
assert!(options.options.contains(&KeyValueOption {
3910+
option_name: "AUTO_COMPRESS".to_string(),
3911+
option_value: KeyValueOptionKind::Single(Value::Boolean(true).with_empty_span()),
3912+
}));
3913+
assert!(options.options.contains(&KeyValueOption {
3914+
option_name: "SOURCE_COMPRESSION".to_string(),
3915+
option_value: KeyValueOptionKind::Single(
3916+
Value::Placeholder("GZIP".to_string()).with_empty_span()
3917+
),
3918+
}));
3919+
assert!(options.options.contains(&KeyValueOption {
3920+
option_name: "OVERWRITE".to_string(),
3921+
option_value: KeyValueOptionKind::Single(Value::Boolean(false).with_empty_span()),
3922+
}));
3923+
}
3924+
_ => unreachable!(),
3925+
};
3926+
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
3927+
}
3928+
38643929
#[test]
38653930
fn test_sql_keywords_as_select_item_ident() {
38663931
// Some keywords that should be parsed as an alias

0 commit comments

Comments
 (0)