Skip to content

Commit 3e73479

Browse files
authored
feat(duckdb): allow casting to geometry from text (#10221)
Playing around the [MTA bus time dataset](https://data.ny.gov/Transportation/MTA-Bus-Route-Segment-Speeds-Beginning-2023/58t6-89vi/about_data), I wanted this to work and found a bug, where casting to geospatial from a non binary type failed because of an implicit assumption that that non binary type was geospatial, as well as a missing feature which was to handle to cast from string -> geo*
1 parent 24e5395 commit 3e73479

File tree

3 files changed

+25
-11
lines changed

3 files changed

+25
-11
lines changed

ibis/backends/duckdb/tests/test_geospatial.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,12 @@ def test_load_spatial_casting(ext_dir):
385385

386386
assert geo_expr.type().is_geospatial()
387387
assert isinstance(con.execute(geo_expr), gpd.GeoSeries)
388+
389+
390+
def test_geom_from_string(con):
391+
value = ibis.literal("POINT (1 2)")
392+
assert value.type().is_string()
393+
394+
expr = value.cast("geometry")
395+
result = con.execute(expr)
396+
assert result == shapely.from_wkt("POINT (1 2)")

ibis/backends/sql/compilers/duckdb.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,13 +410,17 @@ def visit_TimestampFromYMDHMS(
410410
return self.f[func](*args)
411411

412412
def visit_Cast(self, op, *, arg, to):
413+
dtype = op.arg.dtype
413414
if to.is_interval():
414415
func = self.f[f"to_{_INTERVAL_SUFFIXES[to.unit.short]}"]
415416
return func(sg.cast(arg, to=self.type_mapper.from_ibis(dt.int32)))
416-
elif to.is_timestamp() and op.arg.dtype.is_integer():
417+
elif to.is_timestamp() and dtype.is_integer():
417418
return self.f.to_timestamp(arg)
418-
elif to.is_geospatial() and op.arg.dtype.is_binary():
419-
return self.f.st_geomfromwkb(arg)
419+
elif to.is_geospatial():
420+
if dtype.is_binary():
421+
return self.f.st_geomfromwkb(arg)
422+
elif dtype.is_string():
423+
return self.f.st_geomfromtext(arg)
420424

421425
return self.cast(arg, to)
422426

ibis/expr/types/generic.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -203,16 +203,17 @@ def cast(self, target_type: Any) -> Value:
203203
"""
204204
op = ops.Cast(self, to=target_type)
205205

206-
if op.to == self.type():
207-
# noop case if passed type is the same
206+
to = op.to
207+
dtype = self.type()
208+
209+
if to == dtype or (
210+
to.is_geospatial()
211+
and dtype.is_geospatial()
212+
and (dtype.geotype or "geometry") == to.geotype
213+
):
214+
# no-op case if passed type is the same
208215
return self
209216

210-
if op.to.is_geospatial() and not self.type().is_binary():
211-
from_geotype = self.type().geotype or "geometry"
212-
to_geotype = op.to.geotype
213-
if from_geotype == to_geotype:
214-
return self
215-
216217
return op.to_expr()
217218

218219
def try_cast(self, target_type: Any) -> Value:

0 commit comments

Comments
 (0)