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

petern 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 c5a31d60 Add ST_Relate implementation using GEOS (#691)
c5a31d60 is described below

commit c5a31d605cefbec93d8e23a41d1f626b825e7317
Author: Mehak3010 <[email protected]>
AuthorDate: Thu Mar 19 22:39:15 2026 +0530

    Add ST_Relate implementation using GEOS (#691)
    
    Co-authored-by: Mehak3010 <[email protected]>
    Co-authored-by: Peter Nguyen <[email protected]>
---
 c/sedona-geos/src/lib.rs                           |   2 +
 c/sedona-geos/src/register.rs                      |   1 +
 c/sedona-geos/src/st_relate.rs                     | 133 +++++++++++++++++++++
 docs/reference/sql/st_relate.qmd                   |  34 ++++++
 python/sedonadb/tests/functions/test_predicates.py |  53 ++++++++
 5 files changed, 223 insertions(+)

diff --git a/c/sedona-geos/src/lib.rs b/c/sedona-geos/src/lib.rs
index cddf0585..d05367d0 100644
--- a/c/sedona-geos/src/lib.rs
+++ b/c/sedona-geos/src/lib.rs
@@ -14,6 +14,7 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
+
 mod binary_predicates;
 mod distance;
 mod executor;
@@ -43,6 +44,7 @@ mod st_numpoints;
 mod st_perimeter;
 mod st_polygonize;
 mod st_polygonize_agg;
+mod st_relate;
 mod st_simplify;
 mod st_simplifypreservetopology;
 mod st_snap;
diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs
index 31498288..83d90e15 100644
--- a/c/sedona-geos/src/register.rs
+++ b/c/sedona-geos/src/register.rs
@@ -73,6 +73,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, 
Vec<ScalarKernelRef>)> {
         "st_overlaps" => crate::binary_predicates::st_overlaps_impl,
         "st_perimeter" => crate::st_perimeter::st_perimeter_impl,
         "st_polygonize" => crate::st_polygonize::st_polygonize_impl,
+        "st_relate" => crate::st_relate::st_relate_impl,
         "st_simplify" => crate::st_simplify::st_simplify_impl,
         "st_simplifypreservetopology" => 
crate::st_simplifypreservetopology::st_simplify_preserve_topology_impl,
         "st_snap" => crate::st_snap::st_snap_impl,
diff --git a/c/sedona-geos/src/st_relate.rs b/c/sedona-geos/src/st_relate.rs
new file mode 100644
index 00000000..03cc5dca
--- /dev/null
+++ b/c/sedona-geos/src/st_relate.rs
@@ -0,0 +1,133 @@
+// 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::StringBuilder;
+use arrow_schema::DataType;
+use datafusion_common::error::Result;
+use datafusion_common::DataFusionError;
+use datafusion_expr::ColumnarValue;
+use geos::Geom;
+use sedona_expr::{
+    item_crs::ItemCrsKernel,
+    scalar_udf::{ScalarKernelRef, SedonaScalarKernel},
+};
+use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+
+use crate::executor::GeosExecutor;
+
+/// ST_Relate implementation using GEOS
+pub fn st_relate_impl() -> Vec<ScalarKernelRef> {
+    ItemCrsKernel::wrap_impl(STRelate {})
+}
+
+#[derive(Debug)]
+struct STRelate {}
+
+impl SedonaScalarKernel for STRelate {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(
+            vec![ArgMatcher::is_geometry(), ArgMatcher::is_geometry()],
+            SedonaType::Arrow(DataType::Utf8),
+        );
+
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = GeosExecutor::new(arg_types, args);
+
+        // ST_Relate returns a 9-char DE-9IM string per row; 9 bytes * n rows
+        let mut builder =
+            StringBuilder::with_capacity(executor.num_iterations(), 9 * 
executor.num_iterations());
+
+        executor.execute_wkb_wkb_void(|wkb1, wkb2| {
+            match (wkb1, wkb2) {
+                (Some(g1), Some(g2)) => {
+                    let relate = g1
+                        .relate(g2)
+                        .map_err(|e| DataFusionError::External(Box::new(e)))?;
+
+                    builder.append_value(relate);
+                }
+                _ => builder.append_null(),
+            }
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use arrow_array::{create_array as arrow_array, ArrayRef};
+    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_impl("st_relate", st_relate_impl());
+        let tester = ScalarUdfTester::new(udf.into(), 
vec![sedona_type.clone(), sedona_type]);
+        tester.assert_return_type(DataType::Utf8);
+
+        // Two disjoint points — DE-9IM should be "FF0FFF0F2"
+        let result = tester
+            .invoke_scalar_scalar("POINT (0 0)", "POINT (1 1)")
+            .unwrap();
+        tester.assert_scalar_result_equals(result, "FF0FFF0F2");
+
+        // NULL inputs should return NULL
+        let result = tester
+            .invoke_scalar_scalar(ScalarValue::Null, ScalarValue::Null)
+            .unwrap();
+        assert!(result.is_null());
+
+        // Array inputs
+        let lhs = create_array(
+            &[
+                Some("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"),
+                Some("POINT (0.5 0.5)"),
+                None,
+            ],
+            &WKB_GEOMETRY,
+        );
+        let rhs = create_array(
+            &[
+                Some("POINT (0.5 0.5)"),
+                Some("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"),
+                Some("POINT (0 0)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        // actual values from GEOS
+        let expected: ArrayRef = arrow_array!(Utf8, [Some("0F2FF1FF2"), 
Some("0FFFFF212"), None]);
+        assert_array_equal(&tester.invoke_array_array(lhs, rhs).unwrap(), 
&expected);
+    }
+}
diff --git a/docs/reference/sql/st_relate.qmd b/docs/reference/sql/st_relate.qmd
new file mode 100644
index 00000000..ecfd6e5f
--- /dev/null
+++ b/docs/reference/sql/st_relate.qmd
@@ -0,0 +1,34 @@
+---
+# 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.
+title: ST_Relate
+description: Returns the DE-9IM intersection matrix string for two geometries.
+kernels:
+  - returns: string
+    args: [geometry, geometry]
+---
+## Description
+Returns the DE-9IM (Dimensionally Extended 9-Intersection Model) intersection 
matrix
+as a 9-character string describing the spatial relationship between two 
geometries.
+
+## Examples
+```sql
+SELECT ST_Relate(
+    ST_GeomFromWKT('POINT(0 0)'),
+    ST_GeomFromWKT('POINT(1 1)')
+);
+```
diff --git a/python/sedonadb/tests/functions/test_predicates.py 
b/python/sedonadb/tests/functions/test_predicates.py
index 9760ddc2..bba19670 100644
--- a/python/sedonadb/tests/functions/test_predicates.py
+++ b/python/sedonadb/tests/functions/test_predicates.py
@@ -442,3 +442,56 @@ def test_st_overlaps(eng, geom1, geom2, expected):
         f"SELECT ST_Overlaps({geom_or_null(geom1)}, {geom_or_null(geom2)})",
         expected,
     )
+
+
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+    ("geom1", "geom2", "expected"),
+    [
+        (None, None, None),
+        ("POINT (0 0)", None, None),
+        (None, "POINT (0 0)", None),
+        ("POINT (0 0)", "POINT (1 1)", "FF0FFF0F2"),
+        ("POINT (0 0)", "POINT (0 0)", "0FFFFFFF2"),
+        ("POINT (0 0)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "F0FFFF212"),
+        ("POINT (0.5 0.5)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 
"0FFFFF212"),
+        (
+            "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
+            "POLYGON ((5 5, 6 5, 6 6, 5 6, 5 5))",
+            "FF2FF1212",
+        ),
+        (
+            "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
+            "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))",
+            "212101212",
+        ),
+        ("POINT (0 0)", "LINESTRING (0 0, 1 1)", "F0FFFF102"),
+        ("LINESTRING (0 0, 2 2)", "LINESTRING (1 1, 3 3)", "1010F0102"),
+        (
+            "GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1))",
+            "POINT (0 0)",
+            "FF10F0FF2",
+        ),
+        (
+            "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
+            "POLYGON ((2 0, 4 0, 4 2, 2 2, 2 0))",
+            "FF2F11212",
+        ),  # touching polygons
+        (
+            "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))",
+            "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))",
+            "212FF1FF2",
+        ),  # polygon containment
+        (
+            "POLYGON ((0 0, 6 0, 6 6, 0 6, 0 0), (2 2, 4 2, 4 4, 2 4, 2 2))",
+            "POINT (1 1)",
+            "0F2FF1FF2",
+        ),  # point in a polygon hole
+    ],
+)
+def test_st_relate(eng, geom1, geom2, expected):
+    eng = eng.create_or_skip()
+    eng.assert_query_result(
+        f"SELECT ST_Relate({geom_or_null(geom1)}, {geom_or_null(geom2)})",
+        expected,
+    )

Reply via email to