diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1fbc45603..99801d247 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -32,8 +32,9 @@ use crate::ast::value::escape_single_quote_string; use crate::ast::{ display_comma_separated, display_separated, CommentDef, CreateFunctionBody, CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull, - FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName, - OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value, + FunctionDeterminismSpecifier, FunctionParallel, Ident, IndexExpr, MySQLColumnPosition, + ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, + Value, }; use crate::keywords::Keyword; use crate::tokenizer::Token; @@ -832,8 +833,8 @@ pub enum TableConstraint { /// /// [1]: IndexType index_type: Option, - /// Identifiers of the columns that are unique. - columns: Vec, + /// Index expr list. + index_exprs: Vec, index_options: Vec, characteristics: Option, /// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]` @@ -868,8 +869,8 @@ pub enum TableConstraint { /// /// [1]: IndexType index_type: Option, - /// Identifiers of the columns that form the primary key. - columns: Vec, + /// Index expr list. + index_exprs: Vec, index_options: Vec, characteristics: Option, }, @@ -907,8 +908,8 @@ pub enum TableConstraint { /// /// [1]: IndexType index_type: Option, - /// Referred column identifier list. - columns: Vec, + /// Index expr list. + index_exprs: Vec, }, /// 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. @@ -930,8 +931,8 @@ pub enum TableConstraint { index_type_display: KeyOrIndexDisplay, /// Optional index name. opt_index_name: Option, - /// Referred column identifier list. - columns: Vec, + /// Index expr list. + index_exprs: Vec, }, } @@ -943,7 +944,7 @@ impl fmt::Display for TableConstraint { index_name, index_type_display, index_type, - columns, + index_exprs, index_options, characteristics, nulls_distinct, @@ -954,7 +955,7 @@ impl fmt::Display for TableConstraint { display_constraint_name(name), display_option_spaced(index_name), display_option(" USING ", "", index_type), - display_comma_separated(columns), + display_comma_separated(index_exprs), )?; if !index_options.is_empty() { @@ -968,7 +969,7 @@ impl fmt::Display for TableConstraint { name, index_name, index_type, - columns, + index_exprs, index_options, characteristics, } => { @@ -978,7 +979,7 @@ impl fmt::Display for TableConstraint { display_constraint_name(name), display_option_spaced(index_name), display_option(" USING ", "", index_type), - display_comma_separated(columns), + display_comma_separated(index_exprs), )?; if !index_options.is_empty() { @@ -1025,7 +1026,7 @@ impl fmt::Display for TableConstraint { display_as_key, name, index_type, - columns, + index_exprs, } => { write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" })?; if let Some(name) = name { @@ -1034,7 +1035,8 @@ impl fmt::Display for TableConstraint { if let Some(index_type) = index_type { write!(f, " USING {index_type}")?; } - write!(f, " ({})", display_comma_separated(columns))?; + + write!(f, " ({})", display_comma_separated(index_exprs))?; Ok(()) } @@ -1042,7 +1044,7 @@ impl fmt::Display for TableConstraint { fulltext, index_type_display, opt_index_name, - columns, + index_exprs, } => { if *fulltext { write!(f, "FULLTEXT")?; @@ -1056,7 +1058,7 @@ impl fmt::Display for TableConstraint { write!(f, " {name}")?; } - write!(f, " ({})", display_comma_separated(columns))?; + write!(f, " ({})", display_comma_separated(index_exprs))?; Ok(()) } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index aad122fbf..5699ce66b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -8690,6 +8690,37 @@ pub enum CopyIntoSnowflakeKind { Location, } +/// Index Field +/// +/// This structure used here [`MySQL` CREATE INDEX][1], [`PostgreSQL` CREATE INDEX][2], [`MySQL` CREATE TABLE][3]. +/// +/// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-index.html +/// [2]: https://www.postgresql.org/docs/17/sql-createindex.html +/// [3]: https://dev.mysql.com/doc/refman/8.3/en/create-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 struct IndexExpr { + pub expr: Expr, + pub collation: Option, + pub operator_class: Option, + pub order_options: OrderByOptions, +} + +impl fmt::Display for IndexExpr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.expr)?; + if let Some(collation) = &self.collation { + write!(f, "{collation}")?; + } + if let Some(operator) = &self.operator_class { + write!(f, "{operator}")?; + } + write!(f, "{}", self.order_options) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a036271cb..9e7c0589d 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -27,8 +27,8 @@ use super::{ CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, - GroupByExpr, HavingBound, IlikeSelectItem, Insert, Interpolate, InterpolateExpr, Join, - JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, MatchRecognizePattern, + GroupByExpr, HavingBound, IlikeSelectItem, IndexExpr, Insert, Interpolate, InterpolateExpr, + Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem, ReplaceSelectElement, @@ -628,7 +628,7 @@ impl Spanned for TableConstraint { index_name, index_type_display: _, index_type: _, - columns, + index_exprs, index_options: _, characteristics, nulls_distinct: _, @@ -636,21 +636,21 @@ impl Spanned for TableConstraint { name.iter() .map(|i| i.span) .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(index_exprs.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), TableConstraint::PrimaryKey { name, index_name, index_type: _, - columns, + index_exprs, index_options: _, characteristics, } => union_spans( name.iter() .map(|i| i.span) .chain(index_name.iter().map(|i| i.span)) - .chain(columns.iter().map(|i| i.span)) + .chain(index_exprs.iter().map(|i| i.span())) .chain(characteristics.iter().map(|i| i.span())), ), TableConstraint::ForeignKey { @@ -678,22 +678,22 @@ impl Spanned for TableConstraint { display_as_key: _, name, index_type: _, - columns, + index_exprs, } => union_spans( name.iter() .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), + .chain(index_exprs.iter().map(|i| i.span())), ), TableConstraint::FulltextOrSpatial { fulltext: _, index_type_display: _, opt_index_name, - columns, + index_exprs, } => union_spans( opt_index_name .iter() .map(|i| i.span) - .chain(columns.iter().map(|i| i.span)), + .chain(index_exprs.iter().map(|i| i.span())), ), } } @@ -2178,6 +2178,19 @@ impl Spanned for TableObject { } } +impl Spanned for IndexExpr { + fn span(&self) -> Span { + let IndexExpr { + expr, + collation: _, + operator_class: _, + order_options: _, + } = self; + + union_spans(core::iter::once(expr.span())) + } +} + #[cfg(test)] pub mod tests { use crate::dialect::{Dialect, GenericDialect, SnowflakeDialect}; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c08c7049d..b2634dfd7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7440,7 +7440,7 @@ impl<'a> Parser<'a> { let index_name = self.parse_optional_indent()?; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let index_exprs = self.parse_index_exprs()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::Unique { @@ -7448,7 +7448,7 @@ impl<'a> Parser<'a> { index_name, index_type_display, index_type, - columns, + index_exprs, index_options, characteristics, nulls_distinct, @@ -7462,14 +7462,14 @@ impl<'a> Parser<'a> { let index_name = self.parse_optional_indent()?; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let index_exprs = self.parse_index_exprs()?; let index_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::PrimaryKey { name, index_name, index_type, - columns, + index_exprs, index_options, characteristics, })) @@ -7525,13 +7525,13 @@ impl<'a> Parser<'a> { }; let index_type = self.parse_optional_using_then_index_type()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let index_exprs = self.parse_index_exprs()?; Ok(Some(TableConstraint::Index { display_as_key, name, index_type, - columns, + index_exprs, })) } Token::Word(w) @@ -7554,13 +7554,13 @@ impl<'a> Parser<'a> { let opt_index_name = self.parse_optional_indent()?; - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let index_exprs = self.parse_index_exprs()?; Ok(Some(TableConstraint::FulltextOrSpatial { fulltext, index_type_display, opt_index_name, - columns, + index_exprs, })) } _ => { @@ -7646,6 +7646,39 @@ impl<'a> Parser<'a> { } } + pub fn parse_index_exprs(&mut self) -> Result, ParserError> { + self.parse_parenthesized(|p| p.parse_comma_separated(Parser::parse_index_expr)) + } + + pub fn parse_index_expr(&mut self) -> Result { + let expr = self.parse_expr()?; + let collation = if self.parse_keyword(Keyword::COLLATE) { + Some(self.parse_object_name(false)?) + } else { + None + }; + + let (operator_class, order_options) = if self.peek_keyword(Keyword::ASC) + || self.peek_keyword(Keyword::DESC) + || self.peek_keyword(Keyword::NULLS) + { + let order_options = self.parse_order_by_options()?; + (None, order_options) + } else { + let operator_class = self.maybe_parse(|p| p.parse_expr())?; + + let order_options = self.parse_order_by_options()?; + (operator_class, order_options) + }; + + Ok(IndexExpr { + expr, + collation, + operator_class, + order_options, + }) + } + /// Parse `[ident]`, mostly `ident` is name, like: /// `window_name`, `index_name`, ... pub fn parse_optional_indent(&mut self) -> Result, ParserError> { @@ -14688,6 +14721,8 @@ impl Word { #[cfg(test)] mod tests { + use std::vec; + use crate::test_utils::{all_dialects, TestedDialects}; use super::*; @@ -15138,7 +15173,15 @@ mod tests { display_as_key: false, name: None, index_type: None, - columns: vec![Ident::new("c1")], + index_exprs: vec![IndexExpr { + expr: Expr::Identifier(Ident::new("c1")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], } ); @@ -15149,7 +15192,15 @@ mod tests { display_as_key: true, name: None, index_type: None, - columns: vec![Ident::new("c1")], + index_exprs: vec![IndexExpr { + expr: Expr::Identifier(Ident::new("c1")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], } ); @@ -15160,7 +15211,26 @@ mod tests { display_as_key: false, name: Some(Ident::with_quote('\'', "index")), index_type: None, - columns: vec![Ident::new("c1"), Ident::new("c2")], + index_exprs: vec![ + IndexExpr { + expr: Expr::Identifier(Ident::new("c1")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }, + IndexExpr { + expr: Expr::Identifier(Ident::new("c2")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + } + ], } ); @@ -15171,7 +15241,15 @@ mod tests { display_as_key: false, name: None, index_type: Some(IndexType::BTree), - columns: vec![Ident::new("c1")], + index_exprs: vec![IndexExpr { + expr: Expr::Identifier(Ident::new("c1")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], } ); @@ -15182,7 +15260,15 @@ mod tests { display_as_key: false, name: None, index_type: Some(IndexType::Hash), - columns: vec![Ident::new("c1")], + index_exprs: vec![IndexExpr { + expr: Expr::Identifier(Ident::new("c1")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], } ); @@ -15193,7 +15279,15 @@ mod tests { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::BTree), - columns: vec![Ident::new("c1")], + index_exprs: vec![IndexExpr { + expr: Expr::Identifier(Ident::new("c1")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], } ); @@ -15204,7 +15298,75 @@ mod tests { display_as_key: false, name: Some(Ident::new("idx_name")), index_type: Some(IndexType::Hash), - columns: vec![Ident::new("c1")], + index_exprs: vec![IndexExpr { + expr: Expr::Identifier(Ident::new("c1")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], + } + ); + + test_parse_table_constraint!( + dialect, + "KEY (c1(10), (LOWER(c2)) DESC)", + TableConstraint::Index { + display_as_key: true, + name: None, + index_type: None, + index_exprs: vec![ + IndexExpr { + expr: Expr::Function(Function { + name: ObjectName::from(vec![Ident::new("c1")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Value(crate::test_utils::number("10")) + )),], + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }, + IndexExpr { + expr: Expr::Nested(Box::new(Expr::Function(Function { + name: ObjectName::from(vec![Ident::new("LOWER")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Identifier(Ident::new("c2")) + )),], + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }))), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: Some(false), + nulls_first: None, + } + } + ], } ); } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 030710743..d782c8afb 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -665,7 +665,7 @@ fn table_constraint_unique_primary_ctor( name: Option, index_name: Option, index_type: Option, - columns: Vec, + index_exprs: Vec, index_options: Vec, characteristics: Option, unique_index_type_display: Option, @@ -676,7 +676,7 @@ fn table_constraint_unique_primary_ctor( index_name, index_type_display, index_type, - columns, + index_exprs, index_options, characteristics, nulls_distinct: NullsDistinctOption::None, @@ -685,7 +685,7 @@ fn table_constraint_unique_primary_ctor( name, index_name, index_type, - columns, + index_exprs, index_options, characteristics, }, @@ -713,7 +713,15 @@ fn parse_create_table_primary_and_unique_key() { Some(Ident::new("bar_key")), None, None, - vec![Ident::new("bar")], + vec![IndexExpr { + expr: Expr::Identifier(Ident::new("bar")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], vec![], None, index_type_display, @@ -776,7 +784,26 @@ fn parse_create_table_primary_and_unique_key_with_index_options() { Some(Ident::new("constr")), Some(Ident::new("index_name")), None, - vec![Ident::new("bar"), Ident::new("var")], + vec![ + IndexExpr { + expr: Expr::Identifier(Ident::new("bar")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }, + IndexExpr { + expr: Expr::Identifier(Ident::new("var")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }, + ], vec![ IndexOption::Using(IndexType::Hash), IndexOption::Comment("yes, ".into()), @@ -814,7 +841,15 @@ fn parse_create_table_primary_and_unique_key_with_index_type() { None, Some(Ident::new("index_name")), Some(IndexType::BTree), - vec![Ident::new("bar")], + vec![IndexExpr { + expr: Expr::Identifier(Ident::new("bar")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], vec![IndexOption::Using(IndexType::Hash)], None, index_type_display, @@ -2235,6 +2270,86 @@ fn parse_alter_table_add_columns() { } } +#[test] +fn parse_alter_table_add_keys() { + match mysql().verified_stmt("ALTER TABLE tab ADD PRIMARY KEY (a), ADD KEY b (b(20), c)") { + Statement::AlterTable { + name, + if_exists, + only, + operations, + location: _, + on_cluster: _, + } => { + assert_eq!(name.to_string(), "tab"); + assert!(!if_exists); + assert!(!only); + assert_eq!( + operations, + vec![ + AlterTableOperation::AddConstraint(TableConstraint::PrimaryKey { + name: None, + index_name: None, + index_exprs: vec![IndexExpr { + expr: Expr::Identifier(Ident::new("a")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }], + index_type: None, + index_options: vec![], + characteristics: None, + }), + AlterTableOperation::AddConstraint(TableConstraint::Index { + display_as_key: true, + name: Some(Ident::new("b")), + index_type: None, + index_exprs: vec![ + IndexExpr { + expr: Expr::Function(Function { + name: ObjectName::from(vec![Ident::new("b")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Value(crate::test_utils::number("20")) + )),], + clauses: vec![], + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }, + IndexExpr { + expr: Expr::Identifier(Ident::new("c")), + collation: None, + operator_class: None, + order_options: OrderByOptions { + asc: None, + nulls_first: None, + }, + }, + ] + }) + ] + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_alter_table_drop_primary_key() { assert_matches!(