Skip to content

Commit

Permalink
Adding MySQL table option {INDEX | KEY} to the CREATE TABLE definiton…
Browse files Browse the repository at this point in the history
… (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.
  • Loading branch information
AugustoFKL committed Oct 19, 2022
1 parent e3c936a commit 2aba3f8
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 5 deletions.
60 changes: 60 additions & 0 deletions src/ast/ddl.rs
Expand Up @@ -247,6 +247,24 @@ pub enum TableConstraint {
name: Option<Ident>,
expr: Box<Expr>,
},
/// 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<Ident>,
/// Optional [index type][1].
///
/// [1]: IndexType
index_type: Option<IndexType>,
/// Referred column identifier list.
columns: Vec<Ident>,
},
}

impl fmt::Display for TableConstraint {
Expand Down Expand Up @@ -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"),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/ast/mod.rs
Expand Up @@ -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};
Expand Down
2 changes: 2 additions & 0 deletions src/keywords.rs
Expand Up @@ -105,6 +105,7 @@ define_keywords!(
BLOB,
BOOLEAN,
BOTH,
BTREE,
BY,
BYPASSRLS,
BYTEA,
Expand Down Expand Up @@ -265,6 +266,7 @@ define_keywords!(
GROUP,
GROUPING,
GROUPS,
HASH,
HAVING,
HEADER,
HIVEVAR,
Expand Down
131 changes: 131 additions & 0 deletions src/parser.rs
Expand Up @@ -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)
Expand All @@ -3025,6 +3050,16 @@ impl<'a> Parser<'a> {
}
}

pub fn parse_index_type(&mut self) -> Result<IndexType, ParserError> {
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<SqlOption, ParserError> {
let name = self.parse_identifier()?;
self.expect_token(&Token::Eq)?;
Expand Down Expand Up @@ -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")],
}
);
}
}
9 changes: 5 additions & 4 deletions tests/sqlparser_common.rs
Expand Up @@ -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<int>)";
let sql = "CREATE TABLE IF NOT EXISTS something (name int, val array<int>)";
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,
Expand All @@ -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![],
Expand All @@ -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<int)");
let res =
parse_sql_statements("CREATE TABLE IF NOT EXISTS something (name int, val array<int)");
assert!(res
.unwrap_err()
.to_string()
Expand Down
43 changes: 43 additions & 0 deletions tests/sqlparser_mysql.rs
Expand Up @@ -1073,6 +1073,49 @@ fn parse_limit_my_sql_syntax() {
);
}

#[test]
fn parse_create_table_with_index_definition() {
mysql_and_generic().one_statement_parses_to(
"CREATE TABLE tb (id INT, INDEX (id))",
"CREATE TABLE tb (id INT, INDEX (id))",
);

mysql_and_generic().one_statement_parses_to(
"CREATE TABLE tb (id INT, index USING BTREE (id))",
"CREATE TABLE tb (id INT, INDEX USING BTREE (id))",
);

mysql_and_generic().one_statement_parses_to(
"CREATE TABLE tb (id INT, KEY USING HASH (id))",
"CREATE TABLE tb (id INT, KEY USING HASH (id))",
);

mysql_and_generic().one_statement_parses_to(
"CREATE TABLE tb (id INT, key index (id))",
"CREATE TABLE tb (id INT, KEY index (id))",
);

mysql_and_generic().one_statement_parses_to(
"CREATE TABLE tb (id INT, INDEX 'index' (id))",
"CREATE TABLE tb (id INT, INDEX 'index' (id))",
);

mysql_and_generic().one_statement_parses_to(
"CREATE TABLE tb (id INT, INDEX index USING BTREE (id))",
"CREATE TABLE tb (id INT, INDEX index USING BTREE (id))",
);

mysql_and_generic().one_statement_parses_to(
"CREATE TABLE tb (id INT, INDEX index USING HASH (id))",
"CREATE TABLE tb (id INT, INDEX index USING HASH (id))",
);

mysql_and_generic().one_statement_parses_to(
"CREATE TABLE tb (id INT, INDEX (c1, c2, c3, c4,c5))",
"CREATE TABLE tb (id INT, INDEX (c1, c2, c3, c4, c5))",
);
}

fn mysql() -> TestedDialects {
TestedDialects {
dialects: vec![Box::new(MySqlDialect {})],
Expand Down

0 comments on commit 2aba3f8

Please sign in to comment.