Support MySQL UNIQUE table constraint (#1164)

Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
This commit is contained in:
Nikita-str 2024-04-09 23:20:24 +03:00 committed by GitHub
parent 6da8828c1b
commit 8f67d1a713
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 441 additions and 94 deletions

View file

@ -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 <name> ] { PRIMARY KEY | UNIQUE } (<columns>)`
/// MySQL [definition][1] for `UNIQUE` constraints statements:\
/// * `[CONSTRAINT [<name>]] UNIQUE <index_type_display> [<index_name>] [index_type] (<columns>) <index_options>`
///
/// 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<Ident>,
/// Index name
index_name: Option<Ident>,
/// 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<IndexType>,
/// Identifiers of the columns that are unique.
columns: Vec<Ident>,
/// Whether this is a `PRIMARY KEY` or just a `UNIQUE` constraint
is_primary: bool,
index_options: Vec<IndexOption>,
characteristics: Option<ConstraintCharacteristics>,
},
/// MySQL [definition][1] for `PRIMARY KEY` constraints statements:\
/// * `[CONSTRAINT [<name>]] PRIMARY KEY [index_name] [index_type] (<columns>) <index_options>`
///
/// 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<Ident>,
/// Index name
index_name: Option<Ident>,
/// Optional `USING` of [index type][1] statement before columns.
///
/// [1]: IndexType
index_type: Option<IndexType>,
/// Identifiers of the columns that form the primary key.
columns: Vec<Ident>,
index_options: Vec<IndexOption>,
characteristics: Option<ConstraintCharacteristics>,
},
/// A referential integrity constraint (`[ CONSTRAINT <name> ] FOREIGN KEY (<columns>)
@ -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<Ident>) -> impl fmt::Display + '_ {
struct ConstraintName<'a>(&'a Option<Ident>);
impl<'a> fmt::Display for ConstraintName<'a> {
@ -922,6 +1042,36 @@ fn display_constraint_name(name: &'_ Option<Ident>) -> 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<T>,
) -> impl fmt::Display + 'a {
struct OptionDisplay<'a, T>(&'a str, &'a str, &'a Option<T>);
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<T: fmt::Display>(option: &Option<T>) -> impl fmt::Display + '_ {
display_option(" ", "", option)
}
/// `<constraint_characteristics> = [ 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.

View file

@ -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,
};

View file

@ -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<Option<IndexType>, 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<Ident> {
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<Option<IndexOption>, 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<Vec<IndexOption>, 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<SqlOption, ParserError> {
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<WindowSpec, ParserError> {
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,
};

View file

@ -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<Ident>,
index_name: Option<Ident>,
index_type: Option<IndexType>,
columns: Vec<Ident>,
index_options: Vec<IndexOption>,
characteristics: Option<ConstraintCharacteristics>,
unique_index_type_display: Option<KeyOrIndexDisplay>,
) -> 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))");