From 2aba3f8c91287cb5505e00a550a361c585ad1b13 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:24:38 -0300 Subject: [PATCH] Adding MySQL table option {INDEX | KEY} to the CREATE TABLE definiton (partial). (#665) Theoretically the behavior should be the same as CREATE INDEX, but we cannot make that assumption, so the parse is (almost) identical as the input. Breaking changes: - Now HASH and BTREE are KEYWORDS, and using them as names can result in errors. - Now 'KEY' and 'INDEX' column names start the parsing of a table constraint if unquoted for the Generic dialect. This results in possible conficts if canonical results are compared for all dialects if a column is named 'key' without quotes. --- src/ast/ddl.rs | 60 +++++++++++++++++ src/ast/mod.rs | 2 +- src/keywords.rs | 2 + src/parser.rs | 131 ++++++++++++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 9 +-- tests/sqlparser_mysql.rs | 43 +++++++++++++ 6 files changed, 242 insertions(+), 5 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1847f2518..cae0f597b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -247,6 +247,24 @@ pub enum TableConstraint { name: Option, expr: Box, }, + /// MySQLs [index definition][1] for index creation. Not present on ANSI so, for now, the usage + /// is restricted to MySQL, as no other dialects that support this syntax were found. + /// + /// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html + Index { + /// Whether this index starts with KEY (true) or INDEX (false), to maintain the same syntax. + display_as_key: bool, + /// Index name. + name: Option, + /// Optional [index type][1]. + /// + /// [1]: IndexType + index_type: Option, + /// Referred column identifier list. + columns: Vec, + }, } impl fmt::Display for TableConstraint { @@ -290,6 +308,48 @@ impl fmt::Display for TableConstraint { TableConstraint::Check { name, expr } => { write!(f, "{}CHECK ({})", display_constraint_name(name), expr) } + TableConstraint::Index { + display_as_key, + name, + index_type, + columns, + } => { + write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" })?; + if let Some(name) = name { + write!(f, " {}", name)?; + } + if let Some(index_type) = index_type { + write!(f, " USING {}", index_type)?; + } + write!(f, " ({})", display_comma_separated(columns))?; + + Ok(()) + } + } + } +} + +/// Indexing method used by that index. +/// +/// This structure isn't present on ANSI, but is found at least in [MySQL CREATE TABLE][1], +/// [MySQL CREATE INDEX][2], and [Postgresql CREATE INDEX][3] statements. +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html +/// [2]: https://dev.mysql.com/doc/refman/8.0/en/create-index.html +/// [3]: https://www.postgresql.org/docs/14/sql-createindex.html +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum IndexType { + BTree, + Hash, + // TODO add Postgresql's possible indexes +} + +impl fmt::Display for IndexType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::BTree => write!(f, "BTREE"), + Self::Hash => write!(f, "HASH"), } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 49e6fb01e..c83ead544 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -26,7 +26,7 @@ pub use self::data_type::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, }; pub use self::ddl::{ - AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, + AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, IndexType, ReferentialAction, TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; diff --git a/src/keywords.rs b/src/keywords.rs index 1c2c43854..e29ebdfdb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -105,6 +105,7 @@ define_keywords!( BLOB, BOOLEAN, BOTH, + BTREE, BY, BYPASSRLS, BYTEA, @@ -265,6 +266,7 @@ define_keywords!( GROUP, GROUPING, GROUPS, + HASH, HAVING, HEADER, HIVEVAR, diff --git a/src/parser.rs b/src/parser.rs index 5dfb9f10c..0254a2f9a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3003,6 +3003,31 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; Ok(Some(TableConstraint::Check { name, expr })) } + Token::Word(w) + if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) + && dialect_of!(self is GenericDialect | MySqlDialect) => + { + let display_as_key = w.keyword == Keyword::KEY; + + let name = match self.peek_token() { + Token::Word(word) if word.keyword == Keyword::USING => None, + _ => self.maybe_parse(|parser| parser.parse_identifier()), + }; + + let index_type = if self.parse_keyword(Keyword::USING) { + Some(self.parse_index_type()?) + } else { + None + }; + let columns = self.parse_parenthesized_column_list(Mandatory)?; + + Ok(Some(TableConstraint::Index { + display_as_key, + name, + index_type, + columns, + })) + } unexpected => { if name.is_some() { self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", unexpected) @@ -3025,6 +3050,16 @@ impl<'a> Parser<'a> { } } + pub fn parse_index_type(&mut self) -> Result { + if self.parse_keyword(Keyword::BTREE) { + Ok(IndexType::BTree) + } else if self.parse_keyword(Keyword::HASH) { + Ok(IndexType::Hash) + } else { + self.expected("index type {BTREE | HASH}", self.peek_token()) + } + } + pub fn parse_sql_option(&mut self) -> Result { let name = self.parse_identifier()?; self.expect_token(&Token::Eq)?; @@ -5779,4 +5814,100 @@ mod tests { SchemaName::NamedAuthorization(dummy_name.clone(), dummy_authorization.clone()), ); } + + #[test] + fn mysql_parse_index_table_constraint() { + macro_rules! test_parse_table_constraint { + ($dialect:expr, $input:expr, $expected:expr $(,)?) => {{ + $dialect.run_parser_method(&*$input, |parser| { + let constraint = parser.parse_optional_table_constraint().unwrap().unwrap(); + // Validate that the structure is the same as expected + assert_eq!(constraint, $expected); + // Validate that the input and the expected structure serialization are the same + assert_eq!(constraint.to_string(), $input.to_string()); + }); + }}; + } + + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})], + }; + + test_parse_table_constraint!( + dialect, + "INDEX (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: None, + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "KEY (c1)", + TableConstraint::Index { + display_as_key: true, + name: None, + index_type: None, + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX 'index' (c1, c2)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::with_quote('\'', "index")), + index_type: None, + columns: vec![Ident::new("c1"), Ident::new("c2")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX USING BTREE (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: Some(IndexType::BTree), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX USING HASH (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: Some(IndexType::Hash), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX idx_name USING BTREE (c1)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::new("idx_name")), + index_type: Some(IndexType::BTree), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX idx_name USING HASH (c1)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::new("idx_name")), + index_type: Some(IndexType::Hash), + columns: vec![Ident::new("c1")], + } + ); + } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9e5a5cdd1..bae310ef0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2089,10 +2089,10 @@ fn parse_create_table_hive_array() { let dialects = TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(HiveDialect {})], }; - let sql = "CREATE TABLE IF NOT EXISTS something (key int, val array)"; + let sql = "CREATE TABLE IF NOT EXISTS something (name int, val array)"; match dialects.one_statement_parses_to( sql, - "CREATE TABLE IF NOT EXISTS something (key INT, val INT[])", + "CREATE TABLE IF NOT EXISTS something (name INT, val INT[])", ) { Statement::CreateTable { if_not_exists, @@ -2106,7 +2106,7 @@ fn parse_create_table_hive_array() { columns, vec![ ColumnDef { - name: Ident::new("key"), + name: Ident::new("name"), data_type: DataType::Int(None), collation: None, options: vec![], @@ -2123,7 +2123,8 @@ fn parse_create_table_hive_array() { _ => unreachable!(), } - let res = parse_sql_statements("CREATE TABLE IF NOT EXISTS something (key int, val array TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})],