diff --git a/ibis/backends/duckdb/tests/test_geospatial.py b/ibis/backends/duckdb/tests/test_geospatial.py index 2c748c1d9f48..bd4a1428b911 100644 --- a/ibis/backends/duckdb/tests/test_geospatial.py +++ b/ibis/backends/duckdb/tests/test_geospatial.py @@ -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)") diff --git a/ibis/backends/sql/compilers/duckdb.py b/ibis/backends/sql/compilers/duckdb.py index 88c8f277db1e..30a23613e52c 100644 --- a/ibis/backends/sql/compilers/duckdb.py +++ b/ibis/backends/sql/compilers/duckdb.py @@ -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) diff --git a/ibis/expr/types/generic.py b/ibis/expr/types/generic.py index 8c9991f0abd9..8a62ff14a88c 100644 --- a/ibis/expr/types/generic.py +++ b/ibis/expr/types/generic.py @@ -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: