diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 080e8c4d..d86ebad9 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -15,7 +15,7 @@ #[cfg(not(feature = "std"))] use alloc::{boxed::Box, string::String, vec::Vec}; -use core::fmt; +use core::fmt::{self, Write}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -397,12 +397,68 @@ impl fmt::Display for AlterColumnOperation { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TableConstraint { - /// `[ CONSTRAINT ] { PRIMARY KEY | UNIQUE } ()` + /// MySQL [definition][1] for `UNIQUE` constraints statements:\ + /// * `[CONSTRAINT []] UNIQUE [] [index_type] () ` + /// + /// where: + /// * [index_type][2] is `USING {BTREE | HASH}` + /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` + /// * [index_type_display][4] is `[INDEX | KEY]` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html + /// [2]: IndexType + /// [3]: IndexOption + /// [4]: KeyOrIndexDisplay Unique { + /// Constraint name. + /// + /// Can be not the same as `index_name` name: Option, + /// Index name + index_name: Option, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + index_type_display: KeyOrIndexDisplay, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + index_type: Option, + /// Identifiers of the columns that are unique. columns: Vec, - /// Whether this is a `PRIMARY KEY` or just a `UNIQUE` constraint - is_primary: bool, + index_options: Vec, + characteristics: Option, + }, + /// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\ + /// * `[CONSTRAINT []] PRIMARY KEY [index_name] [index_type] () ` + /// + /// Actually the specification have no `[index_name]` but the next query will complete successfully: + /// ```sql + /// CREATE TABLE unspec_table ( + /// xid INT NOT NULL, + /// CONSTRAINT p_name PRIMARY KEY index_name USING BTREE (xid) + /// ); + /// ``` + /// + /// where: + /// * [index_type][2] is `USING {BTREE | HASH}` + /// * [index_options][3] is `{index_type | COMMENT 'string' | ... %currently unsupported stmts% } ...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.3/en/create-table.html + /// [2]: IndexType + /// [3]: IndexOption + PrimaryKey { + /// Constraint name. + /// + /// Can be not the same as `index_name` + name: Option, + /// Index name + index_name: Option, + /// Optional `USING` of [index type][1] statement before columns. + /// + /// [1]: IndexType + index_type: Option, + /// Identifiers of the columns that form the primary key. + columns: Vec, + index_options: Vec, characteristics: Option, }, /// A referential integrity constraint (`[ CONSTRAINT ] FOREIGN KEY () @@ -472,22 +528,51 @@ impl fmt::Display for TableConstraint { match self { TableConstraint::Unique { name, + index_name, + index_type_display, + index_type, columns, - is_primary, + index_options, characteristics, } => { write!( f, - "{}{} ({})", + "{}UNIQUE{index_type_display:>}{}{} ({})", display_constraint_name(name), - if *is_primary { "PRIMARY KEY" } else { "UNIQUE" }, - display_comma_separated(columns) + display_option_spaced(index_name), + display_option(" USING ", "", index_type), + display_comma_separated(columns), )?; - if let Some(characteristics) = characteristics { - write!(f, " {}", characteristics)?; + if !index_options.is_empty() { + write!(f, " {}", display_separated(index_options, " "))?; } + write!(f, "{}", display_option_spaced(characteristics))?; + Ok(()) + } + TableConstraint::PrimaryKey { + name, + index_name, + index_type, + columns, + index_options, + characteristics, + } => { + write!( + f, + "{}PRIMARY KEY{}{} ({})", + display_constraint_name(name), + display_option_spaced(index_name), + display_option(" USING ", "", index_type), + display_comma_separated(columns), + )?; + + if !index_options.is_empty() { + write!(f, " {}", display_separated(index_options, " "))?; + } + + write!(f, "{}", display_option_spaced(characteristics))?; Ok(()) } TableConstraint::ForeignKey { @@ -550,9 +635,7 @@ impl fmt::Display for TableConstraint { write!(f, "SPATIAL")?; } - if !matches!(index_type_display, KeyOrIndexDisplay::None) { - write!(f, " {index_type_display}")?; - } + write!(f, "{index_type_display:>}")?; if let Some(name) = opt_index_name { write!(f, " {name}")?; @@ -585,8 +668,20 @@ pub enum KeyOrIndexDisplay { Index, } +impl KeyOrIndexDisplay { + pub fn is_none(self) -> bool { + matches!(self, Self::None) + } +} + impl fmt::Display for KeyOrIndexDisplay { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let left_space = matches!(f.align(), Some(fmt::Alignment::Right)); + + if left_space && !self.is_none() { + f.write_char(' ')? + } + match self { KeyOrIndexDisplay::None => { write!(f, "") @@ -626,6 +721,30 @@ impl fmt::Display for IndexType { } } } + +/// MySQLs index option. +/// +/// 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 +#[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(IndexType), + Comment(String), +} + +impl fmt::Display for IndexOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Using(index_type) => write!(f, "USING {index_type}"), + Self::Comment(s) => write!(f, "COMMENT '{s}'"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -909,6 +1028,7 @@ pub enum GeneratedExpressionMode { Stored, } +#[must_use] fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { struct ConstraintName<'a>(&'a Option); impl<'a> fmt::Display for ConstraintName<'a> { @@ -922,6 +1042,36 @@ fn display_constraint_name(name: &'_ Option) -> impl fmt::Display + '_ { ConstraintName(name) } +/// If `option` is +/// * `Some(inner)` => create display struct for `"{prefix}{inner}{postfix}"` +/// * `_` => do nothing +#[must_use] +fn display_option<'a, T: fmt::Display>( + prefix: &'a str, + postfix: &'a str, + option: &'a Option, +) -> impl fmt::Display + 'a { + struct OptionDisplay<'a, T>(&'a str, &'a str, &'a Option); + impl<'a, T: fmt::Display> fmt::Display for OptionDisplay<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(inner) = self.2 { + let (prefix, postfix) = (self.0, self.1); + write!(f, "{prefix}{inner}{postfix}")?; + } + Ok(()) + } + } + OptionDisplay(prefix, postfix, option) +} + +/// If `option` is +/// * `Some(inner)` => create display struct for `" {inner}"` +/// * `_` => do nothing +#[must_use] +fn display_option_spaced(option: &Option) -> impl fmt::Display + '_ { + display_option(" ", "", option) +} + /// ` = [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]` /// /// Used in UNIQUE and foreign key constraints. The individual settings may occur in any order. diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2eebbc60..a469338d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -33,7 +33,7 @@ pub use self::dcl::{AlterRoleOperation, ResetConfig, RoleOption, SetConfigValue} pub use self::ddl::{ AlterColumnOperation, AlterIndexOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ConstraintCharacteristics, DeferrableInitial, GeneratedAs, - GeneratedExpressionMode, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, + GeneratedExpressionMode, IndexOption, IndexType, KeyOrIndexDisplay, Partition, ProcedureParam, ReferentialAction, TableConstraint, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, }; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 48eaed92..7bd3ffb2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5149,23 +5149,49 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { - Token::Word(w) if w.keyword == Keyword::PRIMARY || w.keyword == Keyword::UNIQUE => { - let is_primary = w.keyword == Keyword::PRIMARY; + Token::Word(w) if w.keyword == Keyword::UNIQUE => { + let index_type_display = self.parse_index_type_display(); + if !dialect_of!(self is GenericDialect | MySqlDialect) + && !index_type_display.is_none() + { + return self + .expected("`index_name` or `(column_name [, ...])`", self.peek_token()); + } - // parse optional [KEY] - let _ = self.parse_keyword(Keyword::KEY); - - // optional constraint name - let name = self - .maybe_parse(|parser| parser.parse_identifier(false)) - .or(name); + // optional index name + 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_options = self.parse_index_options()?; let characteristics = self.parse_constraint_characteristics()?; Ok(Some(TableConstraint::Unique { name, + index_name, + index_type_display, + index_type, columns, - is_primary, + index_options, + characteristics, + })) + } + Token::Word(w) if w.keyword == Keyword::PRIMARY => { + // after `PRIMARY` always stay `KEY` + self.expect_keyword(Keyword::KEY)?; + + // optional index name + 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_options = self.parse_index_options()?; + let characteristics = self.parse_constraint_characteristics()?; + Ok(Some(TableConstraint::PrimaryKey { + name, + index_name, + index_type, + columns, + index_options, characteristics, })) } @@ -5209,20 +5235,17 @@ impl<'a> Parser<'a> { } Token::Word(w) if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) - && dialect_of!(self is GenericDialect | MySqlDialect) => + && dialect_of!(self is GenericDialect | MySqlDialect) + && name.is_none() => { let display_as_key = w.keyword == Keyword::KEY; let name = match self.peek_token().token { Token::Word(word) if word.keyword == Keyword::USING => None, - _ => self.maybe_parse(|parser| parser.parse_identifier(false)), + _ => self.parse_optional_indent(), }; - let index_type = if self.parse_keyword(Keyword::USING) { - Some(self.parse_index_type()?) - } else { - None - }; + let index_type = self.parse_optional_using_then_index_type()?; let columns = self.parse_parenthesized_column_list(Mandatory, false)?; Ok(Some(TableConstraint::Index { @@ -5248,15 +5271,9 @@ impl<'a> Parser<'a> { let fulltext = w.keyword == Keyword::FULLTEXT; - let index_type_display = if self.parse_keyword(Keyword::KEY) { - KeyOrIndexDisplay::Key - } else if self.parse_keyword(Keyword::INDEX) { - KeyOrIndexDisplay::Index - } else { - KeyOrIndexDisplay::None - }; + let index_type_display = self.parse_index_type_display(); - let opt_index_name = self.maybe_parse(|parser| parser.parse_identifier(false)); + let opt_index_name = self.parse_optional_indent(); let columns = self.parse_parenthesized_column_list(Mandatory, false)?; @@ -5313,6 +5330,56 @@ impl<'a> Parser<'a> { } } + /// Parse [USING {BTREE | HASH}] + pub fn parse_optional_using_then_index_type( + &mut self, + ) -> Result, ParserError> { + if self.parse_keyword(Keyword::USING) { + Ok(Some(self.parse_index_type()?)) + } else { + Ok(None) + } + } + + /// Parse `[ident]`, mostly `ident` is name, like: + /// `window_name`, `index_name`, ... + pub fn parse_optional_indent(&mut self) -> Option { + self.maybe_parse(|parser| parser.parse_identifier(false)) + } + + #[must_use] + pub fn parse_index_type_display(&mut self) -> KeyOrIndexDisplay { + if self.parse_keyword(Keyword::KEY) { + KeyOrIndexDisplay::Key + } else if self.parse_keyword(Keyword::INDEX) { + KeyOrIndexDisplay::Index + } else { + KeyOrIndexDisplay::None + } + } + + pub fn parse_optional_index_option(&mut self) -> Result, ParserError> { + if let Some(index_type) = self.parse_optional_using_then_index_type()? { + Ok(Some(IndexOption::Using(index_type))) + } else if self.parse_keyword(Keyword::COMMENT) { + let s = self.parse_literal_string()?; + Ok(Some(IndexOption::Comment(s))) + } else { + Ok(None) + } + } + + pub fn parse_index_options(&mut self) -> Result, ParserError> { + let mut options = Vec::new(); + + loop { + match self.parse_optional_index_option()? { + Some(index_option) => options.push(index_option), + None => return Ok(options), + } + } + } + pub fn parse_sql_option(&mut self) -> Result { let name = self.parse_identifier(false)?; self.expect_token(&Token::Eq)?; @@ -9537,9 +9604,7 @@ impl<'a> Parser<'a> { pub fn parse_window_spec(&mut self) -> Result { let window_name = match self.peek_token().token { - Token::Word(word) if word.keyword == Keyword::NoKeyword => { - self.maybe_parse(|parser| parser.parse_identifier(false)) - } + Token::Word(word) if word.keyword == Keyword::NoKeyword => self.parse_optional_indent(), _ => None, }; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 59314c1d..5f64079a 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -500,63 +500,186 @@ fn parse_create_table_auto_increment() { } } -#[test] -fn parse_create_table_unique_key() { - let sql = "CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, UNIQUE KEY bar_key (bar))"; - let canonical = "CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, CONSTRAINT bar_key UNIQUE (bar))"; - match mysql().one_statement_parses_to(sql, canonical) { - Statement::CreateTable { +/// if `unique_index_type_display` is `Some` create `TableConstraint::Unique` +/// otherwise create `TableConstraint::Primary` +fn table_constraint_unique_primary_ctor( + name: Option, + index_name: Option, + index_type: Option, + columns: Vec, + index_options: Vec, + characteristics: Option, + unique_index_type_display: Option, +) -> TableConstraint { + match unique_index_type_display { + Some(index_type_display) => TableConstraint::Unique { name, + index_name, + index_type_display, + index_type, columns, - constraints, - .. - } => { - assert_eq!(name.to_string(), "foo"); - assert_eq!( - vec![TableConstraint::Unique { - name: Some(Ident::new("bar_key")), - columns: vec![Ident::new("bar")], - is_primary: false, - characteristics: None, - }], - constraints - ); - assert_eq!( - vec![ - ColumnDef { - name: Ident::new("id"), - data_type: DataType::Int(None), - collation: None, - options: vec![ - ColumnOptionDef { - name: None, - option: ColumnOption::Unique { - is_primary: true, - characteristics: None + index_options, + characteristics, + }, + None => TableConstraint::PrimaryKey { + name, + index_name, + index_type, + columns, + index_options, + characteristics, + }, + } +} + +#[test] +fn parse_create_table_primary_and_unique_key() { + let sqls = ["UNIQUE KEY", "PRIMARY KEY"] + .map(|key_ty|format!("CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, bar INT NOT NULL, CONSTRAINT bar_key {key_ty} (bar))")); + + let index_type_display = [Some(KeyOrIndexDisplay::Key), None]; + + for (sql, index_type_display) in sqls.iter().zip(index_type_display) { + match mysql().one_statement_parses_to(sql, "") { + Statement::CreateTable { + name, + columns, + constraints, + .. + } => { + assert_eq!(name.to_string(), "foo"); + + let expected_constraint = table_constraint_unique_primary_ctor( + Some(Ident::new("bar_key")), + None, + None, + vec![Ident::new("bar")], + vec![], + None, + index_type_display, + ); + assert_eq!(vec![expected_constraint], constraints); + + assert_eq!( + vec![ + ColumnDef { + name: Ident::new("id"), + data_type: DataType::Int(None), + collation: None, + options: vec![ + ColumnOptionDef { + name: None, + option: ColumnOption::Unique { + is_primary: true, + characteristics: None + }, }, - }, - ColumnOptionDef { + ColumnOptionDef { + name: None, + option: ColumnOption::DialectSpecific(vec![ + Token::make_keyword("AUTO_INCREMENT") + ]), + }, + ], + }, + ColumnDef { + name: Ident::new("bar"), + data_type: DataType::Int(None), + collation: None, + options: vec![ColumnOptionDef { name: None, - option: ColumnOption::DialectSpecific(vec![Token::make_keyword( - "AUTO_INCREMENT" - )]), - }, - ], - }, - ColumnDef { - name: Ident::new("bar"), - data_type: DataType::Int(None), - collation: None, - options: vec![ColumnOptionDef { - name: None, - option: ColumnOption::NotNull, - },], - }, - ], - columns - ); + option: ColumnOption::NotNull, + },], + }, + ], + columns + ); + } + _ => unreachable!(), } - _ => unreachable!(), + } +} + +#[test] +fn parse_create_table_primary_and_unique_key_with_index_options() { + let sqls = ["UNIQUE INDEX", "PRIMARY KEY"] + .map(|key_ty|format!("CREATE TABLE foo (bar INT, var INT, CONSTRAINT constr {key_ty} index_name (bar, var) USING HASH COMMENT 'yes, ' USING BTREE COMMENT 'MySQL allows')")); + + let index_type_display = [Some(KeyOrIndexDisplay::Index), None]; + + for (sql, index_type_display) in sqls.iter().zip(index_type_display) { + match mysql_and_generic().one_statement_parses_to(sql, "") { + Statement::CreateTable { + name, constraints, .. + } => { + assert_eq!(name.to_string(), "foo"); + + let expected_constraint = table_constraint_unique_primary_ctor( + Some(Ident::new("constr")), + Some(Ident::new("index_name")), + None, + vec![Ident::new("bar"), Ident::new("var")], + vec![ + IndexOption::Using(IndexType::Hash), + IndexOption::Comment("yes, ".into()), + IndexOption::Using(IndexType::BTree), + IndexOption::Comment("MySQL allows".into()), + ], + None, + index_type_display, + ); + assert_eq!(vec![expected_constraint], constraints); + } + _ => unreachable!(), + } + + mysql_and_generic().verified_stmt(sql); + } +} + +#[test] +fn parse_create_table_primary_and_unique_key_with_index_type() { + let sqls = ["UNIQUE", "PRIMARY KEY"].map(|key_ty| { + format!("CREATE TABLE foo (bar INT, {key_ty} index_name USING BTREE (bar) USING HASH)") + }); + + let index_type_display = [Some(KeyOrIndexDisplay::None), None]; + + for (sql, index_type_display) in sqls.iter().zip(index_type_display) { + match mysql_and_generic().one_statement_parses_to(sql, "") { + Statement::CreateTable { + name, constraints, .. + } => { + assert_eq!(name.to_string(), "foo"); + + let expected_constraint = table_constraint_unique_primary_ctor( + None, + Some(Ident::new("index_name")), + Some(IndexType::BTree), + vec![Ident::new("bar")], + vec![IndexOption::Using(IndexType::Hash)], + None, + index_type_display, + ); + assert_eq!(vec![expected_constraint], constraints); + } + _ => unreachable!(), + } + mysql_and_generic().verified_stmt(sql); + } + + let sql = "CREATE TABLE foo (bar INT, UNIQUE INDEX index_name USING BTREE (bar) USING HASH)"; + mysql_and_generic().verified_stmt(sql); + let sql = "CREATE TABLE foo (bar INT, PRIMARY KEY index_name USING BTREE (bar) USING HASH)"; + mysql_and_generic().verified_stmt(sql); +} + +#[test] +fn parse_create_table_primary_and_unique_key_characteristic_test() { + let sqls = ["UNIQUE INDEX", "PRIMARY KEY"] + .map(|key_ty|format!("CREATE TABLE x (y INT, CONSTRAINT constr {key_ty} (y) NOT DEFERRABLE INITIALLY IMMEDIATE)")); + for sql in &sqls { + mysql_and_generic().verified_stmt(sql); } } @@ -2333,6 +2456,15 @@ fn parse_create_table_with_index_definition() { ); } +#[test] +fn parse_create_table_unallow_constraint_then_index() { + let sql = "CREATE TABLE foo (bar INT, CONSTRAINT constr INDEX index (bar))"; + assert!(mysql_and_generic().parse_sql_statements(sql).is_err()); + + let sql = "CREATE TABLE foo (bar INT, INDEX index (bar))"; + assert!(mysql_and_generic().parse_sql_statements(sql).is_ok()); +} + #[test] fn parse_create_table_with_fulltext_definition() { mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT (id))");