Skip to content

Commit 7f337d1

Browse files
committed
perf(rust/sedona-geo): PointXYGeometryFactory point fast path for ST_Distance/ST_DWithin
Add PointXYGeometryFactory to sedona-functions: a GeometryFactory whose Geom is a PointOrWkb — a finite Point decoded straight from its fixed WKB offset (no full parse), or a parsed Wkb for anything else (incl POINT EMPTY). The GenericExecutor parses each scalar once, so kernels get parse-once-for-scalars for free. Adopt it in ST_Distance (Point/Point computes Euclidean directly; mixed Point/geometry uses a geo_types::Point so the Point is never re-parsed) and ST_DWithin (shares the same backend, distance <= bound). Replaces the hand-rolled PreparedScalar scalar-prep. Adds point/point ST_DWithin benchmarks. ST_X/ST_Y deliberately stay on their direct read_point_xy fast path: the factory enum adds per-element overhead that regresses the already-optimal unary case (~+30% in benchmarks), with no parse savings to offset it.
1 parent f864a37 commit 7f337d1

5 files changed

Lines changed: 258 additions & 77 deletions

File tree

rust/sedona-functions/src/executor.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ use datafusion_common::error::Result;
2323
use datafusion_common::{DataFusionError, ScalarValue};
2424
use datafusion_expr::ColumnarValue;
2525
use sedona_common::sedona_internal_err;
26+
use sedona_geometry::wkb_header::WkbPointLayout;
2627
use sedona_schema::datatypes::SedonaType;
27-
use wkb::reader::Wkb;
28+
use wkb::reader::{read_wkb, Wkb};
2829

2930
/// Helper for writing general kernel implementations with geometry
3031
///
@@ -269,6 +270,51 @@ impl GeometryFactory for WkbBytesFactory {
269270
/// the [WkbExecutor].
270271
pub type WkbBytesExecutor<'a, 'b> = GenericExecutor<'a, 'b, WkbBytesFactory, WkbBytesFactory>;
271272

273+
/// A finite Point's `(x, y)`, or the parsed geometry for anything else.
274+
///
275+
/// Produced by [PointXYGeometryFactory] for kernels whose hot path only needs a
276+
/// Point's coordinates (ST_X, ST_Distance, ST_DWithin, …): a Point is decoded
277+
/// straight from its fixed WKB offset (no full parse), while every other
278+
/// geometry — including `POINT EMPTY` — falls back to a parsed [Wkb] so the
279+
/// kernel can handle it with the general path.
280+
pub enum PointOrWkb<'a> {
281+
/// A finite Point, decoded from its fixed WKB offset without a full parse.
282+
Point(f64, f64),
283+
/// Any non-Point (or `POINT EMPTY`), fully parsed.
284+
Other(Wkb<'a>),
285+
}
286+
287+
/// A [GeometryFactory] that decodes a finite Point's `(x, y)` from its fixed WKB
288+
/// offset and parses anything else into a [Wkb].
289+
///
290+
/// Because [GenericExecutor] parses each scalar argument once (and each array
291+
/// element per row), a kernel built on this factory gets parse-once-for-scalars
292+
/// for free and only pays a coordinate read where the Point fast path applies.
293+
#[derive(Default)]
294+
pub struct PointXYGeometryFactory {}
295+
296+
impl GeometryFactory for PointXYGeometryFactory {
297+
type Geom<'a> = PointOrWkb<'a>;
298+
299+
#[inline]
300+
fn try_from_wkb<'a>(&self, wkb_bytes: &'a [u8]) -> Result<Self::Geom<'a>> {
301+
// A finite Point: read its coordinate from the fixed offset, no parse.
302+
// POINT EMPTY (NaN/NaN) and any header error fall through to the parser.
303+
if let Ok(Some(layout)) = WkbPointLayout::try_from_wkb(wkb_bytes) {
304+
if let Ok(Some((x, y))) = layout.read_xy(wkb_bytes) {
305+
return Ok(PointOrWkb::Point(x, y));
306+
}
307+
}
308+
let wkb = read_wkb(wkb_bytes).map_err(|e| DataFusionError::External(Box::new(e)))?;
309+
Ok(PointOrWkb::Other(wkb))
310+
}
311+
}
312+
313+
/// Alias for an executor that iterates geometries as [PointOrWkb] — the Point
314+
/// fast path for point-coordinate kernels.
315+
pub type PointXYExecutor<'a, 'b> =
316+
GenericExecutor<'a, 'b, PointXYGeometryFactory, PointXYGeometryFactory>;
317+
272318
/// Trait for iterating over a container type as geometry scalars
273319
///
274320
/// Currently the only scalar type supported is [Wkb]; however, for future

