Skip to content

Commit ad021b7

Browse files
authored
Merge branch 'main' into feat-st-regionstats
2 parents 6d436ff + d69ba88 commit ad021b7

File tree

3 files changed

+145
-140
lines changed

3 files changed

+145
-140
lines changed

bigframes/core/compile/ibis_compiler/operations/geo_ops.py

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,93 @@
1414

1515
from __future__ import annotations
1616

17+
from typing import cast
18+
1719
from bigframes_vendored import ibis
1820
from bigframes_vendored.ibis.expr import types as ibis_types
1921
import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes
22+
import bigframes_vendored.ibis.expr.operations.udf as ibis_udf
2023
import bigframes_vendored.ibis.expr.operations.geospatial as ibis_geo
2124

2225
from bigframes.core.compile.ibis_compiler import scalar_op_compiler
23-
from bigframes.operations import geo_ops
26+
from bigframes.operations import geo_ops as ops
2427

2528
register_unary_op = scalar_op_compiler.scalar_op_compiler.register_unary_op
29+
register_binary_op = scalar_op_compiler.scalar_op_compiler.register_binary_op
30+
31+
32+
# Geo Ops
33+
@register_unary_op(ops.geo_area_op)
34+
def geo_area_op_impl(x: ibis_types.Value):
35+
return cast(ibis_types.GeoSpatialValue, x).area()
36+
37+
38+
@register_unary_op(ops.geo_st_astext_op)
39+
def geo_st_astext_op_impl(x: ibis_types.Value):
40+
return cast(ibis_types.GeoSpatialValue, x).as_text()
41+
42+
43+
@register_unary_op(ops.geo_st_boundary_op, pass_op=False)
44+
def geo_st_boundary_op_impl(x: ibis_types.Value):
45+
return st_boundary(x)
46+
47+
48+
@register_unary_op(ops.GeoStBufferOp, pass_op=True)
49+
def geo_st_buffer_op_impl(x: ibis_types.Value, op: ops.GeoStBufferOp):
50+
return st_buffer(
51+
x,
52+
op.buffer_radius,
53+
op.num_seg_quarter_circle,
54+
op.use_spheroid,
55+
)
56+
57+
58+
@register_unary_op(ops.geo_st_centroid_op, pass_op=False)
59+
def geo_st_centroid_op_impl(x: ibis_types.Value):
60+
return cast(ibis_types.GeoSpatialValue, x).centroid()
61+
62+
63+
@register_unary_op(ops.geo_st_convexhull_op, pass_op=False)
64+
def geo_st_convexhull_op_impl(x: ibis_types.Value):
65+
return st_convexhull(x)
66+
67+
68+
@register_binary_op(ops.geo_st_difference_op, pass_op=False)
69+
def geo_st_difference_op_impl(x: ibis_types.Value, y: ibis_types.Value):
70+
return cast(ibis_types.GeoSpatialValue, x).difference(
71+
cast(ibis_types.GeoSpatialValue, y)
72+
)
73+
74+
75+
@register_binary_op(ops.GeoStDistanceOp, pass_op=True)
76+
def geo_st_distance_op_impl(
77+
x: ibis_types.Value, y: ibis_types.Value, op: ops.GeoStDistanceOp
78+
):
79+
return st_distance(x, y, op.use_spheroid)
80+
81+
82+
@register_unary_op(ops.geo_st_geogfromtext_op)
83+
def geo_st_geogfromtext_op_impl(x: ibis_types.Value):
84+
# Ibis doesn't seem to provide a dedicated method to cast from string to geography,
85+
# so we use a BigQuery scalar function, st_geogfromtext(), directly.
86+
return st_geogfromtext(x)
87+
88+
89+
@register_binary_op(ops.geo_st_geogpoint_op, pass_op=False)
90+
def geo_st_geogpoint_op_impl(x: ibis_types.Value, y: ibis_types.Value):
91+
return cast(ibis_types.NumericValue, x).point(cast(ibis_types.NumericValue, y))
92+
93+
94+
@register_binary_op(ops.geo_st_intersection_op, pass_op=False)
95+
def geo_st_intersection_op_impl(x: ibis_types.Value, y: ibis_types.Value):
96+
return cast(ibis_types.GeoSpatialValue, x).intersection(
97+
cast(ibis_types.GeoSpatialValue, y)
98+
)
99+
100+
101+
@register_unary_op(ops.geo_st_isclosed_op, pass_op=False)
102+
def geo_st_isclosed_op_impl(x: ibis_types.Value):
103+
return st_isclosed(x)
26104

