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.
This commit is contained in:
AugustoFKL 2022-10-19 18:24:38 -03:00 committed by GitHub
parent e3c936a6ce
commit 2aba3f8c91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 242 additions and 5 deletions

View file

@ -247,6 +247,24 @@ pub enum TableConstraint {
name: Option<Ident>, name: Option<Ident>,
expr: Box<Expr>, 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 { impl fmt::Display for TableConstraint {
@ -290,6 +308,48 @@ impl fmt::Display for TableConstraint {
TableConstraint::Check { name, expr } => { TableConstraint::Check { name, expr } => {
write!(f, "{}CHECK ({})", display_constraint_name(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"),
} }
} }
} }

View file

@ -26,7 +26,7 @@ pub use self::data_type::{
CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo,
}; };
pub use self::ddl::{ pub use self::ddl::{
AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, IndexType,
ReferentialAction, TableConstraint, ReferentialAction, TableConstraint,
}; };
pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::operator::{BinaryOperator, UnaryOperator};

View file

@ -105,6 +105,7 @@ define_keywords!(
BLOB, BLOB,
BOOLEAN, BOOLEAN,
BOTH, BOTH,
BTREE,
BY, BY,
BYPASSRLS, BYPASSRLS,
BYTEA, BYTEA,
@ -265,6 +266,7 @@ define_keywords!(
GROUP, GROUP,
GROUPING, GROUPING,
GROUPS, GROUPS,
HASH,
HAVING, HAVING,
HEADER, HEADER,
HIVEVAR, HIVEVAR,

View file

@ -3003,6 +3003,31 @@ impl<'a> Parser<'a> {
self.expect_token(&Token::RParen)?; self.expect_token(&Token::RParen)?;
Ok(Some(TableConstraint::Check { name, expr })) 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 => { unexpected => {
if name.is_some() { if name.is_some() {
self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", unexpected) self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", unexpected)
@ -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> { pub fn parse_sql_option(&mut self) -> Result<SqlOption, ParserError> {
let name = self.parse_identifier()?; let name = self.parse_identifier()?;
self.expect_token(&Token::Eq)?; self.expect_token(&Token::Eq)?;
@ -5779,4 +5814,100 @@ mod tests {
SchemaName::NamedAuthorization(dummy_name.clone(), dummy_authorization.clone()), 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")],
}
);
}
} }

View file

@ -2089,10 +2089,10 @@ fn parse_create_table_hive_array() {
let dialects = TestedDialects { let dialects = TestedDialects {
dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(HiveDialect {})], 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( match dialects.one_statement_parses_to(
sql, sql,
"CREATE TABLE IF NOT EXISTS something (key INT, val INT[])", "CREATE TABLE IF NOT EXISTS something (name INT, val INT[])",
) { ) {
Statement::CreateTable { Statement::CreateTable {
if_not_exists, if_not_exists,
@ -2106,7 +2106,7 @@ fn parse_create_table_hive_array() {
columns, columns,
vec![ vec![
ColumnDef { ColumnDef {
name: Ident::new("key"), name: Ident::new("name"),
data_type: DataType::Int(None), data_type: DataType::Int(None),
collation: None, collation: None,
options: vec![], options: vec![],
@ -2123,7 +2123,8 @@ fn parse_create_table_hive_array() {
_ => unreachable!(), _ => 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 assert!(res
.unwrap_err() .unwrap_err()
.to_string() .to_string()

View file

@ -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 { fn mysql() -> TestedDialects {
TestedDialects { TestedDialects {
dialects: vec![Box::new(MySqlDialect {})], dialects: vec![Box::new(MySqlDialect {})],