From 902c30790427446b283cd13173988af6d98fb452 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 | 11 ++++++++++- tests/sqlparser_mysql.rs | 7 +++++++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 42510e2f0..ef39fe8e9 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -223,4 +223,8 @@ impl Dialect for GenericDialect { fn supports_lambda_functions(&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 284fc4172..abe918eb0 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1068,6 +1068,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 81aa9d445..2d1c7899b 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -182,6 +182,11 @@ impl Dialect for MySqlDialect { fn supports_binary_kw_as_cast(&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 cfc173d76..864ed45f5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9246,7 +9246,16 @@ 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_keyword(Keyword::CHECK) + || self.peek_keyword(Keyword::PRIMARY) + || self.peek_keyword(Keyword::UNIQUE) + || self.peek_keyword(Keyword::FOREIGN)) + { + None + } else { + Some(self.parse_identifier()?) + } } else { None }; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e847d3edb..b195e2b09 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3438,6 +3438,13 @@ 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 sql = "CREATE TABLE t (x INT, CONSTRAINT CHECK (x > 1))"; + let normalized = "CREATE TABLE t (x INT, CHECK (x > 1))"; + mysql_and_generic().one_statement_parses_to(sql, normalized); +} + #[test] fn parse_create_table_with_fulltext_definition() { mysql_and_generic().verified_stmt("CREATE TABLE tb (id INT, FULLTEXT (id))");