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