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 67fca824 Improve MySQL option parsing in index definitions (#1997)
67fca824 is described below

commit 67fca82495362fb824a459d0ce8f3e880a3d71b5
Author: Michael Victor Zink <michae...@readyset.io>
AuthorDate: Fri Aug 8 02:32:20 2025 -0700

    Improve MySQL option parsing in index definitions (#1997)
---
 src/ast/ddl.rs                       | 502 ++++++++++++++++++++++++++++++++++-
 src/ast/dml.rs                       | 470 +-------------------------------
 src/ast/helpers/stmt_create_table.rs |   3 +-
 src/ast/mod.rs                       |  14 +-
 src/ast/spans.rs                     |   6 +-
 src/parser/mod.rs                    |  27 ++
 tests/sqlparser_common.rs            |  12 +-
 tests/sqlparser_mysql.rs             |  28 ++
 tests/sqlparser_postgres.rs          |  64 ++++-
 9 files changed, 628 insertions(+), 498 deletions(-)

diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs
index 4d1fc462..1c2aaf48 100644
--- a/src/ast/ddl.rs
+++ b/src/ast/ddl.rs
@@ -31,14 +31,36 @@ 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, DataType, Expr, FunctionBehavior, 
FunctionCalledOnNull,
-    FunctionDeterminismSpecifier, FunctionParallel, Ident, IndexColumn, 
MySQLColumnPosition,
-    ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, 
SequenceOptions, Spanned,
-    SqlOption, Tag, Value, ValueWithSpan,
+    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,
 };
+use crate::display_utils::{DisplayCommaSeparated, Indent, NewLine, 
SpaceOrNewline};
 use crate::keywords::Keyword;
 use crate::tokenizer::{Span, Token};
 
+/// 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<Ident>,
+}
+
+impl fmt::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(())
+    }
+}
+
 /// ALTER TABLE operation REPLICA IDENTITY values
 /// See [Postgres ALTER TABLE 
docs](https://www.postgresql.org/docs/current/sql-altertable.html)
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -1103,6 +1125,8 @@ pub enum TableConstraint {
         index_type: Option<IndexType>,
         /// Referred column identifier list.
         columns: Vec<IndexColumn>,
+        /// Optional index options such as `USING`; see [`IndexOption`].
+        index_options: Vec<IndexOption>,
     },
     /// MySQLs [fulltext][1] definition. Since the [`SPATIAL`][2] definition 
is exactly the same,
     /// and MySQL displays both the same way, it is part of this definition as 
well.
@@ -1231,6 +1255,7 @@ impl fmt::Display for TableConstraint {
                 name,
                 index_type,
                 columns,
+                index_options,
             } => {
                 write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" 
})?;
                 if let Some(name) = name {
@@ -1240,7 +1265,9 @@ impl fmt::Display for TableConstraint {
                     write!(f, " USING {index_type}")?;
                 }
                 write!(f, " ({})", display_comma_separated(columns))?;
-
+                if !index_options.is_empty() {
+                    write!(f, " {}", display_comma_separated(index_options))?;
+                }
                 Ok(())
             }
             Self::FulltextOrSpatial {
@@ -1355,17 +1382,20 @@ impl fmt::Display for IndexType {
     }
 }
 
-/// MySQLs index option.
-///
-/// This structure used here [`MySQL` CREATE TABLE][1], [`MySQL` CREATE 
INDEX][2].
+/// MySQL index option, used in [`CREATE TABLE`], [`CREATE INDEX`], and 
[`ALTER TABLE`].
 ///
-/// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html
-/// [2]: https://dev.mysql.com/doc/refman/8.3/en/create-index.html
+/// [`CREATE TABLE`]: https://dev.mysql.com/doc/refman/8.4/en/create-table.html
+/// [`CREATE INDEX`]: https://dev.mysql.com/doc/refman/8.4/en/create-index.html
+/// [`ALTER TABLE`]: https://dev.mysql.com/doc/refman/8.4/en/alter-table.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 IndexOption {
+    /// `USING { BTREE | HASH }`: Index type to use for the index.
+    ///
+    /// Note that we permissively parse non-MySQL index types, like `GIN`.
     Using(IndexType),
+    /// `COMMENT 'string'`: Specifies a comment for the index.
     Comment(String),
 }
 
@@ -2294,6 +2324,458 @@ impl fmt::Display for ClusteredBy {
     }
 }
 