rust/sedona-geo/benches/geo-functions.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ fn criterion_benchmark(c: &mut Criterion) {
6060
ArrayScalar(Point, Polygon(500)),
6161
);
6262

63+
benchmark::scalar(c, &f, "geo", "st_distance", ArrayArray(Point, Point));
64+
benchmark::scalar(c, &f, "geo", "st_distance", ArrayScalar(Point, Point));
65+
benchmark::scalar(c, &f, "geo", "st_distance", ScalarArray(Point, Point));
6366
benchmark::scalar(c, &f, "geo", "st_distance", ArrayScalar(Point, Polygon(10)));
67+
benchmark::scalar(c, &f, "geo", "st_distance", ArrayScalar(Polygon(10), Point));
68+
benchmark::scalar(c, &f, "geo", "st_distance", ArrayArray(Point, Polygon(10)));
6469
benchmark::scalar(
6570
c,
6671
&f,
@@ -83,6 +88,21 @@ fn criterion_benchmark(c: &mut Criterion) {
8388
"st_dwithin",
8489
ArrayArrayScalar(Polygon(10), Polygon(500), Float64(1.0, 2.0)),
8590
);
91+
// Point/point — the fast-path case (and the spatial-join shape).
92+
benchmark::scalar(
93+
c,
94+
&f,
95+
"geo",
96+
"st_dwithin",
97+
ArrayArrayScalar(Point, Point, Float64(1.0, 2.0)),
98+
);
99+
benchmark::scalar(
100+
c,
101+
&f,
102+
"geo",
103+
"st_dwithin",
104+
ArrayArrayScalar(Point, Polygon(10), Float64(1.0, 2.0)),
105+
);
86106
}
87107

