You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by pa...@apache.org on 2026/06/03 02:06:38 UTC

(sedona-db) branch main updated: fix(c/sedona-proj): Allow bound parameter for CRS argument of ST_Transform (#904)

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

paleolimbot 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 6ec3d02b7 fix(c/sedona-proj): Allow bound parameter for CRS argument of ST_Transform (#904)
6ec3d02b7 is described below

commit 6ec3d02b76ad73947ba86e3bd8c62b5f308d2beb
Author: Dewey Dunnington <de...@dunnington.ca>
AuthorDate: Tue Jun 2 21:06:33 2026 -0500

    fix(c/sedona-proj): Allow bound parameter for CRS argument of ST_Transform (#904)
---
 c/sedona-proj/src/st_transform.rs                  | 54 +++++++++++++++++++++-
 python/sedonadb/tests/functions/test_transforms.py | 39 ++++++++++++++++
 2 files changed, 92 insertions(+), 1 deletion(-)

diff --git a/c/sedona-proj/src/st_transform.rs b/c/sedona-proj/src/st_transform.rs
index f6fa9b096..fb219ec2e 100644
--- a/c/sedona-proj/src/st_transform.rs
+++ b/c/sedona-proj/src/st_transform.rs
@@ -59,6 +59,14 @@ impl SedonaScalarKernel for STTransform {
 
         if inputs.len() == 2 {
             match (inputs[0], inputs[1]) {
+                // Null output CRS is usually a sentinel for a parameter that hasn't been bound yet
+                // For the purposes of computing an output type, we pretend it is a scalar string.
+                (ArgInput::Geo(edges, _), ArgInput::Null) => {
+                    Ok(Some(output_type_from_scalar_crs_value(
+                        edges,
+                        &ScalarValue::Utf8(Some("0".to_string())),
+                    )?))
+                }
                 // ScalarCrs output always returns a Wkb output type with concrete Crs
                 (ArgInput::Geo(edges, _), ArgInput::ScalarCrs(scalar_value))
                 | (ArgInput::ItemCrs(edges), ArgInput::ScalarCrs(scalar_value)) => Ok(Some(
@@ -74,6 +82,14 @@ impl SedonaScalarKernel for STTransform {
             }
         } else if inputs.len() == 3 {
             match (inputs[0], inputs[1], inputs[2]) {
+                // Null output CRS is usually a sentinel for a parameter that hasn't been bound yet
+                // For the purposes of computing an output type, we pretend it is a scalar string.
+                (ArgInput::Geo(edges, _), _, ArgInput::Null) => {
+                    Ok(Some(output_type_from_scalar_crs_value(
+                        edges,
+                        &ScalarValue::Utf8(Some("0".to_string())),
+                    )?))
+                }
                 // ScalarCrs output always returns a Wkb output type with concrete Crs
                 (
                     ArgInput::Geo(edges, _),
@@ -317,12 +333,18 @@ enum ArgInput<'a> {
     /// Array CRS input. Supported for second and third arguments. When present
     /// as the last (to) argument, this forces Item CRS output.
     ArrayCrs,
+    /// A literal null
+    Null,
     /// Sentinel for anything else
     Unsupported,
 }
 
 impl<'a> ArgInput<'a> {
     fn from_return_type_arg(arg_type: &'a SedonaType, scalar_arg: Option<&'a ScalarValue>) -> Self {
+        if ArgMatcher::is_null().match_type(arg_type) {
+            return Self::Null;
+        }
+
         if let Ok((SedonaType::Wkb(edges, _) | SedonaType::WkbView(edges, _), maybe_crs_type)) =
             parse_item_crs_arg_type(arg_type)
         {
@@ -783,7 +805,7 @@ mod tests {
     }
 
     #[test]
-    fn test_invoke_null_crs_to() {
+    fn test_invoke_null_utf8_crs_to() {
         let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
         let tester = ScalarUdfTester::new(
             udf.clone().into(),
@@ -825,6 +847,36 @@ mod tests {
         assert_eq!(result, create_scalar(None, &WKB_GEOMETRY));
     }
 
+    #[test]
+    fn test_invoke_null_crs_to() {
+        let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
+        let tester = ScalarUdfTester::new(
+            udf.clone().into(),
+            vec![WKB_GEOMETRY, SedonaType::Arrow(DataType::Null)],
+        );
+
+        // A null scalar CRS should generate WKB_GEOMETRY output with a type
+        // level CRS that is unset; however, all the output will be null.
+        let result = tester
+            .invoke_scalar_scalar("POINT (0 1)", ScalarValue::Null)
+            .unwrap();
+        assert_eq!(result, create_scalar(None, &WKB_GEOMETRY));
+
+        // This should also work for the three arg variant
+        let tester = ScalarUdfTester::new(
+            udf.clone().into(),
+            vec![
+                WKB_GEOMETRY,
+                SedonaType::Arrow(DataType::Null),
+                SedonaType::Arrow(DataType::Null),
+            ],
+        );
+        let result = tester
+            .invoke_scalar_scalar_scalar("POINT (0 1)", ScalarValue::Null, ScalarValue::Null)
+            .unwrap();
+        assert_eq!(result, create_scalar(None, &WKB_GEOMETRY));
+    }
+
     #[test]
     fn test_invoke_unset_crs_to() {
         let udf = SedonaScalarUDF::from_impl("st_transform", st_transform_impl());
diff --git a/python/sedonadb/tests/functions/test_transforms.py b/python/sedonadb/tests/functions/test_transforms.py
index 65e0631b1..652b5c0a3 100644
--- a/python/sedonadb/tests/functions/test_transforms.py
+++ b/python/sedonadb/tests/functions/test_transforms.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import geopandas
+import geopandas.testing
 import pyproj
 import pytest
 from sedonadb.testing import PostGIS, SedonaDB, geom_or_null, val_or_null
@@ -88,6 +89,44 @@ def test_st_srid(eng, geom, srid, expected_srid):
     )
 
 
+def test_st_transform_bind_crs(con):
+    # Check the two argument version
+    df = con.sql(
+        "SELECT ST_Transform(ST_Point(0, 1, 4326), $1) AS geom", params=("EPSG:3857",)
+    ).to_pandas()
+    expected = con.sql(
+        "SELECT ST_Transform(ST_Point(0, 1, 4326), 'EPSG:3857') AS geom"
+    ).to_pandas()
+    geopandas.testing.assert_geodataframe_equal(df, expected)
+
+    # Check the three argument version
+    df = con.sql(
+        "SELECT ST_Transform(ST_Point(0, 1), 4326, $1) AS geom", params=("EPSG:3857",)
+    ).to_pandas()
+    expected = con.sql(
+        "SELECT ST_Transform(ST_Point(0, 1), 4326, 'EPSG:3857') AS geom"
+    ).to_pandas()
+    geopandas.testing.assert_geodataframe_equal(df, expected)
+
+
+def test_st_set_crs_bind_crs(con):
+    df = con.sql(
+        "SELECT ST_SetCrs(ST_Point(0, 1), $1) AS geom", params=("EPSG:3857",)
+    ).to_pandas()
+    expected = con.sql(
+        "SELECT ST_SetCrs(ST_Point(0, 1), 'EPSG:3857') AS geom"
+    ).to_pandas()
+    geopandas.testing.assert_geodataframe_equal(df, expected)
+
+
+def test_st_set_srid_bind_srid(con):
+    df = con.sql(
+        "SELECT ST_SetSRID(ST_Point(0, 1), $1) AS geom", params=(3857,)
+    ).to_pandas()
+    expected = con.sql("SELECT ST_SetSRID(ST_Point(0, 1), 3857) AS geom").to_pandas()
+    geopandas.testing.assert_geodataframe_equal(df, expected)
+
+
 # PostGIS does not have an API ST_SetCrs, ST_Crs
 @pytest.mark.parametrize("eng", [SedonaDB])
 @pytest.mark.parametrize(