This is an automated email from the ASF dual-hosted git repository.

iffyio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 751dc5af Parse Snowflake COPY INTO <location> (#1669)
751dc5af is described below

commit 751dc5afce794398bcfc148aa6b2f9ecf3b6b5a9
Author: Yoav Cohen <[email protected]>
AuthorDate: Wed Feb 5 20:23:27 2025 +0100

    Parse Snowflake COPY INTO <location> (#1669)
---
 src/ast/helpers/stmt_data_loading.rs |  10 +-
 src/ast/mod.rs                       | 102 ++++++++------
 src/ast/spans.rs                     |   7 +-
 src/dialect/snowflake.rs             | 250 ++++++++++++++++++++---------------
 tests/sqlparser_snowflake.rs         | 197 ++++++++++++++++++++++-----
 5 files changed, 381 insertions(+), 185 deletions(-)

diff --git a/src/ast/helpers/stmt_data_loading.rs 
b/src/ast/helpers/stmt_data_loading.rs
index 42e1df06..77de5d9e 100644
--- a/src/ast/helpers/stmt_data_loading.rs
+++ b/src/ast/helpers/stmt_data_loading.rs
@@ -58,6 +58,7 @@ pub enum DataLoadingOptionType {
     STRING,
     BOOLEAN,
     ENUM,
+    NUMBER,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -128,12 +129,9 @@ impl fmt::Display for DataLoadingOption {
             DataLoadingOptionType::STRING => {
                 write!(f, "{}='{}'", self.option_name, self.value)?;
             }
-            DataLoadingOptionType::ENUM => {
-                // single quote is omitted
-                write!(f, "{}={}", self.option_name, self.value)?;
-            }
-            DataLoadingOptionType::BOOLEAN => {
-                // single quote is omitted
+            DataLoadingOptionType::ENUM
+            | DataLoadingOptionType::BOOLEAN
+            | DataLoadingOptionType::NUMBER => {
                 write!(f, "{}={}", self.option_name, self.value)?;
             }
         }
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 35c6dcc1..dc944c9e 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -2498,24 +2498,30 @@ pub enum Statement {
         values: Vec<Option<String>>,
     },
     /// ```sql
-    /// COPY INTO
+    /// COPY INTO <table> | <location>
     /// ```
-    /// See <https://docs.snowflake.com/en/sql-reference/sql/copy-into-table>
+    /// See:
+    /// <https://docs.snowflake.com/en/sql-reference/sql/copy-into-table>
+    /// <https://docs.snowflake.com/en/sql-reference/sql/copy-into-location>
+    ///
     /// Copy Into syntax available for Snowflake is different than the one 
implemented in
     /// Postgres. Although they share common prefix, it is reasonable to 
implement them
     /// in different enums. This can be refactored later once custom dialects
     /// are allowed to have custom Statements.
     CopyIntoSnowflake {
+        kind: CopyIntoSnowflakeKind,
         into: ObjectName,
-        from_stage: ObjectName,
-        from_stage_alias: Option<Ident>,
+        from_obj: Option<ObjectName>,
+        from_obj_alias: Option<Ident>,
         stage_params: StageParamsObject,
         from_transformations: Option<Vec<StageLoadSelectItem>>,
+        from_query: Option<Box<Query>>,
         files: Option<Vec<String>>,
         pattern: Option<String>,
         file_format: DataLoadingOptions,
         copy_options: DataLoadingOptions,
         validation_mode: Option<String>,
+        partition: Option<Box<Expr>>,
     },
     /// ```sql
     /// CLOSE
@@ -5048,60 +5054,69 @@ impl fmt::Display for Statement {
                 Ok(())
             }
             Statement::CopyIntoSnowflake {
+                kind,
                 into,
-                from_stage,
-                from_stage_alias,
+                from_obj,
+                from_obj_alias,
                 stage_params,
                 from_transformations,
+                from_query,
                 files,
                 pattern,
                 file_format,
                 copy_options,
                 validation_mode,
+                partition,
             } => {
                 write!(f, "COPY INTO {}", into)?;
-                if from_transformations.is_none() {
-                    // Standard data load
-                    write!(f, " FROM {}{}", from_stage, stage_params)?;
-                    if from_stage_alias.as_ref().is_some() {
-                        write!(f, " AS {}", 
from_stage_alias.as_ref().unwrap())?;
-                    }
-                } else {
+                if let Some(from_transformations) = from_transformations {
                     // Data load with transformation
-                    write!(
-                        f,
-                        " FROM (SELECT {} FROM {}{}",
-                        
display_separated(from_transformations.as_ref().unwrap(), ", "),
-                        from_stage,
-                        stage_params,
-                    )?;
-                    if from_stage_alias.as_ref().is_some() {
-                        write!(f, " AS {}", 
from_stage_alias.as_ref().unwrap())?;
+                    if let Some(from_stage) = from_obj {
+                        write!(
+                            f,
+                            " FROM (SELECT {} FROM {}{}",
+                            display_separated(from_transformations, ", "),
+                            from_stage,
+                            stage_params
+                        )?;
+                    }
+                    if let Some(from_obj_alias) = from_obj_alias {
+                        write!(f, " AS {}", from_obj_alias)?;
                     }
                     write!(f, ")")?;
+                } else if let Some(from_obj) = from_obj {
+                    // Standard data load
+                    write!(f, " FROM {}{}", from_obj, stage_params)?;
+                    if let Some(from_obj_alias) = from_obj_alias {
+                        write!(f, " AS {from_obj_alias}")?;
+                    }
+                } else if let Some(from_query) = from_query {
+                    // Data unload from query
+                    write!(f, " FROM ({from_query})")?;
                 }
-                if files.is_some() {
-                    write!(
-                        f,
-                        " FILES = ('{}')",
-                        display_separated(files.as_ref().unwrap(), "', '")
-                    )?;
+
+                if let Some(files) = files {
+                    write!(f, " FILES = ('{}')", display_separated(files, "', 
'"))?;
+                }
+                if let Some(pattern) = pattern {
+                    write!(f, " PATTERN = '{}'", pattern)?;
                 }
-                if pattern.is_some() {
-                    write!(f, " PATTERN = '{}'", pattern.as_ref().unwrap())?;
+                if let Some(partition) = partition {
+                    write!(f, " PARTITION BY {partition}")?;
                 }
                 if !file_format.options.is_empty() {
                     write!(f, " FILE_FORMAT=({})", file_format)?;
                 }
                 if !copy_options.options.is_empty() {
-                    write!(f, " COPY_OPTIONS=({})", copy_options)?;
+                    match kind {
+                        CopyIntoSnowflakeKind::Table => {
+                            write!(f, " COPY_OPTIONS=({})", copy_options)?
+                        }
+                        CopyIntoSnowflakeKind::Location => write!(f, " 
{copy_options}")?,
+                    }
                 }
-                if validation_mode.is_some() {
-                    write!(
-                        f,
-                        " VALIDATION_MODE = {}",
-                        validation_mode.as_ref().unwrap()
-                    )?;
+                if let Some(validation_mode) = validation_mode {
+                    write!(f, " VALIDATION_MODE = {}", validation_mode)?;
                 }
                 Ok(())
             }
@@ -8543,6 +8558,19 @@ impl Display for StorageSerializationPolicy {
     }
 }
 
+/// Variants of the Snowflake `COPY INTO` statement
+#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum CopyIntoSnowflakeKind {
+    /// Loads data from files to a table
+    /// See: <https://docs.snowflake.com/en/sql-reference/sql/copy-into-table>
+    Table,
+    /// Unloads data from a table or query to external files
+    /// See: 
<https://docs.snowflake.com/en/sql-reference/sql/copy-into-location>
+    Location,
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index b2690085..f0c38942 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -333,8 +333,8 @@ impl Spanned for Statement {
             } => source.span(),
             Statement::CopyIntoSnowflake {
                 into: _,
-                from_stage: _,
-                from_stage_alias: _,
+                from_obj: _,
+                from_obj_alias: _,
                 stage_params: _,
                 from_transformations: _,
                 files: _,
@@ -342,6 +342,9 @@ impl Spanned for Statement {
                 file_format: _,
                 copy_options: _,
                 validation_mode: _,
+                kind: _,
+                from_query: _,
+                partition: _,
             } => Span::empty(),
             Statement::Close { cursor } => match cursor {
                 CloseCursor::All => Span::empty(),
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index d775ffc3..fc192671 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -23,23 +23,26 @@ use crate::ast::helpers::stmt_data_loading::{
     StageLoadSelectItem, StageParamsObject,
 };
 use crate::ast::{
-    ColumnOption, ColumnPolicy, ColumnPolicyProperty, Ident, 
IdentityParameters, IdentityProperty,
-    IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, 
ObjectName,
-    RowAccessPolicy, Statement, TagsColumnOption, WrappedCollection,
+    ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, 
Ident,
+    IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, 
IdentityPropertyKind,
+    IdentityPropertyOrder, ObjectName, RowAccessPolicy, Statement, 
TagsColumnOption,
+    WrappedCollection,
 };
 use crate::dialect::{Dialect, Precedence};
 use crate::keywords::Keyword;
 use crate::parser::{Parser, ParserError};
-use crate::tokenizer::Token;
+use crate::tokenizer::{Token, Word};
+#[cfg(not(feature = "std"))]
+use alloc::boxed::Box;
 #[cfg(not(feature = "std"))]
 use alloc::string::String;
 #[cfg(not(feature = "std"))]
 use alloc::vec::Vec;
 #[cfg(not(feature = "std"))]
 use alloc::{format, vec};
-use sqlparser::ast::StorageSerializationPolicy;
 
 use super::keywords::RESERVED_FOR_IDENTIFIER;
+use sqlparser::ast::StorageSerializationPolicy;
 
 /// A [`Dialect`] for [Snowflake](https://www.snowflake.com/)
 #[derive(Debug, Default)]
@@ -665,24 +668,49 @@ pub fn parse_snowflake_stage_name(parser: &mut Parser) -> 
Result<ObjectName, Par
     }
 }
 
+/// Parses a `COPY INTO` statement. Snowflake has two variants, `COPY INTO 
<table>`
+/// and `COPY INTO <location>` which have different syntax.
 pub fn parse_copy_into(parser: &mut Parser) -> Result<Statement, ParserError> {
-    let into: ObjectName = parse_snowflake_stage_name(parser)?;
+    let kind = match parser.peek_token().token {
+        // Indicates an internal stage
+        Token::AtSign => CopyIntoSnowflakeKind::Location,
+        // Indicates an external stage, i.e. s3://, gcs:// or azure://
+        Token::SingleQuotedString(s) if s.contains("://") => 
CopyIntoSnowflakeKind::Location,
+        _ => CopyIntoSnowflakeKind::Table,
+    };
+
     let mut files: Vec<String> = vec![];
     let mut from_transformations: Option<Vec<StageLoadSelectItem>> = None;
-    let from_stage_alias;
-    let from_stage: ObjectName;
-    let stage_params: StageParamsObject;
+    let mut from_stage_alias = None;
+    let mut from_stage = None;
+    let mut stage_params = StageParamsObject {
+        url: None,
+        encryption: DataLoadingOptions { options: vec![] },
+        endpoint: None,
+        storage_integration: None,
+        credentials: DataLoadingOptions { options: vec![] },
+    };
+    let mut from_query = None;
+    let mut partition = None;
+    let mut file_format = Vec::new();
+    let mut pattern = None;
+    let mut validation_mode = None;
+    let mut copy_options = Vec::new();
+
+    let into: ObjectName = parse_snowflake_stage_name(parser)?;
+    if kind == CopyIntoSnowflakeKind::Location {
+        stage_params = parse_stage_params(parser)?;
+    }
 
     parser.expect_keyword_is(Keyword::FROM)?;
-    // check if data load transformations are present
     match parser.next_token().token {
-        Token::LParen => {
-            // data load with transformations
+        Token::LParen if kind == CopyIntoSnowflakeKind::Table => {
+            // Data load with transformations
             parser.expect_keyword_is(Keyword::SELECT)?;
             from_transformations = parse_select_items_for_data_load(parser)?;
 
             parser.expect_keyword_is(Keyword::FROM)?;
-            from_stage = parse_snowflake_stage_name(parser)?;
+            from_stage = Some(parse_snowflake_stage_name(parser)?);
             stage_params = parse_stage_params(parser)?;
 
             // as
@@ -696,9 +724,14 @@ pub fn parse_copy_into(parser: &mut Parser) -> 
Result<Statement, ParserError> {
             };
             parser.expect_token(&Token::RParen)?;
         }
+        Token::LParen if kind == CopyIntoSnowflakeKind::Location => {
+            // Data unload with a query
+            from_query = Some(parser.parse_query()?);
+            parser.expect_token(&Token::RParen)?;
+        }
         _ => {
             parser.prev_token();
-            from_stage = parse_snowflake_stage_name(parser)?;
+            from_stage = Some(parse_snowflake_stage_name(parser)?);
             stage_params = parse_stage_params(parser)?;
 
             // as
@@ -711,67 +744,71 @@ pub fn parse_copy_into(parser: &mut Parser) -> 
Result<Statement, ParserError> {
                 None
             };
         }
-    };
+    }
 
-    // [ files ]
-    if parser.parse_keyword(Keyword::FILES) {
-        parser.expect_token(&Token::Eq)?;
-        parser.expect_token(&Token::LParen)?;
-        let mut continue_loop = true;
-        while continue_loop {
-            continue_loop = false;
+    loop {
+        // FILE_FORMAT
+        if parser.parse_keyword(Keyword::FILE_FORMAT) {
+            parser.expect_token(&Token::Eq)?;
+            file_format = parse_parentheses_options(parser)?;
+        // PARTITION BY
+        } else if parser.parse_keywords(&[Keyword::PARTITION, Keyword::BY]) {
+            partition = Some(Box::new(parser.parse_expr()?))
+        // FILES
+        } else if parser.parse_keyword(Keyword::FILES) {
+            parser.expect_token(&Token::Eq)?;
+            parser.expect_token(&Token::LParen)?;
+            let mut continue_loop = true;
+            while continue_loop {
+                continue_loop = false;
+                let next_token = parser.next_token();
+                match next_token.token {
+                    Token::SingleQuotedString(s) => files.push(s),
+                    _ => parser.expected("file token", next_token)?,
+                };
+                if parser.next_token().token.eq(&Token::Comma) {
+                    continue_loop = true;
+                } else {
+                    parser.prev_token(); // not a comma, need to go back
+                }
+            }
+            parser.expect_token(&Token::RParen)?;
+        // PATTERN
+        } else if parser.parse_keyword(Keyword::PATTERN) {
+            parser.expect_token(&Token::Eq)?;
             let next_token = parser.next_token();
-            match next_token.token {
-                Token::SingleQuotedString(s) => files.push(s),
-                _ => parser.expected("file token", next_token)?,
-            };
-            if parser.next_token().token.eq(&Token::Comma) {
-                continue_loop = true;
-            } else {
-                parser.prev_token(); // not a comma, need to go back
+            pattern = Some(match next_token.token {
+                Token::SingleQuotedString(s) => s,
+                _ => parser.expected("pattern", next_token)?,
+            });
+        // VALIDATION MODE
+        } else if parser.parse_keyword(Keyword::VALIDATION_MODE) {
+            parser.expect_token(&Token::Eq)?;
+            validation_mode = Some(parser.next_token().token.to_string());
+        // COPY OPTIONS
+        } else if parser.parse_keyword(Keyword::COPY_OPTIONS) {
+            parser.expect_token(&Token::Eq)?;
+            copy_options = parse_parentheses_options(parser)?;
+        } else {
+            match parser.next_token().token {
+                Token::SemiColon | Token::EOF => break,
+                Token::Comma => continue,
+                // In `COPY INTO <location>` the copy options do not have a 
shared key
+                // like in `COPY INTO <table>`
+                Token::Word(key) => 
copy_options.push(parse_copy_option(parser, key)?),
+                _ => return parser.expected("another copy option, ; or EOF'", 
parser.peek_token()),
             }
         }
-        parser.expect_token(&Token::RParen)?;
-    }
-
-    // [ pattern ]
-    let mut pattern = None;
-    if parser.parse_keyword(Keyword::PATTERN) {
-        parser.expect_token(&Token::Eq)?;
-        let next_token = parser.next_token();
-        pattern = Some(match next_token.token {
-            Token::SingleQuotedString(s) => s,
-            _ => parser.expected("pattern", next_token)?,
-        });
-    }
-
-    // [ file_format]
-    let mut file_format = Vec::new();
-    if parser.parse_keyword(Keyword::FILE_FORMAT) {
-        parser.expect_token(&Token::Eq)?;
-        file_format = parse_parentheses_options(parser)?;
-    }
-
-    // [ copy_options ]
-    let mut copy_options = Vec::new();
-    if parser.parse_keyword(Keyword::COPY_OPTIONS) {
-        parser.expect_token(&Token::Eq)?;
-        copy_options = parse_parentheses_options(parser)?;
-    }
-
-    // [ VALIDATION_MODE ]
-    let mut validation_mode = None;
-    if parser.parse_keyword(Keyword::VALIDATION_MODE) {
-        parser.expect_token(&Token::Eq)?;
-        validation_mode = Some(parser.next_token().token.to_string());
     }
 
     Ok(Statement::CopyIntoSnowflake {
+        kind,
         into,
-        from_stage,
-        from_stage_alias,
+        from_obj: from_stage,
+        from_obj_alias: from_stage_alias,
         stage_params,
         from_transformations,
+        from_query,
         files: if files.is_empty() { None } else { Some(files) },
         pattern,
         file_format: DataLoadingOptions {
@@ -781,6 +818,7 @@ pub fn parse_copy_into(parser: &mut Parser) -> 
Result<Statement, ParserError> {
             options: copy_options,
         },
         validation_mode,
+        partition,
     })
 }
 
@@ -930,55 +968,55 @@ fn parse_stage_params(parser: &mut Parser) -> 
Result<StageParamsObject, ParserEr
 ///
 fn parse_parentheses_options(parser: &mut Parser) -> 
Result<Vec<DataLoadingOption>, ParserError> {
     let mut options: Vec<DataLoadingOption> = Vec::new();
-
     parser.expect_token(&Token::LParen)?;
     loop {
         match parser.next_token().token {
             Token::RParen => break,
-            Token::Word(key) => {
-                parser.expect_token(&Token::Eq)?;
-                if parser.parse_keyword(Keyword::TRUE) {
-                    options.push(DataLoadingOption {
-                        option_name: key.value,
-                        option_type: DataLoadingOptionType::BOOLEAN,
-                        value: "TRUE".to_string(),
-                    });
-                    Ok(())
-                } else if parser.parse_keyword(Keyword::FALSE) {
-                    options.push(DataLoadingOption {
-                        option_name: key.value,
-                        option_type: DataLoadingOptionType::BOOLEAN,
-                        value: "FALSE".to_string(),
-                    });
-                    Ok(())
-                } else {
-                    match parser.next_token().token {
-                        Token::SingleQuotedString(value) => {
-                            options.push(DataLoadingOption {
-                                option_name: key.value,
-                                option_type: DataLoadingOptionType::STRING,
-                                value,
-                            });
-                            Ok(())
-                        }
-                        Token::Word(word) => {
-                            options.push(DataLoadingOption {
-                                option_name: key.value,
-                                option_type: DataLoadingOptionType::ENUM,
-                                value: word.value,
-                            });
-                            Ok(())
-                        }
-                        _ => parser.expected("expected option value", 
parser.peek_token()),
-                    }
-                }
-            }
-            _ => parser.expected("another option or ')'", parser.peek_token()),
-        }?;
+            Token::Comma => continue,
+            Token::Word(key) => options.push(parse_copy_option(parser, key)?),
+            _ => return parser.expected("another option or ')'", 
parser.peek_token()),
+        };
     }
     Ok(options)
 }
 
+/// Parses a `KEY = VALUE` construct based on the specified key
+fn parse_copy_option(parser: &mut Parser, key: Word) -> 
Result<DataLoadingOption, ParserError> {
+    parser.expect_token(&Token::Eq)?;
+    if parser.parse_keyword(Keyword::TRUE) {
+        Ok(DataLoadingOption {
+            option_name: key.value,
+            option_type: DataLoadingOptionType::BOOLEAN,
+            value: "TRUE".to_string(),
+        })
+    } else if parser.parse_keyword(Keyword::FALSE) {
+        Ok(DataLoadingOption {
+            option_name: key.value,
+            option_type: DataLoadingOptionType::BOOLEAN,
+            value: "FALSE".to_string(),
+        })
+    } else {
+        match parser.next_token().token {
+            Token::SingleQuotedString(value) => Ok(DataLoadingOption {
+                option_name: key.value,
+                option_type: DataLoadingOptionType::STRING,
+                value,
+            }),
+            Token::Word(word) => Ok(DataLoadingOption {
+                option_name: key.value,
+                option_type: DataLoadingOptionType::ENUM,
+                value: word.value,
+            }),
+            Token::Number(n, _) => Ok(DataLoadingOption {
+                option_name: key.value,
+                option_type: DataLoadingOptionType::NUMBER,
+                value: n,
+            }),
+            _ => parser.expected("expected option value", parser.peek_token()),
+        }
+    }
+}
+
 /// Parsing a property of identity or autoincrement column option
 /// Syntax:
 /// ```sql
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index c68ada3c..a18f1a4d 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -2028,20 +2028,25 @@ fn test_copy_into() {
     );
     match snowflake().verified_stmt(sql) {
         Statement::CopyIntoSnowflake {
+            kind,
             into,
-            from_stage,
+            from_obj,
             files,
             pattern,
             validation_mode,
             ..
         } => {
+            assert_eq!(kind, CopyIntoSnowflakeKind::Table);
             assert_eq!(
                 into,
                 ObjectName::from(vec![Ident::new("my_company"), 
Ident::new("emp_basic")])
             );
             assert_eq!(
-                from_stage,
-                ObjectName::from(vec![Ident::with_quote('\'', 
"gcs://mybucket/./../a.csv")])
+                from_obj,
+                Some(ObjectName::from(vec![Ident::with_quote(
+                    '\'',
+                    "gcs://mybucket/./../a.csv"
+                )]))
             );
             assert!(files.is_none());
             assert!(pattern.is_none());
@@ -2050,6 +2055,60 @@ fn test_copy_into() {
         _ => unreachable!(),
     };
     assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
+
+    let sql = concat!("COPY INTO 's3://a/b/c/data.parquet' ", "FROM db.sc.tbl 
", "PARTITION BY ('date=' || to_varchar(dt, 'YYYY-MM-DD') || '/hour=' || 
to_varchar(date_part(hour, ts)))");
+    match snowflake().verified_stmt(sql) {
+        Statement::CopyIntoSnowflake {
+            kind,
+            into,
+            from_obj,
+            from_query,
+            partition,
+            ..
+        } => {
+            assert_eq!(kind, CopyIntoSnowflakeKind::Location);
+            assert_eq!(
+                into,
+                ObjectName::from(vec![Ident::with_quote('\'', 
"s3://a/b/c/data.parquet")])
+            );
+            assert_eq!(
+                from_obj,
+                Some(ObjectName::from(vec![
+                    Ident::new("db"),
+                    Ident::new("sc"),
+                    Ident::new("tbl")
+                ]))
+            );
+            assert!(from_query.is_none());
+            assert!(partition.is_some());
+        }
+        _ => unreachable!(),
+    };
+    assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
+
+    let sql = concat!(
+        "COPY INTO 's3://a/b/c/data.parquet' ",
+        "FROM (SELECT * FROM tbl)"
+    );
+    match snowflake().verified_stmt(sql) {
+        Statement::CopyIntoSnowflake {
+            kind,
+            into,
+            from_obj,
+            from_query,
+            ..
+        } => {
+            assert_eq!(kind, CopyIntoSnowflakeKind::Location);
+            assert_eq!(
+                into,
+                ObjectName::from(vec![Ident::with_quote('\'', 
"s3://a/b/c/data.parquet")])
+            );
+            assert!(from_query.is_some());
+            assert!(from_obj.is_none());
+        }
+        _ => unreachable!(),
+    };
+    assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
 }
 
 #[test]
@@ -2065,14 +2124,17 @@ fn test_copy_into_with_stage_params() {
 
     match snowflake().verified_stmt(sql) {
         Statement::CopyIntoSnowflake {
-            from_stage,
+            from_obj,
             stage_params,
             ..
         } => {
             //assert_eq!("s3://load/files/", stage_params.url.unwrap());
             assert_eq!(
-                from_stage,
-                ObjectName::from(vec![Ident::with_quote('\'', 
"s3://load/files/")])
+                from_obj,
+                Some(ObjectName::from(vec![Ident::with_quote(
+                    '\'',
+                    "s3://load/files/"
+                )]))
             );
             assert_eq!("myint", stage_params.storage_integration.unwrap());
             assert_eq!(
@@ -2125,13 +2187,16 @@ fn test_copy_into_with_stage_params() {
 
     match snowflake().verified_stmt(sql) {
         Statement::CopyIntoSnowflake {
-            from_stage,
+            from_obj,
             stage_params,
             ..
         } => {
             assert_eq!(
-                from_stage,
-                ObjectName::from(vec![Ident::with_quote('\'', 
"s3://load/files/")])
+                from_obj,
+                Some(ObjectName::from(vec![Ident::with_quote(
+                    '\'',
+                    "s3://load/files/"
+                )]))
             );
             assert_eq!("myint", stage_params.storage_integration.unwrap());
         }
@@ -2154,13 +2219,13 @@ fn 
test_copy_into_with_files_and_pattern_and_verification() {
             files,
             pattern,
             validation_mode,
-            from_stage_alias,
+            from_obj_alias,
             ..
         } => {
             assert_eq!(files.unwrap(), vec!["file1.json", "file2.json"]);
             assert_eq!(pattern.unwrap(), ".*employees0[1-5].csv.gz");
             assert_eq!(validation_mode.unwrap(), "RETURN_7_ROWS");
-            assert_eq!(from_stage_alias.unwrap(), Ident::new("some_alias"));
+            assert_eq!(from_obj_alias.unwrap(), Ident::new("some_alias"));
         }
         _ => unreachable!(),
     }
@@ -2179,13 +2244,16 @@ fn test_copy_into_with_transformations() {
 
     match snowflake().verified_stmt(sql) {
         Statement::CopyIntoSnowflake {
-            from_stage,
+            from_obj,
             from_transformations,
             ..
         } => {
             assert_eq!(
-                from_stage,
-                ObjectName::from(vec![Ident::new("@schema"), 
Ident::new("general_finished")])
+                from_obj,
+                Some(ObjectName::from(vec![
+                    Ident::new("@schema"),
+                    Ident::new("general_finished")
+                ]))
             );
             assert_eq!(
                 from_transformations.as_ref().unwrap()[0],
@@ -2254,6 +2322,41 @@ fn test_copy_into_file_format() {
         snowflake_without_unescape().verified_stmt(sql).to_string(),
         sql
     );
+
+    // Test commas in file format
+    let sql = concat!(
+        "COPY INTO my_company.emp_basic ",
+        "FROM 'gcs://mybucket/./../a.csv' ",
+        "FILES = ('file1.json', 'file2.json') ",
+        "PATTERN = '.*employees0[1-5].csv.gz' ",
+        r#"FILE_FORMAT=(COMPRESSION=AUTO, BINARY_FORMAT=HEX, ESCAPE='\\')"#
+    );
+
+    match snowflake_without_unescape()
+        .parse_sql_statements(sql)
+        .unwrap()
+        .first()
+        .unwrap()
+    {
+        Statement::CopyIntoSnowflake { file_format, .. } => {
+            assert!(file_format.options.contains(&DataLoadingOption {
+                option_name: "COMPRESSION".to_string(),
+                option_type: DataLoadingOptionType::ENUM,
+                value: "AUTO".to_string()
+            }));
+            assert!(file_format.options.contains(&DataLoadingOption {
+                option_name: "BINARY_FORMAT".to_string(),
+                option_type: DataLoadingOptionType::ENUM,
+                value: "HEX".to_string()
+            }));
+            assert!(file_format.options.contains(&DataLoadingOption {
+                option_name: "ESCAPE".to_string(),
+                option_type: DataLoadingOptionType::STRING,
+                value: r#"\\"#.to_string()
+            }));
+        }
+        _ => unreachable!(),
+    }
 }
 
 #[test]
@@ -2285,16 +2388,8 @@ fn test_copy_into_copy_options() {
 }
 
 #[test]
-fn test_snowflake_stage_object_names() {
-    let allowed_formatted_names = [
-        "my_company.emp_basic",
-        "@namespace.%table_name",
-        "@namespace.%table_name/path",
-        "@namespace.stage_name/path",
-        "@~/path",
-    ];
+fn test_snowflake_stage_object_names_into_location() {
     let mut allowed_object_names = [
-        ObjectName::from(vec![Ident::new("my_company"), 
Ident::new("emp_basic")]),
         ObjectName::from(vec![Ident::new("@namespace"), 
Ident::new("%table_name")]),
         ObjectName::from(vec![
             Ident::new("@namespace"),
@@ -2307,7 +2402,39 @@ fn test_snowflake_stage_object_names() {
         ObjectName::from(vec![Ident::new("@~/path")]),
     ];
 
-    for it in allowed_formatted_names
+    let allowed_names_into_location = [
+        "@namespace.%table_name",
+        "@namespace.%table_name/path",
+        "@namespace.stage_name/path",
+        "@~/path",
+    ];
+    for it in allowed_names_into_location
+        .iter()
+        .zip(allowed_object_names.iter_mut())
+    {
+        let (formatted_name, object_name) = it;
+        let sql = format!(
+            "COPY INTO {} FROM 'gcs://mybucket/./../a.csv'",
+            formatted_name
+        );
+        match snowflake().verified_stmt(&sql) {
+            Statement::CopyIntoSnowflake { into, .. } => {
+                assert_eq!(into.0, object_name.0)
+            }
+            _ => unreachable!(),
+        }
+    }
+}
+
+#[test]
+fn test_snowflake_stage_object_names_into_table() {
+    let mut allowed_object_names = [
+        ObjectName::from(vec![Ident::new("my_company"), 
Ident::new("emp_basic")]),
+        ObjectName::from(vec![Ident::new("emp_basic")]),
+    ];
+
+    let allowed_names_into_table = ["my_company.emp_basic", "emp_basic"];
+    for it in allowed_names_into_table
         .iter()
         .zip(allowed_object_names.iter_mut())
     {
@@ -2330,16 +2457,17 @@ fn test_snowflake_copy_into() {
     let sql = "COPY INTO a.b FROM @namespace.stage_name";
     assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
     match snowflake().verified_stmt(sql) {
-        Statement::CopyIntoSnowflake {
-            into, from_stage, ..
-        } => {
+        Statement::CopyIntoSnowflake { into, from_obj, .. } => {
             assert_eq!(
                 into,
                 ObjectName::from(vec![Ident::new("a"), Ident::new("b")])
             );
             assert_eq!(
-                from_stage,
-                ObjectName::from(vec![Ident::new("@namespace"), 
Ident::new("stage_name")])
+                from_obj,
+                Some(ObjectName::from(vec![
+                    Ident::new("@namespace"),
+                    Ident::new("stage_name")
+                ]))
             )
         }
         _ => unreachable!(),
@@ -2351,9 +2479,7 @@ fn test_snowflake_copy_into_stage_name_ends_with_parens() 
{
     let sql = "COPY INTO SCHEMA.SOME_MONITORING_SYSTEM FROM (SELECT t.$1:st AS 
st FROM @schema.general_finished)";
     assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
     match snowflake().verified_stmt(sql) {
-        Statement::CopyIntoSnowflake {
-            into, from_stage, ..
-        } => {
+        Statement::CopyIntoSnowflake { into, from_obj, .. } => {
             assert_eq!(
                 into,
                 ObjectName::from(vec![
@@ -2362,8 +2488,11 @@ fn 
test_snowflake_copy_into_stage_name_ends_with_parens() {
                 ])
             );
             assert_eq!(
-                from_stage,
-                ObjectName::from(vec![Ident::new("@schema"), 
Ident::new("general_finished")])
+                from_obj,
+                Some(ObjectName::from(vec![
+                    Ident::new("@schema"),
+                    Ident::new("general_finished")
+                ]))
             )
         }
         _ => unreachable!(),


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to