This is an automated email from the ASF dual-hosted git repository.

iffyio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new b660a3b1 Redshift: `CREATE TABLE ... (LIKE ..)` (#1967)
b660a3b1 is described below

commit b660a3b1ea41ec8c48fcb4341b6112f043d7cd57
Author: Yoav Cohen <59807311+yoavcl...@users.noreply.github.com>
AuthorDate: Sat Aug 16 08:34:23 2025 +0200

    Redshift: `CREATE TABLE ... (LIKE ..)` (#1967)
---
 src/ast/ddl.rs                       | 21 +++++-----
 src/ast/helpers/stmt_create_table.rs | 12 +++---
 src/ast/mod.rs                       | 56 +++++++++++++++++++++++++++
 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                    | 44 ++++++++++++++++++---
 tests/sqlparser_common.rs            | 74 ++++++++++++++++++++++++++++++++++++
 10 files changed, 224 insertions(+), 29 deletions(-)

diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 1c2aaf48..65dfd6c5 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<String>,
     pub query: Option<Box<Query>>,
     pub without_rowid: bool,
-    pub like: Option<ObjectName>,
+    pub like: Option<CreateTableLikeKind>,
     pub clone: Option<ObjectName>,
     // 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/helpers/stmt_create_table.rs 
b/src/ast/helpers/stmt_create_table.rs
index c727276d..9e9d229e 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, 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;
@@ -81,7 +81,7 @@ pub struct CreateTableBuilder {
     pub location: Option<String>,
     pub query: Option<Box<Query>>,
     pub without_rowid: bool,
-    pub like: Option<ObjectName>,
+    pub like: Option<CreateTableLikeKind>,
     pub clone: Option<ObjectName>,
     pub comment: Option<CommentDef>,
     pub on_commit: Option<OnCommit>,
@@ -237,7 +237,7 @@ impl CreateTableBuilder {
         self
     }
 
-    pub fn like(mut self, like: Option<ObjectName>) -> Self {
+    pub fn like(mut self, like: Option<CreateTableLikeKind>) -> Self {
         self.like = like;
         self
     }
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 5b50d020..a30e2423 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -10465,6 +10465,62 @@ impl fmt::Display for CreateUser {
     }
 }
 
+/// Specifies how to create a new table based on an existing table's schema.
+/// '''sql
+/// CREATE TABLE new LIKE old ...
+/// '''
+#[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),
+    /// '''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)]
+#[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<CreateTableLikeDefaults>,
+}
+
+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 e1709026..da47b0f8 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 6ca36478..7cf9d4fd 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_parenthesized(&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 68e025d1..1cd6098a 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_parenthesized(&self) -> bool {
+        true
+    }
 }
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index 8830e09a..7ef4de9c 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;
@@ -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::Plain(
+                        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 a729a525..659bc043 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 3d476254..9cddf827 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -7347,11 +7347,7 @@ 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()
-        } 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()
@@ -7455,6 +7451,44 @@ impl<'a> Parser<'a> {
             .build())
     }
 
+    fn maybe_parse_create_table_like(
+        &mut self,
+        allow_unquoted_hyphen: bool,
+    ) -> Result<Option<CreateTableLikeKind>, 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<OnCommit, 
ParserError> {
         if self.parse_keywords(&[Keyword::DELETE, Keyword::ROWS]) {
             Ok(OnCommit::DeleteRows)
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 4b9d748f..53b0d203 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_parenthesized());
+    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::Plain(CreateTableLike {
+                    name: 
ObjectName::from(vec![Ident::new("old".to_string())]),
+                    defaults: None,
+                }))
+            )
+        }
+        _ => unreachable!(),
+    }
+    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) => {
+            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!(),
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@datafusion.apache.org
For additional commands, e-mail: commits-h...@datafusion.apache.org

Reply via email to