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())?;