This is an automated email from the ASF dual-hosted git repository.

kontinuation 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 1be99eba fix(rust/sedona-functions): propagate NULL for scalar NULL 
SRID/CRS in ST_SetSRID/ST_SetCRS (#629)
1be99eba is described below

commit 1be99eba7fe997e8997ac7c7425f508daa148161
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Wed Feb 18 17:53:46 2026 +0800

    fix(rust/sedona-functions): propagate NULL for scalar NULL SRID/CRS in 
ST_SetSRID/ST_SetCRS (#629)
    
    ## Rationale
    
    ST_SetSRID should have consistent behavior regardless of whether the SRID 
arg is an array or not. Returning NULL when the input SRID is NULL identical to 
the behavior of ST_SetSRID in PostGIS.
    
    ## Summary
    
    - Fix inconsistent NULL handling in `ST_SetSRID` and `ST_SetCRS`: scalar 
NULL SRID/CRS now returns NULL geometry instead of preserving the geometry with 
CRS unset, matching the existing array NULL behavior and standard SQL NULL 
propagation semantics.
    - Extract shared `invoke_set_crs()` helper to deduplicate identical logic 
between `STSetSRID` and `STSetCRS` kernels.
    - Update Rust unit tests and Python integration test to reflect the 
corrected behavior.
---
 python/sedonadb/tests/functions/test_transforms.py | 10 +++
 rust/sedona-functions/src/st_setsrid.rs            | 89 +++++++++++++++-------
 2 files changed, 70 insertions(+), 29 deletions(-)

diff --git a/python/sedonadb/tests/functions/test_transforms.py 
b/python/sedonadb/tests/functions/test_transforms.py
index 22bd0fd9..65e0631b 100644
--- a/python/sedonadb/tests/functions/test_transforms.py
+++ b/python/sedonadb/tests/functions/test_transforms.py
@@ -62,6 +62,16 @@ def test_st_setsrid(eng, geom, srid, expected_srid):
         assert df.crs == pyproj.CRS(expected_srid)
 
 
[email protected]("eng", [SedonaDB, PostGIS])
+def test_st_setsrid_null_srid(eng):
+    """NULL SRID should produce NULL geometry per SQL NULL propagation."""
+    eng = eng.create_or_skip()
+    eng.assert_query_result(
+        "SELECT ST_SetSrid(ST_GeomFromText('POINT (1 1)'), NULL)",
+        [(None,)],
+    )
+
+
 @pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
 @pytest.mark.parametrize(
     ("geom", "srid", "expected_srid"),
diff --git a/rust/sedona-functions/src/st_setsrid.rs 
b/rust/sedona-functions/src/st_setsrid.rs
index 79a95a44..3a11e778 100644
--- a/rust/sedona-functions/src/st_setsrid.rs
+++ b/rust/sedona-functions/src/st_setsrid.rs
@@ -21,7 +21,7 @@ use std::{
 
 use arrow_array::{
     builder::{BinaryBuilder, NullBufferBuilder},
-    ArrayRef, StringViewArray,
+    new_null_array, ArrayRef, StringViewArray,
 };
 use arrow_buffer::NullBuffer;
 use arrow_schema::DataType;
@@ -120,18 +120,13 @@ impl SedonaScalarKernel for STSetSRID {
         let (item_type, maybe_crs_type) = 
parse_item_crs_arg_type_strip_crs(&arg_types[0])?;
         let (item_arg, _) = parse_item_crs_arg(&item_type, &maybe_crs_type, 
&args[0])?;
 
-        let item_crs_matcher = ArgMatcher::is_item_crs();
-        if item_crs_matcher.match_type(return_type) {
-            let normalized_crs_value = normalize_crs_array(&args[1], 
self.engine.as_ref())?;
-            make_item_crs(
-                &item_type,
-                item_arg,
-                &ColumnarValue::Array(normalized_crs_value),
-                crs_input_nulls(&args[1]),
-            )
-        } else {
-            Ok(item_arg)
-        }
+        invoke_set_crs(
+            &item_type,
+            item_arg,
+            &args[1],
+            return_type,
+            self.engine.as_ref(),
+        )
     }
 
     fn return_type(&self, _args: &[SedonaType]) -> Result<Option<SedonaType>> {
@@ -180,18 +175,13 @@ impl SedonaScalarKernel for STSetCRS {
         let (item_type, maybe_crs_type) = 
parse_item_crs_arg_type_strip_crs(&arg_types[0])?;
         let (item_arg, _) = parse_item_crs_arg(&item_type, &maybe_crs_type, 
&args[0])?;
 
-        let item_crs_matcher = ArgMatcher::is_item_crs();
-        if item_crs_matcher.match_type(return_type) {
-            let normalized_crs_value = normalize_crs_array(&args[1], 
self.engine.as_ref())?;
-            make_item_crs(
-                &item_type,
-                item_arg,
-                &ColumnarValue::Array(normalized_crs_value),
-                crs_input_nulls(&args[1]),
-            )
-        } else {
-            Ok(item_arg)
-        }
+        invoke_set_crs(
+            &item_type,
+            item_arg,
+            &args[1],
+            return_type,
+            self.engine.as_ref(),
+        )
     }
 
     fn return_type(&self, _args: &[SedonaType]) -> Result<Option<SedonaType>> {
@@ -209,6 +199,45 @@ impl SedonaScalarKernel for STSetCRS {
     }
 }
 
+/// Shared invoke logic for both STSetSRID and STSetCRS.
+///
+/// When the SRID/CRS is a column (array), builds an `item_crs` struct with 
per-row CRS.
+/// When the SRID/CRS is a scalar, the CRS is already baked into the return 
type and the
+/// geometry is returned as-is. If the scalar SRID/CRS is NULL, the result is 
NULL per
+/// standard SQL NULL propagation semantics.
+fn invoke_set_crs(
+    item_type: &SedonaType,
+    item_arg: ColumnarValue,
+    crs_arg: &ColumnarValue,
+    return_type: &SedonaType,
+    maybe_engine: Option<&Arc<dyn CrsEngine + Send + Sync>>,
+) -> Result<ColumnarValue> {
+    let item_crs_matcher = ArgMatcher::is_item_crs();
+    if item_crs_matcher.match_type(return_type) {
+        let normalized_crs_value = normalize_crs_array(crs_arg, maybe_engine)?;
+        make_item_crs(
+            item_type,
+            item_arg,
+            &ColumnarValue::Array(normalized_crs_value),
+            crs_input_nulls(crs_arg),
+        )
+    } else if matches!(crs_arg, ColumnarValue::Scalar(sv) if sv.is_null()) {
+        // Scalar NULL SRID/CRS: propagate NULL per SQL NULL semantics.
+        let storage_type = return_type.storage_type();
+        match &item_arg {
+            ColumnarValue::Array(array) => 
Ok(ColumnarValue::Array(new_null_array(
+                storage_type,
+                array.len(),
+            ))),
+            ColumnarValue::Scalar(_) => {
+                Ok(ColumnarValue::Scalar(ScalarValue::try_from(storage_type)?))
+            }
+        }
+    } else {
+        Ok(item_arg)
+    }
+}
+
 fn determine_return_type(
     args: &[SedonaType],
     scalar_args: &[Option<&ScalarValue>],
@@ -598,7 +627,7 @@ mod test {
         assert_eq!(return_type, WKB_GEOMETRY);
         assert_value_equal(&result, &geom_arg);
 
-        // Call with a null srid (should *not* set the output crs)
+        // Call with a null srid (result should be NULL per SQL NULL 
propagation)
         let (return_type, result) = call_udf(
             &udf,
             geom_arg.clone(),
@@ -607,7 +636,8 @@ mod test {
         )
         .unwrap();
         assert_eq!(return_type, WKB_GEOMETRY);
-        assert_value_equal(&result, &geom_arg);
+        let null_geom = create_scalar_value(None, &WKB_GEOMETRY);
+        assert_value_equal(&result, &null_geom);
     }
 
     #[test]
@@ -633,7 +663,7 @@ mod test {
         assert_eq!(return_type, wkb_lnglat);
         assert_value_equal(&result, &geom_lnglat);
 
-        // Call with a null scalar destination (should *not* set the output 
crs)
+        // Call with a null scalar destination (result should be NULL per SQL 
NULL propagation)
         let (return_type, result) = call_udf(
             &udf,
             geom_arg.clone(),
@@ -642,7 +672,8 @@ mod test {
         )
         .unwrap();
         assert_eq!(return_type, WKB_GEOMETRY);
-        assert_value_equal(&result, &geom_arg);
+        let null_geom = create_scalar_value(None, &WKB_GEOMETRY);
+        assert_value_equal(&result, &null_geom);
 
         // Ensure that an engine can reject a CRS if the UDF was constructed 
with one
         let udf_with_validation: ScalarUDF =

Reply via email to