Skip to content

Commit 750a7aa

Browse files
authored
Snowflake: support trailing options in CREATE TABLE (#1931)
1 parent bc2c4e2 commit 750a7aa

File tree

6 files changed

+80
-20
lines changed

6 files changed

+80
-20
lines changed

src/ast/helpers/stmt_create_table.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,26 @@ impl CreateTableBuilder {
383383
self
384384
}
385385

386+
/// Returns true if the statement has exactly one source of info on the schema of the new table.
387+
/// This is Snowflake-specific, some dialects allow more than one source.
388+
pub(crate) fn validate_schema_info(&self) -> bool {
389+
let mut sources = 0;
390+
if !self.columns.is_empty() {
391+
sources += 1;
392+
}
393+
if self.query.is_some() {
394+
sources += 1;
395+
}
396+
if self.like.is_some() {
397+
sources += 1;
398+
}
399+
if self.clone.is_some() {
400+
sources += 1;
401+
}
402+
403+
sources == 1
404+
}
405+
386406
pub fn build(self) -> Statement {
387407
Statement::CreateTable(CreateTable {
388408
or_replace: self.or_replace,

src/dialect/bigquery.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,8 @@ impl Dialect for BigQueryDialect {
144144
fn supports_pipe_operator(&self) -> bool {
145145
true
146146
}
147+
148+
fn supports_create_table_multi_schema_info_sources(&self) -> bool {
149+
true
150+
}
147151
}

src/dialect/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,13 @@ pub trait Dialect: Debug + Any {
590590
false
591591
}
592592

593+
/// Returne true if the dialect supports specifying multiple options
594+
/// in a `CREATE TABLE` statement for the structure of the new table. For example:
595+
/// `CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a`
596+
fn supports_create_table_multi_schema_info_sources(&self) -> bool {
597+
false
598+
}
599+
593600
/// Dialect-specific infix parser override
594601
///
595602
/// This method is called to parse the next infix expression.

src/dialect/snowflake.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -555,17 +555,14 @@ pub fn parse_create_table(
555555
Keyword::AS => {
556556
let query = parser.parse_query()?;
557557
builder = builder.query(Some(query));
558-
break;
559558
}
560559
Keyword::CLONE => {
561560
let clone = parser.parse_object_name(false).ok();
562561
builder = builder.clone_clause(clone);
563-
break;
564562
}
565563
Keyword::LIKE => {
566564
let like = parser.parse_object_name(false).ok();
567565
builder = builder.like(like);
568-
break;
569566
}
570567
Keyword::CLUSTER => {
571568
parser.expect_keyword_is(Keyword::BY)?;
@@ -691,7 +688,7 @@ pub fn parse_create_table(
691688
builder = builder.columns(columns).constraints(constraints);
692689
}
693690
Token::EOF => {
694-
if builder.columns.is_empty() {
691+
if !builder.validate_schema_info() {
695692
return Err(ParserError::ParserError(
696693
"unexpected end of input".to_string(),
697694
));
@@ -700,7 +697,7 @@ pub fn parse_create_table(
700697
break;
701698
}
702699
Token::SemiColon => {
703-
if builder.columns.is_empty() {
700+
if !builder.validate_schema_info() {
704701
return Err(ParserError::ParserError(
705702
"unexpected end of input".to_string(),
706703
));

tests/sqlparser_common.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4347,8 +4347,9 @@ fn parse_create_table_as() {
43474347
// BigQuery allows specifying table schema in CTAS
43484348
// ANSI SQL and PostgreSQL let you only specify the list of columns
43494349
// (without data types) in a CTAS, but we have yet to support that.
4350+
let dialects = all_dialects_where(|d| d.supports_create_table_multi_schema_info_sources());
43504351
let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a";
4351-
match verified_stmt(sql) {
4352+
match dialects.verified_stmt(sql) {
43524353
Statement::CreateTable(CreateTable { columns, query, .. }) => {
43534354
assert_eq!(columns.len(), 2);
43544355
assert_eq!(columns[0].to_string(), "a INT".to_string());
@@ -4453,20 +4454,6 @@ fn parse_create_or_replace_table() {
44534454
}
44544455
_ => unreachable!(),
44554456
}
4456-
4457-
let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a";
4458-
match verified_stmt(sql) {
4459-
Statement::CreateTable(CreateTable { columns, query, .. }) => {
4460-
assert_eq!(columns.len(), 2);
4461-
assert_eq!(columns[0].to_string(), "a INT".to_string());
4462-
assert_eq!(columns[1].to_string(), "b INT".to_string());
4463-
assert_eq!(
4464-
query,
4465-
Some(Box::new(verified_query("SELECT 1 AS b, 2 AS a")))
4466-
);
4467-
}
4468-
_ => unreachable!(),
4469-
}
44704457
}
44714458

44724459
#[test]

tests/sqlparser_snowflake.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,51 @@ fn test_snowflake_create_iceberg_table_without_location() {
995995
);
996996
}
997997

998+
#[test]
999+
fn test_snowflake_create_table_trailing_options() {
1000+
// Serialization to SQL assume that in `CREATE TABLE AS` the options come before the `AS (<query>)`
1001+
// but Snowflake supports also the other way around
1002+
snowflake()
1003+
.verified_stmt("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS AS (SELECT * FROM src)");
1004+
snowflake()
1005+
.parse_sql_statements(
1006+
"CREATE TEMPORARY TABLE dst AS (SELECT * FROM src) ON COMMIT PRESERVE ROWS",
1007+
)
1008+
.unwrap();
1009+
1010+
// Same for `CREATE TABLE LIKE|CLONE`:
1011+
snowflake().verified_stmt("CREATE TEMPORARY TABLE dst LIKE src ON COMMIT PRESERVE ROWS");
1012+
snowflake()
1013+
.parse_sql_statements("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS LIKE src")
1014+
.unwrap();
1015+
1016+
snowflake().verified_stmt("CREATE TEMPORARY TABLE dst CLONE src ON COMMIT PRESERVE ROWS");
1017+
snowflake()
1018+
.parse_sql_statements("CREATE TEMPORARY TABLE dst ON COMMIT PRESERVE ROWS CLONE src")
1019+
.unwrap();
1020+
}
1021+
1022+
#[test]
1023+
fn test_snowflake_create_table_valid_schema_info() {
1024+
// Validate there's exactly one source of information on the schema of the new table
1025+
assert_eq!(
1026+
snowflake()
1027+
.parse_sql_statements("CREATE TABLE dst")
1028+
.is_err(),
1029+
true
1030+
);
1031+
assert_eq!(
1032+
snowflake().parse_sql_statements("CREATE OR REPLACE TEMP TABLE dst LIKE src AS (SELECT * FROM CUSTOMERS) ON COMMIT PRESERVE ROWS").is_err(),
1033+
true
1034+
);
1035+
assert_eq!(
1036+
snowflake()
1037+
.parse_sql_statements("CREATE OR REPLACE TEMP TABLE dst CLONE customers LIKE customer2")
1038+
.is_err(),
1039+
true
1040+
);
1041+
}
1042+
9981043
#[test]
9991044
fn parse_sf_create_or_replace_view_with_comment_missing_equal() {
10001045
assert!(snowflake_and_generic()

0 commit comments

Comments
 (0)