We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
bug: `pivot_longer` doesn't work with field names containing spaces
### What happened?

when using pivot_longer, the names_to field and values_to arguments can't have a space in them

### What version of ibis are you using?

5.1.0

### What backend(s) are you using, if any?

DuckDB

### Relevant log output

```sh
>>> import ibis
>>> import ibis.selectors as s
>>> from ibis import _
>>> ibis.options.interactive = True
>>> relig_income = ibis.examples.relig_income_raw.fetch()
>>> relig_income.pivot_longer(~s.c("religion"), names_to="in come", values_to="my count")

This produces a ParserException: Parser Error: syntax error at or near "in"
LINE 2: ...e" := ?, "my count" := t1."$100-150k"), struct_pack("in come" := ?, "my count"...

This can be fixed by removing the space, but would be nice to at least fix the error message or documentation to say there should be no spaces in the names and values
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

--- 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 from typing import Callable, Mapping, Optional
4 
5 import sqlalchemy as sa
6 import sqlalchemy.types as sat
7 import toolz
8 from sqlalchemy.ext.compiler import compiles
9 
10 import ibis.expr.datatypes as dt
11 from ibis.backends.base.sql.alchemy.geospatial import geospatial_supported
12 from ibis.common.collections import FrozenDict
13 
14 if geospatial_supported:
15     import geoalchemy2 as ga
16 
17 
18 class ArrayType(sat.UserDefinedType):
19     def __init__(self, value_type: sat.TypeEngine):
20         self.value_type = sat.to_instance(value_type)
21 
22     def result_processor(self, dialect, coltype) -> None:
23         if not coltype.lower().startswith("array"):
24             return None
25 
26         inner_processor = (
27             self.value_type.result_processor(dialect, coltype[len("array(") : -1])
28             or toolz.identity
29         )
30 
31         return lambda v: v if v is None else list(map(inner_processor, v))
32 
33 
34 @compiles(ArrayType, "default")
35 def compiles_array(element, compiler, **kw):
36     return f"ARRAY({compiler.process(element.value_type, **kw)})"
37 
38 
39 class StructType(sat.UserDefinedType):
40     cache_ok = True
41 
42     def __init__(self, fields: Mapping[str, sat.TypeEngine]) -> None:
43         self.fields = FrozenDict(
44             {name: sat.to_instance(typ) for name, typ in fields.items()}
45         )
46 
47 
48 @compiles(StructType, "default")
49 def compiles_struct(element, compiler, **kw):
50     content = ", ".join(
51         f"{field} {compiler.process(typ, **kw)}"
52         for field, typ in element.fields.items()
53     )
54     return f"STRUCT({content})"
55 
56 
57 class MapType(sat.UserDefinedType):
58     def __init__(self, key_type: sat.TypeEngine, value_type: sat.TypeEngine):
59         self.key_type = sat.to_instance(key_type)
60         self.value_type = sat.to_instance(value_type)
61 
62 
63 @compiles(MapType, "default")
64 def compiles_map(element, compiler, **kw):
65     key_type = compiler.process(element.key_type, **kw)
66     value_type = compiler.process(element.value_type, **kw)
67     return f"MAP({key_type}, {value_type})"
68 
69 
70 class UInt64(sat.Integer):
71     pass
72 
73 
74 class UInt32(sat.Integer):
75     pass
76 
77 
78 class UInt16(sat.Integer):
79     pass
80 
81 
82 class UInt8(sat.Integer):
83     pass
84 
85 
86 @compiles(UInt64, "postgresql")
87 @compiles(UInt32, "postgresql")
88 @compiles(UInt16, "postgresql")
89 @compiles(UInt8, "postgresql")
90 @compiles(UInt64, "mssql")
91 @compiles(UInt32, "mssql")
92 @compiles(UInt16, "mssql")
93 @compiles(UInt8, "mssql")
94 @compiles(UInt64, "mysql")
95 @compiles(UInt32, "mysql")
96 @compiles(UInt16, "mysql")
97 @compiles(UInt8, "mysql")
98 @compiles(UInt64, "snowflake")
99 @compiles(UInt32, "snowflake")
100 @compiles(UInt16, "snowflake")
101 @compiles(UInt8, "snowflake")
102 @compiles(UInt64, "sqlite")
103 @compiles(UInt32, "sqlite")
104 @compiles(UInt16, "sqlite")
105 @compiles(UInt8, "sqlite")
106 @compiles(UInt64, "trino")
107 @compiles(UInt32, "trino")
108 @compiles(UInt16, "trino")
109 @compiles(UInt8, "trino")
110 def compile_uint(element, compiler, **kw):
111     dialect_name = compiler.dialect.name
112     raise TypeError(
113         f"unsigned integers are not supported in the {dialect_name} backend"
114     )
115 
116 
117 try:
118     UUID = sat.UUID
119 except AttributeError:
120 
121     class UUID(sat.String):
122         pass
123 
124 else:
125 
126     @compiles(UUID, "default")
127     def compiles_uuid(element, compiler, **kw):
128         return "UUID"
129 
130 
131 class Unknown(sa.Text):
132     pass
133 
134 
135 _from_sqlalchemy_types = {
136     sat.BOOLEAN: dt.Boolean,
137     sat.Boolean: dt.Boolean,
138     sat.BINARY: dt.Binary,
139     sat.LargeBinary: dt.Binary,
140     sat.DATE: dt.Date,
141     sat.Date: dt.Date,
142     sat.TEXT: dt.String,
143     sat.Text: dt.String,
144     sat.TIME: dt.Time,
145     sat.Time: dt.Time,
146     sat.VARCHAR: dt.String,
147     sat.CHAR: dt.String,
148     sat.String: dt.String,
149     sat.SMALLINT: dt.Int16,
150     sat.SmallInteger: dt.Int16,
151     sat.INTEGER: dt.Int32,
152     sat.Integer: dt.Int32,
153     sat.BIGINT: dt.Int64,
154     sat.BigInteger: dt.Int64,
155     sat.REAL: dt.Float32,
156     sat.FLOAT: dt.Float64,
157     UInt16: dt.UInt16,
158     UInt32: dt.UInt32,
159     UInt64: dt.UInt64,
160     UInt8: dt.UInt8,
161     Unknown: dt.Unknown,
162     sat.JSON: dt.JSON,
163     UUID: dt.UUID,
164 }
165 
166 _to_sqlalchemy_types = {
167     dt.Null: sat.NullType,
168     dt.Date: sat.Date,
169     dt.Time: sat.Time,
170     dt.Boolean: sat.Boolean,
171     dt.Binary: sat.LargeBinary,
172     dt.String: sat.Text,
173     dt.Decimal: sat.Numeric,
174     # Mantissa-based
175     dt.Float16: sat.REAL,
176     dt.Float32: sat.REAL,
177     # precision is the number of bits in the mantissa
178     # without specifying this, some backends interpret the type as FLOAT, which
179     # means float32 (and precision == 24)
180     dt.Float64: sat.FLOAT(precision=53),
181     dt.Int8: sat.SmallInteger,
182     dt.Int16: sat.SmallInteger,
183     dt.Int32: sat.Integer,
184     dt.Int64: sat.BigInteger,
185     dt.UInt8: UInt8,
186     dt.UInt16: UInt16,
187     dt.UInt32: UInt32,
188     dt.UInt64: UInt64,
189     dt.JSON: sat.JSON,
190     dt.Interval: sat.Interval,
191     dt.Unknown: Unknown,
192     dt.MACADDR: sat.Text,
193     dt.INET: sat.Text,
194     dt.UUID: UUID,
195 }
196 
197 
198 def dtype_to_sqlalchemy(
199     dtype: dt.DataType,
200     converter: Optional[Callable[[dt.DataType], sa.TypeEngine]] = None,
201 ):
202     """Convert an Ibis type to a SQLAlchemy type.
203 
204     Parameters
205     ----------
206     dtype
207         Ibis type to convert.
208     converter
209         Converter function to use for nested types. If not provided, this function
210         will be used recursively. Should only be used when defining new converter for
211         dialects.
212 
213     Returns
214     -------
215     sa.TypeEngine
216     """
217     convert = converter or dtype_to_sqlalchemy
218 
219     if dtype.is_decimal():
220         return sat.NUMERIC(dtype.precision, dtype.scale)
221     elif dtype.is_timestamp():
222         return sat.TIMESTAMP(timezone=bool(dtype.timezone))
223     elif dtype.is_array():
224         return ArrayType(convert(dtype.value_type))
225     elif dtype.is_struct():
226         fields = {k: convert(v) for k, v in dtype.fields.items()}
227         return StructType(fields)
228     elif dtype.is_map():
229         return MapType(convert(dtype.key_type), convert(dtype.value_type))
230     elif dtype.is_geospatial():
231         if geospatial_supported:
232             if dtype.geotype == 'geometry':
233                 return ga.Geometry
234             elif dtype.geotype == 'geography':
235                 return ga.Geography
236             else:
237                 return ga.types._GISType
238         else:
239             raise TypeError("geospatial types are not supported")
240     else:
241         return _to_sqlalchemy_types[type(dtype)]
242 
243 
244 _FLOAT_PREC_TO_TYPE = {
245     11: dt.Float16,
246     24: dt.Float32,
247     53: dt.Float64,
248 }
249 
250 _GEOSPATIAL_TYPES = {
251     "POINT": dt.Point,
252     "LINESTRING": dt.LineString,
253     "POLYGON": dt.Polygon,
254     "MULTILINESTRING": dt.MultiLineString,
255     "MULTIPOINT": dt.MultiPoint,
256     "MULTIPOLYGON": dt.MultiPolygon,
257     "GEOMETRY": dt.Geometry,
258     "GEOGRAPHY": dt.Geography,
259 }
260 
261 
262 def dtype_from_sqlalchemy(typ, nullable=True, converter=None):
263     """Convert a SQLAlchemy type to an Ibis type.
264 
265     Parameters
266     ----------
267     typ
268         SQLAlchemy type to convert.
269     nullable : bool, optional
270         Whether the returned type should be nullable.
271     converter
272         Converter function to use for nested types. If not provided, this function
273         will be used recursively. Should only be used when defining new converter for
274         dialects.
275 
276     Returns
277     -------
278     dt.DataType
279     """
280     convert = converter or dtype_from_sqlalchemy
281 
282     if dtype := _from_sqlalchemy_types.get(type(typ)):
283         return dtype(nullable=nullable)
284     elif isinstance(typ, sat.Float):
285         if (float_typ := _FLOAT_PREC_TO_TYPE.get(typ.precision)) is not None:
286             return float_typ(nullable=nullable)
287         return dt.Decimal(typ.precision, typ.scale, nullable=nullable)
288     elif isinstance(typ, sat.Numeric):
289         return dt.Decimal(typ.precision, typ.scale, nullable=nullable)
290     elif isinstance(typ, ArrayType):
291         return dt.Array(convert(typ.value_type), nullable=nullable)
292     elif isinstance(typ, sat.ARRAY):
293         ndim = typ.dimensions
294         if ndim is not None and ndim != 1:
295             raise NotImplementedError("Nested array types not yet supported")
296         return dt.Array(convert(typ.item_type), nullable=nullable)
297     elif isinstance(typ, StructType):
298         fields = {k: convert(v) for k, v in typ.fields.items()}
299         return dt.Struct(fields, nullable=nullable)
300     elif isinstance(typ, MapType):
301         return dt.Map(convert(typ.key_type), convert(typ.value_type), nullable=nullable)
302     elif isinstance(typ, sa.DateTime):
303         timezone = "UTC" if typ.timezone else None
304         return dt.Timestamp(timezone, nullable=nullable)
305     elif geospatial_supported and isinstance(typ, ga.types._GISType):
306         name = typ.geometry_type.upper()
307         try:
308             return _GEOSPATIAL_TYPES[name](geotype=typ.name, nullable=nullable)
309         except KeyError:
310             raise ValueError(f"Unrecognized geometry type: {name}")
311     else:
312         raise TypeError(f"Unable to convert type: {typ!r}")
313
```
--- 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
@@ -47,8 +47,9 @@
 
 @compiles(StructType, "default")
 def compiles_struct(element, compiler, **kw):
