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/08/02 17:43:13 UTC
(sedona-db) branch main updated: Route ST_Transform and sd_order through the injected CRS engine (#1102)
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 8e72ad66e Route ST_Transform and sd_order through the injected CRS engine (#1102)
8e72ad66e is described below
commit 8e72ad66ecc0605b61e1ac3b5e08ccf0ce809579
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Sun Aug 2 10:43:07 2026 -0700
Route ST_Transform and sd_order through the injected CRS engine (#1102)
---
Cargo.lock | 9 +--
c/sedona-proj/Cargo.toml | 13 ----
c/sedona-proj/src/lib.rs | 2 -
c/sedona-proj/src/register.rs | 13 ----
rust/sedona-common/src/option.rs | 24 +++++++-
rust/sedona-functions/Cargo.toml | 5 ++
.../sedona-functions}/benches/proj-functions.rs | 19 ++++--
rust/sedona-functions/src/lib.rs | 2 +
rust/sedona-functions/src/register.rs | 1 +
.../sedona-functions}/src/sd_order_lnglat.rs | 27 +++++++--
.../sedona-functions}/src/st_transform.rs | 54 +++++++++++------
rust/sedona-raster-functions/Cargo.toml | 1 -
rust/sedona-raster-functions/src/crs_utils.rs | 20 +++----
rust/sedona-raster-gdal/src/rs_clip.rs | 70 ++++++++++------------
rust/sedona-testing/src/benchmark_util.rs | 36 ++++++++++-
rust/sedona/src/context.rs | 7 +--
16 files changed, 176 insertions(+), 127 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index aefe6c10a..4009e2125 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5927,6 +5927,7 @@ dependencies = [
"sedona-common",
"sedona-expr",
"sedona-geometry",
+ "sedona-proj",
"sedona-raster",
"sedona-schema",
"sedona-testing",
@@ -6171,22 +6172,14 @@ name = "sedona-proj"
version = "0.4.0"
dependencies = [
"approx",
- "arrow-array",
- "arrow-schema",
- "criterion",
"datafusion-common",
- "datafusion-expr",
"geo-traits",
"geo-types",
"libloading 0.9.0",
"proj-sys",
- "rstest",
"sedona-common",
- "sedona-expr",
- "sedona-functions",
"sedona-geometry",
"sedona-schema",
- "sedona-testing",
"serde_json",
"thiserror 2.0.17",
"wkb",
diff --git a/c/sedona-proj/Cargo.toml b/c/sedona-proj/Cargo.toml
index 072f15bbc..722a9a50b 100644
--- a/c/sedona-proj/Cargo.toml
+++ b/c/sedona-proj/Cargo.toml
@@ -31,10 +31,7 @@ rust-version.workspace = true
[dev-dependencies]
approx = { workspace = true }
geo-types = { workspace = true }
-rstest = { workspace = true }
wkb = { workspace = true }
-sedona-testing = { workspace = true, features = ["criterion"] }
-criterion = { workspace = true}
[features]
default = [ "proj-sys" ]
@@ -43,20 +40,10 @@ proj-sys = [ "dep:proj-sys" ]
[dependencies]
libloading = { workspace = true }
serde_json = { workspace = true }
-arrow-schema = { workspace = true }
-arrow-array = { workspace = true }
datafusion-common = { workspace = true }
-datafusion-expr = { workspace = true }
geo-traits = { workspace = true }
proj-sys = { version = "0.26.0", optional = true }
sedona-common = { workspace = true }
-sedona-expr = { workspace = true }
-sedona-functions = { workspace = true }
sedona-geometry = { workspace = true }
sedona-schema = { workspace = true }
thiserror = { workspace = true }
-wkb = { workspace = true }
-
-[[bench]]
-name = "proj-functions"
-harness = false
diff --git a/c/sedona-proj/src/lib.rs b/c/sedona-proj/src/lib.rs
index c1ed30c2f..6f5a78492 100644
--- a/c/sedona-proj/src/lib.rs
+++ b/c/sedona-proj/src/lib.rs
@@ -19,6 +19,4 @@ pub mod error;
mod proj;
mod proj_dyn_bindgen;
pub mod register;
-pub mod sd_order_lnglat;
-mod st_transform;
pub mod transform;
diff --git a/c/sedona-proj/src/register.rs b/c/sedona-proj/src/register.rs
index 11fa0ea9f..07392a56f 100644
--- a/c/sedona-proj/src/register.rs
+++ b/c/sedona-proj/src/register.rs
@@ -14,18 +14,5 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-use sedona_expr::aggregate_udf::SedonaAccumulatorRef;
-use sedona_expr::scalar_udf::ScalarKernelRef;
-
-use crate::st_transform::st_transform_impl;
-
pub use crate::transform::configure_global_proj_engine;
pub use crate::transform::ProjCrsEngineBuilder;
-
-pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> {
- vec![("st_transform", st_transform_impl())]
-}
-
-pub fn aggregate_kernels() -> Vec<(&'static str, SedonaAccumulatorRef)> {
- vec![]
-}
diff --git a/rust/sedona-common/src/option.rs b/rust/sedona-common/src/option.rs
index c935d4d00..bf9cda0f0 100644
--- a/rust/sedona-common/src/option.rs
+++ b/rust/sedona-common/src/option.rs
@@ -19,7 +19,7 @@ use std::rc::Rc;
use std::sync::Arc;
use datafusion_common::config::{
- ConfigEntry, ConfigExtension, ConfigField, ExtensionOptions, Visit,
+ ConfigEntry, ConfigExtension, ConfigField, ConfigOptions, ExtensionOptions, Visit,
};
use datafusion_common::Result;
use datafusion_common::{config_err, config_namespace};
@@ -545,6 +545,28 @@ impl CrsEngine for DefaultCrsEngine {
}
}
+/// Run `f` with the session's CRS engine.
+///
+/// When `config_options` carries a [`SedonaOptions`] extension (the query
+/// path), the engine configured in its runtime is used. Otherwise (e.g. a
+/// direct `invoke_batch` call, or a context without a configured runtime) this
+/// falls back to [`DefaultCrsEngine`], which errors when no engine has been
+/// registered.
+///
+/// Keeping this `SedonaOptions` -> runtime engine extraction in `sedona-common`
+/// lets geometry and raster CRS functions honor an injected engine without
+/// depending on a concrete engine implementation such as `sedona-proj`.
+pub fn with_crs_engine<T>(
+ config_options: Option<&ConfigOptions>,
+ f: impl FnOnce(&dyn CrsEngine) -> Result<T>,
+) -> Result<T> {
+ if let Some(options) = config_options.and_then(|o| o.extensions.get::<SedonaOptions>()) {
+ f(options.runtime.crs_engine().as_ref())
+ } else {
+ f(&DefaultCrsEngine {})
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/rust/sedona-functions/Cargo.toml b/rust/sedona-functions/Cargo.toml
index 365687dfd..ace14bd56 100644
--- a/rust/sedona-functions/Cargo.toml
+++ b/rust/sedona-functions/Cargo.toml
@@ -34,6 +34,7 @@ result_large_err = "allow"
arrow-json = { workspace = true }
criterion = { workspace = true }
rstest = { workspace = true }
+sedona-proj = { workspace = true, features = ["proj-sys"] }
sedona-testing = { workspace = true, features = ["criterion"] }
datafusion = { workspace = true, features = ["sql"] }
tokio = { workspace = true, features = ["rt", "macros"] }
@@ -58,3 +59,7 @@ glam = { workspace = true }
[[bench]]
harness = false
name = "native-functions"
+
+[[bench]]
+harness = false
+name = "proj-functions"
diff --git a/c/sedona-proj/benches/proj-functions.rs b/rust/sedona-functions/benches/proj-functions.rs
similarity index 78%
rename from c/sedona-proj/benches/proj-functions.rs
rename to rust/sedona-functions/benches/proj-functions.rs
index c06764bf9..503c2c511 100644
--- a/c/sedona-proj/benches/proj-functions.rs
+++ b/rust/sedona-functions/benches/proj-functions.rs
@@ -14,15 +14,15 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
+use std::sync::Arc;
+
use criterion::{criterion_group, criterion_main, Criterion};
-use sedona_expr::function_set::FunctionSet;
+use sedona_functions::register::default_function_set;
+use sedona_proj::transform::LazyProjEngine;
use sedona_testing::benchmark_util::{benchmark, BenchmarkArgSpec::*, BenchmarkArgs};
fn criterion_benchmark(c: &mut Criterion) {
- let mut f = FunctionSet::new();
- for (name, kernel) in sedona_proj::register::scalar_kernels() {
- f.add_scalar_udf_impl(name, kernel).unwrap();
- }
+ let f = default_function_set();
let args = BenchmarkArgs::ArrayScalarScalar(
Point,
@@ -30,7 +30,14 @@ fn criterion_benchmark(c: &mut Criterion) {
String("EPSG:3857".to_string()),
);
- benchmark::scalar(c, &f, "sedona-proj", "st_transform", args);
+ benchmark::scalar_with_crs_engine(
+ c,
+ &f,
+ "sedona-functions",
+ "st_transform",
+ args,
+ Some(Arc::new(LazyProjEngine)),
+ );
}
criterion_group!(benches, criterion_benchmark);
diff --git a/rust/sedona-functions/src/lib.rs b/rust/sedona-functions/src/lib.rs
index c0d565a1d..59a5d3a7b 100644
--- a/rust/sedona-functions/src/lib.rs
+++ b/rust/sedona-functions/src/lib.rs
@@ -19,6 +19,7 @@ pub mod executor;
pub mod register;
mod sd_format;
pub mod sd_order;
+pub mod sd_order_lnglat;
mod sd_simplifystorage;
mod st_affine;
mod st_affine_helpers;
@@ -61,6 +62,7 @@ pub mod st_setsrid;
mod st_srid;
mod st_start_point;
mod st_togeomgeog;
+mod st_transform;
mod st_translate;
mod st_xyzm;
mod st_xyzm_minmax;
diff --git a/rust/sedona-functions/src/register.rs b/rust/sedona-functions/src/register.rs
index d7e18af0f..18271e131 100644
--- a/rust/sedona-functions/src/register.rs
+++ b/rust/sedona-functions/src/register.rs
@@ -101,6 +101,7 @@ pub fn default_function_set() -> FunctionSet {
crate::st_start_point::st_start_point_udf,
crate::st_togeomgeog::st_togeography_udf,
crate::st_togeomgeog::st_togeometry_udf,
+ crate::st_transform::st_transform_udf,
crate::st_translate::st_translate_udf,
crate::st_xyzm_minmax::st_mmax_udf,
crate::st_xyzm_minmax::st_mmin_udf,
diff --git a/c/sedona-proj/src/sd_order_lnglat.rs b/rust/sedona-functions/src/sd_order_lnglat.rs
similarity index 89%
rename from c/sedona-proj/src/sd_order_lnglat.rs
rename to rust/sedona-functions/src/sd_order_lnglat.rs
index 6e7f6db9d..e6c3ef062 100644
--- a/c/sedona-proj/src/sd_order_lnglat.rs
+++ b/rust/sedona-functions/src/sd_order_lnglat.rs
@@ -19,14 +19,15 @@ use std::{fmt::Debug, sync::Arc};
use arrow_array::builder::UInt64Builder;
use arrow_schema::DataType;
+use datafusion_common::config::ConfigOptions;
use datafusion_common::{DataFusionError, Result};
use datafusion_expr::ColumnarValue;
+use sedona_common::option::with_crs_engine;
use sedona_expr::scalar_udf::SedonaScalarKernel;
-use sedona_functions::executor::WkbBytesExecutor;
-use sedona_geometry::{transform::CrsEngine, wkb_header::WkbHeader};
+use sedona_geometry::wkb_header::WkbHeader;
use sedona_schema::{crs::lnglat, datatypes::SedonaType, matchers::ArgMatcher};
-use crate::transform::with_global_proj_engine;
+use crate::executor::WkbBytesExecutor;
/// Generic scalar kernel for sd_order based on the first coordinate
/// of a geometry projected to lon/lat
@@ -65,6 +66,17 @@ impl<F: Fn((f64, f64)) -> u64 + Send + Sync> SedonaScalarKernel for OrderLngLat<
&self,
arg_types: &[SedonaType],
args: &[ColumnarValue],
+ ) -> Result<ColumnarValue> {
+ self.invoke_batch_from_args(arg_types, args, &SedonaType::Arrow(DataType::Null), 0, None)
+ }
+
+ fn invoke_batch_from_args(
+ &self,
+ arg_types: &[SedonaType],
+ args: &[ColumnarValue],
+ _return_type: &SedonaType,
+ _num_rows: usize,
+ config_options: Option<&ConfigOptions>,
) -> Result<ColumnarValue> {
// Extract the source CRS, checking for lon/lat to see if we can avoid
// a transformation. If the CRS is missing we also skip any particular
@@ -87,7 +99,7 @@ impl<F: Fn((f64, f64)) -> u64 + Send + Sync> SedonaScalarKernel for OrderLngLat<
// this to be used even if PROJ isn't available (as long as the data were lon/lat
// already).
if let Some(src_crs) = maybe_src_crs {
- with_global_proj_engine(|engine| {
+ with_crs_engine(config_options, |engine| {
let to_lnglat = engine
.get_transform_crs_to_crs(&src_crs, "OGC:CRS84", None, "")
.map_err(|e| DataFusionError::Execution(format!("{e}")))?;
@@ -143,6 +155,7 @@ mod test {
use sedona_testing::{create::create_array, testers::ScalarUdfTester};
use super::*;
+ use sedona_proj::transform::LazyProjEngine;
#[test]
fn order_geometry() {
@@ -189,12 +202,14 @@ mod test {
create_array!(UInt64, [Some(111320), Some(u64::MAX), None, Some(0)]) as ArrayRef;
assert_eq!(&result, &expected);
- // Check the "not already lnglat" case
+ // Check the "not already lnglat" case, which reprojects to lon/lat via
+ // the injected CRS engine (LazyProjEngine delegates to global PROJ).
let crs = sedona_schema::crs::deserialize_crs("EPSG:3857").unwrap();
let tester = ScalarUdfTester::new(
udf.clone().into(),
vec![SedonaType::Wkb(Edges::Planar, crs)],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
tester.assert_return_type(DataType::UInt64);
let result = tester.invoke_array(array.clone()).unwrap();
diff --git a/c/sedona-proj/src/st_transform.rs b/rust/sedona-functions/src/st_transform.rs
similarity index 96%
rename from c/sedona-proj/src/st_transform.rs
rename to rust/sedona-functions/src/st_transform.rs
index 539e04dc9..5d1dd05fa 100644
--- a/c/sedona-proj/src/st_transform.rs
+++ b/rust/sedona-functions/src/st_transform.rs
@@ -22,12 +22,11 @@ use datafusion_common::cast::{as_string_view_array, as_struct_array};
use datafusion_common::config::ConfigOptions;
use datafusion_common::{exec_err, DataFusionError, Result, ScalarValue};
use datafusion_expr::ColumnarValue;
+use sedona_common::option::with_crs_engine;
use sedona_common::sedona_internal_err;
use sedona_expr::item_crs::{make_item_crs, parse_item_crs_arg_type};
-use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
-use sedona_functions::executor::WkbExecutor;
-use sedona_functions::st_setsrid::{validate_crs_array_for_type, validate_crs_for_type};
-use sedona_geometry::transform::{transform, CrsEngine, CrsTransform};
+use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel, SedonaScalarUDF};
+use sedona_geometry::transform::{transform, CrsTransform};
use sedona_geometry::types::Edges;
use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
use sedona_schema::crs::{deserialize_crs, Crs};
@@ -38,9 +37,16 @@ use std::iter::zip;
use std::sync::Arc;
use wkb::reader::Wkb;
-use crate::transform::with_global_proj_engine;
+use crate::executor::WkbExecutor;
+use crate::st_setsrid::{validate_crs_array_for_type, validate_crs_for_type};
-/// ST_Transform() implementation using the proj crate
+/// ST_Transform() scalar UDF (reprojects geometries/geographies via the
+/// session's [`CrsEngine`](sedona_geometry::transform::CrsEngine))
+pub fn st_transform_udf() -> SedonaScalarUDF {
+ SedonaScalarUDF::from_impl("st_transform", st_transform_impl())
+}
+
+/// ST_Transform() kernel implementation
pub fn st_transform_impl() -> ScalarKernelRef {
Arc::new(STTransform {})
}
@@ -136,7 +142,7 @@ impl SedonaScalarKernel for STTransform {
args: &[ColumnarValue],
return_type: &SedonaType,
_num_rows: usize,
- _config_options: Option<&ConfigOptions>,
+ config_options: Option<&ConfigOptions>,
) -> Result<ColumnarValue> {
let inputs = zip(arg_types, args)
.map(|(arg_type, arg)| ArgInput::from_arg(arg_type, arg))
@@ -165,7 +171,7 @@ impl SedonaScalarKernel for STTransform {
let to_crs_opt: Crs = Some(to_crs.clone());
validate_crs_for_type(&to_crs_opt, &return_item_type)?;
- with_global_proj_engine(|engine| {
+ with_crs_engine(config_options, |engine| {
let crs_transform = engine
.get_transform_crs_to_crs(
&from_crs.to_crs_string(),
@@ -210,7 +216,7 @@ impl SedonaScalarKernel for STTransform {
None
};
- with_global_proj_engine(|engine| {
+ with_crs_engine(config_options, |engine| {
executor.execute_wkb_void(|maybe_wkb| {
match (maybe_wkb, crs_to_crs_iter.next().unwrap()) {
(Some(wkb), (Some(from_crs_str), Some(to_crs_str))) => {
@@ -453,6 +459,9 @@ mod tests {
use sedona_testing::create::create_array_item_crs;
use sedona_testing::create::create_scalar;
use sedona_testing::testers::ScalarUdfTester;
+ use std::sync::Arc;
+
+ use sedona_proj::transform::LazyProjEngine;
const NAD83ZONE6PROJ: &str = "EPSG:2230";
const NAD83_GEOGRAPHIC: &str = "EPSG:4269";
@@ -477,7 +486,8 @@ mod tests {
let tester = ScalarUdfTester::new(
udf.into(),
vec![geometry_input.clone(), SedonaType::Arrow(DataType::Utf8)],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
let wkb = create_array(&[None, Some("POINT (79.3871 43.6426)")], &geometry_input);
let via_wkt = tester.invoke_array_scalar(wkb.clone(), wkt).unwrap();
@@ -492,7 +502,8 @@ mod tests {
let tester = ScalarUdfTester::new(
udf.into(),
vec![geometry_input.clone(), SedonaType::Arrow(DataType::Utf8)],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
// Return type with scalar to argument (returns type-level CRS)
let expected_return_type = SedonaType::Wkb(Edges::Planar, get_crs(NAD83ZONE6PROJ));
@@ -533,7 +544,8 @@ mod tests {
let tester = ScalarUdfTester::new(
udf.into(),
vec![geography_input.clone(), SedonaType::Arrow(DataType::Utf8)],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
// Transform to geographic CRS should succeed
let expected_return_type = SedonaType::Wkb(Edges::Spherical, get_crs(NAD83_GEOGRAPHIC));
@@ -569,7 +581,8 @@ mod tests {
let tester = ScalarUdfTester::new(
udf.into(),
vec![geometry_input.clone(), SedonaType::Arrow(DataType::UInt32)],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
// Return type with scalar to argument (returns type-level CRS)
let expected_return_type = SedonaType::Wkb(Edges::Planar, get_crs(NAD83ZONE6PROJ));
@@ -610,7 +623,8 @@ mod tests {
let tester = ScalarUdfTester::new(
udf.into(),
vec![geography_input.clone(), SedonaType::Arrow(DataType::UInt32)],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
// Transform to geographic CRS (4269 = NAD83) should succeed
let expected_return_type = SedonaType::Wkb(Edges::Spherical, get_crs(NAD83_GEOGRAPHIC));
@@ -640,7 +654,8 @@ mod tests {
let tester = ScalarUdfTester::new(
udf.into(),
vec![geometry_input.clone(), SedonaType::Arrow(DataType::Utf8)],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
// Return type with scalar to argument (returns type-level CRS)
// This is the same as for normal input
@@ -693,7 +708,8 @@ mod tests {
let tester = ScalarUdfTester::new(
udf.into(),
vec![geography_input.clone(), SedonaType::Arrow(DataType::Utf8)],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
// Transform to geographic CRS should succeed
let expected_return_type = SedonaType::Wkb(Edges::Spherical, get_crs(NAD83_GEOGRAPHIC));
@@ -739,7 +755,8 @@ mod tests {
SedonaType::Arrow(DataType::Utf8),
SedonaType::Arrow(DataType::Utf8),
],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
// Return type with scalar to argument (returns type-level CRS)
// This is the same as for normal input
@@ -796,7 +813,8 @@ mod tests {
SedonaType::Arrow(DataType::Utf8),
SedonaType::Arrow(DataType::Utf8),
],
- );
+ )
+ .with_crs_engine(Arc::new(LazyProjEngine));
// Transform to geographic CRS should succeed
let expected_return_type = SedonaType::Wkb(Edges::Spherical, get_crs(NAD83_GEOGRAPHIC));
diff --git a/rust/sedona-raster-functions/Cargo.toml b/rust/sedona-raster-functions/Cargo.toml
index a8ebccf3e..8d8011370 100644
--- a/rust/sedona-raster-functions/Cargo.toml
+++ b/rust/sedona-raster-functions/Cargo.toml
@@ -44,7 +44,6 @@ sedona-geometry = { workspace = true }
sedona-raster = { workspace = true }
sedona-schema = { workspace = true }
sedona-tg = { workspace = true }
-sedona-proj = { workspace = true }
serde_json = { workspace = true }
wkb = { workspace = true }
diff --git a/rust/sedona-raster-functions/src/crs_utils.rs b/rust/sedona-raster-functions/src/crs_utils.rs
index 07ebb754c..1996114b9 100644
--- a/rust/sedona-raster-functions/src/crs_utils.rs
+++ b/rust/sedona-raster-functions/src/crs_utils.rs
@@ -19,28 +19,22 @@ use std::borrow::Cow;
use datafusion_common::config::ConfigOptions;
use datafusion_common::{exec_datafusion_err, exec_err, DataFusionError, Result};
-use sedona_common::option::SedonaOptions;
use sedona_geometry::transform::{transform, CrsEngine};
-use sedona_proj::transform::with_global_proj_engine;
use sedona_schema::crs::{deserialize_crs, CoordinateReferenceSystem, Crs, CrsRef};
use wkb::reader::read_wkb;
-/// Run `f` with the session's CRS engine: the [`SedonaOptions`] runtime engine
+/// Run `f` with the session's CRS engine: the `SedonaOptions` runtime engine
/// when config options are available (the query path), falling back to the
-/// process-global PROJ engine otherwise (e.g. direct `invoke_batch` calls).
+/// default engine (which errors when no engine is registered) otherwise (e.g.
+/// direct `invoke_batch` calls).
+///
+/// The engine resolution lives in `sedona-common` so that geometry and raster
+/// CRS functions can share it; this is a thin re-export for raster call sites.
pub fn with_crs_engine<T>(
config_options: Option<&ConfigOptions>,
f: impl FnOnce(&dyn CrsEngine) -> Result<T>,
) -> Result<T> {
- if let Some(options) = config_options.and_then(|o| o.extensions.get::<SedonaOptions>()) {
- f(options.runtime.crs_engine().as_ref())
- } else {
- let mut f = Some(f);
- with_global_proj_engine(|engine| {
- let f = f.take().expect("engine closure runs once");
- f(engine)
- })
- }
+ sedona_common::option::with_crs_engine(config_options, f)
}
/// Resolve an optional CRS string to a concrete CRS object.
diff --git a/rust/sedona-raster-gdal/src/rs_clip.rs b/rust/sedona-raster-gdal/src/rs_clip.rs
index f6467210c..06b978350 100644
--- a/rust/sedona-raster-gdal/src/rs_clip.rs
+++ b/rust/sedona-raster-gdal/src/rs_clip.rs
@@ -926,8 +926,12 @@ mod tests {
fn test_rs_clip_crs_mismatch() {
// A geometry given in EPSG:3857 must be reprojected to the raster's
// EPSG:4326 before clipping, yielding the same result as the equivalent
- // EPSG:4326 geometry.
- let array = test_raster_array();
+ // EPSG:4326 geometry. Reprojection needs a real CRS engine, injected
+ // through the tester's config options via `with_crs_engine`.
+ let spec = RasterSpec::d2(4, 2)
+ .band_values(&[1u8, 2, 3, 4, 5, 6, 7, 8])
+ .crs(Some("EPSG:4326"))
+ .transform([0.0, 1.0, 0.0, 2.0, 0.0, -1.0]);
// Build the EPSG:3857 geometry with the same PROJ engine the UDF uses,
// so the test is robust to axis-order / normalization across builds.
@@ -940,12 +944,6 @@ mod tests {
})
.unwrap();
- // 3-arg variant: RS_Clip(raster, band, geom).
- let kernel = RsClip { arg_count: 3 };
- let raster_scalar = ColumnarValue::Scalar(ScalarValue::Struct(Arc::new(array)));
- let band_type = SedonaType::Arrow(DataType::Int32);
- let band_val = ColumnarValue::Scalar(ScalarValue::Int32(Some(1)));
-
// Both the native-CRS and the reprojected geometry must produce the
// same clip: the polygon covers columns 0-1 of both rows, cropped to
// that 2x2 window with the UInt8 minimum recorded as nodata.
@@ -954,21 +952,18 @@ mod tests {
.transform([0.0, 1.0, 0.0, 2.0, 0.0, -1.0])
.band_values(&[1u8, 2, 5, 6])
.nodata(0u8);
+
+ // 3-arg variant: RS_Clip(raster, band, geom), invoked through the tester
+ // with a real CRS engine so the reprojecting branch can run.
let clip_band1 = |geom_type: SedonaType, geom_wkb: Vec<u8>| {
- let result = kernel
- .invoke_batch(
- &[RASTER, band_type.clone(), geom_type],
- &[
- raster_scalar.clone(),
- band_val.clone(),
- ColumnarValue::Scalar(ScalarValue::Binary(Some(geom_wkb))),
- ],
- )
+ let udf: datafusion_expr::ScalarUDF = rs_clip_udf().into();
+ let arg_types = vec![RASTER, SedonaType::Arrow(DataType::Int32), geom_type];
+ let tester =
+ ScalarUdfTester::new(udf, arg_types).with_crs_engine(Arc::new(LazyProjEngine));
+ let result = tester
+ .invoke_scalar_scalar_scalar(spec.scalar(), 1, ScalarValue::Binary(Some(geom_wkb)))
.unwrap();
- let ColumnarValue::Scalar(scalar) = result else {
- panic!("Expected raster scalar result");
- };
- assert_raster_scalar_equals(&scalar, &expected);
+ assert_raster_scalar_equals(&result, &expected);
};
clip_band1(
@@ -987,13 +982,14 @@ mod tests {
// (y, x) plane and broadcasts the same mask across both time planes,
// preserving the time dimension. Values 1..=16 (C order): time 0 is
// rows [1,2,3,4] / [5,6,7,8], time 1 is [9..12] / [13..16].
+ // CRS-less raster and geom: this exercises N-D plane broadcasting, not
+ // reprojection, so the CRS engine is never reached.
let array = RasterSpec::nd(&["time", "y", "x"], &[2, 2, 4])
.band_values(&[1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
- .crs(Some("EPSG:4326"))
+ .crs(None)
.transform([0.0, 1.0, 0.0, 2.0, 0.0, -1.0])
.build();
- let crs_4326 = deserialize_crs("EPSG:4326").unwrap().unwrap();
let geom_wkb = make_wkb("POLYGON((0 1, 2 1, 2 2, 0 2, 0 1))");
// RS_Clip(raster, band, geom, allTouched, noData, crop=true).
@@ -1003,7 +999,7 @@ mod tests {
&[
RASTER,
SedonaType::Arrow(DataType::Int32),
- SedonaType::Wkb(Edges::Planar, Some(crs_4326)),
+ SedonaType::Wkb(Edges::Planar, None),
SedonaType::Arrow(DataType::Boolean),
SedonaType::Arrow(DataType::Float64),
SedonaType::Arrow(DataType::Boolean),
@@ -1028,7 +1024,7 @@ mod tests {
&scalar,
&RasterSpec::nd(&["time", "y", "x"], &[2, 1, 2])
.band_values(&[1u8, 2, 9, 10])
- .crs(Some("EPSG:4326"))
+ .crs(None)
.transform([0.0, 1.0, 0.0, 2.0, 0.0, -1.0])
.nodata(0u8),
);
@@ -1056,13 +1052,15 @@ mod tests {
);
}
- /// A 2×1, two-band EPSG:4326 raster (band 1 = [1,2], band 2 = [10,20]) — the
- /// two bands differ so a per-band clip is observably distinct.
+ /// A 2×1, two-band, CRS-less raster (band 1 = [1,2], band 2 = [10,20]) — the
+ /// two bands differ so a per-band clip is observably distinct. CRS-less so
+ /// the band-handling tests reach rasterization rather than the reprojection
+ /// branch (which would need an injected CRS engine).
fn two_band_raster() -> StructArray {
RasterSpec::d2(2, 1)
.band_values(&[1u8, 2])
.band_values(&[10u8, 20])
- .crs(Some("EPSG:4326"))
+ .crs(None)
.transform([0.0, 1.0, 0.0, 1.0, 0.0, -1.0])
.build()
}
@@ -1072,7 +1070,6 @@ mod tests {
// Regression: a constant raster+geom with a per-row band column must
// produce an N-row array, not collapse to row 0. (The executor only sees
// [raster, geom], so output packaging must consider all args.)
- let crs_4326 = deserialize_crs("EPSG:4326").unwrap().unwrap();
let geom_wkb = make_wkb("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))");
let kernel = RsClip { arg_count: 3 };
@@ -1081,7 +1078,7 @@ mod tests {
&[
RASTER,
SedonaType::Arrow(DataType::Int32),
- SedonaType::Wkb(Edges::Planar, Some(crs_4326)),
+ SedonaType::Wkb(Edges::Planar, None),
],
&[
ColumnarValue::Scalar(ScalarValue::Struct(Arc::new(two_band_raster()))),
@@ -1101,7 +1098,7 @@ mod tests {
let row = |values: &[u8]| {
Some(
RasterSpec::d2(1, 1)
- .crs(Some("EPSG:4326"))
+ .crs(None)
.transform([0.0, 1.0, 0.0, 1.0, 0.0, -1.0])
.band_values(values)
.nodata(0u8),
@@ -1114,7 +1111,6 @@ mod tests {
fn band_zero_clips_all_bands() {
// Band 0 means "all bands" — it must reach clip_raster as 0, not be
// clamped to 1.
- let crs_4326 = deserialize_crs("EPSG:4326").unwrap().unwrap();
let geom_wkb = make_wkb("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))");
let kernel = RsClip { arg_count: 3 };
let result = kernel
@@ -1122,7 +1118,7 @@ mod tests {
&[
RASTER,
SedonaType::Arrow(DataType::Int32),
- SedonaType::Wkb(Edges::Planar, Some(crs_4326)),
+ SedonaType::Wkb(Edges::Planar, None),
],
&[
ColumnarValue::Scalar(ScalarValue::Struct(Arc::new(two_band_raster()))),
@@ -1139,7 +1135,7 @@ mod tests {
assert_raster_scalar_equals(
&scalar,
&RasterSpec::d2(1, 1)
- .crs(Some("EPSG:4326"))
+ .crs(None)
.transform([0.0, 1.0, 0.0, 1.0, 0.0, -1.0])
.band_values(&[1u8])
.nodata(0u8)
@@ -1150,7 +1146,6 @@ mod tests {
#[test]
fn negative_band_errors() {
- let crs_4326 = deserialize_crs("EPSG:4326").unwrap().unwrap();
let geom_wkb = make_wkb("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))");
let kernel = RsClip { arg_count: 3 };
let err = kernel
@@ -1158,7 +1153,7 @@ mod tests {
&[
RASTER,
SedonaType::Arrow(DataType::Int32),
- SedonaType::Wkb(Edges::Planar, Some(crs_4326)),
+ SedonaType::Wkb(Edges::Planar, None),
],
&[
ColumnarValue::Scalar(ScalarValue::Struct(Arc::new(two_band_raster()))),
@@ -1205,7 +1200,6 @@ mod tests {
#[test]
fn null_band_yields_null() {
// A NULL band must propagate to NULL, not silently clip every band.
- let crs_4326 = deserialize_crs("EPSG:4326").unwrap().unwrap();
let geom_wkb = make_wkb("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))");
let kernel = RsClip { arg_count: 3 };
let result = kernel
@@ -1213,7 +1207,7 @@ mod tests {
&[
RASTER,
SedonaType::Arrow(DataType::Int32),
- SedonaType::Wkb(Edges::Planar, Some(crs_4326)),
+ SedonaType::Wkb(Edges::Planar, None),
],
&[
ColumnarValue::Scalar(ScalarValue::Struct(Arc::new(two_band_raster()))),
diff --git a/rust/sedona-testing/src/benchmark_util.rs b/rust/sedona-testing/src/benchmark_util.rs
index 70e3d142b..b3bb69cdd 100644
--- a/rust/sedona-testing/src/benchmark_util.rs
+++ b/rust/sedona-testing/src/benchmark_util.rs
@@ -25,6 +25,7 @@ use geo_types::Rect;
use rand::{distr::Uniform, rngs::StdRng, Rng, RngExt, SeedableRng};
use sedona_common::sedona_internal_err;
+use sedona_geometry::transform::CrsEngine;
use sedona_geometry::types::GeometryTypeId;
use sedona_schema::datatypes::{SedonaType, RASTER, WKB_GEOMETRY};
use sedona_schema::raster::BandDataType;
@@ -71,6 +72,20 @@ pub mod benchmark {
lib: &str,
name: &str,
config: impl Into<BenchmarkArgs>,
+ ) {
+ scalar_with_crs_engine(c, functions, lib, name, config, None)
+ }
+
+ /// Benchmark a reprojecting [ScalarUDF], injecting a [CrsEngine] into the
+ /// tester so functions that resolve a CRS (e.g. `ST_Transform`) use a real
+ /// engine instead of the erroring default. Otherwise identical to [scalar].
+ pub fn scalar_with_crs_engine(
+ c: &mut Criterion,
+ functions: &FunctionSet,
+ lib: &str,
+ name: &str,
+ config: impl Into<BenchmarkArgs>,
+ crs_engine: Option<Arc<dyn CrsEngine + Send + Sync>>,
) {
let not_found_err = format!("{name} was not found in function set");
let udf: ScalarUDF = functions
@@ -86,7 +101,10 @@ pub mod benchmark {
)
.unwrap();
c.bench_function(&data.make_label(lib, name), |b| {
- b.iter(|| data.invoke_scalar(&udf).unwrap())
+ b.iter(|| {
+ data.invoke_scalar_with_crs_engine(&udf, crs_engine.clone())
+ .unwrap()
+ })
});
}
@@ -536,7 +554,21 @@ impl BenchmarkData {
/// Invoke a scalar function on this data
pub fn invoke_scalar(&self, udf: &ScalarUDF) -> Result<()> {
- let tester = ScalarUdfTester::new(udf.clone(), self.config.sedona_types().clone());
+ self.invoke_scalar_with_crs_engine(udf, None)
+ }
+
+ /// Invoke a scalar function on this data, injecting a [CrsEngine] into the
+ /// tester so reprojecting functions (e.g. `ST_Transform`) resolve a real
+ /// engine instead of the erroring default.
+ pub fn invoke_scalar_with_crs_engine(
+ &self,
+ udf: &ScalarUDF,
+ crs_engine: Option<Arc<dyn CrsEngine + Send + Sync>>,
+ ) -> Result<()> {
+ let mut tester = ScalarUdfTester::new(udf.clone(), self.config.sedona_types().clone());
+ if let Some(crs_engine) = crs_engine {
+ tester = tester.with_crs_engine(crs_engine);
+ }
match self.config {
BenchmarkArgs::Array(_) => {
diff --git a/rust/sedona/src/context.rs b/rust/sedona/src/context.rs
index 0f4acbd57..e327722fa 100644
--- a/rust/sedona/src/context.rs
+++ b/rust/sedona/src/context.rs
@@ -348,11 +348,6 @@ impl SedonaContext {
#[cfg(feature = "s2geography")]
out.register_s2geography()?;
- // Always register proj scalar kernels (although actually calling them will error
- // without this feature unless sedona_proj::register::configure_global_proj_engine()
- // is called).
- out.register_scalar_kernels(sedona_proj::register::scalar_kernels().into_iter())?;
-
// Always register raster functions
out.register_function_set(sedona_raster_functions::register::default_function_set());
@@ -361,7 +356,7 @@ impl SedonaContext {
#[cfg(feature = "s2geography")]
fn register_s2geography(&mut self) -> Result<()> {
- use sedona_proj::sd_order_lnglat;
+ use sedona_functions::sd_order_lnglat;
self.register_scalar_kernels(sedona_s2geography::register::scalar_kernels()?.into_iter())?;