88108
fn criterion_benchmark_aggr(c: &mut Criterion) {

rust/sedona-geo/src/st_distance.rs

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ use arrow_array::builder::Float64Builder;
2020
use arrow_schema::DataType;
2121
use datafusion_common::error::Result;
2222
use datafusion_expr::ColumnarValue;
23+
use geo_types::Point;
2324
use sedona_expr::{
2425
item_crs::ItemCrsKernel,
2526
scalar_udf::{ScalarKernelRef, SedonaScalarKernel},
2627
};
27-
use sedona_functions::executor::WkbExecutor;
28+
use sedona_functions::executor::{PointOrWkb, PointXYExecutor};
2829
use sedona_geo_generic_alg::line_measures::DistanceExt;
2930
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
30-
use wkb::reader::Wkb;
3131

3232
/// ST_Distance() implementation using [DistanceExt]
3333
pub fn st_distance_impl() -> Vec<ScalarKernelRef> {
@@ -52,42 +52,60 @@ impl SedonaScalarKernel for STDistance {
5252
arg_types: &[SedonaType],
5353
args: &[ColumnarValue],
5454
) -> Result<ColumnarValue> {
55-
let executor = WkbExecutor::new(arg_types, args);
55+
// The PointXY executor decodes each operand as a `PointOrWkb`: a finite
56+
// Point yields its `(x, y)` straight from the WKB offset (no parse),
57+
// anything else is parsed. Scalars are decoded once and reused per row.
58+
let executor = PointXYExecutor::new(arg_types, args);
5659
let mut builder = Float64Builder::with_capacity(executor.num_iterations());
57-
executor.execute_wkb_wkb_void(|maybe_wkb0, maybe_wkb1| {
58-
match (maybe_wkb0, maybe_wkb1) {
59-
(Some(wkb0), Some(wkb1)) => {
60-
builder.append_value(invoke_scalar(wkb0, wkb1)?);
61-
}
60+
61+
executor.execute_wkb_wkb_void(|maybe0, maybe1| {
62+
match (maybe0, maybe1) {
63+
(Some(a), Some(b)) => builder.append_value(point_or_wkb_distance(a, b)),
6264
_ => builder.append_null(),
6365
}
64-
6566
Ok(())
6667
})?;
6768

6869
executor.finish(Arc::new(builder.finish()))
6970
}
7071
}
7172

72-
fn invoke_scalar(wkb_a: &Wkb, wkb_b: &Wkb) -> Result<f64> {
73-
Ok(wkb_a.distance_ext(wkb_b))
73+
/// Euclidean distance between two operands. Point-to-Point is computed directly
74+
/// from the coordinates; any other combination defers to the generic
75+
/// [DistanceExt] (a Point operand becomes a [`geo_types::Point`] so the mixed
76+
/// Point/geometry case never re-parses the Point). Shared with `ST_DWithin`.
77+
pub(crate) fn point_or_wkb_distance(a: &PointOrWkb, b: &PointOrWkb) -> f64 {
78+
match (a, b) {
79+
(PointOrWkb::Point(ax, ay), PointOrWkb::Point(bx, by)) => {
80+
let dx = ax - bx;
81+
let dy = ay - by;
82+
(dx * dx + dy * dy).sqrt()
83+
}
84+
(PointOrWkb::Point(x, y), PointOrWkb::Other(wkb)) => Point::new(*x, *y).distance_ext(wkb),
85+
(PointOrWkb::Other(wkb), PointOrWkb::Point(x, y)) => wkb.distance_ext(&Point::new(*x, *y)),
86+
(PointOrWkb::Other(wkb0), PointOrWkb::Other(wkb1)) => wkb0.distance_ext(wkb1),
87+
}
7488
}
7589

7690
#[cfg(test)]
7791
mod tests {
92+
use arrow_array::{ArrayRef, Float64Array};
7893
use datafusion_common::scalar::ScalarValue;
7994
use rstest::rstest;
8095
use sedona_expr::scalar_udf::SedonaScalarUDF;
8196
use sedona_schema::datatypes::{WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS, WKB_VIEW_GEOMETRY};
82-
use sedona_testing::create::create_scalar;
97+
use sedona_testing::compare::assert_array_equal;
98+
use sedona_testing::create::{create_array, create_scalar};
8399
use sedona_testing::testers::ScalarUdfTester;
84100

85101
use super::*;
86102

87103
#[rstest]
88104
fn udf(
89-
#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY, WKB_GEOMETRY_ITEM_CRS.clone())] left_sedona_type: SedonaType,
90-
#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY, WKB_GEOMETRY_ITEM_CRS.clone())] right_sedona_type: SedonaType,
105+
#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY, WKB_GEOMETRY_ITEM_CRS.clone())]
106+
left_sedona_type: SedonaType,
107+
#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY, WKB_GEOMETRY_ITEM_CRS.clone())]
108+
right_sedona_type: SedonaType,
91109
) {
92110
let udf = SedonaScalarUDF::from_impl("st_distance", st_distance_impl());
93111
let tester = ScalarUdfTester::new(
@@ -123,4 +141,65 @@ mod tests {
123141
.unwrap();
124142
assert!(result.is_null());
125143
}
144+
145+
/// Exercises the array/scalar fast-path routing: a scalar operand is parsed
146+
/// once and reused per row, the point-to-point fast path matches the general
147+
/// distance, and mixed Point/non-Point inputs fall back correctly. Covered
148+
/// across storage encodings, including item-CRS (which arrives unwrapped as
149+
/// plain WKB, so the parse-once scalar handling still applies).
150+
#[rstest]
151+
fn array_scalar_routing(
152+
#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY, WKB_GEOMETRY_ITEM_CRS.clone())] left: SedonaType,
153+
#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY, WKB_GEOMETRY_ITEM_CRS.clone())] right: SedonaType,
154+
) {
155+
let udf = SedonaScalarUDF::from_impl("st_distance", st_distance_impl());
156+
let tester = ScalarUdfTester::new(udf.into(), vec![left.clone(), right.clone()]);
157+
158+
// Array of Points vs a scalar Point: the point-to-point fast path.
159+
let result = tester
160+
.invoke_wkb_array_scalar(
161+
vec![Some("POINT (3 4)"), None, Some("POINT (0 0)")],
162+
create_scalar(Some("POINT (0 0)"), &right),
163+
)
164+
.unwrap();
165+
let expected: ArrayRef = Arc::new(Float64Array::from(vec![Some(5.0), None, Some(0.0)]));
166+
assert_array_equal(&result, &expected);
167+
168+
// Array of Polygons vs a scalar Point: the scalar is parsed once and
169+
// reused across rows; each row falls back to the general distance. The
170+
// scalar Point sits on a polygon vertex, so every distance is 0.
171+
let result = tester
172+
.invoke_wkb_array_scalar(
173+
vec![
174+
Some("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))"),
175+
Some("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))"),
176+
],
177+
create_scalar(Some("POINT (10 10)"), &right),
178+
)
179+
.unwrap();
180+
let expected: ArrayRef = Arc::new(Float64Array::from(vec![Some(0.0), Some(0.0)]));
181+
assert_array_equal(&result, &expected);
182+
183+
// Scalar Point vs array of Points (mirror orientation).
184+
let result = tester
185+
.invoke_scalar_array(
186+
create_scalar(Some("POINT (0 0)"), &left),
187+
create_array(&[Some("POINT (3 4)"), Some("POINT (0 0)")], &right),
188+
)
189+
.unwrap();
190+
let expected: ArrayRef = Arc::new(Float64Array::from(vec![Some(5.0), Some(0.0)]));
191+
assert_array_equal(&result, &expected);
192+
193+
// Array vs array with mixed Point / Polygon rows.
194+
let arg0 = create_array(
195+
&[
196+
Some("POINT (0 0)"),
197+
Some("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))"),
198+
],
199+
&left,
200+
);
201+
let arg1 = create_array(&[Some("POINT (3 4)"), Some("POINT (10 10)")], &right);
202+
let expected: ArrayRef = Arc::new(Float64Array::from(vec![Some(5.0), Some(0.0)]));
203+
assert_array_equal(&tester.invoke_array_array(arg0, arg1).unwrap(), &expected);
204+
}
126205
}

rust/sedona-geo/src/st_dwithin.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717
use std::sync::Arc;
1818

19+
use crate::st_distance::point_or_wkb_distance;
1920
use arrow_array::builder::BooleanBuilder;
2021
use arrow_schema::DataType;
2122
use datafusion_common::{cast::as_float64_array, error::Result};
@@ -24,10 +25,8 @@ use sedona_expr::{
2425
item_crs::ItemCrsKernel,
2526
scalar_udf::{ScalarKernelRef, SedonaScalarKernel},
2627
};
27-
use sedona_functions::executor::WkbExecutor;
28-
use sedona_geo_generic_alg::line_measures::DistanceExt;
28+
use sedona_functions::executor::PointXYExecutor;
2929
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
30-
use wkb::reader::Wkb;
3130

3231
/// ST_DWithin() implementation using [DistanceExt]
3332
pub fn st_dwithin_impl() -> Vec<ScalarKernelRef> {
@@ -57,15 +56,17 @@ impl SedonaScalarKernel for STDWithin {
5756
args: &[ColumnarValue],
5857
) -> Result<ColumnarValue> {
5958
let arg2 = args[2].cast_to(&DataType::Float64, None)?;
60-
let executor = WkbExecutor::new(arg_types, args);
59+
// Same PointXY fast path as ST_Distance's backend: Point/Point pairs
60+
// skip the parse; the bound comparison is `distance <= bound`.
61+
let executor = PointXYExecutor::new(arg_types, args);
6162
let arg2_array = arg2.to_array(executor.num_iterations())?;
6263
let arg2_f64_array = as_float64_array(&arg2_array)?;
6364
let mut arg2_iter = arg2_f64_array.iter();
6465
let mut builder = BooleanBuilder::with_capacity(executor.num_iterations());
65-
executor.execute_wkb_wkb_void(|maybe_wkb0, maybe_wkb1| {
66-
match (maybe_wkb0, maybe_wkb1, arg2_iter.next().unwrap()) {
67-
(Some(wkb0), Some(wkb1), Some(distance)) => {
68-
builder.append_value(invoke_scalar(wkb0, wkb1, distance)?);
66+
executor.execute_wkb_wkb_void(|maybe0, maybe1| {
67+
match (maybe0, maybe1, arg2_iter.next().unwrap()) {
68+
(Some(a), Some(b), Some(bound)) => {
69+
builder.append_value(point_or_wkb_distance(a, b) <= bound);
6970
}
7071
_ => builder.append_null(),
7172
}
@@ -77,11 +78,6 @@ impl SedonaScalarKernel for STDWithin {
7778
}
7879
}
7980

80-
fn invoke_scalar(wkb_a: &Wkb, wkb_b: &Wkb, distance_bound: f64) -> Result<bool> {
81-
let actual_distance = wkb_a.distance_ext(wkb_b);
82-
Ok(actual_distance <= distance_bound)
83-
}
84-
8581
#[cfg(test)]
8682
mod tests {
8783
use arrow_array::{create_array as arrow_array, ArrayRef};

0 commit comments

Comments
 (0)