+    quote = compiler.dialect.identifier_preparer.quote
     content = ", ".join(
-        f"{field} {compiler.process(typ, **kw)}"
+        f"{quote(field)} {compiler.process(typ, **kw)}"
         for field, typ in element.fields.items()
     )
     return f"STRUCT({content})"

{"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@@ -47,8 +47,9 @@\n \n @compiles(StructType, \"default\")\n def compiles_struct(element, compiler, **kw):\n+    quote = compiler.dialect.identifier_preparer.quote\n     content = \", \".join(\n-        f\"{field} {compiler.process(typ, **kw)}\"\n+        f\"{quote(field)} {compiler.process(typ, **kw)}\"\n         for field, typ in element.fields.items()\n     )\n     return f\"STRUCT({content})\"\n", "issue": "bug: `pivot_longer` doesn't work with field names containing spaces\n### What happened?\n\nwhen using pivot_longer, the names_to field and values_to arguments can't have a space in them\n\n### What version of ibis are you using?\n\n5.1.0\n\n### What backend(s) are you using, if any?\n\nDuckDB\n\n### Relevant log output\n\n```sh\n>>> import ibis\r\n>>> import ibis.selectors as s\r\n>>> from ibis import _\r\n>>> ibis.options.interactive = True\r\n>>> relig_income = ibis.examples.relig_income_raw.fetch()\r\n>>> relig_income.pivot_longer(~s.c(\"religion\"), names_to=\"in come\", values_to=\"my count\")\r\n\r\nThis produces a ParserException: Parser Error: syntax error at or near \"in\"\r\nLINE 2: ...e\" := ?, \"my count\" := t1.\"$100-150k\"), struct_pack(\"in come\" := ?, \"my count\"...\r\n\r\nThis can be fixed by removing the space, but would be nice to at least fix the error message or documentation to say there should be no spaces in the names and values\n```\n\n\n### Code of Conduct\n\n- [X] I agree to follow this project's Code of Conduct\n", "before_files": [{"content": "from __future__ import annotations\n\nfrom typing import Callable, Mapping, Optional\n\nimport sqlalchemy as sa\nimport sqlalchemy.types as sat\nimport toolz\nfrom sqlalchemy.ext.compiler import compiles\n\nimport ibis.expr.datatypes as dt\nfrom ibis.backends.base.sql.alchemy.geospatial import geospatial_supported\nfrom ibis.common.collections import FrozenDict\n\nif geospatial_supported:\n    import geoalchemy2 as ga\n\n\nclass ArrayType(sat.UserDefinedType):\n    def __init__(self, value_type: sat.TypeEngine):\n        self.value_type = sat.to_instance(value_type)\n\n    def result_processor(self, dialect, coltype) -> None:\n        if not coltype.lower().startswith(\"array\"):\n            return None\n\n        inner_processor = (\n            self.value_type.result_processor(dialect, coltype[len(\"array(\") : -1])\n            or toolz.identity\n        )\n\n        return lambda v: v if v is None else list(map(inner_processor, v))\n\n\n@compiles(ArrayType, \"default\")\ndef compiles_array(element, compiler, **kw):\n    return f\"ARRAY({compiler.process(element.value_type, **kw)})\"\n\n\nclass StructType(sat.UserDefinedType):\n    cache_ok = True\n\n    def __init__(self, fields: Mapping[str, sat.TypeEngine]) -> None:\n        self.fields = FrozenDict(\n            {name: sat.to_instance(typ) for name, typ in fields.items()}\n        )\n\n\n@compiles(StructType, \"default\")\ndef compiles_struct(element, compiler, **kw):\n    content = \", \".join(\n        f\"{field} {compiler.process(typ, **kw)}\"\n        for field, typ in element.fields.items()\n    )\n    return f\"STRUCT({content})\"\n\n\nclass MapType(sat.UserDefinedType):\n    def __init__(self, key_type: sat.TypeEngine, value_type: sat.TypeEngine):\n        self.key_type = sat.to_instance(key_type)\n        self.value_type = sat.to_instance(value_type)\n\n\n@compiles(MapType, \"default\")\ndef compiles_map(element, compiler, **kw):\n    key_type = compiler.process(element.key_type, **kw)\n    value_type = compiler.process(element.value_type, **kw)\n    return f\"MAP({key_type}, {value_type})\"\n\n\nclass UInt64(sat.Integer):\n    pass\n\n\nclass UInt32(sat.Integer):\n    pass\n\n\nclass UInt16(sat.Integer):\n    pass\n\n\nclass UInt8(sat.Integer):\n    pass\n\n\n@compiles(UInt64, \"postgresql\")\n@compiles(UInt32, \"postgresql\")\n@compiles(UInt16, \"postgresql\")\n@compiles(UInt8, \"postgresql\")\n@compiles(UInt64, \"mssql\")\n@compiles(UInt32, \"mssql\")\n@compiles(UInt16, \"mssql\")\n@compiles(UInt8, \"mssql\")\n@compiles(UInt64, \"mysql\")\n@compiles(UInt32, \"mysql\")\n@compiles(UInt16, \"mysql\")\n@compiles(UInt8, \"mysql\")\n@compiles(UInt64, \"snowflake\")\n@compiles(UInt32, \"snowflake\")\n@compiles(UInt16, \"snowflake\")\n@compiles(UInt8, \"snowflake\")\n@compiles(UInt64, \"sqlite\")\n@compiles(UInt32, \"sqlite\")\n@compiles(UInt16, \"sqlite\")\n@compiles(UInt8, \"sqlite\")\n@compiles(UInt64, \"trino\")\n@compiles(UInt32, \"trino\")\n@compiles(UInt16, \"trino\")\n@compiles(UInt8, \"trino\")\ndef compile_uint(element, compiler, **kw):\n    dialect_name = compiler.dialect.name\n    raise TypeError(\n        f\"unsigned integers are not supported in the {dialect_name} backend\"\n    )\n\n\ntry:\n    UUID = sat.UUID\nexcept AttributeError:\n\n    class UUID(sat.String):\n        pass\n\nelse:\n\n    @compiles(UUID, \"default\")\n    def compiles_uuid(element, compiler, **kw):\n        return \"UUID\"\n\n\nclass Unknown(sa.Text):\n    pass\n\n\n_from_sqlalchemy_types = {\n    sat.BOOLEAN: dt.Boolean,\n    sat.Boolean: dt.Boolean,\n    sat.BINARY: dt.Binary,\n    sat.LargeBinary: dt.Binary,\n    sat.DATE: dt.Date,\n    sat.Date: dt.Date,\n    sat.TEXT: dt.String,\n    sat.Text: dt.String,\n    sat.TIME: dt.Time,\n    sat.Time: dt.Time,\n    sat.VARCHAR: dt.String,\n    sat.CHAR: dt.String,\n    sat.String: dt.String,\n    sat.SMALLINT: dt.Int16,\n    sat.SmallInteger: dt.Int16,\n    sat.INTEGER: dt.Int32,\n    sat.Integer: dt.Int32,\n    sat.BIGINT: dt.Int64,\n    sat.BigInteger: dt.Int64,\n    sat.REAL: dt.Float32,\n    sat.FLOAT: dt.Float64,\n    UInt16: dt.UInt16,\n    UInt32: dt.UInt32,\n    UInt64: dt.UInt64,\n    UInt8: dt.UInt8,\n    Unknown: dt.Unknown,\n    sat.JSON: dt.JSON,\n    UUID: dt.UUID,\n}\n\n_to_sqlalchemy_types = {\n    dt.Null: sat.NullType,\n    dt.Date: sat.Date,\n    dt.Time: sat.Time,\n    dt.Boolean: sat.Boolean,\n    dt.Binary: sat.LargeBinary,\n    dt.String: sat.Text,\n    dt.Decimal: sat.Numeric,\n    # Mantissa-based\n    dt.Float16: sat.REAL,\n    dt.Float32: sat.REAL,\n    # precision is the number of bits in the mantissa\n    # without specifying this, some backends interpret the type as FLOAT, which\n    # means float32 (and precision == 24)\n    dt.Float64: sat.FLOAT(precision=53),\n    dt.Int8: sat.SmallInteger,\n    dt.Int16: sat.SmallInteger,\n    dt.Int32: sat.Integer,\n    dt.Int64: sat.BigInteger,\n    dt.UInt8: UInt8,\n    dt.UInt16: UInt16,\n    dt.UInt32: UInt32,\n    dt.UInt64: UInt64,\n    dt.JSON: sat.JSON,\n    dt.Interval: sat.Interval,\n    dt.Unknown: Unknown,\n    dt.MACADDR: sat.Text,\n    dt.INET: sat.Text,\n    dt.UUID: UUID,\n}\n\n\ndef dtype_to_sqlalchemy(\n    dtype: dt.DataType,\n    converter: Optional[Callable[[dt.DataType], sa.TypeEngine]] = None,\n):\n    \"\"\"Convert an Ibis type to a SQLAlchemy type.\n\n    Parameters\n    ----------\n    dtype\n        Ibis type to convert.\n    converter\n        Converter function to use for nested types. If not provided, this function\n        will be used recursively. Should only be used when defining new converter for\n        dialects.\n\n    Returns\n    -------\n    sa.TypeEngine\n    \"\"\"\n    convert = converter or dtype_to_sqlalchemy\n\n    if dtype.is_decimal():\n        return sat.NUMERIC(dtype.precision, dtype.scale)\n    elif dtype.is_timestamp():\n        return sat.TIMESTAMP(timezone=bool(dtype.timezone))\n    elif dtype.is_array():\n        return ArrayType(convert(dtype.value_type))\n    elif dtype.is_struct():\n        fields = {k: convert(v) for k, v in dtype.fields.items()}\n        return StructType(fields)\n    elif dtype.is_map():\n        return MapType(convert(dtype.key_type), convert(dtype.value_type))\n    elif dtype.is_geospatial():\n        if geospatial_supported:\n            if dtype.geotype == 'geometry':\n                return ga.Geometry\n            elif dtype.geotype == 'geography':\n                return ga.Geography\n            else:\n                return ga.types._GISType\n        else:\n            raise TypeError(\"geospatial types are not supported\")\n    else:\n        return _to_sqlalchemy_types[type(dtype)]\n\n\n_FLOAT_PREC_TO_TYPE = {\n    11: dt.Float16,\n    24: dt.Float32,\n    53: dt.Float64,\n}\n\n_GEOSPATIAL_TYPES = {\n    \"POINT\": dt.Point,\n    \"LINESTRING\": dt.LineString,\n    \"POLYGON\": dt.Polygon,\n    \"MULTILINESTRING\": dt.MultiLineString,\n    \"MULTIPOINT\": dt.MultiPoint,\n    \"MULTIPOLYGON\": dt.MultiPolygon,\n    \"GEOMETRY\": dt.Geometry,\n    \"GEOGRAPHY\": dt.Geography,\n}\n\n\ndef dtype_from_sqlalchemy(typ, nullable=True, converter=None):\n    \"\"\"Convert a SQLAlchemy type to an Ibis type.\n\n    Parameters\n    ----------\n    typ\n        SQLAlchemy type to convert.\n    nullable : bool, optional\n        Whether the returned type should be nullable.\n    converter\n        Converter function to use for nested types. If not provided, this function\n        will be used recursively. Should only be used when defining new converter for\n        dialects.\n\n    Returns\n    -------\n    dt.DataType\n    \"\"\"\n    convert = converter or dtype_from_sqlalchemy\n\n    if dtype := _from_sqlalchemy_types.get(type(typ)):\n        return dtype(nullable=nullable)\n    elif isinstance(typ, sat.Float):\n        if (float_typ := _FLOAT_PREC_TO_TYPE.get(typ.precision)) is not None:\n            return float_typ(nullable=nullable)\n        return dt.Decimal(typ.precision, typ.scale, nullable=nullable)\n    elif isinstance(typ, sat.Numeric):\n        return dt.Decimal(typ.precision, typ.scale, nullable=nullable)\n    elif isinstance(typ, ArrayType):\n        return dt.Array(convert(typ.value_type), nullable=nullable)\n    elif isinstance(typ, sat.ARRAY):\n        ndim = typ.dimensions\n        if ndim is not None and ndim != 1:\n            raise NotImplementedError(\"Nested array types not yet supported\")\n        return dt.Array(convert(typ.item_type), nullable=nullable)\n    elif isinstance(typ, StructType):\n        fields = {k: convert(v) for k, v in typ.fields.items()}\n        return dt.Struct(fields, nullable=nullable)\n    elif isinstance(typ, MapType):\n        return dt.Map(convert(typ.key_type), convert(typ.value_type), nullable=nullable)\n    elif isinstance(typ, sa.DateTime):\n        timezone = \"UTC\" if typ.timezone else None\n        return dt.Timestamp(timezone, nullable=nullable)\n    elif geospatial_supported and isinstance(typ, ga.types._GISType):\n        name = typ.geometry_type.upper()\n        try:\n            return _GEOSPATIAL_TYPES[name](geotype=typ.name, nullable=nullable)\n        except KeyError:\n            raise ValueError(f\"Unrecognized geometry type: {name}\")\n    else:\n        raise TypeError(f\"Unable to convert type: {typ!r}\")\n", "path": "ibis/backends/base/sql/alchemy/datatypes.py"}], "after_files": [{"content": "from __future__ import annotations\n\nfrom typing import Callable, Mapping, Optional\n\nimport sqlalchemy as sa\nimport sqlalchemy.types as sat\nimport toolz\nfrom sqlalchemy.ext.compiler import compiles\n\nimport ibis.expr.datatypes as dt\nfrom ibis.backends.base.sql.alchemy.geospatial import geospatial_supported\nfrom ibis.common.collections import FrozenDict\n\nif geospatial_supported:\n    import geoalchemy2 as ga\n\n\nclass ArrayType(sat.UserDefinedType):\n    def __init__(self, value_type: sat.TypeEngine):\n        self.value_type = sat.to_instance(value_type)\n\n    def result_processor(self, dialect, coltype) -> None:\n        if not coltype.lower().startswith(\"array\"):\n            return None\n\n        inner_processor = (\n            self.value_type.result_processor(dialect, coltype[len(\"array(\") : -1])\n            or toolz.identity\n        )\n\n        return lambda v: v if v is None else list(map(inner_processor, v))\n\n\n@compiles(ArrayType, \"default\")\ndef compiles_array(element, compiler, **kw):\n    return f\"ARRAY({compiler.process(element.value_type, **kw)})\"\n\n\nclass StructType(sat.UserDefinedType):\n    cache_ok = True\n\n    def __init__(self, fields: Mapping[str, sat.TypeEngine]) -> None:\n        self.fields = FrozenDict(\n            {name: sat.to_instance(typ) for name, typ in fields.items()}\n        )\n\n\n@compiles(StructType, \"default\")\ndef compiles_struct(element, compiler, **kw):\n    quote = compiler.dialect.identifier_preparer.quote\n    content = \", \".join(\n        f\"{quote(field)} {compiler.process(typ, **kw)}\"\n        for field, typ in element.fields.items()\n    )\n    return f\"STRUCT({content})\"\n\n\nclass MapType(sat.UserDefinedType):\n    def __init__(self, key_type: sat.TypeEngine, value_type: sat.TypeEngine):\n        self.key_type = sat.to_instance(key_type)\n        self.value_type = sat.to_instance(value_type)\n\n\n@compiles(MapType, \"default\")\ndef compiles_map(element, compiler, **kw):\n    key_type = compiler.process(element.key_type, **kw)\n    value_type = compiler.process(element.value_type, **kw)\n    return f\"MAP({key_type}, {value_type})\"\n\n\nclass UInt64(sat.Integer):\n    pass\n\n\nclass UInt32(sat.Integer):\n    pass\n\n\nclass UInt16(sat.Integer):\n    pass\n\n\nclass UInt8(sat.Integer):\n    pass\n\n\n@compiles(UInt64, \"postgresql\")\n@compiles(UInt32, \"postgresql\")\n@compiles(UInt16, \"postgresql\")\n@compiles(UInt8, \"postgresql\")\n@compiles(UInt64, \"mssql\")\n@compiles(UInt32, \"mssql\")\n@compiles(UInt16, \"mssql\")\n@compiles(UInt8, \"mssql\")\n@compiles(UInt64, \"mysql\")\n@compiles(UInt32, \"mysql\")\n@compiles(UInt16, \"mysql\")\n@compiles(UInt8, \"mysql\")\n@compiles(UInt64, \"snowflake\")\n@compiles(UInt32, \"snowflake\")\n@compiles(UInt16, \"snowflake\")\n@compiles(UInt8, \"snowflake\")\n@compiles(UInt64, \"sqlite\")\n@compiles(UInt32, \"sqlite\")\n@compiles(UInt16, \"sqlite\")\n@compiles(UInt8, \"sqlite\")\n@compiles(UInt64, \"trino\")\n@compiles(UInt32, \"trino\")\n@compiles(UInt16, \"trino\")\n@compiles(UInt8, \"trino\")\ndef compile_uint(element, compiler, **kw):\n    dialect_name = compiler.dialect.name\n    raise TypeError(\n        f\"unsigned integers are not supported in the {dialect_name} backend\"\n    )\n\n\ntry:\n    UUID = sat.UUID\nexcept AttributeError:\n\n    class UUID(sat.String):\n        pass\n\nelse:\n\n    @compiles(UUID, \"default\")\n    def compiles_uuid(element, compiler, **kw):\n        return \"UUID\"\n\n\nclass Unknown(sa.Text):\n    pass\n\n\n_from_sqlalchemy_types = {\n    sat.BOOLEAN: dt.Boolean,\n    sat.Boolean: dt.Boolean,\n    sat.BINARY: dt.Binary,\n    sat.LargeBinary: dt.Binary,\n    sat.DATE: dt.Date,\n    sat.Date: dt.Date,\n    sat.TEXT: dt.String,\n    sat.Text: dt.String,\n    sat.TIME: dt.Time,\n    sat.Time: dt.Time,\n    sat.VARCHAR: dt.String,\n    sat.CHAR: dt.String,\n    sat.String: dt.String,\n    sat.SMALLINT: dt.Int16,\n    sat.SmallInteger: dt.Int16,\n    sat.INTEGER: dt.Int32,\n    sat.Integer: dt.Int32,\n    sat.BIGINT: dt.Int64,\n    sat.BigInteger: dt.Int64,\n    sat.REAL: dt.Float32,\n    sat.FLOAT: dt.Float64,\n    UInt16: dt.UInt16,\n    UInt32: dt.UInt32,\n    UInt64: dt.UInt64,\n    UInt8: dt.UInt8,\n    Unknown: dt.Unknown,\n    sat.JSON: dt.JSON,\n    UUID: dt.UUID,\n}\n\n_to_sqlalchemy_types = {\n    dt.Null: sat.NullType,\n    dt.Date: sat.Date,\n    dt.Time: sat.Time,\n    dt.Boolean: sat.Boolean,\n    dt.Binary: sat.LargeBinary,\n    dt.String: sat.Text,\n    dt.Decimal: sat.Numeric,\n    # Mantissa-based\n    dt.Float16: sat.REAL,\n    dt.Float32: sat.REAL,\n    # precision is the number of bits in the mantissa\n    # without specifying this, some backends interpret the type as FLOAT, which\n    # means float32 (and precision == 24)\n    dt.Float64: sat.FLOAT(precision=53),\n    dt.Int8: sat.SmallInteger,\n    dt.Int16: sat.SmallInteger,\n    dt.Int32: sat.Integer,\n    dt.Int64: sat.BigInteger,\n    dt.UInt8: UInt8,\n    dt.UInt16: UInt16,\n    dt.UInt32: UInt32,\n    dt.UInt64: UInt64,\n    dt.JSON: sat.JSON,\n    dt.Interval: sat.Interval,\n    dt.Unknown: Unknown,\n    dt.MACADDR: sat.Text,\n    dt.INET: sat.Text,\n    dt.UUID: UUID,\n}\n\n\ndef dtype_to_sqlalchemy(\n    dtype: dt.DataType,\n    converter: Optional[Callable[[dt.DataType], sa.TypeEngine]] = None,\n):\n    \"\"\"Convert an Ibis type to a SQLAlchemy type.\n\n    Parameters\n    ----------\n    dtype\n        Ibis type to convert.\n    converter\n        Converter function to use for nested types. If not provided, this function\n        will be used recursively. Should only be used when defining new converter for\n        dialects.\n\n    Returns\n    -------\n    sa.TypeEngine\n    \"\"\"\n    convert = converter or dtype_to_sqlalchemy\n\n    if dtype.is_decimal():\n        return sat.NUMERIC(dtype.precision, dtype.scale)\n    elif dtype.is_timestamp():\n        return sat.TIMESTAMP(timezone=bool(dtype.timezone))\n    elif dtype.is_array():\n        return ArrayType(convert(dtype.value_type))\n    elif dtype.is_struct():\n        fields = {k: convert(v) for k, v in dtype.fields.items()}\n        return StructType(fields)\n    elif dtype.is_map():\n        return MapType(convert(dtype.key_type), convert(dtype.value_type))\n    elif dtype.is_geospatial():\n        if geospatial_supported:\n            if dtype.geotype == 'geometry':\n                return ga.Geometry\n            elif dtype.geotype == 'geography':\n                return ga.Geography\n            else:\n                return ga.types._GISType\n        else:\n            raise TypeError(\"geospatial types are not supported\")\n    else:\n        return _to_sqlalchemy_types[type(dtype)]\n\n\n_FLOAT_PREC_TO_TYPE = {\n    11: dt.Float16,\n    24: dt.Float32,\n    53: dt.Float64,\n}\n\n_GEOSPATIAL_TYPES = {\n    \"POINT\": dt.Point,\n    \"LINESTRING\": dt.LineString,\n    \"POLYGON\": dt.Polygon,\n    \"MULTILINESTRING\": dt.MultiLineString,\n    \"MULTIPOINT\": dt.MultiPoint,\n    \"MULTIPOLYGON\": dt.MultiPolygon,\n    \"GEOMETRY\": dt.Geometry,\n    \"GEOGRAPHY\": dt.Geography,\n}\n\n\ndef dtype_from_sqlalchemy(typ, nullable=True, converter=None):\n    \"\"\"Convert a SQLAlchemy type to an Ibis type.\n\n    Parameters\n    ----------\n    typ\n        SQLAlchemy type to convert.\n    nullable : bool, optional\n        Whether the returned type should be nullable.\n    converter\n        Converter function to use for nested types. If not provided, this function\n        will be used recursively. Should only be used when defining new converter for\n        dialects.\n\n    Returns\n    -------\n    dt.DataType\n    \"\"\"\n    convert = converter or dtype_from_sqlalchemy\n\n    if dtype := _from_sqlalchemy_types.get(type(typ)):\n        return dtype(nullable=nullable)\n    elif isinstance(typ, sat.Float):\n        if (float_typ := _FLOAT_PREC_TO_TYPE.get(typ.precision)) is not None:\n            return float_typ(nullable=nullable)\n        return dt.Decimal(typ.precision, typ.scale, nullable=nullable)\n    elif isinstance(typ, sat.Numeric):\n        return dt.Decimal(typ.precision, typ.scale, nullable=nullable)\n    elif isinstance(typ, ArrayType):\n        return dt.Array(convert(typ.value_type), nullable=nullable)\n    elif isinstance(typ, sat.ARRAY):\n        ndim = typ.dimensions\n        if ndim is not None and ndim != 1:\n            raise NotImplementedError(\"Nested array types not yet supported\")\n        return dt.Array(convert(typ.item_type), nullable=nullable)\n    elif isinstance(typ, StructType):\n        fields = {k: convert(v) for k, v in typ.fields.items()}\n        return dt.Struct(fields, nullable=nullable)\n    elif isinstance(typ, MapType):\n        return dt.Map(convert(typ.key_type), convert(typ.value_type), nullable=nullable)\n    elif isinstance(typ, sa.DateTime):\n        timezone = \"UTC\" if typ.timezone else None\n        return dt.Timestamp(timezone, nullable=nullable)\n    elif geospatial_supported and isinstance(typ, ga.types._GISType):\n        name = typ.geometry_type.upper()\n        try:\n            return _GEOSPATIAL_TYPES[name](geotype=typ.name, nullable=nullable)\n        except KeyError:\n            raise ValueError(f\"Unrecognized geometry type: {name}\")\n    else:\n        raise TypeError(f\"Unable to convert type: {typ!r}\")\n", "path": "ibis/backends/base/sql/alchemy/datatypes.py"}]}