From 257f7d307fa5b367bc06fea8ea355a817b37e35d Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Wed, 23 Jul 2025 11:19:22 +0300 Subject: [PATCH] Redshift: CREATE TABLE ... (LIKE ..) --- src/ast/dml.rs | 14 ++++-- 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 | 17 ++++--- src/keywords.rs | 3 ++ src/parser/mod.rs | 31 +++++++++++- tests/sqlparser_common.rs | 74 ++++++++++++++++++++++++++++ 10 files changed, 210 insertions(+), 18 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index e179f5d70..90a725296 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, DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}; +use crate::{ + ast::CreateTableLikeKind, + display_utils::{indented_list, DisplayCommaSeparated, Indent, NewLine, SpaceOrNewline}, +}; pub use super::ddl::{ColumnDef, TableConstraint}; @@ -153,7 +156,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. @@ -282,6 +285,8 @@ impl 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: @@ -295,9 +300,8 @@ impl Display for CreateTable { write!(f, " WITHOUT ROWID")?; } - // Only for Hive - if let Some(l) = &self.like { - write!(f, " LIKE {l}")?; + if let Some(CreateTableLikeKind::NotParenthesized(like)) = &self.like { + write!(f, " {like}")?; } if let Some(c) = &self.clone { diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 60b8fb2a0..407c618ea 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -26,7 +26,7 @@ use sqlparser_derive::{Visit, VisitMut}; use super::super::dml::CreateTable; use crate::ast::{ - ClusteredBy, ColumnDef, CommentDef, CreateTableOptions, Expr, FileFormat, + ClusteredBy, ColumnDef, CommentDef, CreateTableLikeKind, CreateTableOptions, Expr, FileFormat, HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, OneOrManyWithParens, Query, RowAccessPolicy, Statement, StorageSerializationPolicy, TableConstraint, Tag, WrappedCollection, @@ -82,7 +82,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, @@ -238,7 +238,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 1798223f3..16fc593de 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -10125,6 +10125,63 @@ impl fmt::Display for MemberOf { } } +/// 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 3e82905e1..5ff1d4ccf 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -575,7 +575,7 @@ impl Spanned for CreateTable { location: _, // string, no span query, without_rowid: _, // bool - like, + like: _, clone, comment: _, // todo, no span on_commit: _, @@ -610,7 +610,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 c78b00033..1420e18e6 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1136,6 +1136,25 @@ pub trait Dialect: Debug + Any { fn supports_notnull_operator(&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 68e025d18..c41bbfef8 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 baf99b84b..93252ce97 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -23,10 +23,10 @@ use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ - ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, DollarQuotedString, - Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, ObjectName, ObjectNamePart, RowAccessPolicy, ShowObjects, SqlOption, - Statement, TagsColumnOption, WrappedCollection, + ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, CreateTableLikeKind, + DollarQuotedString, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, ObjectName, ObjectNamePart, RowAccessPolicy, + ShowObjects, SqlOption, Statement, TagsColumnOption, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -577,8 +577,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 7781939bc..08c53ae2e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -265,6 +265,7 @@ define_keywords!( DECLARE, DEDUPLICATE, DEFAULT, + DEFAULTS, DEFAULT_DDL_COLLATION, DEFERRABLE, DEFERRED, @@ -336,6 +337,7 @@ define_keywords!( EXCEPTION, EXCHANGE, EXCLUDE, + EXCLUDING, EXCLUSIVE, EXEC, EXECUTE, @@ -437,6 +439,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 8d5a55da0..5619b5f33 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7220,8 +7220,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 5d8284a46..f4ddb8440 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16256,3 +16256,77 @@ fn parse_notnull() { // for unsupported dialects, parsing should stop at `NOT NULL` notnull_unsupported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL"); } + +#[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!(), + } +}