Repository: tobymao/sqlglot

Issue:
Regression in parsing clickhouse tuple types
On 23.17.0 this works:

```
In [1]: import sqlglot.expressions as sge, sqlglot as sg

In [2]: sg.__version__
Out[2]: '23.17.0'

In [3]: sg.parse_one('select cast(x as Tuple(String, Array(Nullable(Float64))))', read='clickhouse')
Out[3]:
Select(
  expressions=[
    Cast(
      this=Column(
        this=Identifier(this=x, quoted=False)),
      to=DataType(
        this=Type.STRUCT,
        expressions=[
          Identifier(this=String, quoted=False),
          Array(
            expressions=[
              Anonymous(
                this=Nullable,
                expressions=[
                  Column(
                    this=Identifier(this=Float64, quoted=False))])])],
        nested=True))])
```

but fails to parse in 24.0.1:

```
In [1]: import sqlglot.expressions as sge, sqlglot as sg

In [2]: sg.__version__
Out[2]: '24.0.1'

In [3]: sg.parse_one('select cast(x as Tuple(String, Array(Nullable(Float64))))', read='clickhouse')
...
ParseError: Expected TYPE after CAST. Line 1, Col: 22.
  select cast(x as Tuple(String, Array(Nullable(Float64))))
```


Discussion:
It seems like this used to work by accident. The `String` token was being parsed as an `Identifier`, so a recent [fix](https://github.com/tobymao/sqlglot/commit/3fe3c2c0a3e5f465a0c62261c5a0ba6faf8f0846) simply surfaced this issue.

I'm looking into this, thanks for the report as always!

Resolving patch:
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -4274,7 +4274,7 @@ def _parse_types(
 
         if self._match(TokenType.L_PAREN):
             if is_struct:
-                expressions = self._parse_csv(self._parse_struct_types)
+                expressions = self._parse_csv(lambda: self._parse_struct_types(type_required=True))
             elif nested:
                 expressions = self._parse_csv(
                     lambda: self._parse_types(
@@ -4392,15 +4392,16 @@ def _parse_struct_types(self, type_required: bool = False) -> t.Optional[exp.Exp
             or self._parse_id_var()
         )
         self._match(TokenType.COLON)
-        column_def = self._parse_column_def(this)
 
-        if type_required and (
-            (isinstance(this, exp.Column) and this.this is column_def) or this is column_def
+        if (
+            type_required
+            and not isinstance(this, exp.DataType)
+            and not self._match_set(self.TYPE_TOKENS, advance=False)
         ):
             self._retreat(index)
             return self._parse_types()
 
-        return column_def
+        return self._parse_column_def(this)
 
     def _parse_at_time_zone(self, this: t.Optional[exp.Expression]) -> t.Optional[exp.Expression]:
         if not self._match_text_seq("AT", "TIME", "ZONE"):


Test patch:
diff --git a/tests/dialects/test_clickhouse.py b/tests/dialects/test_clickhouse.py
--- a/tests/dialects/test_clickhouse.py
+++ b/tests/dialects/test_clickhouse.py
@@ -42,6 +42,7 @@ def test_clickhouse(self):
         self.assertEqual(expr.sql(dialect="clickhouse"), "COUNT(x)")
         self.assertIsNone(expr._meta)
 
+        self.validate_identity("SELECT CAST(x AS Tuple(String, Array(Nullable(Float64))))")
         self.validate_identity("countIf(x, y)")
         self.validate_identity("x = y")
         self.validate_identity("x <> y")