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,
+ )