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 8b8b75bc feat: add ST_Relate(geometry, geometry, text) boolean variant 
(#741)
8b8b75bc is described below

commit 8b8b75bcada0b92ae58643c7d419246057054922
Author: Mehak3010 <[email protected]>
AuthorDate: Thu Apr 9 20:37:55 2026 +0530

    feat: add ST_Relate(geometry, geometry, text) boolean variant (#741)
    
    Co-authored-by: Mehak3010 <[email protected]>
    Co-authored-by: Peter Nguyen <[email protected]>
---
 c/sedona-geos/src/register.rs                      |   1 +
 c/sedona-geos/src/st_relate.rs                     | 121 ++++++++++++++++++++-
 docs/reference/sql/st_relate.qmd                   |  34 +++++-
 python/sedonadb/tests/functions/test_predicates.py |  87 +++++++++++++++
 4 files changed, 236 insertions(+), 7 deletions(-)

diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs
index 83d90e15..1b9583f4 100644
--- a/c/sedona-geos/src/register.rs
+++ b/c/sedona-geos/src/register.rs
@@ -74,6 +74,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, 
Vec<ScalarKernelRef>)> {
         "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_relate" => crate::st_relate::st_relate_pattern_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
index 03cc5dca..bf090cea 100644
--- a/c/sedona-geos/src/st_relate.rs
+++ b/c/sedona-geos/src/st_relate.rs
@@ -16,8 +16,9 @@
 // under the License.
 use std::sync::Arc;
 
-use arrow_array::builder::StringBuilder;
+use arrow_array::builder::{BooleanBuilder, StringBuilder};
 use arrow_schema::DataType;
+use datafusion_common::cast::as_string_array;
 use datafusion_common::error::Result;
 use datafusion_common::DataFusionError;
 use datafusion_expr::ColumnarValue;
@@ -30,11 +31,16 @@ use sedona_schema::{datatypes::SedonaType, 
matchers::ArgMatcher};
 
 use crate::executor::GeosExecutor;
 
-/// ST_Relate implementation using GEOS
+/// ST_Relate(geometry, geometry) → text implementation using GEOS
 pub fn st_relate_impl() -> Vec<ScalarKernelRef> {
     ItemCrsKernel::wrap_impl(STRelate {})
 }
 
+/// ST_Relate(geometry, geometry, text) → boolean implementation using GEOS
+pub fn st_relate_pattern_impl() -> Vec<ScalarKernelRef> {
+    ItemCrsKernel::wrap_impl(STRelatePattern {})
+}
+
 #[derive(Debug)]
 struct STRelate {}
 
@@ -77,6 +83,56 @@ impl SedonaScalarKernel for STRelate {
     }
 }
 
+#[derive(Debug)]
+struct STRelatePattern {}
+
+impl SedonaScalarKernel for STRelatePattern {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(
+            vec![
+                ArgMatcher::is_geometry(),
+                ArgMatcher::is_geometry(),
+                ArgMatcher::is_string(),
+            ],
+            SedonaType::Arrow(DataType::Boolean),
+        );
+
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = GeosExecutor::new(arg_types, args);
+
+        let pattern_value = args[2]
+            .cast_to(&DataType::Utf8, None)?
+            .to_array(executor.num_iterations())?;
+        let pattern_array = as_string_array(&pattern_value)?;
+        let mut pattern_iter = pattern_array.iter();
+
+        let mut builder = 
BooleanBuilder::with_capacity(executor.num_iterations());
+
+        executor.execute_wkb_wkb_void(|wkb1, wkb2| {
+            match (wkb1, wkb2, pattern_iter.next().unwrap()) {
+                (Some(g1), Some(g2), Some(pattern)) => {
+                    let matches = g1
+                        .relate_pattern(g2, pattern)
+                        .map_err(|e| DataFusionError::External(Box::new(e)))?;
+
+                    builder.append_value(matches);
+                }
+                _ => builder.append_null(),
+            }
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use arrow_array::{create_array as arrow_array, ArrayRef};
@@ -130,4 +186,65 @@ mod tests {
         let expected: ArrayRef = arrow_array!(Utf8, [Some("0F2FF1FF2"), 
Some("0FFFFF212"), None]);
         assert_array_equal(&tester.invoke_array_array(lhs, rhs).unwrap(), 
&expected);
     }
+
+    #[rstest]
+    fn udf_pattern(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: 
SedonaType) {
+        let udf = SedonaScalarUDF::from_impl("st_relate", 
st_relate_pattern_impl());
+        let tester = ScalarUdfTester::new(
+            udf.into(),
+            vec![
+                sedona_type.clone(),
+                sedona_type,
+                SedonaType::Arrow(DataType::Utf8),
+            ],
+        );
+        tester.assert_return_type(DataType::Boolean);
+
+        // Point inside polygon — exact DE-9IM string from GEOS
+        let result = tester
+            .invoke_scalar_scalar_scalar(
+                "POINT (0.5 0.5)",
+                "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
+                "0FFFFF212",
+            )
+            .unwrap();
+        tester.assert_scalar_result_equals(result, true);
+
+        // Disjoint points — exact DE-9IM string matches
+        let result = tester
+            .invoke_scalar_scalar_scalar("POINT (0 0)", "POINT (1 1)", 
"FF0FFF0F2")
+            .unwrap();
+        tester.assert_scalar_result_equals(result, true);
+
+        // NULL inputs should return NULL
+        let result = tester
+            .invoke_scalar_scalar_scalar(
+                ScalarValue::Null,
+                ScalarValue::Null,
+                ScalarValue::Utf8(None),
+            )
+            .unwrap();
+        assert!(result.is_null());
+
+        // Array inputs
+        let lhs = create_array(
+            &[Some("POINT (0.5 0.5)"), Some("POINT (0 0)"), None],
+            &WKB_GEOMETRY,
+        );
+        let rhs = create_array(
+            &[
+                Some("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
+                Some("POINT (1 1)"),
+                Some("POINT (0 0)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+        let patterns: ArrayRef = arrow_array!(Utf8, [Some("0FFFFF212"), 
Some("FF0FFF0F2"), None]);
+
+        let expected: ArrayRef = arrow_array!(Boolean, [Some(true), 
Some(true), None]);
+        assert_array_equal(
+            &tester.invoke_arrays(vec![lhs, rhs, patterns]).unwrap(),
+            &expected,
+        );
+    }
 }
diff --git a/docs/reference/sql/st_relate.qmd b/docs/reference/sql/st_relate.qmd
index ecfd6e5f..fb0448da 100644
--- a/docs/reference/sql/st_relate.qmd
+++ b/docs/reference/sql/st_relate.qmd
@@ -16,19 +16,43 @@
 # specific language governing permissions and limitations
 # under the License.
 title: ST_Relate
-description: Returns the DE-9IM intersection matrix string for two geometries.
+description: >
+    Returns the DE-9IM intersection matrix string for two geometries, or tests
+    whether two geometries satisfy a given intersection matrix pattern.
 kernels:
   - returns: string
     args: [geometry, geometry]
+  - returns: boolean
+    args:
+    - geometry
+    - geometry
+    - name: intersectionMatrixPattern
+      type: string
+      description: >
+        A 9-character DE-9IM pattern string. Each character can be 0-2, T, F, 
or *
+        (wildcard).
 ---
 ## Description
-Returns the DE-9IM (Dimensionally Extended 9-Intersection Model) intersection 
matrix
-as a 9-character string describing the spatial relationship between two 
geometries.
+When called with two geometry arguments, returns the DE-9IM (Dimensionally 
Extended
+9-Intersection Model) intersection matrix as a 9-character string describing 
the
+spatial relationship between two geometries.
+
+When called with a third string argument, returns `true` if the two geometries
+satisfy the given DE-9IM intersection matrix pattern, `false` otherwise. The 
pattern
+can use wildcards (`*`) and `T` (any non-empty intersection) in addition to 
exact
+dimension values (0, 1, 2, F).
 
 ## Examples
 ```sql
 SELECT ST_Relate(
-    ST_GeomFromWKT('POINT(0 0)'),
-    ST_GeomFromWKT('POINT(1 1)')
+    ST_GeomFromText('POINT (0.5 0.5)'),
+    ST_GeomFromText('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')
+);
+```
+```sql
+SELECT ST_Relate(
+    ST_GeomFromText('POINT (0.5 0.5)'),
+    ST_GeomFromText('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))'),
+    '0FFFFF212'
 );
 ```
diff --git a/python/sedonadb/tests/functions/test_predicates.py 
b/python/sedonadb/tests/functions/test_predicates.py
index bba19670..522fb3cb 100644
--- a/python/sedonadb/tests/functions/test_predicates.py
+++ b/python/sedonadb/tests/functions/test_predicates.py
@@ -495,3 +495,90 @@ def test_st_relate(eng, geom1, geom2, expected):
         f"SELECT ST_Relate({geom_or_null(geom1)}, {geom_or_null(geom2)})",
         expected,
     )
+
+
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+    ("geom1", "geom2", "pattern", "expected"),
+    [
+        (None, None, None, None),
+        ("POINT (0 0)", None, "FF0FFF0F2", None),
+        (None, "POINT (0 0)", "FF0FFF0F2", None),
+        ("POINT (0 0)", "POINT (0 0)", None, None),
+        # Exact match — disjoint points
+        ("POINT (0 0)", "POINT (1 1)", "FF0FFF0F2", True),
+        # Exact match — point inside polygon
+        ("POINT (0.5 0.5)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 
"0FFFFF212", True),
+        # Pattern does not match — point on boundary vs interior pattern
+        ("POINT (0 0)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "0FFFFF212", 
False),
+        # Polygon contains point — exact DE-9IM from polygon's perspective
+        (
+            "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
+            "POINT (0.5 0.5)",
+            "0F2FF1FF2",
+            True,
+        ),
+        # Touching polygons
+        (
+            "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
+            "POLYGON ((2 0, 4 0, 4 2, 2 2, 2 0))",
+            "FF2F11212",
+            True,
+        ),
+        # Overlapping polygons match overlap pattern
+        (
+            "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
+            "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))",
+            "212101212",
+            True,
+        ),
+        # Point in polygon hole — point is inside the hole, not the polygon 
interior
+        (
+            "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",
+            True,
+        ),
+        # Linestring relates to linestring
+        (
+            "LINESTRING (0 0, 2 2)",
+            "LINESTRING (1 1, 3 3)",
+            "1010F0102",
+            True,
+        ),
+        # Geometry collection relates to point
+        (
+            "GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1))",
+            "POINT (0 0)",
+            "FF10F0FF2",
+            True,
+        ),
+        # False cases — wrong pattern for the geometry pair
+        (
+            "POINT (0 0)",
+            "POINT (1 1)",
+            "0FFFFFFF2",
+            False,
+        ),
+        (
+            "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
+            "POLYGON ((5 5, 6 5, 6 6, 5 6, 5 5))",
+            "212101212",
+            False,
+        ),
+        # Disjoint — does not match contains pattern
+        (
+            "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
+            "POINT (5 5)",
+            "0F2FF1FF2",
+            False,
+        ),
+    ],
+)
+def test_st_relate_pattern(eng, geom1, geom2, pattern, expected):
+    eng = eng.create_or_skip()
+    pattern_sql = "NULL" if pattern is None else f"'{pattern}'"
+    eng.assert_query_result(
+        f"SELECT ST_Relate({geom_or_null(geom1)}, {geom_or_null(geom2)}, 
{pattern_sql})",
+        expected,
+    )

Reply via email to