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 =