Repository: tobymao/sqlglot

Issue:
Issues parsing MySQL with UNIQUE constraints
After creating a table in mysql, and getting the create sql string with `SHOW CREATE TABLE`, sqlglot is unable to parse the returned string.

I expect that sqlglot to correctly parse valid MySQL strings.

1) Create a MySQL table:
```
CREATE TABLE foo (
  id CHAR(36) DEFAULT (UUID()) UNIQUE,
  created_at TIMESTAMP,
  PRIMARY KEY (id)
)
```

2) Get create table sql:
```
SHOW CREATE TABLE foo

> CREATE TABLE `foo` (
  `id` char(36) NOT NULL DEFAULT (uuid()),
  `created_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

```
3) Parse string:
```
sql = """CREATE TABLE `foo` (
  `id` char(36) NOT NULL DEFAULT (uuid()),
  `created_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci"""

sqlglot.parse_one(sql, "mysql")

>>> ParseError: Expecting ). Line 6, Col: 12.
  T NULL DEFAULT (uuid()),
  `created_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
```

If you remove the `UNIQUE KEY` line the string will parse.

```
sql = """CREATE TABLE `foo` (
  `id` char(36) NOT NULL DEFAULT (uuid()),
  `created_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci"""

sqlglot.parse_one(sql, "mysql")

>>> (CREATE this: 
  (SCHEMA this: 
    (TABLE this: 
      (IDENTIFIER this: foo, quoted: True)), expressions: 
    (COLUMNDEF this: 
      (IDENTIFIER this: id, quoted: True), kind: 
      (DATATYPE this: Type.CHAR, expressions: 
        (DATATYPESIZE this: 
          (LITERAL this: 36, is_string: False)), nested: False, prefix: False), constraints: 
      (COLUMNCONSTRAINT kind: 
        (NOTNULLCOLUMNCONSTRAINT )), 
      (COLUMNCONSTRAINT kind: 
        (DEFAULTCOLUMNCONSTRAINT this: 
          (PAREN this: 
            (ANONYMOUS this: uuid))))), 
    (COLUMNDEF this: 
      (IDENTIFIER this: created_at, quoted: True), kind: 
      (DATATYPE this: Type.TIMESTAMP), constraints: 
      (COLUMNCONSTRAINT kind: 
        (NOTNULLCOLUMNCONSTRAINT allow_null: True)), 
      (COLUMNCONSTRAINT kind: 
        (DEFAULTCOLUMNCONSTRAINT this: 
          (NULL )))), 
    (PRIMARYKEY expressions: 
      (IDENTIFIER this: id, quoted: True))), kind: TABLE, properties: 
  (PROPERTIES expressions: 
    (ENGINEPROPERTY this: 
      (IDENTIFIER this: InnoDB, quoted: False)), 
    (CHARACTERSETPROPERTY this: 
      (VAR this: utf8mb4), default: True), 
    (COLLATEPROPERTY this: 
      (IDENTIFIER this: utf8mb4_0900_ai_ci, quoted: False))))
```
How can I parse sql string with a `UNIQUE` key? 


Thank you in advance for any help.


Discussion:

Resolving patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -1285,7 +1285,7 @@ class TitleColumnConstraint(ColumnConstraintKind):
 
 
 class UniqueColumnConstraint(ColumnConstraintKind):
-    arg_types: t.Dict[str, t.Any] = {}
+    arg_types = {"this": False}
 
 
 class UppercaseColumnConstraint(ColumnConstraintKind):
@@ -1450,10 +1450,6 @@ class PrimaryKey(Expression):
     arg_types = {"expressions": True, "options": False}
 
 
-class Unique(Expression):
-    arg_types = {"expressions": True}
-
-
 # https://www.postgresql.org/docs/9.1/sql-selectinto.html
 # https://docs.aws.amazon.com/redshift/latest/dg/r_SELECT_INTO.html#r_SELECT_INTO-examples
 class Into(Expression):
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -629,8 +629,10 @@ def primarykeycolumnconstraint_sql(self, expression: exp.PrimaryKeyColumnConstra
             return f"PRIMARY KEY{' DESC' if desc else ' ASC'}"
         return f"PRIMARY KEY"
 
-    def uniquecolumnconstraint_sql(self, _) -> str:
-        return "UNIQUE"
+    def uniquecolumnconstraint_sql(self, expression: exp.UniqueColumnConstraint) -> str:
+        this = self.sql(expression, "this")
+        this = f" {this}" if this else ""
+        return f"UNIQUE{this}"
 
     def create_sql(self, expression: exp.Create) -> str:
         kind = self.sql(expression, "kind").upper()
@@ -1771,10 +1773,6 @@ def primarykey_sql(self, expression: exp.ForeignKey) -> str:
         options = f" {options}" if options else ""
         return f"PRIMARY KEY ({expressions}){options}"
 
-    def unique_sql(self, expression: exp.Unique) -> str:
-        columns = self.expressions(expression, key="expressions")
-        return f"UNIQUE ({columns})"
-
     def if_sql(self, expression: exp.If) -> str:
         return self.case_sql(
             exp.Case(ifs=[expression.copy()], default=expression.args.get("false"))
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -3371,9 +3371,10 @@ def _parse_unnamed_constraint(
         return self.CONSTRAINT_PARSERS[constraint](self)
 
     def _parse_unique(self) -> exp.Expression:
-        if not self._match(TokenType.L_PAREN, advance=False):
-            return self.expression(exp.UniqueColumnConstraint)
-        return self.expression(exp.Unique, expressions=self._parse_wrapped_id_vars())
+        self._match_text_seq("KEY")
+        return self.expression(
+            exp.UniqueColumnConstraint, this=self._parse_schema(self._parse_id_var(any_token=False))
+        )
 
     def _parse_key_constraint_options(self) -> t.List[str]:
         options = []


Test patch:
diff --git a/tests/dialects/test_mysql.py b/tests/dialects/test_mysql.py
--- a/tests/dialects/test_mysql.py
+++ b/tests/dialects/test_mysql.py
@@ -6,6 +6,10 @@ class TestMySQL(Validator):
     dialect = "mysql"
 
     def test_ddl(self):
+        self.validate_identity(
+            "INSERT INTO x VALUES (1, 'a', 2.0) ON DUPLICATE KEY UPDATE SET x.id = 1"
+        )
+
         self.validate_all(
             "CREATE TABLE z (a INT) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARACTER SET=utf8 COLLATE=utf8_bin COMMENT='x'",
             write={
@@ -21,10 +25,6 @@ def test_ddl(self):
                 "mysql": "CREATE TABLE t (c DATETIME DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()) DEFAULT CHARACTER SET=utf8 ROW_FORMAT=DYNAMIC",
             },
         )
-        self.validate_identity(
-            "INSERT INTO x VALUES (1, 'a', 2.0) ON DUPLICATE KEY UPDATE SET x.id = 1"
-        )
-
         self.validate_all(
             "CREATE TABLE x (id int not null auto_increment, primary key (id))",
             write={
@@ -37,6 +37,12 @@ def test_ddl(self):
                 "sqlite": "CREATE TABLE x (id INTEGER NOT NULL)",
             },
         )
+        self.validate_all(
+            "CREATE TABLE `foo` (`id` char(36) NOT NULL DEFAULT (uuid()), PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`))",
+            write={
+                "mysql": "CREATE TABLE `foo` (`id` CHAR(36) NOT NULL DEFAULT (UUID()), PRIMARY KEY (`id`), UNIQUE `id` (`id`))",
+            },
+        )
 
     def test_identity(self):
         self.validate_identity("SELECT CURRENT_TIMESTAMP(6)")