From ef55f1f17bc54aff3a89ba15b588713a15089590 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Fri, 16 Jan 2026 12:28:31 -0800 Subject: [PATCH] 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 --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 17 +++++++++++++++++ src/dialect/mysql.rs | 5 +++++ src/parser/mod.rs | 15 ++++++++++++++- tests/sqlparser_mysql.rs | 21 +++++++++++++++++++++ 5 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 345d63fe4..6d25fa2b5 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -275,4 +275,8 @@ impl Dialect for GenericDialect { fn supports_comment_optimizer_hint(&self) -> bool { true } + + fn supports_constraint_keyword_without_name(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 477d60f83..74806ece7 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1156,6 +1156,23 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports the `CONSTRAINT` keyword without a name + /// in table constraint definitions. + /// + /// Example: + /// ```sql + /// CREATE TABLE t (a INT, CONSTRAINT CHECK (a > 0)) + /// ``` + /// + /// This is a MySQL extension; the SQL standard requires a name after `CONSTRAINT`. + /// When the name is omitted, the output normalizes to just the constraint type + /// without the `CONSTRAINT` keyword (e.g., `CHECK (a > 0)`). + /// + /// + fn supports_constraint_keyword_without_name(&self) -> bool { + false + } + /// Returns true if the specified keyword is reserved and cannot be /// used as an identifier without special handling like quoting. fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index ad3ba6f3a..290f706b7 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -186,6 +186,11 @@ impl Dialect for MySqlDialect { fn supports_comment_optimizer_hint(&self) -> bool { true } + + /// See: + fn supports_constraint_keyword_without_name(&self) -> bool { + true + } } /// `LOCK TABLES` diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5fa224f97..188b6b5e3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9243,7 +9243,20 @@ impl<'a> Parser<'a> { &mut self, ) -> Result, ParserError> { let name = if self.parse_keyword(Keyword::CONSTRAINT) { - Some(self.parse_identifier()?) + if self.dialect.supports_constraint_keyword_without_name() + && self + .peek_one_of_keywords(&[ + Keyword::CHECK, + Keyword::PRIMARY, + Keyword::UNIQUE, + Keyword::FOREIGN, + ]) + .is_some() + { + None + } else { + Some(self.parse_identifier()?) + } } else { None }; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 2c942798c..78dc59d7b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3462,6 +3462,27 @@ fn parse_create_table_unallow_constraint_then_index() { assert!(mysql_and_generic().parse_sql_statements(sql).is_ok()); } +#[test] +fn parse_create_table_constraint_check_without_name() { + let dialects = all_dialects_where(|d| d.supports_constraint_keyword_without_name()); + dialects.one_statement_parses_to( + "CREATE TABLE t (x INT, CONSTRAINT PRIMARY KEY (x))", + "CREATE TABLE t (x INT, PRIMARY KEY (x))", + ); + dialects.one_statement_parses_to( + "CREATE TABLE t (x INT, CONSTRAINT UNIQUE (x))", + "CREATE TABLE t (x INT, UNIQUE (x))", + ); + dialects.one_statement_parses_to( + "CREATE TABLE t (x INT, CONSTRAINT FOREIGN KEY (x) REFERENCES t2(id))", + "CREATE TABLE t (x INT, FOREIGN KEY (x) REFERENCES t2(id))", + ); + dialects.one_statement_parses_to( + "CREATE TABLE t (x INT, CONSTRAINT CHECK (x > 1))", + "CREATE TABLE t (x INT, CHECK (x > 1))", + ); +} + #[test] fn parse_create_table_with_fulltext_definition() { mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT (id))");