Skip to content

Commit

Permalink
feat(duckdb): allow casting to geometry from text (ibis-project#10221)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
cpcloud authored Sep 25, 2024
1 parent 24e5395 commit 3e73479
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 11 deletions.
9 changes: 9 additions & 0 deletions ibis/backends/duckdb/tests/test_geospatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,12 @@ def test_load_spatial_casting(ext_dir):

assert geo_expr.type().is_geospatial()
assert isinstance(con.execute(geo_expr), gpd.GeoSeries)


def test_geom_from_string(con):
value = ibis.literal("POINT (1 2)")
assert value.type().is_string()

expr = value.cast("geometry")
result = con.execute(expr)
assert result == shapely.from_wkt("POINT (1 2)")
10 changes: 7 additions & 3 deletions ibis/backends/sql/compilers/duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,13 +410,17 @@ def visit_TimestampFromYMDHMS(
return self.f[func](*args)

def visit_Cast(self, op, *, arg, to):
dtype = op.arg.dtype
if to.is_interval():
func = self.f[f"to_{_INTERVAL_SUFFIXES[to.unit.short]}"]
return func(sg.cast(arg, to=self.type_mapper.from_ibis(dt.int32)))
elif to.is_timestamp() and op.arg.dtype.is_integer():
elif to.is_timestamp() and dtype.is_integer():
return self.f.to_timestamp(arg)
elif to.is_geospatial() and op.arg.dtype.is_binary():
return self.f.st_geomfromwkb(arg)
elif to.is_geospatial():
if dtype.is_binary():
return self.f.st_geomfromwkb(arg)
elif dtype.is_string():
return self.f.st_geomfromtext(arg)

return self.cast(arg, to)

Expand Down
17 changes: 9 additions & 8 deletions ibis/expr/types/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,16 +203,17 @@ def cast(self, target_type: Any) -> Value:
"""
op = ops.Cast(self, to=target_type)

if op.to == self.type():
# noop case if passed type is the same
to = op.to
dtype = self.type()

if to == dtype or (
to.is_geospatial()
and dtype.is_geospatial()
and (dtype.geotype or "geometry") == to.geotype
):
# no-op case if passed type is the same
return self

if op.to.is_geospatial() and not self.type().is_binary():
from_geotype = self.type().geotype or "geometry"
to_geotype = op.to.geotype
if from_geotype == to_geotype:
return self

return op.to_expr()

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

0 comments on commit 3e73479

Please sign in to comment.