From f633d73227de3ea99c0041761dbef8a7f02968b7 Mon Sep 17 00:00:00 2001 From: AugustoFKL Date: Sun, 6 Nov 2022 00:59:51 -0300 Subject: [PATCH] feat: add FULLTEXT option on create table for MySQL and Generic dialects --- src/ast/ddl.rs | 80 ++++++++++++++++++++++++++++++++++++++++ src/ast/mod.rs | 2 +- src/keywords.rs | 2 + src/parser.rs | 32 ++++++++++++++++ tests/sqlparser_mysql.rs | 51 ++++++++++++++++++++++--- 5 files changed, 161 insertions(+), 6 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index c758b4f55..3203f0e20 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -270,6 +270,28 @@ pub enum TableConstraint { /// Referred column identifier list. columns: 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. + /// + /// Supported syntax: + /// + /// ```markdown + /// {FULLTEXT | SPATIAL} [INDEX | KEY] [index_name] (key_part,...) + /// + /// key_part: col_name + /// ``` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-natural-language.html + FulltextOrSpatial { + /// Whether this is a `FULLTEXT` (true) or `SPATIAL` (false) definition. + fulltext: bool, + /// Whether the type is followed by the keyword `KEY`, `INDEX`, or no keyword at all. + index_type_display: KeyOrIndexDisplay, + /// Optional index name. + opt_index_name: Option, + /// Referred column identifier list. + columns: Vec, + }, } impl fmt::Display for TableConstraint { @@ -330,6 +352,64 @@ impl fmt::Display for TableConstraint { Ok(()) } + Self::FulltextOrSpatial { + fulltext, + index_type_display, + opt_index_name, + columns, + } => { + if *fulltext { + write!(f, "FULLTEXT")?; + } else { + write!(f, "SPATIAL")?; + } + + if !matches!(index_type_display, KeyOrIndexDisplay::None) { + write!(f, " {}", index_type_display)?; + } + + if let Some(name) = opt_index_name { + write!(f, " {}", name)?; + } + + write!(f, " ({})", display_comma_separated(columns))?; + + Ok(()) + } + } + } +} + +/// Representation whether a definition can can contains the KEY or INDEX keywords with the same +/// meaning. +/// +/// This enum initially is directed to `FULLTEXT`,`SPATIAL`, and `UNIQUE` indexes on create table +/// statements of `MySQL` [(1)]. +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyOrIndexDisplay { + /// Nothing to display + None, + /// Display the KEY keyword + Key, + /// Display the INDEX keyword + Index, +} + +impl fmt::Display for KeyOrIndexDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeyOrIndexDisplay::None => { + write!(f, "") + } + KeyOrIndexDisplay::Key => { + write!(f, "KEY") + } + KeyOrIndexDisplay::Index => { + write!(f, "INDEX") + } } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4cbd881c3..36913368f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -27,7 +27,7 @@ pub use self::data_type::{ }; pub use self::ddl::{ AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, IndexType, - ReferentialAction, TableConstraint, + KeyOrIndexDisplay, ReferentialAction, TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ diff --git a/src/keywords.rs b/src/keywords.rs index f8d125067..bda144d02 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -255,6 +255,7 @@ define_keywords!( FREEZE, FROM, FULL, + FULLTEXT, FUNCTION, FUNCTIONS, FUSION, @@ -498,6 +499,7 @@ define_keywords!( SNAPSHOT, SOME, SORT, + SPATIAL, SPECIFIC, SPECIFICTYPE, SQL, diff --git a/src/parser.rs b/src/parser.rs index 76d845074..cd22f292c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3036,6 +3036,38 @@ impl<'a> Parser<'a> { columns, })) } + Token::Word(w) + if (w.keyword == Keyword::FULLTEXT || w.keyword == Keyword::SPATIAL) + && dialect_of!(self is GenericDialect | MySqlDialect) => + { + if let Some(name) = name { + return self.expected( + "FULLTEXT or SPATIAL option without constraint name", + Token::make_keyword(&name.to_string()), + ); + } + + 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 opt_index_name = self.maybe_parse(|parser| parser.parse_identifier()); + + let columns = self.parse_parenthesized_column_list(Mandatory)?; + + Ok(Some(TableConstraint::FulltextOrSpatial { + fulltext, + index_type_display, + opt_index_name, + columns, + })) + } unexpected => { if name.is_some() { self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", unexpected) diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 52c0205d6..fb205e29a 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -14,16 +14,15 @@ //! Test SQL syntax specific to MySQL. The parser based on the generic dialect //! is also tested (on the inputs it can handle). -#[macro_use] -mod test_utils; - -use test_utils::*; - use sqlparser::ast::Expr; use sqlparser::ast::Value; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MySqlDialect}; use sqlparser::tokenizer::Token; +use test_utils::*; + +#[macro_use] +mod test_utils; #[test] fn parse_identifiers() { @@ -1129,6 +1128,48 @@ fn parse_create_table_with_index_definition() { ); } +#[test] +fn parse_create_table_with_fulltext_definition() { + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT INDEX (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT KEY (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT potato (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT INDEX potato (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT KEY potato (id))"); + + mysql_and_generic() + .verified_stmt("CREATE TABLE tb (c1 INT, c2 INT, FULLTEXT KEY potato (c1, c2))"); +} + +#[test] +fn parse_create_table_with_spatial_definition() { + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL INDEX (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL KEY (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL potato (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL INDEX potato (id))"); + + mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, SPATIAL KEY potato (id))"); + + mysql_and_generic() + .verified_stmt("CREATE TABLE tb (c1 INT, c2 INT, SPATIAL KEY potato (c1, c2))"); +} + +#[test] +#[should_panic = "Expected FULLTEXT or SPATIAL option without constraint name, found: cons"] +fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name() { + mysql_and_generic().verified_stmt("CREATE TABLE tb (c1 INT, CONSTRAINT cons FULLTEXT (c1))"); +} + fn mysql() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})],