Skip to content

Commit ef55f1f

Browse files
committed
MySQL: Allow optional constraint name after CONSTRAINT keyword
MySQL allows `CONSTRAINT CHECK (expr)` without a name - the database will auto-generate a constraint name. This is a MySQL extension; the SQL standard requires a name after the CONSTRAINT keyword. See [docs]. Previously, we required an identifier after CONSTRAINT, causing us to interpret CONSTRAINT as a column name in cases like: ```sql CREATE TABLE t (x INT, CONSTRAINT CHECK (x > 1)) ``` Now we check if the token after CONSTRAINT is a constraint type keyword (CHECK, PRIMARY, UNIQUE, FOREIGN) and treat the name as optional for dialects that support this. This is just MySQL as far as I know (and Generic), but we introduce a new `Dialect` flag even though it's a minor feature. We could remove the flag and allow the more permissive optional name syntax across dialects if desired. [docs]: https://dev.mysql.com/doc/refman/8.4/en/create-table.html
1 parent 3ac5670 commit ef55f1f

5 files changed

Lines changed: 61 additions & 1 deletion

File tree

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,8 @@ impl Dialect for GenericDialect {
275275
fn supports_comment_optimizer_hint(&self) -> bool {
276276
true
277277
}
278+
279+
fn supports_constraint_keyword_without_name(&self) -> bool {
280+
true
281+
}
278282
}

src/dialect/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,23 @@ pub trait Dialect: Debug + Any {
11561156
false
11571157
}
11581158

1159+
/// Returns true if the dialect supports the `CONSTRAINT` keyword without a name
1160+
/// in table constraint definitions.
1161+
///
1162+
/// Example:
1163+
/// ```sql
1164+
/// CREATE TABLE t (a INT, CONSTRAINT CHECK (a > 0))
1165+
/// ```
1166+
///
1167+
/// This is a MySQL extension; the SQL standard requires a name after `CONSTRAINT`.
1168+
/// When the name is omitted, the output normalizes to just the constraint type
1169+
/// without the `CONSTRAINT` keyword (e.g., `CHECK (a > 0)`).
1170+
///
1171+
/// <https://dev.mysql.com/doc/refman/8.4/en/create-table.html>
1172+
fn supports_constraint_keyword_without_name(&self) -> bool {
1173+
false
1174+
}
1175+
11591176
/// Returns true if the specified keyword is reserved and cannot be
11601177
/// used as an identifier without special handling like quoting.
11611178
fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {

src/dialect/mysql.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ impl Dialect for MySqlDialect {
186186
fn supports_comment_optimizer_hint(&self) -> bool {
187187
true
188188
}
189+
190+
/// See: <https://dev.mysql.com/doc/refman/8.4/en/create-table.html>
191+
fn supports_constraint_keyword_without_name(&self) -> bool {
192+
true
193+
}
189194
}
190195

191196
/// `LOCK TABLES`

src/parser/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9243,7 +9243,20 @@ impl<'a> Parser<'a> {
92439243
&mut self,
92449244
) -> Result<Option<TableConstraint>, ParserError> {
92459245
let name = if self.parse_keyword(Keyword::CONSTRAINT) {
9246-
Some(self.parse_identifier()?)
9246+
if self.dialect.supports_constraint_keyword_without_name()
9247+
&& self
9248+
.peek_one_of_keywords(&[
9249+
Keyword::CHECK,
9250+
Keyword::PRIMARY,
9251+
Keyword::UNIQUE,
9252+
Keyword::FOREIGN,
9253+
])
9254+
.is_some()
9255+
{
9256+
None
9257+
} else {
9258+
Some(self.parse_identifier()?)
9259+
}
92479260
} else {
92489261
None
92499262
};

tests/sqlparser_mysql.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3462,6 +3462,27 @@ fn parse_create_table_unallow_constraint_then_index() {
34623462
assert!(mysql_and_generic().parse_sql_statements(sql).is_ok());
34633463
}
34643464

3465+
#[test]
3466+
fn parse_create_table_constraint_check_without_name() {
3467+
let dialects = all_dialects_where(|d| d.supports_constraint_keyword_without_name());
3468+
dialects.one_statement_parses_to(
3469+
"CREATE TABLE t (x INT, CONSTRAINT PRIMARY KEY (x))",
3470+
"CREATE TABLE t (x INT, PRIMARY KEY (x))",
3471+
);
3472+
dialects.one_statement_parses_to(
3473+
"CREATE TABLE t (x INT, CONSTRAINT UNIQUE (x))",
3474+
"CREATE TABLE t (x INT, UNIQUE (x))",
3475+
);
3476+
dialects.one_statement_parses_to(
3477+
"CREATE TABLE t (x INT, CONSTRAINT FOREIGN KEY (x) REFERENCES t2(id))",
3478+
"CREATE TABLE t (x INT, FOREIGN KEY (x) REFERENCES t2(id))",
3479+
);
3480+
dialects.one_statement_parses_to(
3481+
"CREATE TABLE t (x INT, CONSTRAINT CHECK (x > 1))",
3482+
"CREATE TABLE t (x INT, CHECK (x > 1))",
3483+
);
3484+
}
3485+
34653486
#[test]
34663487
fn parse_create_table_with_fulltext_definition() {
34673488
mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT (id))");

0 commit comments

Comments
 (0)