We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
feat(sqlalchemy): allow non-primitive arrays
SQLAlchemy type inference fails on arrays where the value type is not a primitive or string prevent use cases like `array<array<int>>` from working.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ibis/backends/base/sql/alchemy/datatypes.py`
Content:
```
1 from __future__ import annotations
2 
3 import functools
4 from typing import Iterable
5 
6 import sqlalchemy as sa
7 from sqlalchemy.dialects import mysql, postgresql, sqlite
8 from sqlalchemy.dialects.mysql.base import MySQLDialect
9 from sqlalchemy.dialects.postgresql.base import PGDialect
10 from sqlalchemy.dialects.sqlite.base import SQLiteDialect
11 from sqlalchemy.engine.interfaces import Dialect
12 from sqlalchemy.types import UserDefinedType
13 
14 import ibis.expr.datatypes as dt
15 import ibis.expr.schema as sch
16 from ibis.backends.base.sql.alchemy.geospatial import geospatial_supported
17 
18 if geospatial_supported:
19     import geoalchemy2 as ga
20 
21 
22 class StructType(UserDefinedType):
23     def __init__(
24         self,
25         pairs: Iterable[tuple[str, sa.types.TypeEngine]],
26     ):
27         self.pairs = [
28             (name, sa.types.to_instance(type)) for name, type in pairs
29         ]
30 
31     def get_col_spec(self, **_):
32         pairs = ", ".join(f"{k} {v}" for k, v in self.pairs)
33         return f"STRUCT({pairs})"
34 
35 
36 def table_from_schema(name, meta, schema, database: str | None = None):
37     # Convert Ibis schema to SQLA table
38     columns = []
39 
40     for colname, dtype in zip(schema.names, schema.types):
41         satype = to_sqla_type(dtype)
42         column = sa.Column(colname, satype, nullable=dtype.nullable)
43         columns.append(column)
44 
45     return sa.Table(name, meta, schema=database, *columns)
46 
47 
48 # TODO(cleanup)
49 ibis_type_to_sqla = {
50     dt.Null: sa.types.NullType,
51     dt.Date: sa.Date,
52     dt.Time: sa.Time,
53     dt.Boolean: sa.Boolean,
54     dt.Binary: sa.LargeBinary,
55     dt.String: sa.Text,
56     dt.Decimal: sa.NUMERIC,
57     # Mantissa-based
58     dt.Float16: sa.REAL,
59     dt.Float32: sa.REAL,
60     dt.Float64: sa.FLOAT,
61     dt.Int8: sa.SmallInteger,
62     dt.Int16: sa.SmallInteger,
63     dt.Int32: sa.Integer,
64     dt.Int64: sa.BigInteger,
65     dt.JSON: sa.JSON,
66 }
67 
68 
69 @functools.singledispatch
70 def to_sqla_type(itype, type_map=None):
71     if type_map is None:
72         type_map = ibis_type_to_sqla
73     return type_map[type(itype)]
74 
75 
76 @to_sqla_type.register(dt.Decimal)
77 def _(itype, **kwargs):
78     return sa.types.NUMERIC(itype.precision, itype.scale)
79 
80 
81 @to_sqla_type.register(dt.Interval)
82 def _(itype, **kwargs):
83     return sa.types.Interval()
84 
85 
86 @to_sqla_type.register(dt.Date)
87 def _(itype, **kwargs):
88     return sa.Date()
89 
90 
91 @to_sqla_type.register(dt.Timestamp)
92 def _(itype, **kwargs):
93     return sa.TIMESTAMP(bool(itype.timezone))
94 
95 
96 @to_sqla_type.register(dt.Array)
97 def _(itype, **kwargs):
98     ibis_type = itype.value_type
99     if not isinstance(ibis_type, (dt.Primitive, dt.String)):
100         raise TypeError(f'Type {ibis_type} is not a primitive or string type')
101     return sa.ARRAY(to_sqla_type(ibis_type, **kwargs))
102 
103 
104 @to_sqla_type.register(dt.Struct)
105 def _(itype, **_):
106     return StructType(
107         [(name, to_sqla_type(type)) for name, type in itype.pairs.items()]
108     )
109 
110 
111 @to_sqla_type.register(dt.GeoSpatial)
112 def _(itype, **kwargs):
113     if itype.geotype == 'geometry':
114         return ga.Geometry
115     elif itype.geotype == 'geography':
116         return ga.Geography
117     else:
118         return ga.types._GISType
119 
120 
121 @dt.dtype.register(Dialect, sa.types.NullType)
122 def sa_null(_, satype, nullable=True):
123     return dt.null
124 
125 
126 @dt.dtype.register(Dialect, sa.types.Boolean)
127 def sa_boolean(_, satype, nullable=True):
128     return dt.Boolean(nullable=nullable)
129 
130 
131 @dt.dtype.register(MySQLDialect, mysql.NUMERIC)
132 def sa_mysql_numeric(_, satype, nullable=True):
133     # https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html
134     return dt.Decimal(
135         satype.precision or 10, satype.scale or 0, nullable=nullable
136     )
137 
138 
139 @dt.dtype.register(Dialect, sa.types.Numeric)
140 @dt.dtype.register(SQLiteDialect, sqlite.NUMERIC)
141 def sa_numeric(_, satype, nullable=True):
142     return dt.Decimal(satype.precision, satype.scale, nullable=nullable)
143 
144 
145 @dt.dtype.register(Dialect, sa.types.SmallInteger)
146 def sa_smallint(_, satype, nullable=True):
147     return dt.Int16(nullable=nullable)
148 
149 
150 @dt.dtype.register(Dialect, sa.types.Integer)
151 def sa_integer(_, satype, nullable=True):
152     return dt.Int32(nullable=nullable)
153 
154 
155 @dt.dtype.register(Dialect, mysql.TINYINT)
156 def sa_mysql_tinyint(_, satype, nullable=True):
157     return dt.Int8(nullable=nullable)
158 
159 
160 @dt.dtype.register(Dialect, sa.types.BigInteger)
161 def sa_bigint(_, satype, nullable=True):
162     return dt.Int64(nullable=nullable)
163 
164 
165 @dt.dtype.register(Dialect, sa.REAL)
166 def sa_float(_, satype, nullable=True):
167     return dt.Float32(nullable=nullable)
168 
169 
170 @dt.dtype.register(Dialect, sa.FLOAT)
171 @dt.dtype.register(SQLiteDialect, sa.REAL)
172 @dt.dtype.register(PGDialect, postgresql.DOUBLE_PRECISION)
173 def sa_double(_, satype, nullable=True):
174     return dt.Float64(nullable=nullable)
175 
176 
177 @dt.dtype.register(PGDialect, postgresql.UUID)
178 def sa_uuid(_, satype, nullable=True):
179     return dt.UUID(nullable=nullable)
180 
181 
182 @dt.dtype.register(PGDialect, postgresql.MACADDR)
183 def sa_macaddr(_, satype, nullable=True):
184     return dt.MACADDR(nullable=nullable)
185 
186 
187 @dt.dtype.register(PGDialect, postgresql.INET)
188 def sa_inet(_, satype, nullable=True):
189     return dt.INET(nullable=nullable)
190 
191 
192 @dt.dtype.register(PGDialect, postgresql.JSON)
193 def sa_json(_, satype, nullable=True):
194     return dt.JSON(nullable=nullable)
195 
196 
197 @dt.dtype.register(PGDialect, postgresql.JSONB)
198 def sa_jsonb(_, satype, nullable=True):
199     return dt.JSONB(nullable=nullable)
200 
201 
202 if geospatial_supported:
203 
204     @dt.dtype.register(Dialect, (ga.Geometry, ga.types._GISType))
205     def ga_geometry(_, gatype, nullable=True):
206         t = gatype.geometry_type
207         if t == 'POINT':
208             return dt.Point(nullable=nullable)
209         if t == 'LINESTRING':
210             return dt.LineString(nullable=nullable)
211         if t == 'POLYGON':
212             return dt.Polygon(nullable=nullable)
213         if t == 'MULTILINESTRING':
214             return dt.MultiLineString(nullable=nullable)
215         if t == 'MULTIPOINT':
216             return dt.MultiPoint(nullable=nullable)
217         if t == 'MULTIPOLYGON':
218             return dt.MultiPolygon(nullable=nullable)
219         if t == 'GEOMETRY':
220             return dt.Geometry(nullable=nullable)
221         else:
222             raise ValueError(f"Unrecognized geometry type: {t}")
223 
224 
225 POSTGRES_FIELD_TO_IBIS_UNIT = {
226     "YEAR": "Y",
227     "MONTH": "M",
228     "DAY": "D",
229     "HOUR": "h",
230     "MINUTE": "m",
231     "SECOND": "s",
232     "YEAR TO MONTH": "M",
233     "DAY TO HOUR": "h",
234     "DAY TO MINUTE": "m",
235     "DAY TO SECOND": "s",
236     "HOUR TO MINUTE": "m",
237     "HOUR TO SECOND": "s",
238     "MINUTE TO SECOND": "s",
239 }
240 
241 
242 @dt.dtype.register(PGDialect, postgresql.INTERVAL)
243 def sa_postgres_interval(_, satype, nullable=True):
244     field = satype.fields.upper()
245     unit = POSTGRES_FIELD_TO_IBIS_UNIT.get(field, None)
246     if unit is None:
247         raise ValueError(f"Unknown PostgreSQL interval field {field!r}")
248     elif unit in {"Y", "M"}:
249         raise ValueError(
250             "Variable length timedeltas are not yet supported with PostgreSQL"
251         )
252     return dt.Interval(unit=unit, nullable=nullable)
253 
254 
255 @dt.dtype.register(MySQLDialect, mysql.DOUBLE)
256 def sa_mysql_double(_, satype, nullable=True):
257     # TODO: handle asdecimal=True
258     return dt.Float64(nullable=nullable)
259 
260 
261 @dt.dtype.register(Dialect, sa.types.String)
262 def sa_string(_, satype, nullable=True):
263     return dt.String(nullable=nullable)
264 
265 
266 @dt.dtype.register(Dialect, sa.LargeBinary)
267 def sa_binary(_, satype, nullable=True):
268     return dt.Binary(nullable=nullable)
269 
270 
271 @dt.dtype.register(Dialect, sa.Time)
272 def sa_time(_, satype, nullable=True):
273     return dt.Time(nullable=nullable)
274 
275 
276 @dt.dtype.register(Dialect, sa.Date)
277 def sa_date(_, satype, nullable=True):
278     return dt.Date(nullable=nullable)
279 
280 
281 @dt.dtype.register(Dialect, sa.DateTime)
282 def sa_datetime(_, satype, nullable=True, default_timezone='UTC'):
283     timezone = default_timezone if satype.timezone else None
284     return dt.Timestamp(timezone=timezone, nullable=nullable)
285 
286 
287 @dt.dtype.register(Dialect, sa.ARRAY)
288 def sa_array(dialect, satype, nullable=True):
289     dimensions = satype.dimensions
290     if dimensions is not None and dimensions != 1:
291         raise NotImplementedError('Nested array types not yet supported')
292 
293     value_dtype = dt.dtype(dialect, satype.item_type)
294     return dt.Array(value_dtype, nullable=nullable)
295 
296 
297 @dt.dtype.register(Dialect, StructType)
298 def sa_struct(dialect, satype, nullable=True):
299     pairs = [(name, dt.dtype(dialect, typ)) for name, typ in satype.pairs]
300     return dt.Struct.from_tuples(pairs, nullable=nullable)
301 
302 
303 @sch.infer.register((sa.Table, sa.sql.TableClause))
304 def schema_from_table(table, schema=None):
305     """Retrieve an ibis schema from a SQLAlchemy ``Table``.
306 
307     Parameters
308     ----------
309     table : sa.Table
310 
311     Returns
312     -------
313     schema : ibis.expr.datatypes.Schema
314         An ibis schema corresponding to the types of the columns in `table`.
315     """
316     schema = schema if schema is not None else {}
317     pairs = []
318     for name, column in zip(table.columns.keys(), table.columns):
319         if name in schema:
320             dtype = dt.dtype(schema[name])
321         else:
322             dtype = dt.dtype(
323                 getattr(table.bind, 'dialect', Dialect()),
324                 column.type,
325                 nullable=column.nullable,
326             )
327         pairs.append((name, dtype))
328     return sch.schema(pairs)
329
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ibis/backends/base/sql/alchemy/datatypes.py b/ibis/backends/base/sql/alchemy/datatypes.py
--- a/ibis/backends/base/sql/alchemy/datatypes.py
+++ b/ibis/backends/base/sql/alchemy/datatypes.py
@@ -96,8 +96,6 @@
 @to_sqla_type.register(dt.Array)
 def _(itype, **kwargs):
     ibis_type = itype.value_type
