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

jiayu 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 e01a542  Add ST_CRS (#38)
e01a542 is described below

commit e01a54274d678a8315ddb4e30ca20f8d81e4e52d
Author: jp <[email protected]>
AuthorDate: Wed Sep 10 14:28:20 2025 -0700

    Add ST_CRS (#38)
    
    * Add ST_CRS
    
    * update comment
    
    Co-authored-by: Copilot <[email protected]>
    
    * Update comment
    
    Co-authored-by: Copilot <[email protected]>
    
    * Switch to Utf8View, use json, don't parse wkb
    
    * Added a couple array tests
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 python/sedonadb/tests/functions/test_transforms.py |  31 +++-
 rust/sedona-functions/src/register.rs              |   1 +
 rust/sedona-functions/src/st_srid.rs               | 165 +++++++++++++++++++--
 3 files changed, 182 insertions(+), 15 deletions(-)

diff --git a/python/sedonadb/tests/functions/test_transforms.py 
b/python/sedonadb/tests/functions/test_transforms.py
index a5bd64d..fb9d413 100644
--- a/python/sedonadb/tests/functions/test_transforms.py
+++ b/python/sedonadb/tests/functions/test_transforms.py
@@ -50,7 +50,23 @@ def test_st_setsrid(eng, geom, srid, expected_srid):
         assert df.crs == pyproj.CRS(expected_srid)
 
 
-# PostGIS does not have an API ST_SetCrs
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+    ("geom", "srid", "expected_srid"),
+    [
+        ("POINT (1 1)", 3857, 3857),
+        ("POINT (1 1)", 0, 0),
+    ],
+)
+def test_st_srid(eng, geom, srid, expected_srid):
+    eng = eng.create_or_skip()
+    eng.assert_query_result(
+        f"SELECT ST_SRID(ST_SetSrid({geom_or_null(geom)}, 
{val_or_null(srid)}))",
+        expected_srid,
+    )
+
+
+# PostGIS does not have an API ST_SetCrs, ST_Crs
 @pytest.mark.parametrize("eng", [SedonaDB])
 @pytest.mark.parametrize(
     ("geom", "crs", "expected_srid"),
@@ -64,3 +80,16 @@ def test_st_setcrs_sedonadb(eng, geom, crs, expected_srid):
     result = eng.execute_and_collect(f"SELECT ST_SetCrs({geom_or_null(geom)}, 
'{crs}')")
     df = eng.result_to_pandas(result)
     assert df.crs.to_epsg() == expected_srid
+
+
[email protected]("eng", [SedonaDB])
+def test_st_crs_sedonadb(eng):
+    eng = eng.create_or_skip()
+    eng.assert_query_result(
+        "SELECT ST_CRS(ST_SetCrs(ST_GeomFromText('POINT (1 1)'), 
'EPSG:26920'))",
+        '"EPSG:26920"',
+    )
+    eng.assert_query_result(
+        "SELECT ST_CRS(ST_SetCrs(ST_GeomFromText('POINT (1 1)'), NULL))",
+        None,
+    )
diff --git a/rust/sedona-functions/src/register.rs 
b/rust/sedona-functions/src/register.rs
index 4318cbb..07493ea 100644
--- a/rust/sedona-functions/src/register.rs
+++ b/rust/sedona-functions/src/register.rs
@@ -88,6 +88,7 @@ pub fn default_function_set() -> FunctionSet {
         crate::st_transform::st_transform_udf,
         crate::st_setsrid::st_set_crs_udf,
         crate::st_setsrid::st_set_srid_udf,
+        crate::st_srid::st_crs_udf,
         crate::st_srid::st_srid_udf,
         crate::st_xyzm::st_m_udf,
         crate::st_xyzm::st_x_udf,
diff --git a/rust/sedona-functions/src/st_srid.rs 
b/rust/sedona-functions/src/st_srid.rs
index 16e560e..3e79567 100644
--- a/rust/sedona-functions/src/st_srid.rs
+++ b/rust/sedona-functions/src/st_srid.rs
@@ -14,17 +14,19 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-use arrow_array::builder::UInt32Builder;
-use std::{sync::Arc, vec};
-
 use crate::executor::WkbExecutor;
+use arrow_array::builder::StringBuilder;
+use arrow_array::builder::UInt32Builder;
+use arrow_array::Array;
 use arrow_schema::DataType;
 use datafusion_common::{DataFusionError, Result};
 use datafusion_expr::{
     scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation, 
Volatility,
 };
 use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
-use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+use sedona_schema::datatypes::SedonaType;
+use sedona_schema::matchers::ArgMatcher;
+use std::{sync::Arc, vec};
 
 /// ST_Srid() scalar UDF implementation
 ///
@@ -38,6 +40,18 @@ pub fn st_srid_udf() -> SedonaScalarUDF {
     )
 }
 