27105

28106
@register_unary_op(geo_ops.StRegionStatsOp, pass_op=True)
@@ -53,3 +131,61 @@ def st_regionstats(
53131
include=include,
54132
options=options,
55133
).to_expr()
134+
135+
136+
@register_unary_op(ops.geo_x_op)
137+
def geo_x_op_impl(x: ibis_types.Value):
138+
return cast(ibis_types.GeoSpatialValue, x).x()
139+
140+
141+
@register_unary_op(ops.GeoStLengthOp, pass_op=True)
142+
def geo_length_op_impl(x: ibis_types.Value, op: ops.GeoStLengthOp):
143+
# Call the st_length UDF defined in this file (or imported)
144+
return st_length(x, op.use_spheroid)
145+
146+
147+
@register_unary_op(ops.geo_y_op)
148+
def geo_y_op_impl(x: ibis_types.Value):
149+
return cast(ibis_types.GeoSpatialValue, x).y()
150+
151+
152+
@ibis_udf.scalar.builtin
153+
def st_convexhull(x: ibis_dtypes.geography) -> ibis_dtypes.geography: # type: ignore
154+
"""ST_CONVEXHULL"""
155+
...
156+
157+
158+
@ibis_udf.scalar.builtin
159+
def st_geogfromtext(a: str) -> ibis_dtypes.geography: # type: ignore
160+
"""Convert string to geography."""
161+
162+
163+
@ibis_udf.scalar.builtin
164+
def st_boundary(a: ibis_dtypes.geography) -> ibis_dtypes.geography: # type: ignore
165+
"""Find the boundary of a geography."""
166+
167+
168+
@ibis_udf.scalar.builtin
169+
def st_buffer(
170+
geography: ibis_dtypes.geography, # type: ignore
171+
buffer_radius: ibis_dtypes.Float64,
172+
num_seg_quarter_circle: ibis_dtypes.Float64,
173+
use_spheroid: ibis_dtypes.Boolean,
174+
) -> ibis_dtypes.geography: # type: ignore
175+
...
176+
177+
178+
@ibis_udf.scalar.builtin
179+
def st_distance(a: ibis_dtypes.geography, b: ibis_dtypes.geography, use_spheroid: bool) -> ibis_dtypes.float: # type: ignore
180+
"""Convert string to geography."""
181+
182+
183+
@ibis_udf.scalar.builtin
184+
def st_length(geog: ibis_dtypes.geography, use_spheroid: bool) -> ibis_dtypes.float: # type: ignore
185+
"""ST_LENGTH BQ builtin. This body is never executed."""
186+
pass
187+
188+
189+
@ibis_udf.scalar.builtin
190+
def st_isclosed(a: ibis_dtypes.geography) -> ibis_dtypes.boolean: # type: ignore
191+
"""Checks if a geography is closed."""

bigframes/core/compile/ibis_compiler/scalar_op_registry.py

Lines changed: 0 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -837,98 +837,6 @@ def normalize_op_impl(x: ibis_types.Value):
837837
return result.cast(result_type)
838838

839839