+/// 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<ObjectName>,
+    #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
+    pub table_name: ObjectName,
+    pub using: Option<IndexType>,
+    pub columns: Vec<IndexColumn>,
+    pub unique: bool,
+    pub concurrently: bool,
+    pub if_not_exists: bool,
+    pub include: Vec<Ident>,
+    pub nulls_distinct: Option<bool>,
+    /// WITH clause: 
<https://www.postgresql.org/docs/current/sql-createindex.html>
+    pub with: Vec<Expr>,
+    pub predicate: Option<Expr>,
+    pub index_options: Vec<IndexOption>,
+    /// [MySQL] allows a subset of options normally used for `ALTER TABLE`:
+    ///
+    /// - `ALGORITHM`
+    /// - `LOCK`
+    ///
+    /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/create-index.html
+    pub alter_options: Vec<AlterTableOperation>,
+}
+
+impl fmt::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_comma_separated(&self.columns))?;
+        if !self.include.is_empty() {
+            write!(f, " INCLUDE ({})", 
display_comma_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}")?;
+        }
+        if !self.index_options.is_empty() {
+            write!(f, " {}", display_separated(&self.index_options, " "))?;
+        }
+        if !self.alter_options.is_empty() {
+            write!(f, " {}", display_separated(&self.alter_options, " "))?;
+        }
+        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<bool>,
+    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<ColumnDef>,
+    pub constraints: Vec<TableConstraint>,
+    pub hive_distribution: HiveDistributionStyle,
+    pub hive_formats: Option<HiveFormat>,
+    pub table_options: CreateTableOptions,
+    pub file_format: Option<FileFormat>,
+    pub location: Option<String>,
+    pub query: Option<Box<Query>>,
+    pub without_rowid: bool,
+    pub like: Option<ObjectName>,
+    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.
+    // 
[Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable)
+    pub comment: Option<CommentDef>,
+    pub on_commit: Option<OnCommit>,
+    /// ClickHouse "ON CLUSTER" clause:
+    /// <https://clickhouse.com/docs/en/sql-reference/distributed-ddl/>
+    pub on_cluster: Option<Ident>,
+    /// ClickHouse "PRIMARY KEY " clause.
+    /// <https://clickhouse.com/docs/en/sql-reference/statements/create/table/>
+    pub primary_key: Option<Box<Expr>>,
+    /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different
+    /// than empty (represented as ()), the latter meaning "no sorting".
+    /// <https://clickhouse.com/docs/en/sql-reference/statements/create/table/>
+    pub order_by: Option<OneOrManyWithParens<Expr>>,
+    /// BigQuery: A partition expression for the table.
+    /// 
<https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#partition_expression>
+    pub partition_by: Option<Box<Expr>>,
+    /// BigQuery: Table clustering column list.
+    /// 
<https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#table_option_list>
+    /// Snowflake: Table clustering list which contains base column, 
expressions on base columns.
+    /// 
<https://docs.snowflake.com/en/user-guide/tables-clustering-keys#defining-a-clustering-key-for-a-table>
+    pub cluster_by: Option<WrappedCollection<Vec<Expr>>>,
+    /// Hive: Table clustering column list.
+    /// 
<https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable>
+    pub clustered_by: Option<ClusteredBy>,
+    /// Postgres `INHERITs` clause, which contains the list of tables from 
which
+    /// the new table inherits.
+    /// <https://www.postgresql.org/docs/current/ddl-inherit.html>
+    /// 
<https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-INHERITS>
+    pub inherits: Option<Vec<ObjectName>>,
+    /// 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
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub copy_grants: bool,
+    /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub enable_schema_evolution: Option<bool>,
+    /// Snowflake "CHANGE_TRACKING" clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub change_tracking: Option<bool>,
+    /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub data_retention_time_in_days: Option<u64>,
+    /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub max_data_extension_time_in_days: Option<u64>,
+    /// Snowflake "DEFAULT_DDL_COLLATION" clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub default_ddl_collation: Option<String>,
+    /// Snowflake "WITH AGGREGATION POLICY" clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub with_aggregation_policy: Option<ObjectName>,
+    /// Snowflake "WITH ROW ACCESS POLICY" clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub with_row_access_policy: Option<RowAccessPolicy>,
+    /// Snowflake "WITH TAG" clause
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
+    pub with_tags: Option<Vec<Tag>>,
+    /// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
+    pub external_volume: Option<String>,
+    /// Snowflake "BASE_LOCATION" clause for Iceberg tables
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
+    pub base_location: Option<String>,
+    /// Snowflake "CATALOG" clause for Iceberg tables
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
+    pub catalog: Option<String>,
+    /// Snowflake "CATALOG_SYNC" clause for Iceberg tables
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
+    pub catalog_sync: Option<String>,
+    /// Snowflake "STORAGE_SERIALIZATION_POLICY" clause for Iceberg tables
+    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
+    pub storage_serialization_policy: Option<StorageSerializationPolicy>,
+}
+
+impl fmt::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(" ()")?;
+        }
+
+        // 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")?;
+        }
+
+        // Only for Hive
+        if let Some(l) = &self.like {
+            write!(f, " LIKE {l}")?;
+        }
+
+        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(())
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
diff --git a/src/ast/dml.rs b/src/ast/dml.rs
index e179f5d7..63d6b86c 100644
--- a/src/ast/dml.rs
+++ b/src/ast/dml.rs
@@ -29,476 +29,14 @@ use serde::{Deserialize, Serialize};
 #[cfg(feature = "visitor")]
 use sqlparser_derive::{Visit, VisitMut};
 