-    if not isinstance(ibis_type, (dt.Primitive, dt.String)):
-        raise TypeError(f'Type {ibis_type} is not a primitive or string type')
     return sa.ARRAY(to_sqla_type(ibis_type, **kwargs))

{"golden_diff": "diff --git a/ibis/backends/base/sql/alchemy/datatypes.py b/ibis/backends/base/sql/alchemy/datatypes.py\n--- a/ibis/backends/base/sql/alchemy/datatypes.py\n+++ b/ibis/backends/base/sql/alchemy/datatypes.py\n@@ -96,8 +96,6 @@\n @to_sqla_type.register(dt.Array)\n def _(itype, **kwargs):\n     ibis_type = itype.value_type\n-    if not isinstance(ibis_type, (dt.Primitive, dt.String)):\n-        raise TypeError(f'Type {ibis_type} is not a primitive or string type')\n     return sa.ARRAY(to_sqla_type(ibis_type, **kwargs))\n", "issue": "feat(sqlalchemy): allow non-primitive arrays\nSQLAlchemy type inference fails on arrays where the value type is not a primitive or string prevent use cases like `array<array<int>>` from working.\n", "before_files": [{"content": "from __future__ import annotations\n\nimport functools\nfrom typing import Iterable\n\nimport sqlalchemy as sa\nfrom sqlalchemy.dialects import mysql, postgresql, sqlite\nfrom sqlalchemy.dialects.mysql.base import MySQLDialect\nfrom sqlalchemy.dialects.postgresql.base import PGDialect\nfrom sqlalchemy.dialects.sqlite.base import SQLiteDialect\nfrom sqlalchemy.engine.interfaces import Dialect\nfrom sqlalchemy.types import UserDefinedType\n\nimport ibis.expr.datatypes as dt\nimport ibis.expr.schema as sch\nfrom ibis.backends.base.sql.alchemy.geospatial import geospatial_supported\n\nif geospatial_supported:\n    import geoalchemy2 as ga\n\n\nclass StructType(UserDefinedType):\n    def __init__(\n        self,\n        pairs: Iterable[tuple[str, sa.types.TypeEngine]],\n    ):\n        self.pairs = [\n            (name, sa.types.to_instance(type)) for name, type in pairs\n        ]\n\n    def get_col_spec(self, **_):\n        pairs = \", \".join(f\"{k} {v}\" for k, v in self.pairs)\n        return f\"STRUCT({pairs})\"\n\n\ndef table_from_schema(name, meta, schema, database: str | None = None):\n    # Convert Ibis schema to SQLA table\n    columns = []\n\n    for colname, dtype in zip(schema.names, schema.types):\n        satype = to_sqla_type(dtype)\n        column = sa.Column(colname, satype, nullable=dtype.nullable)\n        columns.append(column)\n\n    return sa.Table(name, meta, schema=database, *columns)\n\n\n# TODO(cleanup)\nibis_type_to_sqla = {\n    dt.Null: sa.types.NullType,\n    dt.Date: sa.Date,\n    dt.Time: sa.Time,\n    dt.Boolean: sa.Boolean,\n    dt.Binary: sa.LargeBinary,\n    dt.String: sa.Text,\n    dt.Decimal: sa.NUMERIC,\n    # Mantissa-based\n    dt.Float16: sa.REAL,\n    dt.Float32: sa.REAL,\n    dt.Float64: sa.FLOAT,\n    dt.Int8: sa.SmallInteger,\n    dt.Int16: sa.SmallInteger,\n    dt.Int32: sa.Integer,\n    dt.Int64: sa.BigInteger,\n    dt.JSON: sa.JSON,\n}\n\n\n@functools.singledispatch\ndef to_sqla_type(itype, type_map=None):\n    if type_map is None:\n        type_map = ibis_type_to_sqla\n    return type_map[type(itype)]\n\n\n@to_sqla_type.register(dt.Decimal)\ndef _(itype, **kwargs):\n    return sa.types.NUMERIC(itype.precision, itype.scale)\n\n\n@to_sqla_type.register(dt.Interval)\ndef _(itype, **kwargs):\n    return sa.types.Interval()\n\n\n@to_sqla_type.register(dt.Date)\ndef _(itype, **kwargs):\n    return sa.Date()\n\n\n@to_sqla_type.register(dt.Timestamp)\ndef _(itype, **kwargs):\n    return sa.TIMESTAMP(bool(itype.timezone))\n\n\n@to_sqla_type.register(dt.Array)\ndef _(itype, **kwargs):\n    ibis_type = itype.value_type\n    if not isinstance(ibis_type, (dt.Primitive, dt.String)):\n        raise TypeError(f'Type {ibis_type} is not a primitive or string type')\n    return sa.ARRAY(to_sqla_type(ibis_type, **kwargs))\n\n\n@to_sqla_type.register(dt.Struct)\ndef _(itype, **_):\n    return StructType(\n        [(name, to_sqla_type(type)) for name, type in itype.pairs.items()]\n    )\n\n\n@to_sqla_type.register(dt.GeoSpatial)\ndef _(itype, **kwargs):\n    if itype.geotype == 'geometry':\n        return ga.Geometry\n    elif itype.geotype == 'geography':\n        return ga.Geography\n    else:\n        return ga.types._GISType\n\n\n@dt.dtype.register(Dialect, sa.types.NullType)\ndef sa_null(_, satype, nullable=True):\n    return dt.null\n\n\n@dt.dtype.register(Dialect, sa.types.Boolean)\ndef sa_boolean(_, satype, nullable=True):\n    return dt.Boolean(nullable=nullable)\n\n\n@dt.dtype.register(MySQLDialect, mysql.NUMERIC)\ndef sa_mysql_numeric(_, satype, nullable=True):\n    # https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html\n    return dt.Decimal(\n        satype.precision or 10, satype.scale or 0, nullable=nullable\n    )\n\n\n@dt.dtype.register(Dialect, sa.types.Numeric)\n@dt.dtype.register(SQLiteDialect, sqlite.NUMERIC)\ndef sa_numeric(_, satype, nullable=True):\n    return dt.Decimal(satype.precision, satype.scale, nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.types.SmallInteger)\ndef sa_smallint(_, satype, nullable=True):\n    return dt.Int16(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.types.Integer)\ndef sa_integer(_, satype, nullable=True):\n    return dt.Int32(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, mysql.TINYINT)\ndef sa_mysql_tinyint(_, satype, nullable=True):\n    return dt.Int8(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.types.BigInteger)\ndef sa_bigint(_, satype, nullable=True):\n    return dt.Int64(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.REAL)\ndef sa_float(_, satype, nullable=True):\n    return dt.Float32(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.FLOAT)\n@dt.dtype.register(SQLiteDialect, sa.REAL)\n@dt.dtype.register(PGDialect, postgresql.DOUBLE_PRECISION)\ndef sa_double(_, satype, nullable=True):\n    return dt.Float64(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.UUID)\ndef sa_uuid(_, satype, nullable=True):\n    return dt.UUID(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.MACADDR)\ndef sa_macaddr(_, satype, nullable=True):\n    return dt.MACADDR(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.INET)\ndef sa_inet(_, satype, nullable=True):\n    return dt.INET(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.JSON)\ndef sa_json(_, satype, nullable=True):\n    return dt.JSON(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.JSONB)\ndef sa_jsonb(_, satype, nullable=True):\n    return dt.JSONB(nullable=nullable)\n\n\nif geospatial_supported:\n\n    @dt.dtype.register(Dialect, (ga.Geometry, ga.types._GISType))\n    def ga_geometry(_, gatype, nullable=True):\n        t = gatype.geometry_type\n        if t == 'POINT':\n            return dt.Point(nullable=nullable)\n        if t == 'LINESTRING':\n            return dt.LineString(nullable=nullable)\n        if t == 'POLYGON':\n            return dt.Polygon(nullable=nullable)\n        if t == 'MULTILINESTRING':\n            return dt.MultiLineString(nullable=nullable)\n        if t == 'MULTIPOINT':\n            return dt.MultiPoint(nullable=nullable)\n        if t == 'MULTIPOLYGON':\n            return dt.MultiPolygon(nullable=nullable)\n        if t == 'GEOMETRY':\n            return dt.Geometry(nullable=nullable)\n        else:\n            raise ValueError(f\"Unrecognized geometry type: {t}\")\n\n\nPOSTGRES_FIELD_TO_IBIS_UNIT = {\n    \"YEAR\": \"Y\",\n    \"MONTH\": \"M\",\n    \"DAY\": \"D\",\n    \"HOUR\": \"h\",\n    \"MINUTE\": \"m\",\n    \"SECOND\": \"s\",\n    \"YEAR TO MONTH\": \"M\",\n    \"DAY TO HOUR\": \"h\",\n    \"DAY TO MINUTE\": \"m\",\n    \"DAY TO SECOND\": \"s\",\n    \"HOUR TO MINUTE\": \"m\",\n    \"HOUR TO SECOND\": \"s\",\n    \"MINUTE TO SECOND\": \"s\",\n}\n\n\n@dt.dtype.register(PGDialect, postgresql.INTERVAL)\ndef sa_postgres_interval(_, satype, nullable=True):\n    field = satype.fields.upper()\n    unit = POSTGRES_FIELD_TO_IBIS_UNIT.get(field, None)\n    if unit is None:\n        raise ValueError(f\"Unknown PostgreSQL interval field {field!r}\")\n    elif unit in {\"Y\", \"M\"}:\n        raise ValueError(\n            \"Variable length timedeltas are not yet supported with PostgreSQL\"\n        )\n    return dt.Interval(unit=unit, nullable=nullable)\n\n\n@dt.dtype.register(MySQLDialect, mysql.DOUBLE)\ndef sa_mysql_double(_, satype, nullable=True):\n    # TODO: handle asdecimal=True\n    return dt.Float64(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.types.String)\ndef sa_string(_, satype, nullable=True):\n    return dt.String(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.LargeBinary)\ndef sa_binary(_, satype, nullable=True):\n    return dt.Binary(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.Time)\ndef sa_time(_, satype, nullable=True):\n    return dt.Time(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.Date)\ndef sa_date(_, satype, nullable=True):\n    return dt.Date(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.DateTime)\ndef sa_datetime(_, satype, nullable=True, default_timezone='UTC'):\n    timezone = default_timezone if satype.timezone else None\n    return dt.Timestamp(timezone=timezone, nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.ARRAY)\ndef sa_array(dialect, satype, nullable=True):\n    dimensions = satype.dimensions\n    if dimensions is not None and dimensions != 1:\n        raise NotImplementedError('Nested array types not yet supported')\n\n    value_dtype = dt.dtype(dialect, satype.item_type)\n    return dt.Array(value_dtype, nullable=nullable)\n\n\n@dt.dtype.register(Dialect, StructType)\ndef sa_struct(dialect, satype, nullable=True):\n    pairs = [(name, dt.dtype(dialect, typ)) for name, typ in satype.pairs]\n    return dt.Struct.from_tuples(pairs, nullable=nullable)\n\n\n@sch.infer.register((sa.Table, sa.sql.TableClause))\ndef schema_from_table(table, schema=None):\n    \"\"\"Retrieve an ibis schema from a SQLAlchemy ``Table``.\n\n    Parameters\n    ----------\n    table : sa.Table\n\n    Returns\n    -------\n    schema : ibis.expr.datatypes.Schema\n        An ibis schema corresponding to the types of the columns in `table`.\n    \"\"\"\n    schema = schema if schema is not None else {}\n    pairs = []\n    for name, column in zip(table.columns.keys(), table.columns):\n        if name in schema:\n            dtype = dt.dtype(schema[name])\n        else:\n            dtype = dt.dtype(\n                getattr(table.bind, 'dialect', Dialect()),\n                column.type,\n                nullable=column.nullable,\n            )\n        pairs.append((name, dtype))\n    return sch.schema(pairs)\n", "path": "ibis/backends/base/sql/alchemy/datatypes.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport functools\nfrom typing import Iterable\n\nimport sqlalchemy as sa\nfrom sqlalchemy.dialects import mysql, postgresql, sqlite\nfrom sqlalchemy.dialects.mysql.base import MySQLDialect\nfrom sqlalchemy.dialects.postgresql.base import PGDialect\nfrom sqlalchemy.dialects.sqlite.base import SQLiteDialect\nfrom sqlalchemy.engine.interfaces import Dialect\nfrom sqlalchemy.types import UserDefinedType\n\nimport ibis.expr.datatypes as dt\nimport ibis.expr.schema as sch\nfrom ibis.backends.base.sql.alchemy.geospatial import geospatial_supported\n\nif geospatial_supported:\n    import geoalchemy2 as ga\n\n\nclass StructType(UserDefinedType):\n    def __init__(\n        self,\n        pairs: Iterable[tuple[str, sa.types.TypeEngine]],\n    ):\n        self.pairs = [\n            (name, sa.types.to_instance(type)) for name, type in pairs\n        ]\n\n    def get_col_spec(self, **_):\n        pairs = \", \".join(f\"{k} {v}\" for k, v in self.pairs)\n        return f\"STRUCT({pairs})\"\n\n\ndef table_from_schema(name, meta, schema, database: str | None = None):\n    # Convert Ibis schema to SQLA table\n    columns = []\n\n    for colname, dtype in zip(schema.names, schema.types):\n        satype = to_sqla_type(dtype)\n        column = sa.Column(colname, satype, nullable=dtype.nullable)\n        columns.append(column)\n\n    return sa.Table(name, meta, schema=database, *columns)\n\n\n# TODO(cleanup)\nibis_type_to_sqla = {\n    dt.Null: sa.types.NullType,\n    dt.Date: sa.Date,\n    dt.Time: sa.Time,\n    dt.Boolean: sa.Boolean,\n    dt.Binary: sa.LargeBinary,\n    dt.String: sa.Text,\n    dt.Decimal: sa.NUMERIC,\n    # Mantissa-based\n    dt.Float16: sa.REAL,\n    dt.Float32: sa.REAL,\n    dt.Float64: sa.FLOAT,\n    dt.Int8: sa.SmallInteger,\n    dt.Int16: sa.SmallInteger,\n    dt.Int32: sa.Integer,\n    dt.Int64: sa.BigInteger,\n    dt.JSON: sa.JSON,\n}\n\n\n@functools.singledispatch\ndef to_sqla_type(itype, type_map=None):\n    if type_map is None:\n        type_map = ibis_type_to_sqla\n    return type_map[type(itype)]\n\n\n@to_sqla_type.register(dt.Decimal)\ndef _(itype, **kwargs):\n    return sa.types.NUMERIC(itype.precision, itype.scale)\n\n\n@to_sqla_type.register(dt.Interval)\ndef _(itype, **kwargs):\n    return sa.types.Interval()\n\n\n@to_sqla_type.register(dt.Date)\ndef _(itype, **kwargs):\n    return sa.Date()\n\n\n@to_sqla_type.register(dt.Timestamp)\ndef _(itype, **kwargs):\n    return sa.TIMESTAMP(bool(itype.timezone))\n\n\n@to_sqla_type.register(dt.Array)\ndef _(itype, **kwargs):\n    ibis_type = itype.value_type\n    return sa.ARRAY(to_sqla_type(ibis_type, **kwargs))\n\n\n@to_sqla_type.register(dt.Struct)\ndef _(itype, **_):\n    return StructType(\n        [(name, to_sqla_type(type)) for name, type in itype.pairs.items()]\n    )\n\n\n@to_sqla_type.register(dt.GeoSpatial)\ndef _(itype, **kwargs):\n    if itype.geotype == 'geometry':\n        return ga.Geometry\n    elif itype.geotype == 'geography':\n        return ga.Geography\n    else:\n        return ga.types._GISType\n\n\n@dt.dtype.register(Dialect, sa.types.NullType)\ndef sa_null(_, satype, nullable=True):\n    return dt.null\n\n\n@dt.dtype.register(Dialect, sa.types.Boolean)\ndef sa_boolean(_, satype, nullable=True):\n    return dt.Boolean(nullable=nullable)\n\n\n@dt.dtype.register(MySQLDialect, mysql.NUMERIC)\ndef sa_mysql_numeric(_, satype, nullable=True):\n    # https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html\n    return dt.Decimal(\n        satype.precision or 10, satype.scale or 0, nullable=nullable\n    )\n\n\n@dt.dtype.register(Dialect, sa.types.Numeric)\n@dt.dtype.register(SQLiteDialect, sqlite.NUMERIC)\ndef sa_numeric(_, satype, nullable=True):\n    return dt.Decimal(satype.precision, satype.scale, nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.types.SmallInteger)\ndef sa_smallint(_, satype, nullable=True):\n    return dt.Int16(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.types.Integer)\ndef sa_integer(_, satype, nullable=True):\n    return dt.Int32(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, mysql.TINYINT)\ndef sa_mysql_tinyint(_, satype, nullable=True):\n    return dt.Int8(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.types.BigInteger)\ndef sa_bigint(_, satype, nullable=True):\n    return dt.Int64(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.REAL)\ndef sa_float(_, satype, nullable=True):\n    return dt.Float32(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.FLOAT)\n@dt.dtype.register(SQLiteDialect, sa.REAL)\n@dt.dtype.register(PGDialect, postgresql.DOUBLE_PRECISION)\ndef sa_double(_, satype, nullable=True):\n    return dt.Float64(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.UUID)\ndef sa_uuid(_, satype, nullable=True):\n    return dt.UUID(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.MACADDR)\ndef sa_macaddr(_, satype, nullable=True):\n    return dt.MACADDR(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.INET)\ndef sa_inet(_, satype, nullable=True):\n    return dt.INET(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.JSON)\ndef sa_json(_, satype, nullable=True):\n    return dt.JSON(nullable=nullable)\n\n\n@dt.dtype.register(PGDialect, postgresql.JSONB)\ndef sa_jsonb(_, satype, nullable=True):\n    return dt.JSONB(nullable=nullable)\n\n\nif geospatial_supported:\n\n    @dt.dtype.register(Dialect, (ga.Geometry, ga.types._GISType))\n    def ga_geometry(_, gatype, nullable=True):\n        t = gatype.geometry_type\n        if t == 'POINT':\n            return dt.Point(nullable=nullable)\n        if t == 'LINESTRING':\n            return dt.LineString(nullable=nullable)\n        if t == 'POLYGON':\n            return dt.Polygon(nullable=nullable)\n        if t == 'MULTILINESTRING':\n            return dt.MultiLineString(nullable=nullable)\n        if t == 'MULTIPOINT':\n            return dt.MultiPoint(nullable=nullable)\n        if t == 'MULTIPOLYGON':\n            return dt.MultiPolygon(nullable=nullable)\n        if t == 'GEOMETRY':\n            return dt.Geometry(nullable=nullable)\n        else:\n            raise ValueError(f\"Unrecognized geometry type: {t}\")\n\n\nPOSTGRES_FIELD_TO_IBIS_UNIT = {\n    \"YEAR\": \"Y\",\n    \"MONTH\": \"M\",\n    \"DAY\": \"D\",\n    \"HOUR\": \"h\",\n    \"MINUTE\": \"m\",\n    \"SECOND\": \"s\",\n    \"YEAR TO MONTH\": \"M\",\n    \"DAY TO HOUR\": \"h\",\n    \"DAY TO MINUTE\": \"m\",\n    \"DAY TO SECOND\": \"s\",\n    \"HOUR TO MINUTE\": \"m\",\n    \"HOUR TO SECOND\": \"s\",\n    \"MINUTE TO SECOND\": \"s\",\n}\n\n\n@dt.dtype.register(PGDialect, postgresql.INTERVAL)\ndef sa_postgres_interval(_, satype, nullable=True):\n    field = satype.fields.upper()\n    unit = POSTGRES_FIELD_TO_IBIS_UNIT.get(field, None)\n    if unit is None:\n        raise ValueError(f\"Unknown PostgreSQL interval field {field!r}\")\n    elif unit in {\"Y\", \"M\"}:\n        raise ValueError(\n            \"Variable length timedeltas are not yet supported with PostgreSQL\"\n        )\n    return dt.Interval(unit=unit, nullable=nullable)\n\n\n@dt.dtype.register(MySQLDialect, mysql.DOUBLE)\ndef sa_mysql_double(_, satype, nullable=True):\n    # TODO: handle asdecimal=True\n    return dt.Float64(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.types.String)\ndef sa_string(_, satype, nullable=True):\n    return dt.String(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.LargeBinary)\ndef sa_binary(_, satype, nullable=True):\n    return dt.Binary(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.Time)\ndef sa_time(_, satype, nullable=True):\n    return dt.Time(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.Date)\ndef sa_date(_, satype, nullable=True):\n    return dt.Date(nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.DateTime)\ndef sa_datetime(_, satype, nullable=True, default_timezone='UTC'):\n    timezone = default_timezone if satype.timezone else None\n    return dt.Timestamp(timezone=timezone, nullable=nullable)\n\n\n@dt.dtype.register(Dialect, sa.ARRAY)\ndef sa_array(dialect, satype, nullable=True):\n    dimensions = satype.dimensions\n    if dimensions is not None and dimensions != 1:\n        raise NotImplementedError('Nested array types not yet supported')\n\n    value_dtype = dt.dtype(dialect, satype.item_type)\n    return dt.Array(value_dtype, nullable=nullable)\n\n\n@dt.dtype.register(Dialect, StructType)\ndef sa_struct(dialect, satype, nullable=True):\n    pairs = [(name, dt.dtype(dialect, typ)) for name, typ in satype.pairs]\n    return dt.Struct.from_tuples(pairs, nullable=nullable)\n\n\n@sch.infer.register((sa.Table, sa.sql.TableClause))\ndef schema_from_table(table, schema=None):\n    \"\"\"Retrieve an ibis schema from a SQLAlchemy ``Table``.\n\n    Parameters\n    ----------\n    table : sa.Table\n\n    Returns\n    -------\n    schema : ibis.expr.datatypes.Schema\n        An ibis schema corresponding to the types of the columns in `table`.\n    \"\"\"\n    schema = schema if schema is not None else {}\n    pairs = []\n    for name, column in zip(table.columns.keys(), table.columns):\n        if name in schema:\n            dtype = dt.dtype(schema[name])\n        else:\n            dtype = dt.dtype(\n                getattr(table.bind, 'dialect', Dialect()),\n                column.type,\n                nullable=column.nullable,\n            )\n        pairs.append((name, dtype))\n    return sch.schema(pairs)\n", "path": "ibis/backends/base/sql/alchemy/datatypes.py"}]}