840-
# Geo Ops
841-
@scalar_op_compiler.register_unary_op(ops.geo_area_op)
842-
def geo_area_op_impl(x: ibis_types.Value):
843-
return typing.cast(ibis_types.GeoSpatialValue, x).area()
844-
845-
846-
@scalar_op_compiler.register_unary_op(ops.geo_st_astext_op)
847-
def geo_st_astext_op_impl(x: ibis_types.Value):
848-
return typing.cast(ibis_types.GeoSpatialValue, x).as_text()
849-
850-
851-
@scalar_op_compiler.register_unary_op(ops.geo_st_boundary_op, pass_op=False)
852-
def geo_st_boundary_op_impl(x: ibis_types.Value):
853-
return st_boundary(x)
854-
855-
856-
@scalar_op_compiler.register_unary_op(ops.GeoStBufferOp, pass_op=True)
857-
def geo_st_buffer_op_impl(x: ibis_types.Value, op: ops.GeoStBufferOp):
858-
return st_buffer(
859-
x,
860-
op.buffer_radius,
861-
op.num_seg_quarter_circle,
862-
op.use_spheroid,
863-
)
864-
865-
866-
@scalar_op_compiler.register_unary_op(ops.geo_st_centroid_op, pass_op=False)
867-
def geo_st_centroid_op_impl(x: ibis_types.Value):
868-
return typing.cast(ibis_types.GeoSpatialValue, x).centroid()
869-
870-
871-
@scalar_op_compiler.register_unary_op(ops.geo_st_convexhull_op, pass_op=False)
872-
def geo_st_convexhull_op_impl(x: ibis_types.Value):
873-
return st_convexhull(x)
874-
875-
876-
@scalar_op_compiler.register_binary_op(ops.geo_st_difference_op, pass_op=False)
877-
def geo_st_difference_op_impl(x: ibis_types.Value, y: ibis_types.Value):
878-
return typing.cast(ibis_types.GeoSpatialValue, x).difference(
879-
typing.cast(ibis_types.GeoSpatialValue, y)
880-
)
881-
882-
883-
@scalar_op_compiler.register_binary_op(ops.GeoStDistanceOp, pass_op=True)
884-
def geo_st_distance_op_impl(
885-
x: ibis_types.Value, y: ibis_types.Value, op: ops.GeoStDistanceOp
886-
):
887-
return st_distance(x, y, op.use_spheroid)
888-
889-
890-
@scalar_op_compiler.register_unary_op(ops.geo_st_geogfromtext_op)
891-
def geo_st_geogfromtext_op_impl(x: ibis_types.Value):
892-
# Ibis doesn't seem to provide a dedicated method to cast from string to geography,
893-
# so we use a BigQuery scalar function, st_geogfromtext(), directly.
894-
return st_geogfromtext(x)
895-
896-
897-
@scalar_op_compiler.register_binary_op(ops.geo_st_geogpoint_op, pass_op=False)
898-
def geo_st_geogpoint_op_impl(x: ibis_types.Value, y: ibis_types.Value):
899-
return typing.cast(ibis_types.NumericValue, x).point(
900-
typing.cast(ibis_types.NumericValue, y)
901-
)
902-
903-
904-
@scalar_op_compiler.register_binary_op(ops.geo_st_intersection_op, pass_op=False)
905-
def geo_st_intersection_op_impl(x: ibis_types.Value, y: ibis_types.Value):
906-
return typing.cast(ibis_types.GeoSpatialValue, x).intersection(
907-
typing.cast(ibis_types.GeoSpatialValue, y)
908-
)
909-
910-
911-
@scalar_op_compiler.register_unary_op(ops.geo_st_isclosed_op, pass_op=False)
912-
def geo_st_isclosed_op_impl(x: ibis_types.Value):
913-
return st_isclosed(x)
914-
915-
916-
@scalar_op_compiler.register_unary_op(ops.geo_x_op)
917-
def geo_x_op_impl(x: ibis_types.Value):
918-
return typing.cast(ibis_types.GeoSpatialValue, x).x()
919-
920-
921-
@scalar_op_compiler.register_unary_op(ops.GeoStLengthOp, pass_op=True)
922-
def geo_length_op_impl(x: ibis_types.Value, op: ops.GeoStLengthOp):
923-
# Call the st_length UDF defined in this file (or imported)
924-
return st_length(x, op.use_spheroid)
925-
926-
927-
@scalar_op_compiler.register_unary_op(ops.geo_y_op)
928-
def geo_y_op_impl(x: ibis_types.Value):
929-
return typing.cast(ibis_types.GeoSpatialValue, x).y()
930-
931-
932840
# Parameterized ops
933841
@scalar_op_compiler.register_unary_op(ops.StructFieldOp, pass_op=True)
934842
def struct_field_op_impl(x: ibis_types.Value, op: ops.StructFieldOp):
@@ -2092,17 +2000,6 @@ def _ibis_num(number: float):
20922000
return typing.cast(ibis_types.NumericValue, ibis_types.literal(number))
20932001

20942002

2095-
@ibis_udf.scalar.builtin
2096-
def st_convexhull(x: ibis_dtypes.geography) -> ibis_dtypes.geography: # type: ignore
2097-
"""ST_CONVEXHULL"""
2098-
...
2099-
2100-
2101-
@ibis_udf.scalar.builtin
2102-
def st_geogfromtext(a: str) -> ibis_dtypes.geography: # type: ignore
2103-
"""Convert string to geography."""
2104-
2105-
21062003
@ibis_udf.scalar.builtin
21072004
def timestamp(a: str) -> ibis_dtypes.timestamp: # type: ignore
21082005
"""Convert string to timestamp."""
@@ -2113,32 +2010,6 @@ def unix_millis(a: ibis_dtypes.timestamp) -> int: # type: ignore
21132010
"""Convert a timestamp to milliseconds"""
21142011

