You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ja...@apache.org on 2026/09/17 21:08:56 UTC
(sedona-db) branch main updated: feat(rust/sedona-raster-functions): RS_WorldToRasterCoord(X/Y) point-geometry overload (#1314)
This is an automated email from the ASF dual-hosted git repository.
james-willis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new 22623e6c2 feat(rust/sedona-raster-functions): RS_WorldToRasterCoord(X/Y) point-geometry overload (#1314)
22623e6c2 is described below
commit 22623e6c2d602127bf08a165527719ef321cf991
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Thu Sep 17 14:08:49 2026 -0700
feat(rust/sedona-raster-functions): RS_WorldToRasterCoord(X/Y) point-geometry overload (#1314)
---
docs/reference/sql/rs_worldtorastercoord.qmd | 14 ++
docs/reference/sql/rs_worldtorastercoordx.qmd | 14 ++
docs/reference/sql/rs_worldtorastercoordy.qmd | 14 ++
.../spark-parity/test_rs_worldtorastercoord.py | 15 +-
.../spark-parity/test_rs_worldtorastercoordx.py | 16 +-
.../spark-parity/test_rs_worldtorastercoordy.py | 16 +-
.../tests/functions/test_raster_functions.py | 33 +++
.../src/rs_rastercoordinate.rs | 238 ++++++++++++++++-----
8 files changed, 278 insertions(+), 82 deletions(-)
diff --git a/docs/reference/sql/rs_worldtorastercoord.qmd b/docs/reference/sql/rs_worldtorastercoord.qmd
index 40aa9c824..5fc4f2cd3 100644
--- a/docs/reference/sql/rs_worldtorastercoord.qmd
+++ b/docs/reference/sql/rs_worldtorastercoord.qmd
@@ -26,6 +26,14 @@ kernels:
type: double
- name: "y"
type: double
+ - returns: geometry
+ args:
+ - raster
+ - name: point
+ type: geometry
+ description: >
+ The world coordinate is read from this point. Its coordinates are used
+ as given, in the raster's own space — this form does not reproject.
---
## Examples
@@ -33,3 +41,9 @@ kernels:
```sql
SELECT RS_WorldToRasterCoord(RS_Example(), 10.0, 20.0);
```
+
+The world coordinate can also be given as a point:
+
+```sql
+SELECT RS_WorldToRasterCoord(RS_Example(), ST_Point(10.0, 20.0));
+```
diff --git a/docs/reference/sql/rs_worldtorastercoordx.qmd b/docs/reference/sql/rs_worldtorastercoordx.qmd
index 322156bc1..f667a35b2 100644
--- a/docs/reference/sql/rs_worldtorastercoordx.qmd
+++ b/docs/reference/sql/rs_worldtorastercoordx.qmd
@@ -26,6 +26,14 @@ kernels:
type: double
- name: "y"
type: double
+ - returns: integer
+ args:
+ - raster
+ - name: point
+ type: geometry
+ description: >
+ The world coordinate is read from this point. Its coordinates are used
+ as given, in the raster's own space — this form does not reproject.
---
## Examples
@@ -33,3 +41,9 @@ kernels:
```sql
SELECT RS_WorldToRasterCoordX(RS_Example(), 10.0, 20.0);
```
+
+The world coordinate can also be given as a point:
+
+```sql
+SELECT RS_WorldToRasterCoordX(RS_Example(), ST_Point(10.0, 20.0));
+```
diff --git a/docs/reference/sql/rs_worldtorastercoordy.qmd b/docs/reference/sql/rs_worldtorastercoordy.qmd
index c69017121..a0588a704 100644
--- a/docs/reference/sql/rs_worldtorastercoordy.qmd
+++ b/docs/reference/sql/rs_worldtorastercoordy.qmd
@@ -26,6 +26,14 @@ kernels:
type: double
- name: "y"
type: double
+ - returns: integer
+ args:
+ - raster
+ - name: point
+ type: geometry
+ description: >
+ The world coordinate is read from this point. Its coordinates are used
+ as given, in the raster's own space — this form does not reproject.
---
## Examples
@@ -33,3 +41,9 @@ kernels:
```sql
SELECT RS_WorldToRasterCoordY(RS_Example(), 10.0, 20.0);
```
+
+The world coordinate can also be given as a point:
+
+```sql
+SELECT RS_WorldToRasterCoordY(RS_Example(), ST_Point(10.0, 20.0));
+```
diff --git a/integration/spark-parity/test_rs_worldtorastercoord.py b/integration/spark-parity/test_rs_worldtorastercoord.py
index 1bfd39765..bbd9691a3 100644
--- a/integration/spark-parity/test_rs_worldtorastercoord.py
+++ b/integration/spark-parity/test_rs_worldtorastercoord.py
@@ -23,10 +23,9 @@ case is a cataloged divergence: SedonaDB treats pixel coordinates as
0-based where Sedona Spark (following PostGIS, and SedonaDB's own
RS_PixelAs* functions) is 1-based (apache/sedona-db#1235) — and on a
fractional negative index the engines also disagree on rounding, so
-outside the grid the answers are not even a uniform pixel apart.
-Sedona Spark additionally accepts a point geometry in place of the
-(x, y) pair, an overload SedonaDB lacks (the X/Y variants share both
-gaps).
+outside the grid the answers are not even a uniform pixel apart. Both
+engines also accept a point geometry in place of the (x, y) pair, and
+that form carries the same 0- vs 1-based divergence.
"""
import pytest
@@ -79,10 +78,10 @@ def test_rs_worldtorastercoord_outside(tmp_path):
@pytest.mark.xfail(
- reason="SedonaDB has no point-geometry overload of RS_WorldToRasterCoord "
- "(it raises 'No kernel matching arguments'); Sedona Spark accepts "
- "(raster, point) and answers 1-based, so once the overload exists the "
- "0- vs 1-based divergence (apache/sedona-db#1235) still applies"
+ reason="both engines now accept the (raster, point) overload, but SedonaDB "
+ "reads pixel coordinates 0-based where Sedona Spark is 1-based "
+ "(apache/sedona-db#1235), so the point form lands one pixel apart, exactly "
+ "like the numeric form"
)
def test_rs_worldtorastercoord_point_overload(tmp_path):
"""The point-geometry form maps the interior point (104 494) to the
diff --git a/integration/spark-parity/test_rs_worldtorastercoordx.py b/integration/spark-parity/test_rs_worldtorastercoordx.py
index 7376db3c4..1ee057221 100644
--- a/integration/spark-parity/test_rs_worldtorastercoordx.py
+++ b/integration/spark-parity/test_rs_worldtorastercoordx.py
@@ -19,11 +19,9 @@
Every case is a cataloged divergence: SedonaDB treats pixel coordinates
as 0-based where Sedona Spark (following PostGIS, and SedonaDB's own
RS_PixelAs* functions) is 1-based, so results are exactly one pixel
-apart everywhere, extrapolation included (apache/sedona-db#1235). Sedona
-Spark also accepts a point geometry in place of the (x, y) pair, an
-overload SedonaDB lacks entirely. The geometry-returning combined form
-is deferred until the harness can compare geometry columns without ST_
-wrappers.
+apart everywhere, extrapolation included (apache/sedona-db#1235). Both
+engines also accept a point geometry in place of the (x, y) pair, and
+that form carries the same 0- vs 1-based divergence.
"""
import pytest
@@ -50,10 +48,10 @@ def test_rs_worldtorastercoordx(x, y, tmp_path):
@pytest.mark.xfail(
- reason="SedonaDB has no point-geometry overload of RS_WorldToRasterCoordX "
- "(it raises 'No kernel matching arguments'); Sedona Spark accepts "
- "(raster, point) and answers 1-based, so once the overload exists the "
- "0- vs 1-based divergence (apache/sedona-db#1235) still applies"
+ reason="both engines now accept the (raster, point) overload, but SedonaDB "
+ "reads pixel coordinates 0-based where Sedona Spark is 1-based "
+ "(apache/sedona-db#1235), so the point form diverges by one column, exactly "
+ "like the numeric form"
)
def test_rs_worldtorastercoordx_point_overload(tmp_path):
"""The point-geometry form maps the interior point (104 494) to the
diff --git a/integration/spark-parity/test_rs_worldtorastercoordy.py b/integration/spark-parity/test_rs_worldtorastercoordy.py
index a49014c15..a7ac96ef9 100644
--- a/integration/spark-parity/test_rs_worldtorastercoordy.py
+++ b/integration/spark-parity/test_rs_worldtorastercoordy.py
@@ -19,11 +19,9 @@
Every case is a cataloged divergence: SedonaDB treats pixel coordinates
as 0-based where Sedona Spark (following PostGIS, and SedonaDB's own
RS_PixelAs* functions) is 1-based, so results are exactly one pixel
-apart everywhere, extrapolation included (apache/sedona-db#1235). Sedona
-Spark also accepts a point geometry in place of the (x, y) pair, an
-overload SedonaDB lacks entirely. The geometry-returning combined form
-is deferred until the harness can compare geometry columns without ST_
-wrappers.
+apart everywhere, extrapolation included (apache/sedona-db#1235). Both
+engines also accept a point geometry in place of the (x, y) pair, and
+that form carries the same 0- vs 1-based divergence.
"""
import pytest
@@ -46,10 +44,10 @@ def test_rs_worldtorastercoordy(tmp_path):
@pytest.mark.xfail(
- reason="SedonaDB has no point-geometry overload of RS_WorldToRasterCoordY "
- "(it raises 'No kernel matching arguments'); Sedona Spark accepts "
- "(raster, point) and answers 1-based, so once the overload exists the "
- "0- vs 1-based divergence (apache/sedona-db#1235) still applies"
+ reason="both engines now accept the (raster, point) overload, but SedonaDB "
+ "reads pixel coordinates 0-based where Sedona Spark is 1-based "
+ "(apache/sedona-db#1235), so the point form diverges by one row, exactly "
+ "like the numeric form"
)
def test_rs_worldtorastercoordy_point_overload(tmp_path):
"""The point-geometry form maps the interior point (104 494) to the
diff --git a/python/sedonadb/tests/functions/test_raster_functions.py b/python/sedonadb/tests/functions/test_raster_functions.py
index adac69ee3..6bf939c0c 100644
--- a/python/sedonadb/tests/functions/test_raster_functions.py
+++ b/python/sedonadb/tests/functions/test_raster_functions.py
@@ -150,6 +150,39 @@ def test_rs_value_default_band_requires_single_band(con):
).to_arrow_table()
+# RS_WorldToRasterCoord and its X/Y variants map a world coordinate into the
+# raster's pixel space. The two argument forms — a pair of ordinates, or a
+# single point geometry — are the same mapping, so both are exercised against
+# the same expectation. RS_Example is skewed (scale 2, skew 1), and (10, 20)
+# maps outside the grid: the mapping extrapolates rather than clamping.
+@pytest.mark.parametrize(
+ "coord",
+ [
+ pytest.param("10.0, 20.0", id="ordinates"),
+ pytest.param("ST_Point(10.0, 20.0)", id="point"),
+ ],
+)
+@pytest.mark.parametrize(
+ ("fn", "expected"),
+ [
+ ("RS_WorldToRasterCoordX", -2),
+ ("RS_WorldToRasterCoordY", -28),
+ ("RS_WorldToRasterCoord", "POINT (-2 -28)"),
+ ],
+)
+def test_rs_worldtorastercoord(fn, expected, coord):
+ SedonaDB().assert_query_result(f"SELECT {fn}(RS_Example(), {coord})", expected)
+
+
+def test_rs_worldtorastercoord_empty_point_is_null():
+ # POINT EMPTY has no location to map, so it yields NULL rather than an
+ # error — matching how the two-ordinate form treats a null ordinate.
+ SedonaDB().assert_query_result(
+ "SELECT RS_WorldToRasterCoordX(RS_Example(), ST_GeomFromText('POINT EMPTY'))",
+ None,
+ )
+
+
def test_rs_value_matches_rasterio(con):
"""Cross-check RS_Value against rasterio on a random raster.
diff --git a/rust/sedona-raster-functions/src/rs_rastercoordinate.rs b/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
index 59e688074..e0024b395 100644
--- a/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
+++ b/rust/sedona-raster-functions/src/rs_rastercoordinate.rs
@@ -17,15 +17,21 @@
use std::{sync::Arc, vec};
use crate::executor::RasterExecutor;
+use arrow_array::Float64Array;
use arrow_array::builder::{BinaryBuilder, Int64Builder};
use arrow_schema::DataType;
use datafusion_common::cast::as_float64_array;
use datafusion_common::error::Result;
+use datafusion_common::exec_datafusion_err;
use datafusion_expr::{ColumnarValue, Volatility};
use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
use sedona_geometry::types::Edges;
+use sedona_geometry::wkb_header::read_point_xy;
use sedona_raster::affine_transformation::to_raster_coordinate;
-use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+use sedona_schema::{
+ datatypes::SedonaType,
+ matchers::{ArgMatcher, TypeMatcher},
+};
/// RS_WorldToRasterCoordY() scalar UDF documentation
///
@@ -33,7 +39,16 @@ use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
pub fn rs_worldtorastercoordy_udf() -> SedonaScalarUDF {
SedonaScalarUDF::new(
"rs_worldtorastercoordy",
- vec![Arc::new(RsCoordinateMapper { coord: Coord::Y })],
+ vec![
+ Arc::new(RsCoordinateMapper {
+ coord: Coord::Y,
+ from_point: false,
+ }),
+ Arc::new(RsCoordinateMapper {
+ coord: Coord::Y,
+ from_point: true,
+ }),
+ ],
Volatility::Immutable,
)
}
@@ -44,7 +59,16 @@ pub fn rs_worldtorastercoordy_udf() -> SedonaScalarUDF {
pub fn rs_worldtorastercoordx_udf() -> SedonaScalarUDF {
SedonaScalarUDF::new(
"rs_worldtorastercoordx",
- vec![Arc::new(RsCoordinateMapper { coord: Coord::X })],
+ vec![
+ Arc::new(RsCoordinateMapper {
+ coord: Coord::X,
+ from_point: false,
+ }),
+ Arc::new(RsCoordinateMapper {
+ coord: Coord::X,
+ from_point: true,
+ }),
+ ],
Volatility::Immutable,
)
}
@@ -55,7 +79,10 @@ pub fn rs_worldtorastercoordx_udf() -> SedonaScalarUDF {
pub fn rs_worldtorastercoord_udf() -> SedonaScalarUDF {
SedonaScalarUDF::new(
"rs_worldtorastercoord",
- vec![Arc::new(RsCoordinatePoint {})],
+ vec![
+ Arc::new(RsCoordinatePoint { from_point: false }),
+ Arc::new(RsCoordinatePoint { from_point: true }),
+ ],
Volatility::Immutable,
)
}
@@ -66,22 +93,30 @@ enum Coord {
Y,
}
+impl Coord {
+ fn pick(&self, raster_x: i64, raster_y: i64) -> i64 {
+ match self {
+ Coord::X => raster_x,
+ Coord::Y => raster_y,
+ }
+ }
+}
+
#[derive(Debug)]
struct RsCoordinateMapper {
coord: Coord,
+ /// `false`: `(raster, worldX, worldY)`. `true`: the Sedona Spark parity
+ /// overload `(raster, point)`, which reads the world coordinate from a
+ /// point geometry.
+ from_point: bool,
}
impl SedonaScalarKernel for RsCoordinateMapper {
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
let matcher = ArgMatcher::new(
- vec![
- ArgMatcher::is_raster(),
- ArgMatcher::is_numeric(),
- ArgMatcher::is_numeric(),
- ],
+ coord_input_matchers(self.from_point),
SedonaType::Arrow(DataType::Int64),
);
-
matcher.match_args(args)
}
@@ -93,29 +128,12 @@ impl SedonaScalarKernel for RsCoordinateMapper {
let executor = RasterExecutor::new(arg_types, args);
let mut builder = Int64Builder::with_capacity(executor.num_iterations());
- // Expand world x and y coordinate parameters to arrays and cast to Float64
- let world_x_array = args[1].clone().cast_to(&DataType::Float64, None)?;
- let world_x_array = world_x_array.into_array(executor.num_iterations())?;
- let world_x_array = as_float64_array(&world_x_array)?;
- let world_y_array = args[2].clone().cast_to(&DataType::Float64, None)?;
- let world_y_array = world_y_array.into_array(executor.num_iterations())?;
- let world_y_array = as_float64_array(&world_y_array)?;
- let mut world_x_iter = world_x_array.iter();
- let mut world_y_iter = world_y_array.iter();
-
- executor.execute_raster_void(|_i, raster_opt| {
- let x_opt = world_x_iter.next().unwrap();
- let y_opt = world_y_iter.next().unwrap();
-
- match (raster_opt, x_opt, y_opt) {
- (Some(raster), Some(x), Some(y)) => {
- let (raster_x, raster_y) = to_raster_coordinate(raster, x, y)?;
- match self.coord {
- Coord::X => builder.append_value(raster_x),
- Coord::Y => builder.append_value(raster_y),
- };
+ visit_raster_coordinates(&executor, args, self.from_point, |coord| {
+ match coord {
+ Some((raster_x, raster_y)) => {
+ builder.append_value(self.coord.pick(raster_x, raster_y))
}
- (_, _, _) => builder.append_null(),
+ None => builder.append_null(),
}
Ok(())
})?;
@@ -125,15 +143,15 @@ impl SedonaScalarKernel for RsCoordinateMapper {
}
#[derive(Debug)]
-struct RsCoordinatePoint;
+struct RsCoordinatePoint {
+ /// `false`: `(raster, worldX, worldY)`. `true`: the Sedona Spark parity
+ /// overload `(raster, point)`.
+ from_point: bool,
+}
impl SedonaScalarKernel for RsCoordinatePoint {
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
let matcher = ArgMatcher::new(
- vec![
- ArgMatcher::is_raster(),
- ArgMatcher::is_numeric(),
- ArgMatcher::is_numeric(),
- ],
+ coord_input_matchers(self.from_point),
SedonaType::Wkb(Edges::Planar, None),
);
@@ -154,28 +172,14 @@ impl SedonaScalarKernel for RsCoordinatePoint {
item.len() * executor.num_iterations(),
);
- // Expand world x and y coordinate parameters to arrays and cast to Float64
- let world_x_array = args[1].clone().cast_to(&DataType::Float64, None)?;
- let world_x_array = world_x_array.into_array(executor.num_iterations())?;
- let world_x_array = as_float64_array(&world_x_array)?;
- let world_y_array = args[2].clone().cast_to(&DataType::Float64, None)?;
- let world_y_array = world_y_array.into_array(executor.num_iterations())?;
- let world_y_array = as_float64_array(&world_y_array)?;
- let mut world_x_iter = world_x_array.iter();
- let mut world_y_iter = world_y_array.iter();
-
- executor.execute_raster_void(|_i, raster_opt| {
- let x_opt = world_x_iter.next().unwrap();
- let y_opt = world_y_iter.next().unwrap();
-
- match (raster_opt, x_opt, y_opt) {
- (Some(raster), Some(world_x), Some(world_y)) => {
- let (raster_x, raster_y) = to_raster_coordinate(raster, world_x, world_y)?;
+ visit_raster_coordinates(&executor, args, self.from_point, |coord| {
+ match coord {
+ Some((raster_x, raster_y)) => {
item[5..13].copy_from_slice(&(raster_x as f64).to_le_bytes());
item[13..21].copy_from_slice(&(raster_y as f64).to_le_bytes());
builder.append_value(item);
}
- (_, _, _) => builder.append_null(),
+ None => builder.append_null(),
}
Ok(())
})?;
@@ -184,6 +188,91 @@ impl SedonaScalarKernel for RsCoordinatePoint {
}
}
+/// Visit each row's world coordinate mapped into the raster's `(col, row)`
+/// space, from whichever input form the kernel takes.
+///
+/// This is the whole difference between the two input forms — the numeric
+/// `(raster, worldX, worldY)` and the `(raster, point)` overload — so the
+/// kernels differ only in what they build from the result. `visit` receives
+/// `None` for a row with no coordinate to map: a null raster, a null or empty
+/// point, or a null ordinate.
+fn visit_raster_coordinates(
+ executor: &RasterExecutor,
+ args: &[ColumnarValue],
+ from_point: bool,
+ mut visit: impl FnMut(Option<(i64, i64)>) -> Result<()>,
+) -> Result<()> {
+ if from_point {
+ executor.execute_raster_wkb_crs_void(|raster_opt, wkb_opt, _crs| {
+ match (raster_opt, point_xy(wkb_opt)?) {
+ (Some(raster), Some((x, y))) => visit(Some(to_raster_coordinate(raster, x, y)?)),
+ _ => visit(None),
+ }
+ })
+ } else {
+ let (world_x, world_y) = world_xy_arrays(args, executor.num_iterations())?;
+ let mut world_x_iter = world_x.iter();
+ let mut world_y_iter = world_y.iter();
+ executor.execute_raster_void(|_i, raster_opt| {
+ // Both ordinate iterators advance every row, null raster included,
+ // so they stay in lockstep with the row index.
+ match (
+ raster_opt,
+ world_x_iter.next().unwrap(),
+ world_y_iter.next().unwrap(),
+ ) {
+ (Some(raster), Some(x), Some(y)) => {
+ visit(Some(to_raster_coordinate(raster, x, y)?))
+ }
+ (_, _, _) => visit(None),
+ }
+ })
+ }
+}
+
+/// Input matchers for a world→raster coordinate function: the numeric
+/// `(raster, worldX, worldY)` form, or the `(raster, point)` overload.
+fn coord_input_matchers(from_point: bool) -> Vec<Arc<dyn TypeMatcher + Send + Sync>> {
+ if from_point {
+ vec![
+ ArgMatcher::is_raster(),
+ ArgMatcher::is_geometry_or_geography(),
+ ]
+ } else {
+ vec![
+ ArgMatcher::is_raster(),
+ ArgMatcher::is_numeric(),
+ ArgMatcher::is_numeric(),
+ ]
+ }
+}
+
+/// The world `(x, y)` carried by a point geometry, or `None` for a null or
+/// empty point. Errors on a non-point geometry. The point's coordinates are
+/// used as-is, in the raster's own space — the numeric form takes no CRS
+/// either, so the point overload does not reproject.
+fn point_xy(wkb_opt: Option<&[u8]>) -> Result<Option<(f64, f64)>> {
+ match wkb_opt {
+ Some(wkb) => {
+ read_point_xy(wkb).map_err(|e| exec_datafusion_err!("RS_WorldToRasterCoord: {e}"))
+ }
+ None => Ok(None),
+ }
+}
+
+/// The world X and Y numeric arguments (args 1 and 2) as Float64 row arrays.
+fn world_xy_arrays(args: &[ColumnarValue], n: usize) -> Result<(Float64Array, Float64Array)> {
+ let x = args[1]
+ .clone()
+ .cast_to(&DataType::Float64, None)?
+ .into_array(n)?;
+ let y = args[2]
+ .clone()
+ .cast_to(&DataType::Float64, None)?
+ .into_array(n)?;
+ Ok((as_float64_array(&x)?.clone(), as_float64_array(&y)?.clone()))
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -254,6 +343,43 @@ mod tests {
);
}
+ #[rstest]
+ fn udf_invoke_xy_from_point(#[values(Coord::Y, Coord::X)] coord: Coord) {
+ // The (raster, point) overload reads the world coordinate from a point
+ // geometry and answers exactly like the numeric form: world (2, 3) is
+ // raster 1's origin -> raster coordinate (0, 0). Raster 0 is nulled.
+ let udf = match coord {
+ Coord::X => rs_worldtorastercoordx_udf(),
+ Coord::Y => rs_worldtorastercoordy_udf(),
+ };
+ let tester = ScalarUdfTester::new(udf.into(), vec![RASTER, WKB_GEOMETRY]);
+ let rasters = generate_test_rasters(2, Some(0)).unwrap();
+ let points = create_array(&[Some("POINT (2 3)"), Some("POINT (2 3)")], &WKB_GEOMETRY);
+ let expected: Arc<dyn arrow_array::Array> =
+ Arc::new(arrow_array::Int64Array::from(vec![None, Some(0_i64)]));
+ let result = tester
+ .invoke_array_array(Arc::new(rasters), points)
+ .unwrap();
+ assert_array_equal(&result, &expected);
+ }
+
+ #[rstest]
+ fn udf_invoke_pt_from_point() {
+ // The combined (raster, point) overload returns the raster-coordinate
+ // point, matching the numeric form: world (2, 3) -> POINT (0 0).
+ let tester = ScalarUdfTester::new(
+ rs_worldtorastercoord_udf().into(),
+ vec![RASTER, WKB_GEOMETRY],
+ );
+ let rasters = generate_test_rasters(2, Some(0)).unwrap();
+ let points = create_array(&[Some("POINT (2 3)"), Some("POINT (2 3)")], &WKB_GEOMETRY);
+ let expected = &create_array(&[None, Some("POINT (0 0)")], &WKB_GEOMETRY);
+ let result = tester
+ .invoke_array_array(Arc::new(rasters), points)
+ .unwrap();
+ assert_array_equal(&result, expected);
+ }
+
#[rstest]
fn udf_invoke_pt() {
let udf = rs_worldtorastercoord_udf();