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 eb47819  feat(c/sedona-geos): Implement ST_UnaryUnion (#234)
eb47819 is described below

commit eb4781980cea784292e6c1513c14120786255813
Author: Abeeujah <[email protected]>
AuthorDate: Thu Oct 23 16:48:47 2025 +0100

    feat(c/sedona-geos): Implement ST_UnaryUnion (#234)
---
 c/sedona-geos/benches/geos-functions.rs           |   3 +
 c/sedona-geos/src/lib.rs                          |   1 +
 c/sedona-geos/src/register.rs                     |   3 +-
 c/sedona-geos/src/st_unaryunion.rs                | 134 ++++++++++++++++++++++
 python/sedonadb/tests/functions/test_functions.py |  41 +++++++
 5 files changed, 181 insertions(+), 1 deletion(-)

diff --git a/c/sedona-geos/benches/geos-functions.rs 
b/c/sedona-geos/benches/geos-functions.rs
index 2a9eaf1..9bb70cd 100644
--- a/c/sedona-geos/benches/geos-functions.rs
+++ b/c/sedona-geos/benches/geos-functions.rs
@@ -270,6 +270,9 @@ fn criterion_benchmark(c: &mut Criterion) {
         ArrayScalar(Polygon(10), Polygon(500)),
     );
 
+    benchmark::scalar(c, &f, "geos", "st_unaryunion", Polygon(10));
+    benchmark::scalar(c, &f, "geos", "st_unaryunion", Polygon(500));
+
     benchmark::scalar(
         c,
         &f,
diff --git a/c/sedona-geos/src/lib.rs b/c/sedona-geos/src/lib.rs
index 037d8e9..22185c0 100644
--- a/c/sedona-geos/src/lib.rs
+++ b/c/sedona-geos/src/lib.rs
@@ -30,4 +30,5 @@ mod st_isvalid;
 mod st_isvalidreason;
 mod st_length;
 mod st_perimeter;
+mod st_unaryunion;
 pub mod wkb_to_geos;
diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs
index 9a819b2..7229f18 100644
--- a/c/sedona-geos/src/register.rs
+++ b/c/sedona-geos/src/register.rs
@@ -21,7 +21,7 @@ use crate::{
     st_centroid::st_centroid_impl, st_convexhull::st_convex_hull_impl, 
st_dwithin::st_dwithin_impl,
     st_isring::st_is_ring_impl, st_isvalid::st_is_valid_impl,
     st_isvalidreason::st_is_valid_reason_impl, st_length::st_length_impl,
-    st_perimeter::st_perimeter_impl,
+    st_perimeter::st_perimeter_impl, st_unaryunion::st_unary_union_impl,
 };
 
 use crate::binary_predicates::{
@@ -58,6 +58,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, 
ScalarKernelRef)> {
         ("st_perimeter", st_perimeter_impl()),
         ("st_symdifference", st_sym_difference_impl()),
         ("st_touches", st_touches_impl()),
+        ("st_unaryunion", st_unary_union_impl()),
         ("st_union", st_union_impl()),
         ("st_within", st_within_impl()),
         ("st_crosses", st_crosses_impl()),
diff --git a/c/sedona-geos/src/st_unaryunion.rs 
b/c/sedona-geos/src/st_unaryunion.rs
new file mode 100644
index 0000000..01ffadd
--- /dev/null
+++ b/c/sedona-geos/src/st_unaryunion.rs
@@ -0,0 +1,134 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::sync::Arc;
+
+use arrow_array::builder::BinaryBuilder;
+use datafusion_common::{DataFusionError, Result};
+use datafusion_expr::ColumnarValue;
+use geos::Geom;
+use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
+use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
+use sedona_schema::datatypes::SedonaType;
+use sedona_schema::{datatypes::WKB_GEOMETRY, matchers::ArgMatcher};
+
+use crate::executor::GeosExecutor;
+
+/// ST_UnaryUnion() implementation using the geos crate
+pub fn st_unary_union_impl() -> ScalarKernelRef {
+    Arc::new(STUnaryUnion {})
+}
+
+#[derive(Debug)]
+struct STUnaryUnion {}
+
+impl SedonaScalarKernel for STUnaryUnion {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(vec![ArgMatcher::is_geometry()], 
WKB_GEOMETRY);
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = GeosExecutor::new(arg_types, args);
+        let mut builder = BinaryBuilder::with_capacity(
+            executor.num_iterations(),
+            WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
+        );
+        executor.execute_wkb_void(|maybe_wkb| {
+            match maybe_wkb {
+                Some(wkb) => {
+                    let result_wkb = invoke_scalar(&wkb)?;
+                    builder.append_value(&result_wkb);
+                }
+                _ => builder.append_null(),
+            }
+
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+fn invoke_scalar(geos_geom: &geos::Geometry) -> Result<Vec<u8>> {
+    let geometry = geos_geom
+        .unary_union()
+        .map_err(|e| DataFusionError::Execution(format!("Failed to perform 
unary union: {e}")))?;
+
+    let wkb = geometry
+        .to_wkb()
+        .map_err(|e| DataFusionError::Execution(format!("Failed to convert to 
wkb: {e}")))?;
+
+    Ok(wkb.into())
+}
+
+#[cfg(test)]
+mod tests {
+    use datafusion_common::ScalarValue;
+    use rstest::rstest;
+    use sedona_expr::scalar_udf::SedonaScalarUDF;
+    use sedona_schema::datatypes::{WKB_GEOMETRY, WKB_VIEW_GEOMETRY};
+    use sedona_testing::compare::assert_array_equal;
+    use sedona_testing::create::create_array;
+    use sedona_testing::testers::ScalarUdfTester;
+
+    use super::*;
+
+    #[rstest]
+    fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) 
{
+        let udf = SedonaScalarUDF::from_kernel("st_unary_union", 
st_unary_union_impl());
+        let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type]);
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let result = tester
+            .invoke_scalar(
+                "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((1 0, 2 0, 2 1, 1 
1, 1 0)))",
+            )
+            .unwrap();
+        tester.assert_scalar_result_equals(result, "POLYGON ((0 0, 0 1, 1 1, 2 
1, 2 0, 1 0, 0 0))");
+
+        let result = tester
+            .invoke_scalar("MULTIPOINT ((0 0), (1 1), (2 2))")
+            .unwrap();
+        tester.assert_scalar_result_equals(result, "MULTIPOINT ((0 0), (1 1), 
(2 2))");
+
+        let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+        assert!(result.is_null());
+
+        let input_wkt = vec![
+            Some("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((1 0, 2 0, 2 1, 
1 1, 1 0)))"),
+            Some("LINESTRING (0 0, 1 1, 2 2)"),
+            Some("POINT EMPTY"),
+            None,
+        ];
+
+        let expected = create_array(
+            &[
+                Some("POLYGON ((0 0, 0 1, 1 1, 2 1, 2 0, 1 0, 0 0))"),
+                Some("LINESTRING (0 0, 1 1, 2 2)"),
+                Some("POINT EMPTY"),
+                None,
+            ],
+            &WKB_GEOMETRY,
+        );
+        assert_array_equal(&tester.invoke_wkb_array(input_wkt).unwrap(), 
&expected);
+    }
+}
diff --git a/python/sedonadb/tests/functions/test_functions.py 
b/python/sedonadb/tests/functions/test_functions.py
index 67cb81c..0bbd097 100644
--- a/python/sedonadb/tests/functions/test_functions.py
+++ b/python/sedonadb/tests/functions/test_functions.py
@@ -284,6 +284,47 @@ def test_st_convexhull(eng, geom, expected):
     eng.assert_query_result(f"SELECT ST_ConvexHull({geom_or_null(geom)})", 
expected)
 
 
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+    ("geom", "expected"),
+    [
+        (None, None),
+        ("POINT (0 0)", "POINT (0 0)"),
+        ("POINT EMPTY", "POINT EMPTY"),
+        ("LINESTRING (0 0, 1 1, 2 2)", "LINESTRING (0 0, 1 1, 2 2)"),
+        ("LINESTRING EMPTY", "LINESTRING EMPTY"),
+        ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POLYGON ((0 0, 1 0, 1 1, 0 1, 
0 0))"),
+        ("MULTIPOINT ((0 0), (1 1), (2 2))", "MULTIPOINT (0 0, 1 1, 2 2)"),
+        (
+            "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((1 0, 2 0, 2 1, 1 1, 
1 0)))",
+            "POLYGON ((0 0, 0 1, 1 1, 2 1, 2 0, 1 0, 0 0))",
+        ),
+        (
+            "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 
2 2)))",
+            "MULTIPOLYGON (((0 1, 1 1, 1 0, 0 0, 0 1)), ((2 3, 3 3, 3 2, 2 2, 
2 3)))",
+        ),
+        (
+            "GEOMETRYCOLLECTION (POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), POLYGON 
((1 0, 2 0, 2 1, 1 1, 1 0)))",
+            "POLYGON ((0 0, 0 1, 1 1, 2 1, 2 0, 1 0, 0 0))",
+        ),
+    ],
+)
+def test_st_unaryunion(eng, geom, expected):
+    eng = eng.create_or_skip()
+
+    if expected is None:
+        eng.assert_query_result(f"SELECT ST_UnaryUnion({geom_or_null(geom)})", 
expected)
+    elif "EMPTY" in expected.upper():
+        eng.assert_query_result(
+            f"SELECT ST_IsEmpty(ST_UnaryUnion({geom_or_null(geom)}))", True
+        )
+    else:
+        eng.assert_query_result(
+            f"SELECT ST_Equals(ST_UnaryUnion({geom_or_null(geom)}), 
{geom_or_null(expected)})",
+            True,
+        )
+
+
 @pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
 def test_st_makeline(eng):
     eng = eng.create_or_skip()

Reply via email to