21152012

2116-
@ibis_udf.scalar.builtin
2117-
def st_boundary(a: ibis_dtypes.geography) -> ibis_dtypes.geography: # type: ignore
2118-
"""Find the boundary of a geography."""
2119-
2120-
2121-
@ibis_udf.scalar.builtin
2122-
def st_buffer(
2123-
geography: ibis_dtypes.geography, # type: ignore
2124-
buffer_radius: ibis_dtypes.Float64,
2125-
num_seg_quarter_circle: ibis_dtypes.Float64,
2126-
use_spheroid: ibis_dtypes.Boolean,
2127-
) -> ibis_dtypes.geography: # type: ignore
2128-
...
2129-
2130-
2131-
@ibis_udf.scalar.builtin
2132-
def st_distance(a: ibis_dtypes.geography, b: ibis_dtypes.geography, use_spheroid: bool) -> ibis_dtypes.float: # type: ignore
2133-
"""Convert string to geography."""
2134-
2135-
2136-
@ibis_udf.scalar.builtin
2137-
def st_length(geog: ibis_dtypes.geography, use_spheroid: bool) -> ibis_dtypes.float: # type: ignore
2138-
"""ST_LENGTH BQ builtin. This body is never executed."""
2139-
pass
2140-
2141-
21422013
@ibis_udf.scalar.builtin
21432014
def unix_micros(a: ibis_dtypes.timestamp) -> int: # type: ignore
21442015
"""Convert a timestamp to microseconds"""
@@ -2272,11 +2143,6 @@ def str_lstrip_op( # type: ignore[empty-body]
22722143
"""Remove leading and trailing characters."""
22732144

22742145

2275-
@ibis_udf.scalar.builtin
2276-
def st_isclosed(a: ibis_dtypes.geography) -> ibis_dtypes.boolean: # type: ignore
2277-
"""Checks if a geography is closed."""
2278-
2279-
22802146
@ibis_udf.scalar.builtin(name="rtrim")
22812147
def str_rstrip_op( # type: ignore[empty-body]
22822148
x: ibis_dtypes.String, to_strip: ibis_dtypes.String

specs/2025-08-04-geoseries-scalars.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,14 @@ Raster functions: Functions for analyzing geospatial rasters using geographies.
267267
- [ ] **Export the new operation:**
268268
- [ ] In `bigframes/operations/__init__.py`, import your new operation dataclass and add it to the `__all__` list.
269269
- [ ] **Implement the compilation logic:**
270-
- [ ] In `bigframes/core/compile/scalar_op_compiler.py`:
271-
- [ ] If the BigQuery function has a direct equivalent in Ibis, you can often reuse an existing Ibis method.
272-
- [ ] If not, define a new Ibis UDF using `@ibis_udf.scalar.builtin` to map to the specific BigQuery function signature.
273-
- [ ] Create a new compiler implementation function (e.g., `geo_length_op_impl`).
274-
- [ ] Register this function to your operation dataclass using `@scalar_op_compiler.register_unary_op` or `@scalar_op_compiler.register_binary_op`.
270+
- [ ] In `bigframes/core/compile/ibis_compiler/operations/geo_ops.py`:
271+
- [ ] If the BigQuery function has a direct equivalent in Ibis, you can often reuse an existing Ibis method.
272+
- [ ] If not, define a new Ibis UDF using `@ibis_udf.scalar.builtin` to map to the specific BigQuery function signature.
273+
- [ ] Create a new compiler implementation function (e.g., `geo_length_op_impl`).
274+
- [ ] Register this function to your operation dataclass using `@register_unary_op` or `@register_binary_op`.
275+
- [ ] In `bigframes/core/compile/sqlglot/expressions/geo_ops.py`:
276+
- [ ] Create a new compiler implementation function that generates the appropriate `sqlglot.exp` expression.
277+
- [ ] Register this function to your operation dataclass using `@register_unary_op` or `@register_binary_op`.
275278
- [ ] **Implement the user-facing function or property:**
276279
- [ ] For a `bigframes.bigquery` function:
277280
- [ ] In `bigframes/bigquery/_operations/geo.py`, create the user-facing function (e.g., `st_length`).

0 commit comments

Comments
 (0)