Improve MySQL option parsing in index definitions (#1997)

This commit is contained in:
Michael Victor Zink 2025-08-08 02:32:20 -07:00 committed by GitHub
parent 18b4a14e0f
commit 67fca82495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 628 additions and 498 deletions

View file

@ -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.
/// MySQL index option, used in [`CREATE TABLE`], [`CREATE INDEX`], and [`ALTER TABLE`].
///
/// This structure used here [`MySQL` CREATE TABLE][1], [`MySQL` CREATE INDEX][2].
///
/// [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))]

View file

@ -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))]

View file

@ -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,

View file

@ -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,

View file

@ -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())),
)
}
}

View file

@ -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![],
}
);
}

View file

@ -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!(),
}

View file

@ -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",
);
}

View file

@ -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!(),
}