From c0c2ae55e6f3e7585d9f60f9a0c8173cf6612248 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Wed, 23 Jul 2025 11:19:22 +0300 Subject: [PATCH 1/2] Redshift: CREATE TABLE ... (LIKE ..) --- src/ast/dml.rs | 463 ++++++++++++++++++++++++++- src/ast/helpers/stmt_create_table.rs | 6 +- src/ast/mod.rs | 57 ++++ src/ast/spans.rs | 3 +- src/dialect/mod.rs | 19 ++ src/dialect/redshift.rs | 4 + src/dialect/snowflake.rs | 9 +- src/keywords.rs | 3 + src/parser/mod.rs | 31 +- tests/sqlparser_common.rs | 74 +++++ 10 files changed, 659 insertions(+), 10 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 63d6b86c7d..cc227835c4 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -29,7 +29,10 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; +use crate::{ + ast::CreateTableLikeKind, + display_utils::{indented_list, Indent, SpaceOrNewline}, +}; use super::{ display_comma_separated, query::InputFormatClause, Assignment, Expr, FromTable, Ident, @@ -37,6 +40,464 @@ use super::{ Setting, SqliteOnConflict, TableObject, TableWithJoins, }; +/// Index column type. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IndexColumn { + pub column: OrderByExpr, + pub operator_class: Option, +} + +impl Display for IndexColumn { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.column)?; + if let Some(operator_class) = &self.operator_class { + write!(f, " {operator_class}")?; + } + Ok(()) + } +} + +/// CREATE INDEX statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateIndex { + /// index name + pub name: Option, + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub table_name: ObjectName, + pub using: Option, + pub columns: Vec, + pub unique: bool, + pub concurrently: bool, + pub if_not_exists: bool, + pub include: Vec, + pub nulls_distinct: Option, + /// WITH clause: + pub with: Vec, + pub predicate: Option, +} + +impl Display for CreateIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "CREATE {unique}INDEX {concurrently}{if_not_exists}", + unique = if self.unique { "UNIQUE " } else { "" }, + concurrently = if self.concurrently { + "CONCURRENTLY " + } else { + "" + }, + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + )?; + if let Some(value) = &self.name { + write!(f, "{value} ")?; + } + write!(f, "ON {}", self.table_name)?; + if let Some(value) = &self.using { + write!(f, " USING {value} ")?; + } + write!(f, "({})", display_separated(&self.columns, ","))?; + if !self.include.is_empty() { + write!(f, " INCLUDE ({})", display_separated(&self.include, ","))?; + } + if let Some(value) = self.nulls_distinct { + if value { + write!(f, " NULLS DISTINCT")?; + } else { + write!(f, " NULLS NOT DISTINCT")?; + } + } + if !self.with.is_empty() { + write!(f, " WITH ({})", display_comma_separated(&self.with))?; + } + if let Some(predicate) = &self.predicate { + write!(f, " WHERE {predicate}")?; + } + Ok(()) + } +} + +/// CREATE TABLE statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateTable { + pub or_replace: bool, + pub temporary: bool, + pub external: bool, + pub global: Option, + pub if_not_exists: bool, + pub transient: bool, + pub volatile: bool, + pub iceberg: bool, + /// Table name + #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] + pub name: ObjectName, + /// Optional schema + pub columns: Vec, + pub constraints: Vec, + pub hive_distribution: HiveDistributionStyle, + pub hive_formats: Option, + pub table_options: CreateTableOptions, + pub file_format: Option, + pub location: Option, + pub query: Option>, + pub without_rowid: bool, + pub like: Option, + pub clone: Option, + // For Hive dialect, the table comment is after the column definitions without `=`, + // so the `comment` field is optional and different than the comment field in the general options list. + // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) + pub comment: Option, + pub on_commit: Option, + /// ClickHouse "ON CLUSTER" clause: + /// + pub on_cluster: Option, + /// ClickHouse "PRIMARY KEY " clause. + /// + pub primary_key: Option>, + /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different + /// than empty (represented as ()), the latter meaning "no sorting". + /// + pub order_by: Option>, + /// BigQuery: A partition expression for the table. + /// + pub partition_by: Option>, + /// BigQuery: Table clustering column list. + /// + /// Snowflake: Table clustering list which contains base column, expressions on base columns. + /// + pub cluster_by: Option>>, + /// Hive: Table clustering column list. + /// + pub clustered_by: Option, + /// Postgres `INHERITs` clause, which contains the list of tables from which + /// the new table inherits. + /// + /// + pub inherits: Option>, + /// SQLite "STRICT" clause. + /// if the "STRICT" table-option keyword is added to the end, after the closing ")", + /// then strict typing rules apply to that table. + pub strict: bool, + /// Snowflake "COPY GRANTS" clause + /// + pub copy_grants: bool, + /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause + /// + pub enable_schema_evolution: Option, + /// Snowflake "CHANGE_TRACKING" clause + /// + pub change_tracking: Option, + /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause + /// + pub data_retention_time_in_days: Option, + /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause + /// + pub max_data_extension_time_in_days: Option, + /// Snowflake "DEFAULT_DDL_COLLATION" clause + /// + pub default_ddl_collation: Option, + /// Snowflake "WITH AGGREGATION POLICY" clause + /// + pub with_aggregation_policy: Option, + /// Snowflake "WITH ROW ACCESS POLICY" clause + /// + pub with_row_access_policy: Option, + /// Snowflake "WITH TAG" clause + /// + pub with_tags: Option>, + /// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables + /// + pub external_volume: Option, + /// Snowflake "BASE_LOCATION" clause for Iceberg tables + /// + pub base_location: Option, + /// Snowflake "CATALOG" clause for Iceberg tables + /// + pub catalog: Option, + /// Snowflake "CATALOG_SYNC" clause for Iceberg tables + /// + pub catalog_sync: Option, + /// Snowflake "STORAGE_SERIALIZATION_POLICY" clause for Iceberg tables + /// + pub storage_serialization_policy: Option, +} + +impl Display for CreateTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // We want to allow the following options + // Empty column list, allowed by PostgreSQL: + // `CREATE TABLE t ()` + // No columns provided for CREATE TABLE AS: + // `CREATE TABLE t AS SELECT a from t2` + // Columns provided for CREATE TABLE AS: + // `CREATE TABLE t (a INT) AS SELECT a from t2` + write!( + f, + "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}{iceberg}TABLE {if_not_exists}{name}", + or_replace = if self.or_replace { "OR REPLACE " } else { "" }, + external = if self.external { "EXTERNAL " } else { "" }, + global = self.global + .map(|global| { + if global { + "GLOBAL " + } else { + "LOCAL " + } + }) + .unwrap_or(""), + if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, + 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 { "" }, + name = self.name, + )?; + if let Some(on_cluster) = &self.on_cluster { + write!(f, " ON CLUSTER {on_cluster}")?; + } + if !self.columns.is_empty() || !self.constraints.is_empty() { + f.write_str(" (")?; + NewLine.fmt(f)?; + Indent(DisplayCommaSeparated(&self.columns)).fmt(f)?; + if !self.columns.is_empty() && !self.constraints.is_empty() { + f.write_str(",")?; + SpaceOrNewline.fmt(f)?; + } + Indent(DisplayCommaSeparated(&self.constraints)).fmt(f)?; + NewLine.fmt(f)?; + f.write_str(")")?; + } else if self.query.is_none() && self.like.is_none() && self.clone.is_none() { + // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens + f.write_str(" ()")?; + } else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like { + write!(f, " ({like_in_columns_list})")?; + } + + // Hive table comment should be after column definitions, please refer to: + // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) + if let Some(comment) = &self.comment { + write!(f, " COMMENT '{comment}'")?; + } + + // Only for SQLite + if self.without_rowid { + write!(f, " WITHOUT ROWID")?; + } + + if let Some(CreateTableLikeKind::NotParenthesized(like)) = &self.like { + write!(f, " {like}")?; + } + + if let Some(c) = &self.clone { + write!(f, " CLONE {c}")?; + } + + match &self.hive_distribution { + HiveDistributionStyle::PARTITIONED { columns } => { + write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?; + } + HiveDistributionStyle::SKEWED { + columns, + on, + stored_as_directories, + } => { + write!( + f, + " SKEWED BY ({})) ON ({})", + display_comma_separated(columns), + display_comma_separated(on) + )?; + if *stored_as_directories { + write!(f, " STORED AS DIRECTORIES")?; + } + } + _ => (), + } + + if let Some(clustered_by) = &self.clustered_by { + write!(f, " {clustered_by}")?; + } + + if let Some(HiveFormat { + row_format, + serde_properties, + storage, + location, + }) = &self.hive_formats + { + match row_format { + Some(HiveRowFormat::SERDE { class }) => write!(f, " ROW FORMAT SERDE '{class}'")?, + Some(HiveRowFormat::DELIMITED { delimiters }) => { + write!(f, " ROW FORMAT DELIMITED")?; + if !delimiters.is_empty() { + write!(f, " {}", display_separated(delimiters, " "))?; + } + } + None => (), + } + match storage { + Some(HiveIOFormat::IOF { + input_format, + output_format, + }) => write!( + f, + " STORED AS INPUTFORMAT {input_format} OUTPUTFORMAT {output_format}" + )?, + Some(HiveIOFormat::FileFormat { format }) if !self.external => { + write!(f, " STORED AS {format}")? + } + _ => (), + } + if let Some(serde_properties) = serde_properties.as_ref() { + write!( + f, + " WITH SERDEPROPERTIES ({})", + display_comma_separated(serde_properties) + )?; + } + if !self.external { + if let Some(loc) = location { + write!(f, " LOCATION '{loc}'")?; + } + } + } + if self.external { + if let Some(file_format) = self.file_format { + write!(f, " STORED AS {file_format}")?; + } + write!(f, " LOCATION '{}'", self.location.as_ref().unwrap())?; + } + + match &self.table_options { + options @ CreateTableOptions::With(_) + | options @ CreateTableOptions::Plain(_) + | options @ CreateTableOptions::TableProperties(_) => write!(f, " {options}")?, + _ => (), + } + + if let Some(primary_key) = &self.primary_key { + write!(f, " PRIMARY KEY {primary_key}")?; + } + if let Some(order_by) = &self.order_by { + write!(f, " ORDER BY {order_by}")?; + } + if let Some(inherits) = &self.inherits { + write!(f, " INHERITS ({})", display_comma_separated(inherits))?; + } + if let Some(partition_by) = self.partition_by.as_ref() { + write!(f, " PARTITION BY {partition_by}")?; + } + if let Some(cluster_by) = self.cluster_by.as_ref() { + write!(f, " CLUSTER BY {cluster_by}")?; + } + if let options @ CreateTableOptions::Options(_) = &self.table_options { + write!(f, " {options}")?; + } + if let Some(external_volume) = self.external_volume.as_ref() { + write!(f, " EXTERNAL_VOLUME = '{external_volume}'")?; + } + + if let Some(catalog) = self.catalog.as_ref() { + write!(f, " CATALOG = '{catalog}'")?; + } + + if self.iceberg { + if let Some(base_location) = self.base_location.as_ref() { + write!(f, " BASE_LOCATION = '{base_location}'")?; + } + } + + if let Some(catalog_sync) = self.catalog_sync.as_ref() { + write!(f, " CATALOG_SYNC = '{catalog_sync}'")?; + } + + if let Some(storage_serialization_policy) = self.storage_serialization_policy.as_ref() { + write!( + f, + " STORAGE_SERIALIZATION_POLICY = {storage_serialization_policy}" + )?; + } + + if self.copy_grants { + write!(f, " COPY GRANTS")?; + } + + if let Some(is_enabled) = self.enable_schema_evolution { + write!( + f, + " ENABLE_SCHEMA_EVOLUTION={}", + if is_enabled { "TRUE" } else { "FALSE" } + )?; + } + + if let Some(is_enabled) = self.change_tracking { + write!( + f, + " CHANGE_TRACKING={}", + if is_enabled { "TRUE" } else { "FALSE" } + )?; + } + + if let Some(data_retention_time_in_days) = self.data_retention_time_in_days { + write!( + f, + " DATA_RETENTION_TIME_IN_DAYS={data_retention_time_in_days}", + )?; + } + + if let Some(max_data_extension_time_in_days) = self.max_data_extension_time_in_days { + write!( + f, + " MAX_DATA_EXTENSION_TIME_IN_DAYS={max_data_extension_time_in_days}", + )?; + } + + if let Some(default_ddl_collation) = &self.default_ddl_collation { + write!(f, " DEFAULT_DDL_COLLATION='{default_ddl_collation}'",)?; + } + + if let Some(with_aggregation_policy) = &self.with_aggregation_policy { + write!(f, " WITH AGGREGATION POLICY {with_aggregation_policy}",)?; + } + + if let Some(row_access_policy) = &self.with_row_access_policy { + write!(f, " {row_access_policy}",)?; + } + + if let Some(tag) = &self.with_tags { + write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; + } + + if self.on_commit.is_some() { + let on_commit = match self.on_commit { + Some(OnCommit::DeleteRows) => "ON COMMIT DELETE ROWS", + Some(OnCommit::PreserveRows) => "ON COMMIT PRESERVE ROWS", + Some(OnCommit::Drop) => "ON COMMIT DROP", + None => "", + }; + write!(f, " {on_commit}")?; + } + if self.strict { + write!(f, " STRICT")?; + } + if let Some(query) = &self.query { + write!(f, " AS {query}")?; + } + Ok(()) + } +} + /// INSERT statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index c727276d34..2c56ad3d26 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{ - ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableOptions, Expr, FileFormat, + ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, Statement, StorageSerializationPolicy, TableConstraint, Tag, WrappedCollection, @@ -81,7 +81,7 @@ pub struct CreateTableBuilder { pub location: Option, pub query: Option>, pub without_rowid: bool, - pub like: Option, + pub like: Option, pub clone: Option, pub comment: Option, pub on_commit: Option, @@ -237,7 +237,7 @@ impl CreateTableBuilder { self } - pub fn like(mut self, like: Option) -> Self { + pub fn like(mut self, like: Option) -> Self { self.like = like; self } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5b50d020c0..d065289a41 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -10465,6 +10465,63 @@ impl fmt::Display for CreateUser { } } +/// Specifies how to create a new table based on an existing table's schema. +/// +/// Not parenthesized: +/// '''sql +/// CREATE TABLE new LIKE old ... +/// ''' +/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-table#label-create-table-like) +/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_like) +/// +/// Parenthesized: +/// '''sql +/// CREATE TABLE new (LIKE old ...) +/// ''' +/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum CreateTableLikeKind { + Parenthesized(CreateTableLike), + NotParenthesized(CreateTableLike), +} + +#[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 CreateTableLikeDefaults { + Including, + Excluding, +} + +impl fmt::Display for CreateTableLikeDefaults { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreateTableLikeDefaults::Including => write!(f, "INCLUDING DEFAULTS"), + CreateTableLikeDefaults::Excluding => write!(f, "EXCLUDING DEFAULTS"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CreateTableLike { + pub name: ObjectName, + pub defaults: Option, +} + +impl fmt::Display for CreateTableLike { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "LIKE {}", self.name)?; + if let Some(defaults) = &self.defaults { + write!(f, " {defaults}")?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use crate::tokenizer::Location; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e170902681..da47b0f85b 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -592,7 +592,7 @@ impl Spanned for CreateTable { location: _, // string, no span query, without_rowid: _, // bool - like, + like: _, clone, comment: _, // todo, no span on_commit: _, @@ -627,7 +627,6 @@ impl Spanned for CreateTable { .chain(columns.iter().map(|i| i.span())) .chain(constraints.iter().map(|i| i.span())) .chain(query.iter().map(|i| i.span())) - .chain(like.iter().map(|i| i.span())) .chain(clone.iter().map(|i| i.span())), ) } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6ca364784c..cb1ca559c5 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1163,6 +1163,25 @@ pub trait Dialect: Debug + Any { fn supports_interval_options(&self) -> bool { false } + + /// Returns true if the dialect supports specifying which table to copy + /// the schema from inside parenthesis. + /// + /// Not parenthesized: + /// '''sql + /// CREATE TABLE new LIKE old ... + /// ''' + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-table#label-create-table-like) + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_like) + /// + /// Parenthesized: + /// '''sql + /// CREATE TABLE new (LIKE old ...) + /// ''' + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html) + fn supports_create_table_like_in_parens(&self) -> bool { + false + } } /// This represents the operators for which precedence must be defined diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index 68e025d180..c41bbfef83 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -139,4 +139,8 @@ impl Dialect for RedshiftSqlDialect { fn supports_select_exclude(&self) -> bool { true } + + fn supports_create_table_like_in_parens(&self) -> bool { + true + } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 8830e09a01..f30db89d1b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -668,8 +668,13 @@ pub fn parse_create_table( builder = builder.clone_clause(clone); } Keyword::LIKE => { - let like = parser.parse_object_name(false).ok(); - builder = builder.like(like); + let name = parser.parse_object_name(false)?; + builder = builder.like(Some(CreateTableLikeKind::NotParenthesized( + crate::ast::CreateTableLike { + name, + defaults: None, + }, + ))); } Keyword::CLUSTER => { parser.expect_keyword_is(Keyword::BY)?; diff --git a/src/keywords.rs b/src/keywords.rs index a729a525fc..659bc04399 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -268,6 +268,7 @@ define_keywords!( DECLARE, DEDUPLICATE, DEFAULT, + DEFAULTS, DEFAULT_DDL_COLLATION, DEFERRABLE, DEFERRED, @@ -339,6 +340,7 @@ define_keywords!( EXCEPTION, EXCHANGE, EXCLUDE, + EXCLUDING, EXCLUSIVE, EXEC, EXECUTE, @@ -441,6 +443,7 @@ define_keywords!( IN, INCLUDE, INCLUDE_NULL_VALUES, + INCLUDING, INCREMENT, INDEX, INDICATOR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3d47625410..085a0c2e63 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7347,8 +7347,35 @@ impl<'a> Parser<'a> { // Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs let on_cluster = self.parse_optional_on_cluster()?; - let like = if self.parse_keyword(Keyword::LIKE) || self.parse_keyword(Keyword::ILIKE) { - self.parse_object_name(allow_unquoted_hyphen).ok() + // Try to parse `CREATE TABLE new (LIKE old [{INCLUDING | EXCLUDING} DEFAULTS])` or `CREATE TABLE new LIKE old` + let like = if self.dialect.supports_create_table_like_in_parens() + && self.consume_token(&Token::LParen) + { + if self.parse_keyword(Keyword::LIKE) { + let name = self.parse_object_name(allow_unquoted_hyphen)?; + let defaults = if self.parse_keywords(&[Keyword::INCLUDING, Keyword::DEFAULTS]) { + Some(CreateTableLikeDefaults::Including) + } else if self.parse_keywords(&[Keyword::EXCLUDING, Keyword::DEFAULTS]) { + Some(CreateTableLikeDefaults::Excluding) + } else { + None + }; + self.expect_token(&Token::RParen)?; + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name, + defaults, + })) + } else { + // Rollback the '(' it's probably the columns list + self.prev_token(); + None + } + } else if self.parse_keyword(Keyword::LIKE) || self.parse_keyword(Keyword::ILIKE) { + let name = self.parse_object_name(allow_unquoted_hyphen)?; + Some(CreateTableLikeKind::NotParenthesized(CreateTableLike { + name, + defaults: None, + })) } else { None }; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4b9d748fc5..f983c8a9fb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16655,3 +16655,77 @@ fn test_parse_default_with_collate_column_option() { panic!("Expected create table statement"); } } + +#[test] +fn parse_create_table_like() { + let dialects = all_dialects_except(|d| d.supports_create_table_like_in_parens()); + let sql = "CREATE TABLE new LIKE old"; + match dialects.verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::NotParenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: None, + })) + ) + } + _ => unreachable!(), + } + let dialects = all_dialects_where(|d| d.supports_create_table_like_in_parens()); + let sql = "CREATE TABLE new (LIKE old)"; + match dialects.verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: None, + })) + ) + } + _ => unreachable!(), + } + let sql = "CREATE TABLE new (LIKE old INCLUDING DEFAULTS)"; + match dialects.verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: Some(CreateTableLikeDefaults::Including), + })) + ) + } + _ => unreachable!(), + } + let sql = "CREATE TABLE new (LIKE old EXCLUDING DEFAULTS)"; + match dialects.verified_stmt(sql) { + Statement::CreateTable(stmt) => { + assert_eq!( + stmt.name, + ObjectName::from(vec![Ident::new("new".to_string())]) + ); + assert_eq!( + stmt.like, + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name: ObjectName::from(vec![Ident::new("old".to_string())]), + defaults: Some(CreateTableLikeDefaults::Excluding), + })) + ) + } + _ => unreachable!(), + } +} From aedcce96dbca8426728617010253d1b2a4783878 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Thu, 14 Aug 2025 11:43:24 +0200 Subject: [PATCH 2/2] Code review comments --- src/ast/ddl.rs | 21 +- src/ast/dml.rs | 463 +-------------------------- src/ast/helpers/stmt_create_table.rs | 8 +- src/ast/mod.rs | 21 +- src/dialect/mod.rs | 2 +- src/dialect/redshift.rs | 2 +- src/dialect/snowflake.rs | 10 +- src/parser/mod.rs | 71 ++-- tests/sqlparser_common.rs | 6 +- 9 files changed, 75 insertions(+), 529 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1c2aaf48d1..65dfd6c56f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -31,12 +31,12 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, ArgMode, CommentDef, CreateFunctionBody, - CreateFunctionUsing, CreateTableOptions, DataType, Expr, FileFormat, FunctionBehavior, - FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, HiveDistributionStyle, - HiveFormat, HiveIOFormat, HiveRowFormat, Ident, MySQLColumnPosition, ObjectName, OnCommit, - OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, Query, RowAccessPolicy, - SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, Tag, Value, ValueWithSpan, - WrappedCollection, + CreateFunctionUsing, CreateTableLikeKind, CreateTableOptions, DataType, Expr, FileFormat, + FunctionBehavior, FunctionCalledOnNull, FunctionDeterminismSpecifier, FunctionParallel, + HiveDistributionStyle, HiveFormat, HiveIOFormat, HiveRowFormat, Ident, MySQLColumnPosition, + ObjectName, OnCommit, OneOrManyWithParens, OperateFunctionArg, OrderByExpr, ProjectionSelect, + Query, RowAccessPolicy, SequenceOptions, Spanned, SqlOption, StorageSerializationPolicy, Tag, + Value, ValueWithSpan, WrappedCollection, }; use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; use crate::keywords::Keyword; @@ -2430,7 +2430,7 @@ pub struct CreateTable { pub location: Option, pub query: Option>, pub without_rowid: bool, - pub like: Option, + pub like: Option, pub clone: Option, // For Hive dialect, the table comment is after the column definitions without `=`, // so the `comment` field is optional and different than the comment field in the general options list. @@ -2559,6 +2559,8 @@ impl fmt::Display for CreateTable { } else if self.query.is_none() && self.like.is_none() && self.clone.is_none() { // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens f.write_str(" ()")?; + } else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like { + write!(f, " ({like_in_columns_list})")?; } // Hive table comment should be after column definitions, please refer to: @@ -2572,9 +2574,8 @@ impl fmt::Display for CreateTable { write!(f, " WITHOUT ROWID")?; } - // Only for Hive - if let Some(l) = &self.like { - write!(f, " LIKE {l}")?; + if let Some(CreateTableLikeKind::Plain(like)) = &self.like { + write!(f, " {like}")?; } if let Some(c) = &self.clone { diff --git a/src/ast/dml.rs b/src/ast/dml.rs index cc227835c4..63d6b86c7d 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -29,10 +29,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::{ - ast::CreateTableLikeKind, - display_utils::{indented_list, Indent, SpaceOrNewline}, -}; +use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; use super::{ display_comma_separated, query::InputFormatClause, Assignment, Expr, FromTable, Ident, @@ -40,464 +37,6 @@ use super::{ Setting, SqliteOnConflict, TableObject, TableWithJoins, }; -/// Index column type. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct IndexColumn { - pub column: OrderByExpr, - pub operator_class: Option, -} - -impl Display for IndexColumn { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.column)?; - if let Some(operator_class) = &self.operator_class { - write!(f, " {operator_class}")?; - } - Ok(()) - } -} - -/// CREATE INDEX statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct CreateIndex { - /// index name - pub name: Option, - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - pub table_name: ObjectName, - pub using: Option, - pub columns: Vec, - pub unique: bool, - pub concurrently: bool, - pub if_not_exists: bool, - pub include: Vec, - pub nulls_distinct: Option, - /// WITH clause: - pub with: Vec, - pub predicate: Option, -} - -impl Display for CreateIndex { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "CREATE {unique}INDEX {concurrently}{if_not_exists}", - unique = if self.unique { "UNIQUE " } else { "" }, - concurrently = if self.concurrently { - "CONCURRENTLY " - } else { - "" - }, - if_not_exists = if self.if_not_exists { - "IF NOT EXISTS " - } else { - "" - }, - )?; - if let Some(value) = &self.name { - write!(f, "{value} ")?; - } - write!(f, "ON {}", self.table_name)?; - if let Some(value) = &self.using { - write!(f, " USING {value} ")?; - } - write!(f, "({})", display_separated(&self.columns, ","))?; - if !self.include.is_empty() { - write!(f, " INCLUDE ({})", display_separated(&self.include, ","))?; - } - if let Some(value) = self.nulls_distinct { - if value { - write!(f, " NULLS DISTINCT")?; - } else { - write!(f, " NULLS NOT DISTINCT")?; - } - } - if !self.with.is_empty() { - write!(f, " WITH ({})", display_comma_separated(&self.with))?; - } - if let Some(predicate) = &self.predicate { - write!(f, " WHERE {predicate}")?; - } - Ok(()) - } -} - -/// CREATE TABLE statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct CreateTable { - pub or_replace: bool, - pub temporary: bool, - pub external: bool, - pub global: Option, - pub if_not_exists: bool, - pub transient: bool, - pub volatile: bool, - pub iceberg: bool, - /// Table name - #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] - pub name: ObjectName, - /// Optional schema - pub columns: Vec, - pub constraints: Vec, - pub hive_distribution: HiveDistributionStyle, - pub hive_formats: Option, - pub table_options: CreateTableOptions, - pub file_format: Option, - pub location: Option, - pub query: Option>, - pub without_rowid: bool, - pub like: Option, - pub clone: Option, - // For Hive dialect, the table comment is after the column definitions without `=`, - // so the `comment` field is optional and different than the comment field in the general options list. - // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) - pub comment: Option, - pub on_commit: Option, - /// ClickHouse "ON CLUSTER" clause: - /// - pub on_cluster: Option, - /// ClickHouse "PRIMARY KEY " clause. - /// - pub primary_key: Option>, - /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different - /// than empty (represented as ()), the latter meaning "no sorting". - /// - pub order_by: Option>, - /// BigQuery: A partition expression for the table. - /// - pub partition_by: Option>, - /// BigQuery: Table clustering column list. - /// - /// Snowflake: Table clustering list which contains base column, expressions on base columns. - /// - pub cluster_by: Option>>, - /// Hive: Table clustering column list. - /// - pub clustered_by: Option, - /// Postgres `INHERITs` clause, which contains the list of tables from which - /// the new table inherits. - /// - /// - pub inherits: Option>, - /// SQLite "STRICT" clause. - /// if the "STRICT" table-option keyword is added to the end, after the closing ")", - /// then strict typing rules apply to that table. - pub strict: bool, - /// Snowflake "COPY GRANTS" clause - /// - pub copy_grants: bool, - /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause - /// - pub enable_schema_evolution: Option, - /// Snowflake "CHANGE_TRACKING" clause - /// - pub change_tracking: Option, - /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause - /// - pub data_retention_time_in_days: Option, - /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause - /// - pub max_data_extension_time_in_days: Option, - /// Snowflake "DEFAULT_DDL_COLLATION" clause - /// - pub default_ddl_collation: Option, - /// Snowflake "WITH AGGREGATION POLICY" clause - /// - pub with_aggregation_policy: Option, - /// Snowflake "WITH ROW ACCESS POLICY" clause - /// - pub with_row_access_policy: Option, - /// Snowflake "WITH TAG" clause - /// - pub with_tags: Option>, - /// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables - /// - pub external_volume: Option, - /// Snowflake "BASE_LOCATION" clause for Iceberg tables - /// - pub base_location: Option, - /// Snowflake "CATALOG" clause for Iceberg tables - /// - pub catalog: Option, - /// Snowflake "CATALOG_SYNC" clause for Iceberg tables - /// - pub catalog_sync: Option, - /// Snowflake "STORAGE_SERIALIZATION_POLICY" clause for Iceberg tables - /// - pub storage_serialization_policy: Option, -} - -impl Display for CreateTable { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // We want to allow the following options - // Empty column list, allowed by PostgreSQL: - // `CREATE TABLE t ()` - // No columns provided for CREATE TABLE AS: - // `CREATE TABLE t AS SELECT a from t2` - // Columns provided for CREATE TABLE AS: - // `CREATE TABLE t (a INT) AS SELECT a from t2` - write!( - f, - "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}{iceberg}TABLE {if_not_exists}{name}", - or_replace = if self.or_replace { "OR REPLACE " } else { "" }, - external = if self.external { "EXTERNAL " } else { "" }, - global = self.global - .map(|global| { - if global { - "GLOBAL " - } else { - "LOCAL " - } - }) - .unwrap_or(""), - if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, - 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 { "" }, - name = self.name, - )?; - if let Some(on_cluster) = &self.on_cluster { - write!(f, " ON CLUSTER {on_cluster}")?; - } - if !self.columns.is_empty() || !self.constraints.is_empty() { - f.write_str(" (")?; - NewLine.fmt(f)?; - Indent(DisplayCommaSeparated(&self.columns)).fmt(f)?; - if !self.columns.is_empty() && !self.constraints.is_empty() { - f.write_str(",")?; - SpaceOrNewline.fmt(f)?; - } - Indent(DisplayCommaSeparated(&self.constraints)).fmt(f)?; - NewLine.fmt(f)?; - f.write_str(")")?; - } else if self.query.is_none() && self.like.is_none() && self.clone.is_none() { - // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens - f.write_str(" ()")?; - } else if let Some(CreateTableLikeKind::Parenthesized(like_in_columns_list)) = &self.like { - write!(f, " ({like_in_columns_list})")?; - } - - // Hive table comment should be after column definitions, please refer to: - // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) - if let Some(comment) = &self.comment { - write!(f, " COMMENT '{comment}'")?; - } - - // Only for SQLite - if self.without_rowid { - write!(f, " WITHOUT ROWID")?; - } - - if let Some(CreateTableLikeKind::NotParenthesized(like)) = &self.like { - write!(f, " {like}")?; - } - - if let Some(c) = &self.clone { - write!(f, " CLONE {c}")?; - } - - match &self.hive_distribution { - HiveDistributionStyle::PARTITIONED { columns } => { - write!(f, " PARTITIONED BY ({})", display_comma_separated(columns))?; - } - HiveDistributionStyle::SKEWED { - columns, - on, - stored_as_directories, - } => { - write!( - f, - " SKEWED BY ({})) ON ({})", - display_comma_separated(columns), - display_comma_separated(on) - )?; - if *stored_as_directories { - write!(f, " STORED AS DIRECTORIES")?; - } - } - _ => (), - } - - if let Some(clustered_by) = &self.clustered_by { - write!(f, " {clustered_by}")?; - } - - if let Some(HiveFormat { - row_format, - serde_properties, - storage, - location, - }) = &self.hive_formats - { - match row_format { - Some(HiveRowFormat::SERDE { class }) => write!(f, " ROW FORMAT SERDE '{class}'")?, - Some(HiveRowFormat::DELIMITED { delimiters }) => { - write!(f, " ROW FORMAT DELIMITED")?; - if !delimiters.is_empty() { - write!(f, " {}", display_separated(delimiters, " "))?; - } - } - None => (), - } - match storage { - Some(HiveIOFormat::IOF { - input_format, - output_format, - }) => write!( - f, - " STORED AS INPUTFORMAT {input_format} OUTPUTFORMAT {output_format}" - )?, - Some(HiveIOFormat::FileFormat { format }) if !self.external => { - write!(f, " STORED AS {format}")? - } - _ => (), - } - if let Some(serde_properties) = serde_properties.as_ref() { - write!( - f, - " WITH SERDEPROPERTIES ({})", - display_comma_separated(serde_properties) - )?; - } - if !self.external { - if let Some(loc) = location { - write!(f, " LOCATION '{loc}'")?; - } - } - } - if self.external { - if let Some(file_format) = self.file_format { - write!(f, " STORED AS {file_format}")?; - } - write!(f, " LOCATION '{}'", self.location.as_ref().unwrap())?; - } - - match &self.table_options { - options @ CreateTableOptions::With(_) - | options @ CreateTableOptions::Plain(_) - | options @ CreateTableOptions::TableProperties(_) => write!(f, " {options}")?, - _ => (), - } - - if let Some(primary_key) = &self.primary_key { - write!(f, " PRIMARY KEY {primary_key}")?; - } - if let Some(order_by) = &self.order_by { - write!(f, " ORDER BY {order_by}")?; - } - if let Some(inherits) = &self.inherits { - write!(f, " INHERITS ({})", display_comma_separated(inherits))?; - } - if let Some(partition_by) = self.partition_by.as_ref() { - write!(f, " PARTITION BY {partition_by}")?; - } - if let Some(cluster_by) = self.cluster_by.as_ref() { - write!(f, " CLUSTER BY {cluster_by}")?; - } - if let options @ CreateTableOptions::Options(_) = &self.table_options { - write!(f, " {options}")?; - } - if let Some(external_volume) = self.external_volume.as_ref() { - write!(f, " EXTERNAL_VOLUME = '{external_volume}'")?; - } - - if let Some(catalog) = self.catalog.as_ref() { - write!(f, " CATALOG = '{catalog}'")?; - } - - if self.iceberg { - if let Some(base_location) = self.base_location.as_ref() { - write!(f, " BASE_LOCATION = '{base_location}'")?; - } - } - - if let Some(catalog_sync) = self.catalog_sync.as_ref() { - write!(f, " CATALOG_SYNC = '{catalog_sync}'")?; - } - - if let Some(storage_serialization_policy) = self.storage_serialization_policy.as_ref() { - write!( - f, - " STORAGE_SERIALIZATION_POLICY = {storage_serialization_policy}" - )?; - } - - if self.copy_grants { - write!(f, " COPY GRANTS")?; - } - - if let Some(is_enabled) = self.enable_schema_evolution { - write!( - f, - " ENABLE_SCHEMA_EVOLUTION={}", - if is_enabled { "TRUE" } else { "FALSE" } - )?; - } - - if let Some(is_enabled) = self.change_tracking { - write!( - f, - " CHANGE_TRACKING={}", - if is_enabled { "TRUE" } else { "FALSE" } - )?; - } - - if let Some(data_retention_time_in_days) = self.data_retention_time_in_days { - write!( - f, - " DATA_RETENTION_TIME_IN_DAYS={data_retention_time_in_days}", - )?; - } - - if let Some(max_data_extension_time_in_days) = self.max_data_extension_time_in_days { - write!( - f, - " MAX_DATA_EXTENSION_TIME_IN_DAYS={max_data_extension_time_in_days}", - )?; - } - - if let Some(default_ddl_collation) = &self.default_ddl_collation { - write!(f, " DEFAULT_DDL_COLLATION='{default_ddl_collation}'",)?; - } - - if let Some(with_aggregation_policy) = &self.with_aggregation_policy { - write!(f, " WITH AGGREGATION POLICY {with_aggregation_policy}",)?; - } - - if let Some(row_access_policy) = &self.with_row_access_policy { - write!(f, " {row_access_policy}",)?; - } - - if let Some(tag) = &self.with_tags { - write!(f, " WITH TAG ({})", display_comma_separated(tag.as_slice()))?; - } - - if self.on_commit.is_some() { - let on_commit = match self.on_commit { - Some(OnCommit::DeleteRows) => "ON COMMIT DELETE ROWS", - Some(OnCommit::PreserveRows) => "ON COMMIT PRESERVE ROWS", - Some(OnCommit::Drop) => "ON COMMIT DROP", - None => "", - }; - write!(f, " {on_commit}")?; - } - if self.strict { - write!(f, " STRICT")?; - } - if let Some(query) = &self.query { - write!(f, " AS {query}")?; - } - Ok(()) - } -} - /// INSERT statement. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 2c56ad3d26..9e9d229efa 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -25,10 +25,10 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::ast::{ - ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions, Expr, FileFormat, - HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, OneOrManyWithParens, Query, - RowAccessPolicy, Statement, StorageSerializationPolicy, TableConstraint, Tag, - WrappedCollection, + ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableLikeKind, CreateTableOptions, Expr, + FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, + OneOrManyWithParens, Query, RowAccessPolicy, Statement, StorageSerializationPolicy, + TableConstraint, Tag, WrappedCollection, }; use crate::parser::ParserError; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d065289a41..a30e24239c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -10466,25 +10466,24 @@ impl fmt::Display for CreateUser { } /// Specifies how to create a new table based on an existing table's schema. -/// -/// Not parenthesized: /// '''sql /// CREATE TABLE new LIKE old ... /// ''' -/// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-table#label-create-table-like) -/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_like) -/// -/// Parenthesized: -/// '''sql -/// CREATE TABLE new (LIKE old ...) -/// ''' -/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum CreateTableLikeKind { + /// '''sql + /// CREATE TABLE new (LIKE old ...) + /// ''' + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html) Parenthesized(CreateTableLike), - NotParenthesized(CreateTableLike), + /// '''sql + /// CREATE TABLE new LIKE old ... + /// ''' + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/sql/create-table#label-create-table-like) + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_like) + Plain(CreateTableLike), } #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index cb1ca559c5..7cf9d4fd16 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1179,7 +1179,7 @@ pub trait Dialect: Debug + Any { /// CREATE TABLE new (LIKE old ...) /// ''' /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_TABLE_NEW.html) - fn supports_create_table_like_in_parens(&self) -> bool { + fn supports_create_table_like_parenthesized(&self) -> bool { false } } diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index c41bbfef83..1cd6098a6c 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -140,7 +140,7 @@ impl Dialect for RedshiftSqlDialect { true } - fn supports_create_table_like_in_parens(&self) -> bool { + fn supports_create_table_like_parenthesized(&self) -> bool { true } } diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index f30db89d1b..7ef4de9c25 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -27,10 +27,10 @@ use crate::ast::helpers::stmt_data_loading::{ }; use crate::ast::{ CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, - CopyIntoSnowflakeKind, DollarQuotedString, Ident, IdentityParameters, IdentityProperty, - IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, - ObjectNamePart, RowAccessPolicy, ShowObjects, SqlOption, Statement, StorageSerializationPolicy, - TagsColumnOption, WrappedCollection, + CopyIntoSnowflakeKind, CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters, + IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, + ObjectName, ObjectNamePart, RowAccessPolicy, ShowObjects, SqlOption, Statement, + StorageSerializationPolicy, TagsColumnOption, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -669,7 +669,7 @@ pub fn parse_create_table( } Keyword::LIKE => { let name = parser.parse_object_name(false)?; - builder = builder.like(Some(CreateTableLikeKind::NotParenthesized( + builder = builder.like(Some(CreateTableLikeKind::Plain( crate::ast::CreateTableLike { name, defaults: None, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 085a0c2e63..9cddf8272d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7347,38 +7347,7 @@ impl<'a> Parser<'a> { // Clickhouse has `ON CLUSTER 'cluster'` syntax for DDLs let on_cluster = self.parse_optional_on_cluster()?; - // Try to parse `CREATE TABLE new (LIKE old [{INCLUDING | EXCLUDING} DEFAULTS])` or `CREATE TABLE new LIKE old` - let like = if self.dialect.supports_create_table_like_in_parens() - && self.consume_token(&Token::LParen) - { - if self.parse_keyword(Keyword::LIKE) { - let name = self.parse_object_name(allow_unquoted_hyphen)?; - let defaults = if self.parse_keywords(&[Keyword::INCLUDING, Keyword::DEFAULTS]) { - Some(CreateTableLikeDefaults::Including) - } else if self.parse_keywords(&[Keyword::EXCLUDING, Keyword::DEFAULTS]) { - Some(CreateTableLikeDefaults::Excluding) - } else { - None - }; - self.expect_token(&Token::RParen)?; - Some(CreateTableLikeKind::Parenthesized(CreateTableLike { - name, - defaults, - })) - } else { - // Rollback the '(' it's probably the columns list - self.prev_token(); - None - } - } else if self.parse_keyword(Keyword::LIKE) || self.parse_keyword(Keyword::ILIKE) { - let name = self.parse_object_name(allow_unquoted_hyphen)?; - Some(CreateTableLikeKind::NotParenthesized(CreateTableLike { - name, - defaults: None, - })) - } else { - None - }; + let like = self.maybe_parse_create_table_like(allow_unquoted_hyphen)?; let clone = if self.parse_keyword(Keyword::CLONE) { self.parse_object_name(allow_unquoted_hyphen).ok() @@ -7482,6 +7451,44 @@ impl<'a> Parser<'a> { .build()) } + fn maybe_parse_create_table_like( + &mut self, + allow_unquoted_hyphen: bool, + ) -> Result, ParserError> { + let like = if self.dialect.supports_create_table_like_parenthesized() + && self.consume_token(&Token::LParen) + { + if self.parse_keyword(Keyword::LIKE) { + let name = self.parse_object_name(allow_unquoted_hyphen)?; + let defaults = if self.parse_keywords(&[Keyword::INCLUDING, Keyword::DEFAULTS]) { + Some(CreateTableLikeDefaults::Including) + } else if self.parse_keywords(&[Keyword::EXCLUDING, Keyword::DEFAULTS]) { + Some(CreateTableLikeDefaults::Excluding) + } else { + None + }; + self.expect_token(&Token::RParen)?; + Some(CreateTableLikeKind::Parenthesized(CreateTableLike { + name, + defaults, + })) + } else { + // Rollback the '(' it's probably the columns list + self.prev_token(); + None + } + } else if self.parse_keyword(Keyword::LIKE) || self.parse_keyword(Keyword::ILIKE) { + let name = self.parse_object_name(allow_unquoted_hyphen)?; + Some(CreateTableLikeKind::Plain(CreateTableLike { + name, + defaults: None, + })) + } else { + None + }; + Ok(like) + } + pub(crate) fn parse_create_table_on_commit(&mut self) -> Result { if self.parse_keywords(&[Keyword::DELETE, Keyword::ROWS]) { Ok(OnCommit::DeleteRows) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f983c8a9fb..53b0d20365 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16658,7 +16658,7 @@ fn test_parse_default_with_collate_column_option() { #[test] fn parse_create_table_like() { - let dialects = all_dialects_except(|d| d.supports_create_table_like_in_parens()); + let dialects = all_dialects_except(|d| d.supports_create_table_like_parenthesized()); let sql = "CREATE TABLE new LIKE old"; match dialects.verified_stmt(sql) { Statement::CreateTable(stmt) => { @@ -16668,7 +16668,7 @@ fn parse_create_table_like() { ); assert_eq!( stmt.like, - Some(CreateTableLikeKind::NotParenthesized(CreateTableLike { + Some(CreateTableLikeKind::Plain(CreateTableLike { name: ObjectName::from(vec![Ident::new("old".to_string())]), defaults: None, })) @@ -16676,7 +16676,7 @@ fn parse_create_table_like() { } _ => unreachable!(), } - let dialects = all_dialects_where(|d| d.supports_create_table_like_in_parens()); + let dialects = all_dialects_where(|d| d.supports_create_table_like_parenthesized()); let sql = "CREATE TABLE new (LIKE old)"; match dialects.verified_stmt(sql) { Statement::CreateTable(stmt) => {