Repository: tobymao/sqlglot

Issue:
parsing `struct(number bigint)` as a DataType treats number as a data type
**Fully reproducible code snippet**

```
In [11]: import sqlglot as sg

In [12]: sg.__version__
Out[12]: '17.12.0'

In [13]: sg.parse_one("struct(number bigint)", read="duckdb", into=sg.exp.DataType)
Out[13]:
(DATATYPE this: Type.STRUCT, expressions:
  (DATATYPE this: Type.DECIMAL, expressions:
    (DATATYPESIZE this:
      (LITERAL this: 18, is_string: False)),
    (DATATYPESIZE this:
      (LITERAL this: 3, is_string: False)), nested: False, prefix: False), nested: True, prefix: False)
```

This is part of the schema inferred from running DuckDB's `read_json_auto` on a response from the [GitHub issues REST API](https://docs.github.com/en/free-pro-team@latest/rest/issues/issues?apiVersion=2022-11-28) 

**Official Documentation**

N/A



Discussion:
It seems like at least for duckdb, struct field name parsing should always be attempted before attempting to parse a type. 
It seems like this indeed works in DuckDB, but I can't find documentation about its syntax:

```python
>>> duckdb.connect().execute("select cast(row(1) as struct(number bigint))").fetchdf()
  CAST(main.row(1) AS STRUCT(number BIGINT))
0                              {'number': 1}
```

Is it supposed to be like `STRUCT(id type [, ...])`?
I don't think there's any documentation for this syntax, but it's what comes back from a `DESCRIBE` query containing any struct columns.
Actually there's a small example here: https://duckdb.org/docs/sql/data_types/overview#nested--composite-types
Gotcha, let me see..

Resolving patch:
diff --git a/sqlglot/dialects/duckdb.py b/sqlglot/dialects/duckdb.py
--- a/sqlglot/dialects/duckdb.py
+++ b/sqlglot/dialects/duckdb.py
@@ -207,6 +207,9 @@ def _parse_types(
 
             return this
 
+        def _parse_struct_types(self) -> t.Optional[exp.Expression]:
+            return self._parse_field_def()
+
         def _pivot_column_names(self, aggregations: t.List[exp.Expression]) -> t.List[str]:
             if len(aggregations) == 1:
                 return super()._pivot_column_names(aggregations)
diff --git a/sqlglot/dialects/oracle.py b/sqlglot/dialects/oracle.py
--- a/sqlglot/dialects/oracle.py
+++ b/sqlglot/dialects/oracle.py
@@ -22,7 +22,7 @@ def _parse_xml_table(self: parser.Parser) -> exp.XMLTable:
     by_ref = self._match_text_seq("RETURNING", "SEQUENCE", "BY", "REF")
 
     if self._match_text_seq("COLUMNS"):
-        columns = self._parse_csv(lambda: self._parse_column_def(self._parse_field(any_token=True)))
+        columns = self._parse_csv(self._parse_field_def)
 
     return self.expression(exp.XMLTable, this=this, passing=passing, columns=columns, by_ref=by_ref)
 
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -3489,14 +3489,14 @@ def _parse_schema(self, this: t.Optional[exp.Expression] = None) -> t.Optional[e
         if not self._match(TokenType.L_PAREN):
             return this
 
-        args = self._parse_csv(
-            lambda: self._parse_constraint()
-            or self._parse_column_def(self._parse_field(any_token=True))
-        )
+        args = self._parse_csv(lambda: self._parse_constraint() or self._parse_field_def())
 
         self._match_r_paren()
         return self.expression(exp.Schema, this=this, expressions=args)
 
+    def _parse_field_def(self) -> t.Optional[exp.Expression]:
+        return self._parse_column_def(self._parse_field(any_token=True))
+
     def _parse_column_def(self, this: t.Optional[exp.Expression]) -> t.Optional[exp.Expression]:
         # column defs are not really columns, they're identifiers
         if isinstance(this, exp.Column):
@@ -4506,7 +4506,7 @@ def _parse_add_column(self) -> t.Optional[exp.Expression]:
 
         self._match(TokenType.COLUMN)
         exists_column = self._parse_exists(not_=True)
-        expression = self._parse_column_def(self._parse_field(any_token=True))
+        expression = self._parse_field_def()
 
         if expression:
             expression.set("exists", exists_column)


Test patch:
diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py
--- a/tests/dialects/test_duckdb.py
+++ b/tests/dialects/test_duckdb.py
@@ -508,6 +508,10 @@ def test_cast(self):
         self.validate_identity("CAST(x AS INT128)")
         self.validate_identity("CAST(x AS DOUBLE)")
         self.validate_identity("CAST(x AS DECIMAL(15, 4))")
+        self.validate_identity("CAST(x AS STRUCT(number BIGINT))")
+        self.validate_identity(
+            "CAST(ROW(1, ROW(1)) AS STRUCT(number BIGINT, row STRUCT(number BIGINT)))"
+        )
 
         self.validate_all("CAST(x AS NUMERIC(1, 2))", write={"duckdb": "CAST(x AS DECIMAL(1, 2))"})
         self.validate_all("CAST(x AS HUGEINT)", write={"duckdb": "CAST(x AS INT128)"})