From 02070c4afcf02f3e28b5e542512119e2fb8437e6 Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Wed, 4 Mar 2026 15:18:37 -0800 Subject: [PATCH] Fixed create snapshot table for bigquery --- src/ast/ddl.rs | 6 ++++- src/ast/helpers/stmt_create_table.rs | 10 ++++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 37 +++++++++++++++++++++++++++- tests/sqlparser_bigquery.rs | 21 ++++++++++++++++ tests/sqlparser_duckdb.rs | 1 + tests/sqlparser_mssql.rs | 2 ++ tests/sqlparser_postgres.rs | 1 + 8 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 895959a3d..f6701cd66 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2903,6 +2903,9 @@ pub struct CreateTable { pub volatile: bool, /// `ICEBERG` clause pub iceberg: bool, + /// BigQuery `SNAPSHOT` clause + /// + pub snapshot: bool, /// Table name #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] pub name: ObjectName, @@ -3051,9 +3054,10 @@ impl fmt::Display for CreateTable { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}{dynamic}{iceberg}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}{dynamic}{iceberg}{snapshot}TABLE {if_not_exists}{name}", or_replace = if self.or_replace { "OR REPLACE " } else { "" }, external = if self.external { "EXTERNAL " } else { "" }, + snapshot = if self.snapshot { "SNAPSHOT " } else { "" }, global = self.global .map(|global| { if global { diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 258f9c835..a00b43ca4 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -81,6 +81,8 @@ pub struct CreateTableBuilder { pub volatile: bool, /// Iceberg-specific table flag. pub iceberg: bool, + /// BigQuery `SNAPSHOT` table flag. + pub snapshot: bool, /// Whether `DYNAMIC` table option is set. pub dynamic: bool, /// The table name. @@ -189,6 +191,7 @@ impl CreateTableBuilder { transient: false, volatile: false, iceberg: false, + snapshot: false, dynamic: false, name, columns: vec![], @@ -278,6 +281,11 @@ impl CreateTableBuilder { self.iceberg = iceberg; self } + /// Set `SNAPSHOT` table flag (BigQuery). + pub fn snapshot(mut self, snapshot: bool) -> Self { + self.snapshot = snapshot; + self + } /// Set `DYNAMIC` table option. pub fn dynamic(mut self, dynamic: bool) -> Self { self.dynamic = dynamic; @@ -532,6 +540,7 @@ impl CreateTableBuilder { transient: self.transient, volatile: self.volatile, iceberg: self.iceberg, + snapshot: self.snapshot, dynamic: self.dynamic, name: self.name, columns: self.columns, @@ -609,6 +618,7 @@ impl From for CreateTableBuilder { transient: table.transient, volatile: table.volatile, iceberg: table.iceberg, + snapshot: table.snapshot, dynamic: table.dynamic, name: table.name, columns: table.columns, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 3f73af408..10c1f7af4 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -538,6 +538,7 @@ impl Spanned for CreateTable { transient: _, // bool volatile: _, // bool iceberg: _, // bool, Snowflake specific + snapshot: _, // bool, BigQuery specific name, columns, constraints, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 75b5bfa76..6392c6161 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5099,7 +5099,9 @@ impl<'a> Parser<'a> { let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); let create_view_params = self.parse_create_view_params()?; - if self.parse_keyword(Keyword::TABLE) { + if self.parse_keywords(&[Keyword::SNAPSHOT, Keyword::TABLE]) { + self.parse_create_snapshot_table().map(Into::into) + } else if self.parse_keyword(Keyword::TABLE) { self.parse_create_table(or_replace, temporary, global, transient) .map(Into::into) } else if self.peek_keyword(Keyword::MATERIALIZED) @@ -6314,6 +6316,39 @@ impl<'a> Parser<'a> { .build()) } + /// Parse BigQuery `CREATE SNAPSHOT TABLE` statement. + /// + /// + pub fn parse_create_snapshot_table(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + let table_name = self.parse_object_name(true)?; + + self.expect_keyword_is(Keyword::CLONE)?; + let clone = Some(self.parse_object_name(true)?); + + let version = + if self.parse_keywords(&[Keyword::FOR, Keyword::SYSTEM_TIME, Keyword::AS, Keyword::OF]) + { + Some(TableVersion::ForSystemTimeAsOf(self.parse_expr()?)) + } else { + None + }; + + let table_options = if let Some(options) = self.maybe_parse_options(Keyword::OPTIONS)? { + CreateTableOptions::Options(options) + } else { + CreateTableOptions::None + }; + + Ok(CreateTableBuilder::new(table_name) + .snapshot(true) + .if_not_exists(if_not_exists) + .clone_clause(clone) + .version(version) + .table_options(table_options) + .build()) + } + /// Parse a file format for external tables. pub fn parse_file_format(&mut self) -> Result { let next_token = self.next_token(); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index ce962cb80..3ed2043e8 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2890,3 +2890,24 @@ fn test_alter_schema() { bigquery_and_generic() .verified_stmt("ALTER SCHEMA IF EXISTS mydataset SET OPTIONS (location = 'us')"); } + +#[test] +fn test_create_snapshot_table() { + bigquery().verified_stmt("CREATE SNAPSHOT TABLE dataset_id.table1 CLONE dataset_id.table2"); + + bigquery().verified_stmt( + "CREATE SNAPSHOT TABLE IF NOT EXISTS dataset_id.table1 CLONE dataset_id.table2", + ); + + bigquery().verified_stmt( + "CREATE SNAPSHOT TABLE dataset_id.table1 CLONE dataset_id.table2 FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)", + ); + + bigquery().verified_stmt( + "CREATE SNAPSHOT TABLE dataset_id.table1 CLONE dataset_id.table2 OPTIONS(expiration_timestamp = TIMESTAMP '2025-01-01 00:00:00 UTC', friendly_name = 'my_table')", + ); + + bigquery().verified_stmt( + "CREATE SNAPSHOT TABLE IF NOT EXISTS dataset_id.table1 CLONE dataset_id.table2 FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR) OPTIONS(expiration_timestamp = TIMESTAMP '2025-01-01 00:00:00 UTC')", + ); +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 0512053a4..913ec96bc 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -709,6 +709,7 @@ fn test_duckdb_union_datatype() { transient: Default::default(), volatile: Default::default(), iceberg: Default::default(), + snapshot: false, dynamic: Default::default(), name: ObjectName::from(vec!["tbl1".into()]), columns: vec![ diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index aa31b6327..c3fe6dbdd 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1985,6 +1985,7 @@ fn parse_create_table_with_valid_options() { for_values: None, strict: false, iceberg: false, + snapshot: false, copy_grants: false, enable_schema_evolution: None, change_tracking: None, @@ -2119,6 +2120,7 @@ fn parse_create_table_with_identity_column() { transient: false, volatile: false, iceberg: false, + snapshot: false, name: ObjectName::from(vec![Ident { value: "mytable".to_string(), quote_style: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c83860ff5..b929ea7bc 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6409,6 +6409,7 @@ fn parse_trigger_related_functions() { transient: false, volatile: false, iceberg: false, + snapshot: false, name: ObjectName::from(vec![Ident::new("emp")]), columns: vec![ ColumnDef {