Skip to content

Commit d69ba88

Browse files
authored
refactor: update geo "spec" and split geo ops in ibis compiler (#2208)
1 parent d410046 commit d69ba88

File tree

4 files changed

+168
-139
lines changed

4 files changed

+168
-139
lines changed

bigframes/core/compile/ibis_compiler/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@
2121
from __future__ import annotations
2222

2323
import bigframes.core.compile.ibis_compiler.operations.generic_ops # noqa: F401
24+
import bigframes.core.compile.ibis_compiler.operations.geo_ops # noqa: F401
2425
import bigframes.core.compile.ibis_compiler.scalar_op_registry # noqa: F401
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import cast
18+
19+
from bigframes_vendored.ibis.expr import types as ibis_types
20+
import bigframes_vendored.ibis.expr.datatypes as ibis_dtypes
21+
import bigframes_vendored.ibis.expr.operations.udf as ibis_udf
22+
23+
from bigframes.core.compile.ibis_compiler import scalar_op_compiler
24+
from bigframes.operations import geo_ops as ops
25+
26+
register_unary_op = scalar_op_compiler.scalar_op_compiler.register_unary_op
27+
register_binary_op = scalar_op_compiler.scalar_op_compiler.register_binary_op
28+
29+
30+
# Geo Ops
31+
@register_unary_op(ops.geo_area_op)
32+
def geo_area_op_impl(x: ibis_types.Value):
33+
return cast(ibis_types.GeoSpatialValue, x).area()
34+
35+
36+
@register_unary_op(ops.geo_st_astext_op)
37+
def geo_st_astext_op_impl(x: ibis_types.Value):
38+
return cast(ibis_types.GeoSpatialValue, x).as_text()
39+
40+
41+
@register_unary_op(ops.geo_st_boundary_op, pass_op=False)
42+
def geo_st_boundary_op_impl(x: ibis_types.Value):
43+
return st_boundary(x)
44+
45+
46+
@register_unary_op(ops.GeoStBufferOp, pass_op=True)
47+
def geo_st_buffer_op_impl(x: ibis_types.Value, op: ops.GeoStBufferOp):
48+
return st_buffer(
49+
x,
50+
op.buffer_radius,
51+
op.num_seg_quarter_circle,
52+
op.use_spheroid,
53+
)
54+
55+
56+
@register_unary_op(ops.geo_st_centroid_op, pass_op=False)
57+
def geo_st_centroid_op_impl(x: ibis_types.Value):
58+
return cast(ibis_types.GeoSpatialValue, x).centroid()
59+
60+
61+
@register_unary_op(ops.geo_st_convexhull_op, pass_op=False)
62+
def geo_st_convexhull_op_impl(x: ibis_types.Value):
63+
return st_convexhull(x)
64+
65+
66+
@register_binary_op(ops.geo_st_difference_op, pass_op=False)
67+
def geo_st_difference_op_impl(x: ibis_types.Value, y: ibis_types.Value):
68+
return cast(ibis_types.GeoSpatialValue, x).difference(
69+
cast(ibis_types.GeoSpatialValue, y)
70+
)
71+
72+
73+
@register_binary_op(ops.GeoStDistanceOp, pass_op=True)
74+
def geo_st_distance_op_impl(
75+
x: ibis_types.Value, y: ibis_types.Value, op: ops.GeoStDistanceOp
76+
):
77+
return st_distance(x, y, op.use_spheroid)
78+
79+
80+
@register_unary_op(ops.geo_st_geogfromtext_op)
81+
def geo_st_geogfromtext_op_impl(x: ibis_types.Value):
82+
# Ibis doesn't seem to provide a dedicated method to cast from string to geography,
83+
# so we use a BigQuery scalar function, st_geogfromtext(), directly.
84+
return st_geogfromtext(x)
85+
86+
87+
@register_binary_op(ops.geo_st_geogpoint_op, pass_op=False)
88+
def geo_st_geogpoint_op_impl(x: ibis_types.Value, y: ibis_types.Value):
89+
return cast(ibis_types.NumericValue, x).point(cast(ibis_types.NumericValue, y))
90+
91+
92+
@register_binary_op(ops.geo_st_intersection_op, pass_op=False)
93+
def geo_st_intersection_op_impl(x: ibis_types.Value, y: ibis_types.Value):
94+
return cast(ibis_types.GeoSpatialValue, x).intersection(
95+
cast(ibis_types.GeoSpatialValue, y)
96+
)
97+
98+
99+
@register_unary_op(ops.geo_st_isclosed_op, pass_op=False)
100+
def geo_st_isclosed_op_impl(x: ibis_types.Value):
101+
return st_isclosed(x)
102+
103+
104+
@register_unary_op(ops.geo_x_op)
105+
def geo_x_op_impl(x: ibis_types.Value):
106+
return cast(ibis_types.GeoSpatialValue, x).x()
107+
108+
109+
@register_unary_op(ops.GeoStLengthOp, pass_op=True)
110+
def geo_length_op_impl(x: ibis_types.Value, op: ops.GeoStLengthOp):
111+
# Call the st_length UDF defined in this file (or imported)
112+
return st_length(x, op.use_spheroid)
113+
114+
115+
@register_unary_op(ops.geo_y_op)
116+
def geo_y_op_impl(x: ibis_types.Value):
117+
return cast(ibis_types.GeoSpatialValue, x).y()
118+
119+
120+
@ibis_udf.scalar.builtin
121+
def st_convexhull(x: ibis_dtypes.geography) -> ibis_dtypes.geography: # type: ignore
122+
"""ST_CONVEXHULL"""
123+
...
124+
125+
126+
@ibis_udf.scalar.builtin
127+
def st_geogfromtext(a: str) -> ibis_dtypes.geography: # type: ignore
128+
"""Convert string to geography."""
129+
130+
131+
@ibis_udf.scalar.builtin
132+
def st_boundary(a: ibis_dtypes.geography) -> ibis_dtypes.geography: # type: ignore
133+
"""Find the boundary of a geography."""
134+
135+
136+
@ibis_udf.scalar.builtin
137+
def st_buffer(
138+
geography: ibis_dtypes.geography, # type: ignore
139+
buffer_radius: ibis_dtypes.Float64,
140+
num_seg_quarter_circle: ibis_dtypes.Float64,
141+
use_spheroid: ibis_dtypes.Boolean,
142+
) -> ibis_dtypes.geography: # type: ignore
143+
...
144+
145+
146+
@ibis_udf.scalar.builtin
147+
def st_distance(a: ibis_dtypes.geography, b: ibis_dtypes.geography, use_spheroid: bool) -> ibis_dtypes.float: # type: ignore
148+
"""Convert string to geography."""
149+
150+
151+
@ibis_udf.scalar.builtin
152+
def st_length(geog: ibis_dtypes.geography, use_spheroid: bool) -> ibis_dtypes.float: # type: ignore
153+
"""ST_LENGTH BQ builtin. This body is never executed."""
154+
pass
155+
156+
157+
@ibis_udf.scalar.builtin
158+
def st_isclosed(a: ibis_dtypes.geography) -> ibis_dtypes.boolean: # type: ignore
159+
"""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)