From 55d45c47487e8cb35d737eddc9ca870919f7d2f3 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Tue, 8 Jul 2025 22:29:23 +0200 Subject: [PATCH 1/4] Add support for more create table options after AS --- src/dialect/snowflake.rs | 5 ++--- tests/sqlparser_snowflake.rs | 10 ++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 3b1eff39a..f6af2b060 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -555,7 +555,6 @@ pub fn parse_create_table( Keyword::AS => { let query = parser.parse_query()?; builder = builder.query(Some(query)); - break; } Keyword::CLONE => { let clone = parser.parse_object_name(false).ok(); @@ -691,7 +690,7 @@ pub fn parse_create_table( builder = builder.columns(columns).constraints(constraints); } Token::EOF => { - if builder.columns.is_empty() { + if builder.columns.is_empty() && builder.query.is_none() { return Err(ParserError::ParserError( "unexpected end of input".to_string(), )); @@ -700,7 +699,7 @@ pub fn parse_create_table( break; } Token::SemiColon => { - if builder.columns.is_empty() { + if builder.columns.is_empty() && builder.query.is_none() { return Err(ParserError::ParserError( "unexpected end of input".to_string(), )); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 65546bee0..8acee3955 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -995,6 +995,16 @@ fn test_snowflake_create_iceberg_table_without_location() { ); } +#[test] +fn test_snowflake_create_table_as() { + // Test additional options after AS (query) + snowflake() + .parse_sql_statements( + "CREATE TEMP TABLE dst AS (SELECT * FROM src) ON COMMIT PRESERVE ROWS", + ) + .unwrap(); +} + #[test] fn parse_sf_create_or_replace_view_with_comment_missing_equal() { assert!(snowflake_and_generic() From 95b0e53b2a1b7b03c4d804b8278764d69236c573 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Tue, 8 Jul 2025 22:44:47 +0200 Subject: [PATCH 2/4] Add support for more create table options after AS --- src/ast/helpers/stmt_create_table.rs | 10 ++++++++++ src/dialect/snowflake.rs | 6 ++---- tests/sqlparser_snowflake.rs | 9 +++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index d66a869bf..543f8f2c9 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -383,6 +383,16 @@ impl CreateTableBuilder { self } + /// Returns true if information on the structure of the table + /// to be created was provided to the builder. If not, the + /// statement is invalid. + pub fn has_schema_info(&self) -> bool { + !self.columns.is_empty() + || self.query.is_some() + || self.like.is_some() + || self.clone.is_some() + } + pub fn build(self) -> Statement { Statement::CreateTable(CreateTable { or_replace: self.or_replace, diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index f6af2b060..8a9a52a12 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -559,12 +559,10 @@ pub fn parse_create_table( Keyword::CLONE => { let clone = parser.parse_object_name(false).ok(); builder = builder.clone_clause(clone); - break; } Keyword::LIKE => { let like = parser.parse_object_name(false).ok(); builder = builder.like(like); - break; } Keyword::CLUSTER => { parser.expect_keyword_is(Keyword::BY)?; @@ -690,7 +688,7 @@ pub fn parse_create_table( builder = builder.columns(columns).constraints(constraints); } Token::EOF => { - if builder.columns.is_empty() && builder.query.is_none() { + if !builder.has_schema_info() { return Err(ParserError::ParserError( "unexpected end of input".to_string(), )); @@ -699,7 +697,7 @@ pub fn parse_create_table( break; } Token::SemiColon => { - if builder.columns.is_empty() && builder.query.is_none() { + if !builder.has_schema_info() { return Err(ParserError::ParserError( "unexpected end of input".to_string(), )); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 8acee3955..6be81e962 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -996,13 +996,18 @@ fn test_snowflake_create_iceberg_table_without_location() { } #[test] -fn test_snowflake_create_table_as() { - // Test additional options after AS (query) +fn test_snowflake_create_table_trailing_options() { snowflake() .parse_sql_statements( "CREATE TEMP TABLE dst AS (SELECT * FROM src) ON COMMIT PRESERVE ROWS", ) .unwrap(); + snowflake() + .parse_sql_statements("CREATE TEMP TABLE tbl LIKE customers ON COMMIT PRESERVE ROWS;") + .unwrap(); + snowflake() + .parse_sql_statements("CREATE TEMP TABLE tbl CLONE customers ON COMMIT PRESERVE ROWS;") + .unwrap(); } #[test] From a8a8b8148e5a3b0019e65572cc0ced8bb5218602 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Fri, 11 Jul 2025 08:27:56 +0200 Subject: [PATCH 3/4] Improve tests --- src/ast/helpers/stmt_create_table.rs | 2 +- tests/sqlparser_snowflake.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 543f8f2c9..828ab7990 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -386,7 +386,7 @@ impl CreateTableBuilder { /// Returns true if information on the structure of the table /// to be created was provided to the builder. If not, the /// statement is invalid. - pub fn has_schema_info(&self) -> bool { + pub(crate) fn has_schema_info(&self) -> bool { !self.columns.is_empty() || self.query.is_some() || self.like.is_some() diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 6be81e962..2bb08cf93 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -997,19 +997,40 @@ fn test_snowflake_create_iceberg_table_without_location() { #[test] fn test_snowflake_create_table_trailing_options() { + // Serialization to SQL assume that in `CREATE TABLE AS` the options come before the `AS ()` + // but Snowflake supports also the other way around + snowflake() + .verified_stmt("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS AS (SELECT * FROM src)"); snowflake() .parse_sql_statements( - "CREATE TEMP TABLE dst AS (SELECT * FROM src) ON COMMIT PRESERVE ROWS", + "CREATE TEMPORARY TABLE dst AS (SELECT * FROM src) ON COMMIT PRESERVE ROWS", ) .unwrap(); + + // Same for `CREATE TABLE LIKE|CLONE`: + snowflake().verified_stmt("CREATE TEMPORARY TABLE dst LIKE src ON COMMIT PRESERVE ROWS"); snowflake() - .parse_sql_statements("CREATE TEMP TABLE tbl LIKE customers ON COMMIT PRESERVE ROWS;") + .parse_sql_statements("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS LIKE src") .unwrap(); + + snowflake().verified_stmt("CREATE TEMPORARY TABLE dst CLONE src ON COMMIT PRESERVE ROWS"); snowflake() - .parse_sql_statements("CREATE TEMP TABLE tbl CLONE customers ON COMMIT PRESERVE ROWS;") + .parse_sql_statements("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS CLONE src") .unwrap(); } +#[test] +fn test_snowflake_create_table_has_schema_info() { + // The parser validates there's information on the schema of the new + // table, such as a list of columns or a source table\query to copy it from. + assert_eq!( + snowflake() + .parse_sql_statements("CREATE TABLE dst") + .is_err(), + true + ); +} + #[test] fn parse_sf_create_or_replace_view_with_comment_missing_equal() { assert!(snowflake_and_generic() From 3aa41f3f8fb2106b5fd15e3fe3e62af26fd2c0b0 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Fri, 11 Jul 2025 09:11:26 +0200 Subject: [PATCH 4/4] Code review feedback --- src/ast/helpers/stmt_create_table.rs | 26 ++++++++++++++++++-------- src/dialect/bigquery.rs | 4 ++++ src/dialect/mod.rs | 7 +++++++ src/dialect/snowflake.rs | 4 ++-- tests/sqlparser_common.rs | 17 ++--------------- tests/sqlparser_snowflake.rs | 15 ++++++++++++--- 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 828ab7990..60b8fb2a0 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -383,14 +383,24 @@ impl CreateTableBuilder { self } - /// Returns true if information on the structure of the table - /// to be created was provided to the builder. If not, the - /// statement is invalid. - pub(crate) fn has_schema_info(&self) -> bool { - !self.columns.is_empty() - || self.query.is_some() - || self.like.is_some() - || self.clone.is_some() + /// Returns true if the statement has exactly one source of info on the schema of the new table. + /// This is Snowflake-specific, some dialects allow more than one source. + pub(crate) fn validate_schema_info(&self) -> bool { + let mut sources = 0; + if !self.columns.is_empty() { + sources += 1; + } + if self.query.is_some() { + sources += 1; + } + if self.like.is_some() { + sources += 1; + } + if self.clone.is_some() { + sources += 1; + } + + sources == 1 } pub fn build(self) -> Statement { diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index c2cd507ce..d53c9db05 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -144,4 +144,8 @@ impl Dialect for BigQueryDialect { fn supports_pipe_operator(&self) -> bool { true } + + fn supports_create_table_multi_schema_info_sources(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index deb5719d5..c79b279df 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -590,6 +590,13 @@ pub trait Dialect: Debug + Any { false } + /// Returne true if the dialect supports specifying multiple options + /// in a `CREATE TABLE` statement for the structure of the new table. For example: + /// `CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a` + fn supports_create_table_multi_schema_info_sources(&self) -> bool { + false + } + /// Dialect-specific infix parser override /// /// This method is called to parse the next infix expression. diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 8a9a52a12..fcf94ee75 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -688,7 +688,7 @@ pub fn parse_create_table( builder = builder.columns(columns).constraints(constraints); } Token::EOF => { - if !builder.has_schema_info() { + if !builder.validate_schema_info() { return Err(ParserError::ParserError( "unexpected end of input".to_string(), )); @@ -697,7 +697,7 @@ pub fn parse_create_table( break; } Token::SemiColon => { - if !builder.has_schema_info() { + if !builder.validate_schema_info() { return Err(ParserError::ParserError( "unexpected end of input".to_string(), )); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b7b5b630b..f43ab8dfb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4346,8 +4346,9 @@ fn parse_create_table_as() { // BigQuery allows specifying table schema in CTAS // ANSI SQL and PostgreSQL let you only specify the list of columns // (without data types) in a CTAS, but we have yet to support that. + let dialects = all_dialects_where(|d| d.supports_create_table_multi_schema_info_sources()); let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a"; - match verified_stmt(sql) { + match dialects.verified_stmt(sql) { Statement::CreateTable(CreateTable { columns, query, .. }) => { assert_eq!(columns.len(), 2); assert_eq!(columns[0].to_string(), "a INT".to_string()); @@ -4452,20 +4453,6 @@ fn parse_create_or_replace_table() { } _ => unreachable!(), } - - let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a"; - match verified_stmt(sql) { - Statement::CreateTable(CreateTable { columns, query, .. }) => { - assert_eq!(columns.len(), 2); - assert_eq!(columns[0].to_string(), "a INT".to_string()); - assert_eq!(columns[1].to_string(), "b INT".to_string()); - assert_eq!( - query, - Some(Box::new(verified_query("SELECT 1 AS b, 2 AS a"))) - ); - } - _ => unreachable!(), - } } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2bb08cf93..562ddfea7 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1020,15 +1020,24 @@ fn test_snowflake_create_table_trailing_options() { } #[test] -fn test_snowflake_create_table_has_schema_info() { - // The parser validates there's information on the schema of the new - // table, such as a list of columns or a source table\query to copy it from. +fn test_snowflake_create_table_valid_schema_info() { + // Validate there's exactly one source of information on the schema of the new table assert_eq!( snowflake() .parse_sql_statements("CREATE TABLE dst") .is_err(), true ); + assert_eq!( + snowflake().parse_sql_statements("CREATE OR REPLACE TEMP TABLE dst LIKE src AS (SELECT * FROM CUSTOMERS) ON COMMIT PRESERVE ROWS").is_err(), + true + ); + assert_eq!( + snowflake() + .parse_sql_statements("CREATE OR REPLACE TEMP TABLE dst CLONE customers LIKE customer2") + .is_err(), + true + ); } #[test]