@@ -20,14 +20,14 @@ use arrow_array::builder::Float64Builder;
2020use arrow_schema:: DataType ;
2121use datafusion_common:: error:: Result ;
2222use datafusion_expr:: ColumnarValue ;
23+ use geo_types:: Point ;
2324use 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 } ;
2829use sedona_geo_generic_alg:: line_measures:: DistanceExt ;
2930use sedona_schema:: { datatypes:: SedonaType , matchers:: ArgMatcher } ;
30- use wkb:: reader:: Wkb ;
3131
3232/// ST_Distance() implementation using [DistanceExt]
3333pub 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) ]
7791mod 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}
0 commit comments