Skip to content

Commit 902c307

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 6550ec8 commit 902c307

5 files changed

Lines changed: 43 additions & 1 deletion

File tree

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,8 @@ impl Dialect for GenericDialect {
223223
fn supports_lambda_functions(&self) -> bool {
224224
true
225225
}
226+
227+
fn supports_constraint_keyword_without_name(&self) -> bool {
228+
true
229+
}
226230
}

src/dialect/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,23 @@ pub trait Dialect: Debug + Any {
10681068
false
10691069
}
10701070

1071+
/// Returns true if the dialect supports the `CONSTRAINT` keyword without a name
1072+
/// in table constraint definitions.
1073+
///
1074+
/// Example:
1075+
/// ```sql
1076+
/// CREATE TABLE t (a INT, CONSTRAINT CHECK (a > 0))
1077+
/// ```
1078+
///
1079+
/// This is a MySQL extension; the SQL standard requires a name after `CONSTRAINT`.
1080+
/// When the name is omitted, the output normalizes to just the constraint type
1081+
/// without the `CONSTRAINT` keyword (e.g., `CHECK (a > 0)`).
1082+
///
1083+
/// <https://dev.mysql.com/doc/refman/8.4/en/create-table.html>
1084+
fn supports_constraint_keyword_without_name(&self) -> bool {
1085+
false
1086+
}
1087+
10711088
/// Returns true if the specified keyword is reserved and cannot be
10721089
/// used as an identifier without special handling like quoting.
10731090
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
@@ -182,6 +182,11 @@ impl Dialect for MySqlDialect {
182182
fn supports_binary_kw_as_cast(&self) -> bool {
183183
true
184184
}
185+
186+
/// See: <https://dev.mysql.com/doc/refman/8.4/en/create-table.html>
187+
fn supports_constraint_keyword_without_name(&self) -> bool {
188+
true
189+
}
185190
}
186191

187192
/// `LOCK TABLES`

src/parser/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9246,7 +9246,16 @@ impl<'a> Parser<'a> {
92469246
&mut self,
92479247
) -> Result<Option<TableConstraint>, ParserError> {
92489248
let name = if self.parse_keyword(Keyword::CONSTRAINT) {
9249-
Some(self.parse_identifier()?)
9249+
if self.dialect.supports_constraint_keyword_without_name()
9250+
&& (self.peek_keyword(Keyword::CHECK)
9251+
|| self.peek_keyword(Keyword::PRIMARY)
9252+
|| self.peek_keyword(Keyword::UNIQUE)
9253+
|| self.peek_keyword(Keyword::FOREIGN))
9254+
{
9255+
None
9256+
} else {
9257+
Some(self.parse_identifier()?)
9258+
}
92509259
} else {
92519260
None
92529261
};

tests/sqlparser_mysql.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3438,6 +3438,13 @@ fn parse_create_table_unallow_constraint_then_index() {
34383438
assert!(mysql_and_generic().parse_sql_statements(sql).is_ok());
34393439
}
34403440

3441+
#[test]
3442+
fn parse_create_table_constraint_check_without_name() {
3443+
let sql = "CREATE TABLE t (x INT, CONSTRAINT CHECK (x > 1))";
3444+
let normalized = "CREATE TABLE t (x INT, CHECK (x > 1))";
3445+
mysql_and_generic().one_statement_parses_to(sql, normalized);
3446+
}
3447+
34413448
#[test]
34423449
fn parse_create_table_with_fulltext_definition() {
34433450
mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT (id))");

0 commit comments

Comments
 (0)