diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 67aefb392..ad22d5c27 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -3060,6 +3060,20 @@ pub struct CreateTable { /// Redshift `BACKUP` option: `BACKUP { YES | NO }` /// pub backup: Option, + /// `MULTISET | SET` table-kind prefix. + /// `Some(true)` => `MULTISET`, `Some(false)` => `SET`. + /// + /// [Teradata](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Table-Statements/CREATE-TABLE-and-CREATE-TABLE-AS/Syntax-Elements/MULTISET-or-SET) + pub multiset: Option, + /// `FALLBACK` clause. + /// `Some(true)` => `FALLBACK`, `Some(false)` => `NO FALLBACK` + /// + /// [Teradata](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Table-Statements/CREATE-TABLE-and-CREATE-TABLE-AS/Syntax-Elements/FALLBACK-or-NO-FALLBACK) + pub fallback: Option, + /// `WITH DATA` clause on a `CREATE TABLE ... AS` statement. + /// + /// [Teradata](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Table-Statements/CREATE-TABLE-and-CREATE-TABLE-AS/Syntax-Elements/AS_clause/WITH-Clause-Phrase) + pub with_data: Option, } impl fmt::Display for CreateTable { @@ -3073,7 +3087,7 @@ 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}{snapshot}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{multiset}{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 { "" }, @@ -3087,14 +3101,20 @@ impl fmt::Display for CreateTable { }) .unwrap_or(""), if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + multiset = self + .multiset + .map(|m| if m { "MULTISET " } else { "SET " }) + .unwrap_or(""), temporary = if self.temporary { "TEMPORARY " } else { "" }, transient = if self.transient { "TRANSIENT " } else { "" }, volatile = if self.volatile { "VOLATILE " } else { "" }, - // Only for Snowflake iceberg = if self.iceberg { "ICEBERG " } else { "" }, dynamic = if self.dynamic { "DYNAMIC " } else { "" }, name = self.name, )?; + if let Some(fallback) = self.fallback { + write!(f, ", {}", if fallback { "FALLBACK" } else { "NO FALLBACK" })?; + } if let Some(partition_of) = &self.partition_of { write!(f, " PARTITION OF {partition_of}")?; } @@ -3379,6 +3399,41 @@ impl fmt::Display for CreateTable { if let Some(query) = &self.query { write!(f, " AS {query}")?; } + if let Some(with_data) = &self.with_data { + write!(f, " {with_data}")?; + } + Ok(()) + } +} + +/// `WITH DATA` clause on `CREATE TABLE ... AS` statement. +/// +/// [Teradata](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Table-Statements/CREATE-TABLE-and-CREATE-TABLE-AS/Syntax-Elements/AS_clause/WITH-Clause-Phrase) +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct WithData { + /// `true` for `WITH DATA`, `false` for `WITH NO DATA`. + pub data: bool, + /// `Some(true)` for `AND STATISTICS`, `Some(false)` for `AND NO STATISTICS`, + /// `None` if the `AND [NO] STATISTICS` sub-clause is omitted. + pub statistics: Option, +} + +impl fmt::Display for WithData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("WITH ")?; + if !self.data { + f.write_str("NO ")?; + } + f.write_str("DATA")?; + if let Some(stats) = self.statistics { + f.write_str(" AND ")?; + if !stats { + f.write_str("NO ")?; + } + f.write_str("STATISTICS")?; + } Ok(()) } } diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index ab2feb693..fc81d3b86 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -29,7 +29,7 @@ use crate::ast::{ DistStyle, Expr, FileFormat, ForValues, HiveDistributionStyle, HiveFormat, Ident, InitializeKind, ObjectName, OnCommit, OneOrManyWithParens, Query, RefreshModeKind, RowAccessPolicy, Statement, StorageLifecyclePolicy, StorageSerializationPolicy, - TableConstraint, TableVersion, Tag, WrappedCollection, + TableConstraint, TableVersion, Tag, WithData, WrappedCollection, }; use crate::parser::ParserError; @@ -183,6 +183,12 @@ pub struct CreateTableBuilder { pub sortkey: Option>, /// Redshift `BACKUP` option. pub backup: Option, + /// `MULTISET | SET` table-kind prefix. + pub multiset: Option, + /// `FALLBACK` clause. + pub fallback: Option, + /// `WITH DATA` clause. + pub with_data: Option, } impl CreateTableBuilder { @@ -248,6 +254,9 @@ impl CreateTableBuilder { distkey: None, sortkey: None, backup: None, + multiset: None, + fallback: None, + with_data: None, } } /// Set `OR REPLACE` for the CREATE TABLE statement. @@ -556,6 +565,22 @@ impl CreateTableBuilder { self.backup = backup; self } + /// Set `MULTISET | SET` table-kind prefix. + /// Some(true) => `MULTISET`, Some(false) => `SET`. + pub fn multiset(mut self, multiset: Option) -> Self { + self.multiset = multiset; + self + } + /// Set `FALLBACK` / `NO FALLBACK` flag. + pub fn fallback(mut self, fallback: Option) -> Self { + self.fallback = fallback; + self + } + /// Set `WITH DATA` clause. + pub fn with_data(mut self, with_data: Option) -> Self { + self.with_data = with_data; + self + } /// Consume the builder and produce a `CreateTable`. pub fn build(self) -> CreateTable { CreateTable { @@ -618,6 +643,9 @@ impl CreateTableBuilder { distkey: self.distkey, sortkey: self.sortkey, backup: self.backup, + multiset: self.multiset, + fallback: self.fallback, + with_data: self.with_data, } } } @@ -699,6 +727,9 @@ impl From for CreateTableBuilder { distkey: table.distkey, sortkey: table.sortkey, backup: table.backup, + multiset: table.multiset, + fallback: table.fallback, + with_data: table.with_data, } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 886bea26d..26de859e8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -81,7 +81,7 @@ pub use self::ddl::{ PartitionBoundValue, ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, - UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, + UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData, }; pub use self::dml::{ Delete, Insert, Merge, MergeAction, MergeClause, MergeClauseKind, MergeInsertExpr, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0dc834ba0..18691c039 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -604,6 +604,9 @@ impl Spanned for CreateTable { distkey: _, sortkey: _, backup: _, + multiset: _, + fallback: _, + with_data: _, } = self; union_spans( diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6ab6cb15e..84032847a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1224,6 +1224,16 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect accepts a comma-separated list of table-level + /// options placed between the table name and the column-list parenthesis, e.g. + /// + /// ```sql + /// CREATE TABLE foo, NO FALLBACK, NO BEFORE JOURNAL (col INTEGER) + /// ``` + fn supports_leading_comma_before_table_options(&self) -> bool { + false + } + /// Returns true if the dialect supports PartiQL for querying semi-structured data /// fn supports_partiql(&self) -> bool { diff --git a/src/dialect/teradata.rs b/src/dialect/teradata.rs index e88a40075..c8470cb64 100644 --- a/src/dialect/teradata.rs +++ b/src/dialect/teradata.rs @@ -89,4 +89,9 @@ impl Dialect for TeradataDialect { fn supports_string_literal_concatenation(&self) -> bool { true } + + /// See + fn supports_leading_comma_before_table_options(&self) -> bool { + true + } } diff --git a/src/keywords.rs b/src/keywords.rs index 4fc8f72d1..a0a65be68 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -414,6 +414,7 @@ define_keywords!( FACTS, FAIL, FAILOVER, + FALLBACK, FALSE, FAMILY, FETCH, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 668c520e5..e76677439 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5124,6 +5124,7 @@ impl<'a> Parser<'a> { pub fn parse_create(&mut self) -> Result { let or_replace = self.parse_keywords(&[Keyword::OR, Keyword::REPLACE]); let or_alter = self.parse_keywords(&[Keyword::OR, Keyword::ALTER]); + let multiset = self.maybe_parse_multiset(); let local = self.parse_one_of_keywords(&[Keyword::LOCAL]).is_some(); let global = self.parse_one_of_keywords(&[Keyword::GLOBAL]).is_some(); let transient = self.parse_one_of_keywords(&[Keyword::TRANSIENT]).is_some(); @@ -5137,13 +5138,14 @@ impl<'a> Parser<'a> { let temporary = self .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) .is_some(); + let volatile = self.parse_keyword(Keyword::VOLATILE); 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.peek_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) + self.parse_create_table(or_replace, temporary, global, transient, volatile, multiset) .map(Into::into) } else if self.peek_keyword(Keyword::MATERIALIZED) || self.peek_keyword(Keyword::VIEW) @@ -8470,11 +8472,25 @@ impl<'a> Parser<'a> { temporary: bool, global: Option, transient: bool, + volatile: bool, + multiset: Option, ) -> Result { let allow_unquoted_hyphen = dialect_of!(self is BigQueryDialect); let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let table_name = self.parse_object_name(allow_unquoted_hyphen)?; + let fallback = if self.dialect.supports_leading_comma_before_table_options() + && self.consume_token(&Token::Comma) + { + let fallback = self.maybe_parse_fallback()?; + if fallback.is_none() { + self.prev_token(); // Put back comma. + } + fallback + } else { + None + }; + // PostgreSQL PARTITION OF for child partition tables // Note: This is a PostgreSQL-specific feature, but the dialect check was intentionally // removed to allow GenericDialect and other dialects to parse this syntax. This enables @@ -8626,6 +8642,13 @@ impl<'a> Parser<'a> { None }; + // `WITH DATA` clause only applies if there is a query body. + let with_data = if query.is_some() { + self.maybe_parse_with_data()? + } else { + None + }; + Ok(CreateTableBuilder::new(table_name) .temporary(temporary) .columns(columns) @@ -8633,6 +8656,9 @@ impl<'a> Parser<'a> { .or_replace(or_replace) .if_not_exists(if_not_exists) .transient(transient) + .volatile(volatile) + .multiset(multiset) + .fallback(fallback) .hive_distribution(hive_distribution) .hive_formats(hive_formats) .global(global) @@ -8652,6 +8678,7 @@ impl<'a> Parser<'a> { .for_values(for_values) .table_options(create_table_config.table_options) .primary_key(primary_key) + .with_data(with_data) .strict(strict) .backup(backup) .diststyle(diststyle) @@ -8660,6 +8687,47 @@ impl<'a> Parser<'a> { .build()) } + /// Parse `MULTISET` table-kind prefix on `CREATE TABLE`. + fn maybe_parse_multiset(&mut self) -> Option { + match self.parse_one_of_keywords(&[Keyword::SET, Keyword::MULTISET]) { + Some(Keyword::MULTISET) => Some(true), + Some(Keyword::SET) => Some(false), + _ => None, + } + } + + /// Parse `FALLBACK` option on a `CREATE TABLE` statement, + fn maybe_parse_fallback(&mut self) -> Result, ParserError> { + if self.parse_keywords(&[Keyword::NO, Keyword::FALLBACK]) { + Ok(Some(false)) + } else if self.parse_keyword(Keyword::FALLBACK) { + Ok(Some(true)) + } else { + Ok(None) + } + } + + /// Parse [`WithData`] clause on `CREATE TABLE ... AS` statement. + fn maybe_parse_with_data(&mut self) -> Result, ParserError> { + let data = if self.parse_keywords(&[Keyword::WITH, Keyword::DATA]) { + true + } else if self.parse_keywords(&[Keyword::WITH, Keyword::NO, Keyword::DATA]) { + false + } else { + return Ok(None); + }; + + let statistics = if self.parse_keywords(&[Keyword::AND, Keyword::STATISTICS]) { + Some(true) + } else if self.parse_keywords(&[Keyword::AND, Keyword::NO, Keyword::STATISTICS]) { + Some(false) + } else { + None + }; + + Ok(Some(WithData { data, statistics })) + } + fn maybe_parse_create_table_like( &mut self, allow_unquoted_hyphen: bool, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index df6268580..548ad27cf 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -794,6 +794,9 @@ fn test_duckdb_union_datatype() { distkey: Default::default(), sortkey: Default::default(), backup: Default::default(), + multiset: Default::default(), + fallback: Default::default(), + with_data: Default::default(), }), stmt ); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 1e053da78..d784c74ae 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2013,6 +2013,9 @@ fn parse_create_table_with_valid_options() { distkey: None, sortkey: None, backup: None, + multiset: None, + fallback: None, + with_data: None, }) ); } @@ -2187,6 +2190,9 @@ fn parse_create_table_with_identity_column() { distkey: None, sortkey: None, backup: None, + multiset: None, + fallback: None, + with_data: None, }), ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 86315b1ef..87d17d1b4 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6716,6 +6716,9 @@ fn parse_trigger_related_functions() { distkey: None, sortkey: None, backup: None, + multiset: None, + fallback: None, + with_data: None, } ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 790bf1515..52b4a841f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -471,13 +471,6 @@ fn test_snowflake_create_invalid_temporal_table() { )) ); - assert_eq!( - snowflake().parse_sql_statements("CREATE TEMP VOLATILE TABLE my_table (a INT)"), - Err(ParserError::ParserError( - "Expected: an object type after CREATE, found: VOLATILE".to_string() - )) - ); - assert_eq!( snowflake().parse_sql_statements("CREATE TEMP TRANSIENT TABLE my_table (a INT)"), Err(ParserError::ParserError( diff --git a/tests/sqlparser_teradata.rs b/tests/sqlparser_teradata.rs index b8e6a53ce..a4e66af0d 100644 --- a/tests/sqlparser_teradata.rs +++ b/tests/sqlparser_teradata.rs @@ -17,7 +17,8 @@ //! Test SQL syntax, specific to [sqlparser::dialect::TeradataDialect]. -use sqlparser::dialect::{Dialect, TeradataDialect}; +use sqlparser::dialect::{Dialect, GenericDialect, TeradataDialect}; +use sqlparser::test_utils::all_dialects_where; use test_utils::TestedDialects; mod test_utils; @@ -26,6 +27,10 @@ fn teradata() -> TestedDialects { TestedDialects::new(vec![Box::new(TeradataDialect)]) } +fn teradata_and_generic() -> TestedDialects { + TestedDialects::new(vec![Box::new(TeradataDialect), Box::new(GenericDialect {})]) +} + #[test] fn dialect_methods() { let d: &dyn Dialect = &TeradataDialect; @@ -45,6 +50,7 @@ fn dialect_methods() { assert!(d.supports_top_before_distinct()); assert!(d.supports_window_function_null_treatment_arg()); assert!(d.supports_string_literal_concatenation()); + assert!(d.supports_leading_comma_before_table_options()); } #[test] @@ -61,3 +67,57 @@ fn parse_identifier() { r#"NULL AS "quoted id""# )); } + +#[test] +fn parse_create_table_multiset() { + teradata_and_generic().verified_stmt("CREATE MULTISET TABLE foo (id INT)"); + teradata_and_generic().verified_stmt("CREATE SET TABLE foo (id INT)"); +} + +#[test] +fn parse_create_table_volatile() { + teradata_and_generic().verified_stmt("CREATE VOLATILE TABLE foo (id INT)"); + teradata_and_generic().verified_stmt("CREATE MULTISET VOLATILE TABLE foo (id INT)"); +} + +#[test] +fn parse_create_table_fallback() { + teradata().verified_stmt("CREATE TABLE foo, FALLBACK (id INT)"); + teradata().verified_stmt("CREATE TABLE foo, NO FALLBACK (id INT)"); + teradata().verified_stmt("CREATE MULTISET TABLE foo, NO FALLBACK (id INT)"); +} + +#[test] +fn parse_create_table_as_with_data() { + teradata_and_generic().verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH DATA"); + teradata_and_generic().verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH NO DATA"); + teradata_and_generic() + .verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH DATA AND STATISTICS"); + teradata_and_generic() + .verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH DATA AND NO STATISTICS"); + teradata_and_generic() + .verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH NO DATA AND STATISTICS"); + teradata_and_generic() + .verified_stmt("CREATE TABLE foo AS (SELECT 1 AS a) WITH NO DATA AND NO STATISTICS"); +} + +#[test] +fn parse_create_table_options() { + teradata().verified_stmt(concat!( + "CREATE MULTISET VOLATILE TABLE foo, NO FALLBACK ", + "(id INT, name VARCHAR(100)) ", + "ON COMMIT PRESERVE ROWS" + )); +} + +#[test] +fn parse_leading_comma_before_table_options() { + let dialect = all_dialects_where(|d| d.supports_leading_comma_before_table_options()); + dialect.verified_stmt("CREATE TABLE foo, FALLBACK (id INT)"); + + let unsupported_dialects = + all_dialects_where(|d| !d.supports_leading_comma_before_table_options()); + assert!(unsupported_dialects + .parse_sql_statements("CREATE TABLE foo, FALLBACK (id INT)") + .is_err()); +}