-use crate::display_utils::{indented_list, DisplayCommaSeparated, Indent, 
NewLine, SpaceOrNewline};
-
-pub use super::ddl::{ColumnDef, TableConstraint};
+use crate::display_utils::{indented_list, Indent, SpaceOrNewline};
 
 use super::{
-    display_comma_separated, display_separated, query::InputFormatClause, 
Assignment, ClusteredBy,
-    CommentDef, CreateTableOptions, Expr, FileFormat, FromTable, 
HiveDistributionStyle, HiveFormat,
-    HiveIOFormat, HiveRowFormat, Ident, IndexType, InsertAliases, 
MysqlInsertPriority, ObjectName,
-    OnCommit, OnInsert, OneOrManyWithParens, OrderByExpr, Query, 
RowAccessPolicy, SelectItem,
-    Setting, SqliteOnConflict, StorageSerializationPolicy, TableObject, 
TableWithJoins, Tag,
-    WrappedCollection,
+    display_comma_separated, query::InputFormatClause, Assignment, Expr, 
FromTable, Ident,
+    InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, 
Query, SelectItem,
+    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<Ident>,
-}
-
-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<ObjectName>,
-    #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
-    pub table_name: ObjectName,
-    pub using: Option<IndexType>,
-    pub columns: Vec<IndexColumn>,
-    pub unique: bool,
-    pub concurrently: bool,
-    pub if_not_exists: bool,
-    pub include: Vec<Ident>,
-    pub nulls_distinct: Option<bool>,
-    /// WITH clause: 
<https://www.postgresql.org/docs/current/sql-createindex.html>
-    pub with: Vec<Expr>,
-    pub predicate: Option<Expr>,
-}
-
-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<bool>,
-    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<ColumnDef>,
-    pub constraints: Vec<TableConstraint>,
-    pub hive_distribution: HiveDistributionStyle,
-    pub hive_formats: Option<HiveFormat>,
-    pub table_options: CreateTableOptions,
-    pub file_format: Option<FileFormat>,
-    pub location: Option<String>,
-    pub query: Option<Box<Query>>,
-    pub without_rowid: bool,
-    pub like: Option<ObjectName>,
-    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.
-    // 
[Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable)
-    pub comment: Option<CommentDef>,
-    pub on_commit: Option<OnCommit>,
-    /// ClickHouse "ON CLUSTER" clause:
-    /// <https://clickhouse.com/docs/en/sql-reference/distributed-ddl/>
-    pub on_cluster: Option<Ident>,
-    /// ClickHouse "PRIMARY KEY " clause.
-    /// <https://clickhouse.com/docs/en/sql-reference/statements/create/table/>
-    pub primary_key: Option<Box<Expr>>,
-    /// ClickHouse "ORDER BY " clause. Note that omitted ORDER BY is different
-    /// than empty (represented as ()), the latter meaning "no sorting".
-    /// <https://clickhouse.com/docs/en/sql-reference/statements/create/table/>
-    pub order_by: Option<OneOrManyWithParens<Expr>>,
-    /// BigQuery: A partition expression for the table.
-    /// 
<https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#partition_expression>
-    pub partition_by: Option<Box<Expr>>,
-    /// BigQuery: Table clustering column list.
-    /// 
<https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#table_option_list>
-    /// Snowflake: Table clustering list which contains base column, 
expressions on base columns.
-    /// 
<https://docs.snowflake.com/en/user-guide/tables-clustering-keys#defining-a-clustering-key-for-a-table>
-    pub cluster_by: Option<WrappedCollection<Vec<Expr>>>,
-    /// Hive: Table clustering column list.
-    /// 
<https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable>
-    pub clustered_by: Option<ClusteredBy>,
-    /// Postgres `INHERITs` clause, which contains the list of tables from 
which
-    /// the new table inherits.
-    /// <https://www.postgresql.org/docs/current/ddl-inherit.html>
-    /// 
<https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-INHERITS>
-    pub inherits: Option<Vec<ObjectName>>,
-    /// 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
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub copy_grants: bool,
-    /// Snowflake "ENABLE_SCHEMA_EVOLUTION" clause
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub enable_schema_evolution: Option<bool>,
-    /// Snowflake "CHANGE_TRACKING" clause
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub change_tracking: Option<bool>,
-    /// Snowflake "DATA_RETENTION_TIME_IN_DAYS" clause
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub data_retention_time_in_days: Option<u64>,
-    /// Snowflake "MAX_DATA_EXTENSION_TIME_IN_DAYS" clause
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub max_data_extension_time_in_days: Option<u64>,
-    /// Snowflake "DEFAULT_DDL_COLLATION" clause
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub default_ddl_collation: Option<String>,
-    /// Snowflake "WITH AGGREGATION POLICY" clause
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub with_aggregation_policy: Option<ObjectName>,
-    /// Snowflake "WITH ROW ACCESS POLICY" clause
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub with_row_access_policy: Option<RowAccessPolicy>,
-    /// Snowflake "WITH TAG" clause
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-table>
-    pub with_tags: Option<Vec<Tag>>,
-    /// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
-    pub external_volume: Option<String>,
-    /// Snowflake "BASE_LOCATION" clause for Iceberg tables
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
-    pub base_location: Option<String>,
-    /// Snowflake "CATALOG" clause for Iceberg tables
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
-    pub catalog: Option<String>,
-    /// Snowflake "CATALOG_SYNC" clause for Iceberg tables
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
-    pub catalog_sync: Option<String>,
-    /// Snowflake "STORAGE_SERIALIZATION_POLICY" clause for Iceberg tables
-    /// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
-    pub storage_serialization_policy: Option<StorageSerializationPolicy>,
-}
-
-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(" ()")?;
-        }
-
-        // 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")?;
-        }
-
-        // Only for Hive
-        if let Some(l) = &self.like {
-            write!(f, " LIKE {l}")?;
-        }
-
-        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 60b8fb2a..c727276d 100644
--- a/src/ast/helpers/stmt_create_table.rs
+++ b/src/ast/helpers/stmt_create_table.rs
@@ -24,9 +24,8 @@ use serde::{Deserialize, Serialize};
 #[cfg(feature = "visitor")]
 use sqlparser_derive::{Visit, VisitMut};
 