+/// ST_Crs() scalar UDF implementation
+///
+/// Scalar function to return the CRS of a geometry or geography
+pub fn st_crs_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "st_crs",
+        vec![Arc::new(StCrs {})],
+        Volatility::Immutable,
+        Some(st_crs_doc()),
+    )
+}
+
 fn st_srid_doc() -> Documentation {
     Documentation::builder(
         DOC_SECTION_OTHER,
@@ -49,6 +63,17 @@ fn st_srid_doc() -> Documentation {
     .build()
 }
 
+fn st_crs_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Return the coordinate reference system (CRS) of the geometry.",
+        "ST_CRS (geom: Geometry)",
+    )
+    .with_argument("geom", "geometry: Input geometry or geography")
+    .with_sql_example("SELECT ST_CRS(polygon)".to_string())
+    .build()
+}
+
 #[derive(Debug)]
 struct StSrid {}
 
@@ -79,16 +104,64 @@ impl SedonaScalarKernel for StSrid {
             _ => Some(0),
         };
 
-        executor.execute_wkb_void(|maybe_wkb| {
-            match maybe_wkb {
-                Some(_wkb) => {
-                    builder.append_option(srid_opt);
-                }
-                _ => builder.append_null(),
+        match &args[0] {
+            ColumnarValue::Array(array) => {
+                (0..array.len()).for_each(|i| {
+                    builder.append_option(if array.is_null(i) { None } else { 
srid_opt });
+                });
+            }
+            ColumnarValue::Scalar(scalar) => {
+                builder.append_option(if scalar.is_null() { None } else { 
srid_opt });
+            }
+        }
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+#[derive(Debug)]
+struct StCrs {}
+
+impl SedonaScalarKernel for StCrs {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(
+            vec![ArgMatcher::is_geometry_or_geography()],
+            SedonaType::Arrow(DataType::Utf8View),
+        );
+
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = WkbExecutor::new(arg_types, args);
+        let preallocate_bytes = "EPSG:4326".len() * executor.num_iterations();
+        let mut builder =
+            StringBuilder::with_capacity(executor.num_iterations(), 
preallocate_bytes);
+        let crs_opt: Option<String> = match &arg_types[0] {
+            SedonaType::Wkb(_, Some(crs)) | SedonaType::WkbView(_, Some(crs)) 
=> {
+                Some(crs.to_json())
             }
+            _ => None,
+        };
 
-            Ok(())
-        })?;
+        match &args[0] {
+            ColumnarValue::Array(array) => {
+                (0..array.len()).for_each(|i| {
+                    builder.append_option(if array.is_null(i) {
+                        None
+                    } else {
+                        crs_opt.clone()
+                    });
+                });
+            }
+            ColumnarValue::Scalar(scalar) => {
+                builder.append_option(if scalar.is_null() { None } else { 
crs_opt });
+            }
+        }
 
         executor.finish(Arc::new(builder.finish()))
     }
@@ -96,10 +169,12 @@ impl SedonaScalarKernel for StSrid {
 
 #[cfg(test)]
 mod test {
+    use arrow_array::create_array;
     use datafusion_common::ScalarValue;
     use datafusion_expr::ScalarUDF;
     use sedona_schema::crs::deserialize_crs;
     use sedona_schema::datatypes::Edges;
+    use sedona_testing::create::create_array;
     use sedona_testing::testers::ScalarUdfTester;
     use std::str::FromStr;
 
@@ -109,11 +184,15 @@ mod test {
     fn udf_metadata() {
         let udf: ScalarUDF = st_srid_udf().into();
         assert_eq!(udf.name(), "st_srid");
+        assert!(udf.documentation().is_some());
+
+        let udf: ScalarUDF = st_crs_udf().into();
+        assert_eq!(udf.name(), "st_crs");
         assert!(udf.documentation().is_some())
     }
 
     #[test]
-    fn udf() {
+    fn udf_srid() {
         let udf: ScalarUDF = st_srid_udf().into();
 
         // Test that when no CRS is set, SRID is 0
@@ -133,7 +212,7 @@ mod test {
         let crs_value = serde_json::Value::String("EPSG:4837".to_string());
         let crs = deserialize_crs(&crs_value).unwrap();
         let sedona_type = SedonaType::Wkb(Edges::Planar, crs.clone());
-        let tester = ScalarUdfTester::new(udf.clone(), vec![sedona_type]);
+        let tester = ScalarUdfTester::new(udf.clone(), 
vec![sedona_type.clone()]);
         let result = tester
             .invoke_scalar("POLYGON ((0 0, 1 0, 0 1, 0 0))")
             .unwrap();
@@ -143,6 +222,17 @@ mod test {
         let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
         tester.assert_scalar_result_equals(result, ScalarValue::Null);
 
+        // Call with an array
+        let wkb_array = create_array(
+            &[Some("POINT (1 2)"), None, Some("MULTIPOINT (3 4)")],
+            &sedona_type,
+        );
+        let expected = create_array!(UInt32, [Some(4837_u32), None, 
Some(4837_u32)]);
+        assert_eq!(
+            &tester.invoke_array(wkb_array).unwrap().as_ref(),
+            &expected.as_ref()
+        );
+
         // Call with a CRS with no SRID (should error)
         let crs_value = serde_json::Value::from_str("{}");
         let crs = deserialize_crs(&crs_value.unwrap()).unwrap();
@@ -152,4 +242,51 @@ mod test {
         assert!(result.is_err());
         assert!(result.unwrap_err().to_string().contains("CRS has no SRID"));
     }
+
+    #[test]
+    fn udf_crs() {
+        let udf: ScalarUDF = st_crs_udf().into();
+
+        // Test that when no CRS is set, CRS is null
+        let sedona_type = SedonaType::Wkb(Edges::Planar, None);
+        let tester = ScalarUdfTester::new(udf.clone(), vec![sedona_type]);
+        tester.assert_return_type(DataType::Utf8View);
+        let result = tester
+            .invoke_scalar("POLYGON ((0 0, 1 0, 0 1, 0 0))")
+            .unwrap();
+        tester.assert_scalar_result_equals(result, ScalarValue::Utf8(None));
+
+        // Test that NULL input returns NULL output
+        let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+        tester.assert_scalar_result_equals(result, ScalarValue::Null);
+
+        // Test with a CRS with an EPSG code
+        let crs_value = serde_json::Value::String("EPSG:4837".to_string());
+        let crs = deserialize_crs(&crs_value).unwrap();
+        let sedona_type = SedonaType::Wkb(Edges::Planar, crs.clone());
+        let tester = ScalarUdfTester::new(udf.clone(), 
vec![sedona_type.clone()]);
+        let expected_crs = "\"EPSG:4837\"".to_string();
+        let result = tester
+            .invoke_scalar("POLYGON ((0 0, 1 0, 0 1, 0 0))")
+            .unwrap();
+        tester.assert_scalar_result_equals(result, 
ScalarValue::Utf8(Some(expected_crs.clone())));
+
+        // Call with an array
+        let wkb_array = create_array(
+            &[Some("POINT (1 2)"), None, Some("MULTIPOINT (3 4)")],
+            &sedona_type,
+        );
+        let expected = create_array!(
+            Utf8,
+            [Some(expected_crs.clone()), None, Some(expected_crs.clone())]
+        );
+        assert_eq!(
+            &tester.invoke_array(wkb_array).unwrap().as_ref(),
+            &expected.as_ref()
+        );
+
+        // Test with a CRS but null geom
+        let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+        tester.assert_scalar_result_equals(result, ScalarValue::Null);
+    }
 }

Reply via email to