-use super::super::dml::CreateTable;
 use crate::ast::{
-    ClusteredBy, ColumnDef, CommentDef, CreateTableOptions, Expr, FileFormat,
+    ClusteredBy, ColumnDef, CommentDef, CreateTable, CreateTableOptions, Expr, 
FileFormat,
     HiveDistributionStyle, HiveFormat, Ident, ObjectName, OnCommit, 
OneOrManyWithParens, Query,
     RowAccessPolicy, Statement, StorageSerializationPolicy, TableConstraint, 
Tag,
     WrappedCollection,
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 0bf412e8..d21f8357 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -63,14 +63,14 @@ pub use self::ddl::{
     AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, 
AlterTypeRenameValue,
     ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, 
ColumnPolicy,
     ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, 
CreateDomain, CreateFunction,
-    Deduplicate, DeferrableInitial, DropBehavior, GeneratedAs, 
GeneratedExpressionMode,
-    IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, 
IdentityPropertyKind,
-    IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, 
NullsDistinctOption, Owner,
-    Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, 
ReplicaIdentity,
-    TableConstraint, TagsColumnOption, UserDefinedTypeCompositeAttributeDef,
-    UserDefinedTypeRepresentation, ViewColumnDef,
+    CreateIndex, CreateTable, Deduplicate, DeferrableInitial, DropBehavior, 
GeneratedAs,
+    GeneratedExpressionMode, IdentityParameters, IdentityProperty, 
IdentityPropertyFormatKind,
+    IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, IndexOption, 
IndexType,
+    KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition, ProcedureParam, 
ReferentialAction,
+    RenameTableNameKind, ReplicaIdentity, TableConstraint, TagsColumnOption,
+    UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, 
ViewColumnDef,
 };
-pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert};
+pub use self::dml::{Delete, Insert};
 pub use self::operator::{BinaryOperator, UnaryOperator};
 pub use self::query::{
     AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, 
EmptyMatchesMode,
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index dec26566..e6e601c3 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -713,6 +713,7 @@ impl Spanned for TableConstraint {
                 name,
                 index_type: _,
                 columns,
+                index_options: _,
             } => union_spans(
                 name.iter()
                     .map(|i| i.span)
@@ -747,6 +748,8 @@ impl Spanned for CreateIndex {
             nulls_distinct: _, // bool
             with,
             predicate,
+            index_options: _,
+            alter_options,
         } = self;
 
         union_spans(
@@ -756,7 +759,8 @@ impl Spanned for CreateIndex {
                 .chain(columns.iter().map(|i| i.column.span()))
                 .chain(include.iter().map(|i| i.span))
                 .chain(with.iter().map(|i| i.span()))
-                .chain(predicate.iter().map(|i| i.span())),
+                .chain(predicate.iter().map(|i| i.span()))
+                .chain(alter_options.iter().map(|i| i.span())),
         )
     }
 }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 57da8777..78ce6de6 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -7083,6 +7083,22 @@ impl<'a> Parser<'a> {
             None
         };
 
+        // MySQL options (including the modern style of `USING` after the 
column list instead of
+        // before, which is deprecated) shouldn't conflict with other 
preceding options (e.g. `WITH
+        // PARSER` won't be caught by the above `WITH` clause parsing because 
MySQL doesn't set that
+        // support flag). This is probably invalid syntax for other dialects, 
but it is simpler to
+        // parse it anyway (as we do inside `ALTER TABLE` and `CREATE TABLE` 
parsing).
+        let index_options = self.parse_index_options()?;
+
+        // MySQL allows `ALGORITHM` and `LOCK` options. Unlike in `ALTER 
TABLE`, they need not be comma separated.
+        let mut alter_options = Vec::new();
+        while self
+            .peek_one_of_keywords(&[Keyword::ALGORITHM, Keyword::LOCK])
+            .is_some()
+        {
+            alter_options.push(self.parse_alter_table_operation()?)
+        }
+
         Ok(Statement::CreateIndex(CreateIndex {
             name: index_name,
             table_name,
@@ -7095,6 +7111,8 @@ impl<'a> Parser<'a> {
             nulls_distinct,
             with,
             predicate,
+            index_options,
+            alter_options,
         }))
     }
 
@@ -8407,12 +8425,14 @@ impl<'a> Parser<'a> {
 
                 let index_type = self.parse_optional_using_then_index_type()?;
                 let columns = self.parse_parenthesized_index_column_list()?;
+                let index_options = self.parse_index_options()?;
 
                 Ok(Some(TableConstraint::Index {
                     display_as_key,
                     name,
                     index_type,
                     columns,
+                    index_options,
                 }))
             }
             Token::Word(w)
@@ -17475,6 +17495,7 @@ mod tests {
                 name: None,
                 index_type: None,
                 columns: vec![mk_expected_col("c1")],
+                index_options: vec![],
             }
         );
 
@@ -17486,6 +17507,7 @@ mod tests {
                 name: None,
                 index_type: None,
                 columns: vec![mk_expected_col("c1")],
+                index_options: vec![],
             }
         );
 
@@ -17497,6 +17519,7 @@ mod tests {
                 name: Some(Ident::with_quote('\'', "index")),
                 index_type: None,
                 columns: vec![mk_expected_col("c1"), mk_expected_col("c2")],
+                index_options: vec![],
             }
         );
 
@@ -17508,6 +17531,7 @@ mod tests {
                 name: None,
                 index_type: Some(IndexType::BTree),
                 columns: vec![mk_expected_col("c1")],
+                index_options: vec![],
             }
         );
 
@@ -17519,6 +17543,7 @@ mod tests {
                 name: None,
                 index_type: Some(IndexType::Hash),
                 columns: vec![mk_expected_col("c1")],
+                index_options: vec![],
             }
         );
 
@@ -17530,6 +17555,7 @@ mod tests {
                 name: Some(Ident::new("idx_name")),
                 index_type: Some(IndexType::BTree),
                 columns: vec![mk_expected_col("c1")],
+                index_options: vec![],
             }
         );
 
@@ -17541,6 +17567,7 @@ mod tests {
                 name: Some(Ident::new("idx_name")),
                 index_type: Some(IndexType::Hash),
                 columns: vec![mk_expected_col("c1")],
+                index_options: vec![],
             }
         );
     }
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 7eff5503..d71449d9 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -9189,7 +9189,7 @@ fn ensure_multiple_dialects_are_tested() {
 
 #[test]
 fn parse_create_index() {
-    let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test(name,age 
DESC)";
+    let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test(name, age 
DESC)";
     let indexed_columns: Vec<IndexColumn> = vec![
         IndexColumn {
             operator_class: None,
@@ -9235,7 +9235,7 @@ fn parse_create_index() {
 
 #[test]
 fn test_create_index_with_using_function() {
-    let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test USING BTREE 
(name,age DESC)";
+    let sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON test USING BTREE 
(name, age DESC)";
     let indexed_columns: Vec<IndexColumn> = vec![
         IndexColumn {
             operator_class: None,
@@ -9273,6 +9273,8 @@ fn test_create_index_with_using_function() {
             nulls_distinct: None,
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq!("idx_name", name.to_string());
             assert_eq!("test", table_name.to_string());
@@ -9283,6 +9285,8 @@ fn test_create_index_with_using_function() {
             assert!(if_not_exists);
             assert!(include.is_empty());
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
@@ -9324,6 +9328,8 @@ fn test_create_index_with_with_clause() {
             nulls_distinct: None,
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             pretty_assertions::assert_eq!("title_idx", name.to_string());
             pretty_assertions::assert_eq!("films", table_name.to_string());
@@ -9333,6 +9339,8 @@ fn test_create_index_with_with_clause() {
             assert!(!if_not_exists);
             assert!(include.is_empty());
             pretty_assertions::assert_eq!(with_parameters, with);
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs
index c13f2266..2b8d4b5e 100644
--- a/tests/sqlparser_mysql.rs
+++ b/tests/sqlparser_mysql.rs
@@ -4215,3 +4215,31 @@ fn parse_show_charset() {
     mysql().verified_stmt("SHOW CHARSET WHERE charset = 'utf8mb4%'");
     mysql().verified_stmt("SHOW CHARSET LIKE 'utf8mb4%'");
 }
+
+#[test]
+fn test_ddl_with_index_using() {
+    let columns = "(name, age DESC)";
+    let using = "USING BTREE";
+
+    for sql in [
+        format!("CREATE INDEX idx_name ON test {using} {columns}"),
+        format!("CREATE TABLE foo (name VARCHAR(255), age INT, KEY idx_name 
{using} {columns})"),
+        format!("ALTER TABLE foo ADD KEY idx_name {using} {columns}"),
+        format!("CREATE INDEX idx_name ON test{columns} {using}"),
+        format!("CREATE TABLE foo (name VARCHAR(255), age INT, KEY idx_name 
{columns} {using})"),
+        format!("ALTER TABLE foo ADD KEY idx_name {columns} {using}"),
+    ] {
+        mysql_and_generic().verified_stmt(&sql);
+    }
+}
+
+#[test]
+fn test_create_index_options() {
+    mysql_and_generic()
+        .verified_stmt("CREATE INDEX idx_name ON t(c1, c2) USING HASH LOCK = 
SHARED");
+    mysql_and_generic()
+        .verified_stmt("CREATE INDEX idx_name ON t(c1, c2) USING BTREE 
ALGORITHM = INPLACE");
+    mysql_and_generic().verified_stmt(
+        "CREATE INDEX idx_name ON t(c1, c2) USING BTREE LOCK = EXCLUSIVE 
ALGORITHM = DEFAULT",
+    );
+}
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index 997384a6..a7c9779b 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -2486,7 +2486,7 @@ fn parse_array_multi_subscript() {
 
 #[test]
 fn parse_create_index() {
-    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2)";
+    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2)";
     match pg().verified_stmt(sql) {
         Statement::CreateIndex(CreateIndex {
             name: Some(ObjectName(name)),
@@ -2500,6 +2500,8 @@ fn parse_create_index() {
             include,
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq_vec(&["my_index"], &name);
             assert_eq_vec(&["my_table"], &table_name);
@@ -2510,6 +2512,8 @@ fn parse_create_index() {
             assert_eq_vec(&["col1", "col2"], &columns);
             assert!(include.is_empty());
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
@@ -2517,7 +2521,7 @@ fn parse_create_index() {
 
 #[test]
 fn parse_create_anonymous_index() {
-    let sql = "CREATE INDEX ON my_table(col1,col2)";
+    let sql = "CREATE INDEX ON my_table(col1, col2)";
     match pg().verified_stmt(sql) {
         Statement::CreateIndex(CreateIndex {
             name,
@@ -2531,6 +2535,8 @@ fn parse_create_anonymous_index() {
             nulls_distinct: None,
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq!(None, name);
             assert_eq_vec(&["my_table"], &table_name);
@@ -2541,6 +2547,8 @@ fn parse_create_anonymous_index() {
             assert_eq_vec(&["col1", "col2"], &columns);
             assert!(include.is_empty());
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
@@ -2577,7 +2585,7 @@ fn parse_create_indices_with_operator_classes() {
                     .unwrap_or_default()
             );
             let multi_column_sql_statement = format!(
-                "CREATE INDEX the_index_name ON users USING 
{expected_index_type} (column_name,concat_users_name(first_name, last_name){})",
+                "CREATE INDEX the_index_name ON users USING 
{expected_index_type} (column_name, concat_users_name(first_name, 
last_name){})",
                 expected_operator_class.as_ref().map(|oc| format!(" {oc}"))
                     .unwrap_or_default()
             );
@@ -2639,6 +2647,8 @@ fn parse_create_indices_with_operator_classes() {
                     nulls_distinct: None,
                     with,
                     predicate: None,
+                    index_options,
+                    alter_options,
                 }) => {
                     assert_eq_vec(&["the_index_name"], &name);
                     assert_eq_vec(&["users"], &table_name);
@@ -2646,6 +2656,8 @@ fn parse_create_indices_with_operator_classes() {
                     assert_eq!(expected_function_column, columns[0],);
                     assert!(include.is_empty());
                     assert!(with.is_empty());
+                    assert!(index_options.is_empty());
+                    assert!(alter_options.is_empty());
                 }
                 _ => unreachable!(),
             }
@@ -2663,6 +2675,8 @@ fn parse_create_indices_with_operator_classes() {
                     nulls_distinct: None,
                     with,
                     predicate: None,
+                    index_options,
+                    alter_options,
                 }) => {
                     assert_eq_vec(&["the_index_name"], &name);
                     assert_eq_vec(&["users"], &table_name);
@@ -2688,6 +2702,8 @@ fn parse_create_indices_with_operator_classes() {
                     assert_eq!(expected_function_column, columns[1],);
                     assert!(include.is_empty());
                     assert!(with.is_empty());
+                    assert!(index_options.is_empty());
+                    assert!(alter_options.is_empty());
                 }
                 _ => unreachable!(),
             }
@@ -2698,7 +2714,7 @@ fn parse_create_indices_with_operator_classes() {
 #[test]
 fn parse_create_bloom() {
     let sql =
-        "CREATE INDEX bloomidx ON tbloom USING BLOOM (i1,i2,i3) WITH (length = 
80, col1 = 2, col2 = 2, col3 = 4)";
+        "CREATE INDEX bloomidx ON tbloom USING BLOOM (i1, i2, i3) WITH (length 
= 80, col1 = 2, col2 = 2, col3 = 4)";
     match pg().verified_stmt(sql) {
         Statement::CreateIndex(CreateIndex {
             name: Some(ObjectName(name)),
@@ -2712,6 +2728,8 @@ fn parse_create_bloom() {
             nulls_distinct: None,
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq_vec(&["bloomidx"], &name);
             assert_eq_vec(&["tbloom"], &table_name);
@@ -2743,6 +2761,8 @@ fn parse_create_bloom() {
                 ],
                 with
             );
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
@@ -2764,6 +2784,8 @@ fn parse_create_brin() {
             nulls_distinct: None,
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq_vec(&["brin_sensor_data_recorded_at"], &name);
             assert_eq_vec(&["sensor_data"], &table_name);
@@ -2771,6 +2793,8 @@ fn parse_create_brin() {
             assert_eq_vec(&["recorded_at"], &columns);
             assert!(include.is_empty());
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
@@ -2813,7 +2837,7 @@ fn parse_create_table_with_empty_inherits_fails() {
 
 #[test]
 fn parse_create_index_concurrently() {
-    let sql = "CREATE INDEX CONCURRENTLY IF NOT EXISTS my_index ON 
my_table(col1,col2)";
+    let sql = "CREATE INDEX CONCURRENTLY IF NOT EXISTS my_index ON 
my_table(col1, col2)";
     match pg().verified_stmt(sql) {
         Statement::CreateIndex(CreateIndex {
             name: Some(ObjectName(name)),
@@ -2827,6 +2851,8 @@ fn parse_create_index_concurrently() {
             nulls_distinct: None,
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq_vec(&["my_index"], &name);
             assert_eq_vec(&["my_table"], &table_name);
@@ -2837,6 +2863,8 @@ fn parse_create_index_concurrently() {
             assert_eq_vec(&["col1", "col2"], &columns);
             assert!(include.is_empty());
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
@@ -2844,7 +2872,7 @@ fn parse_create_index_concurrently() {
 
 #[test]
 fn parse_create_index_with_predicate() {
-    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) 
WHERE col3 IS NULL";
+    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) 
WHERE col3 IS NULL";
     match pg().verified_stmt(sql) {
         Statement::CreateIndex(CreateIndex {
             name: Some(ObjectName(name)),
@@ -2858,6 +2886,8 @@ fn parse_create_index_with_predicate() {
             nulls_distinct: None,
             with,
             predicate: Some(_),
+            index_options,
+            alter_options,
         }) => {
             assert_eq_vec(&["my_index"], &name);
             assert_eq_vec(&["my_table"], &table_name);
@@ -2868,6 +2898,8 @@ fn parse_create_index_with_predicate() {
             assert_eq_vec(&["col1", "col2"], &columns);
             assert!(include.is_empty());
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
@@ -2875,7 +2907,7 @@ fn parse_create_index_with_predicate() {
 
 #[test]
 fn parse_create_index_with_include() {
-    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) 
INCLUDE (col3)";
+    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) 
INCLUDE (col3, col4)";
     match pg().verified_stmt(sql) {
         Statement::CreateIndex(CreateIndex {
             name: Some(ObjectName(name)),
@@ -2889,6 +2921,8 @@ fn parse_create_index_with_include() {
             nulls_distinct: None,
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq_vec(&["my_index"], &name);
             assert_eq_vec(&["my_table"], &table_name);
@@ -2897,8 +2931,10 @@ fn parse_create_index_with_include() {
             assert!(!concurrently);
             assert!(if_not_exists);
             assert_eq_vec(&["col1", "col2"], &columns);
-            assert_eq_vec(&["col3"], &include);
+            assert_eq_vec(&["col3", "col4"], &include);
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
@@ -2906,7 +2942,7 @@ fn parse_create_index_with_include() {
 
 #[test]
 fn parse_create_index_with_nulls_distinct() {
-    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) 
NULLS NOT DISTINCT";
+    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) 
NULLS NOT DISTINCT";
     match pg().verified_stmt(sql) {
         Statement::CreateIndex(CreateIndex {
             name: Some(ObjectName(name)),
@@ -2920,6 +2956,8 @@ fn parse_create_index_with_nulls_distinct() {
             nulls_distinct: Some(nulls_distinct),
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq_vec(&["my_index"], &name);
             assert_eq_vec(&["my_table"], &table_name);
@@ -2931,11 +2969,13 @@ fn parse_create_index_with_nulls_distinct() {
             assert!(include.is_empty());
             assert!(!nulls_distinct);
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }
 
-    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1,col2) 
NULLS DISTINCT";
+    let sql = "CREATE INDEX IF NOT EXISTS my_index ON my_table(col1, col2) 
NULLS DISTINCT";
     match pg().verified_stmt(sql) {
         Statement::CreateIndex(CreateIndex {
             name: Some(ObjectName(name)),
@@ -2949,6 +2989,8 @@ fn parse_create_index_with_nulls_distinct() {
             nulls_distinct: Some(nulls_distinct),
             with,
             predicate: None,
+            index_options,
+            alter_options,
         }) => {
             assert_eq_vec(&["my_index"], &name);
             assert_eq_vec(&["my_table"], &table_name);
@@ -2960,6 +3002,8 @@ fn parse_create_index_with_nulls_distinct() {
             assert!(include.is_empty());
             assert!(nulls_distinct);
             assert!(with.is_empty());
+            assert!(index_options.is_empty());
+            assert!(alter_options.is_empty());
         }
         _ => unreachable!(),
     }


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

Reply via email to