From 2ccf81e57b81ecda00886b977ec66c07b5c5eb9f Mon Sep 17 00:00:00 2001 From: Abeeujah Date: Fri, 10 Oct 2025 15:31:23 +0100 Subject: [PATCH 1/5] feat: Implement ST_Crosses and ST_Overlaps predicates --- c/sedona-geos/benches/geos-functions.rs | 28 ++++++++ c/sedona-geos/src/binary_predicates.rs | 89 ++++++++++++++++++++++++- c/sedona-geos/src/geos.rs | 18 +++++ c/sedona-geos/src/register.rs | 6 +- 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/c/sedona-geos/benches/geos-functions.rs b/c/sedona-geos/benches/geos-functions.rs index 74152c54..34382bd9 100644 --- a/c/sedona-geos/benches/geos-functions.rs +++ b/c/sedona-geos/benches/geos-functions.rs @@ -267,6 +267,34 @@ fn criterion_benchmark(c: &mut Criterion) { "st_within", ArrayScalar(Polygon(10), Polygon(500)), ); + benchmark::scalar( + c, + &f, + "geos", + "st_crosses", + ArrayScalar(Polygon(10), Polygon(10)), + ); + benchmark::scalar( + c, + &f, + "geos", + "st_crosses", + ArrayScalar(Polygon(10), Polygon(500)), + ); + benchmark::scalar( + c, + &f, + "geos", + "st_overlaps", + ArrayScalar(Polygon(10), Polygon(10)), + ); + benchmark::scalar( + c, + &f, + "geos", + "st_overlaps", + ArrayScalar(Polygon(10), Polygon(500)), + ); } criterion_group!(benches, criterion_benchmark); diff --git a/c/sedona-geos/src/binary_predicates.rs b/c/sedona-geos/src/binary_predicates.rs index d66a09ba..04255b3e 100644 --- a/c/sedona-geos/src/binary_predicates.rs +++ b/c/sedona-geos/src/binary_predicates.rs @@ -17,8 +17,8 @@ use std::sync::Arc; use crate::geos::{ - BinaryPredicate, Contains, CoveredBy, Covers, Disjoint, Equals, GeosPredicate, Intersects, - Touches, Within, + BinaryPredicate, Contains, CoveredBy, Covers, Crosses, Disjoint, Equals, GeosPredicate, + Intersects, Overlaps, Touches, Within, }; use arrow_array::builder::BooleanBuilder; use arrow_schema::DataType; @@ -61,6 +61,14 @@ pub fn st_within_impl() -> ScalarKernelRef { Arc::new(GeosPredicate::::default()) } +pub fn st_crosses_impl() -> ScalarKernelRef { + Arc::new(GeosPredicate::::default()) +} + +pub fn st_overlaps_impl() -> ScalarKernelRef { + Arc::new(GeosPredicate::::default()) +} + impl SedonaScalarKernel for GeosPredicate { fn return_type(&self, args: &[SedonaType]) -> Result> { let matcher: ArgMatcher = ArgMatcher::new( @@ -377,4 +385,81 @@ mod tests { let expected: ArrayRef = arrow_array!(Boolean, [Some(true), Some(false), None]); assert_array_equal(&tester.invoke_array_array(arg1, arg2).unwrap(), &expected); } + + #[rstest] + fn crosses_udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) { + use datafusion_common::ScalarValue; + let udf = SedonaScalarUDF::from_kernel("st_crosses", st_crosses_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone(), sedona_type]); + tester.assert_return_type(DataType::Boolean); + + let result = tester + .invoke_scalar_scalar("LINESTRING (0 0, 1 1)", "LINESTRING (0 1, 1 0)") + .unwrap(); + tester.assert_scalar_result_equals(result, true); + + let result = tester + .invoke_scalar_scalar(ScalarValue::Null, ScalarValue::Null) + .unwrap(); + assert!(result.is_null()); + + let arg1 = create_array( + &[ + Some("LINESTRING (0 0, 1 1)"), + Some("LINESTRING (0 0, 1 0)"), + None, + ], + &WKB_GEOMETRY, + ); + let arg2 = create_array( + &[ + Some("LINESTRING (0 1, 1 0)"), + Some("POLYGON ((2 2, 2 3, 3 3, 3 2, 2 2))"), + Some("LINESTRING (0 0, 1 1)"), + ], + &WKB_GEOMETRY, + ); + let expected: ArrayRef = arrow_array!(Boolean, [Some(true), Some(false), None]); + assert_array_equal(&tester.invoke_array_array(arg1, arg2).unwrap(), &expected); + } + + #[rstest] + fn overlaps_udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) { + use datafusion_common::ScalarValue; + let udf = SedonaScalarUDF::from_kernel("st_overlaps", st_overlaps_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone(), sedona_type]); + tester.assert_return_type(DataType::Boolean); + + let result = tester + .invoke_scalar_scalar( + "POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))", + "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + ) + .unwrap(); + tester.assert_scalar_result_equals(result, true); + + let result = tester + .invoke_scalar_scalar(ScalarValue::Null, ScalarValue::Null) + .unwrap(); + assert!(result.is_null()); + + let arg1 = create_array( + &[ + Some("POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))"), + Some("LINESTRING (0 0, 2 0)"), + None, + ], + &WKB_GEOMETRY, + ); + let arg2 = create_array( + &[ + Some("POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))"), + Some("LINESTRING (1 0, 3 0)"), + Some("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"), + ], + &WKB_GEOMETRY, + ); + let expected: ArrayRef = arrow_array!(Boolean, [Some(true), Some(true), None]); + assert_array_equal(&tester.invoke_array_array(arg1, arg2).unwrap(), &expected); + } } diff --git a/c/sedona-geos/src/geos.rs b/c/sedona-geos/src/geos.rs index 6a96dd1f..fccf5654 100644 --- a/c/sedona-geos/src/geos.rs +++ b/c/sedona-geos/src/geos.rs @@ -96,3 +96,21 @@ impl BinaryPredicate for Touches { lhs.touches(rhs) } } + +/// Check if the geometries crosses +#[derive(Debug, Default)] +pub struct Crosses {} +impl BinaryPredicate for Crosses { + fn evaluate(lhs: &Geometry, rhs: &Geometry) -> GResult { + lhs.crosses(rhs) + } +} + +/// Check if the geometries overlaps +#[derive(Debug, Default)] +pub struct Overlaps {} +impl BinaryPredicate for Overlaps { + fn evaluate(lhs: &Geometry, rhs: &Geometry) -> GResult { + lhs.overlaps(rhs) + } +} diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs index 15e18c66..f349f7e0 100644 --- a/c/sedona-geos/src/register.rs +++ b/c/sedona-geos/src/register.rs @@ -24,8 +24,8 @@ use crate::{ }; use crate::binary_predicates::{ - st_contains_impl, st_covered_by_impl, st_covers_impl, st_disjoint_impl, st_equals_impl, - st_intersects_impl, st_touches_impl, st_within_impl, + st_contains_impl, st_covered_by_impl, st_covers_impl, st_crosses_impl, st_disjoint_impl, + st_equals_impl, st_intersects_impl, st_overlaps_impl, st_touches_impl, st_within_impl, }; use crate::overlay::{ @@ -54,5 +54,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> { ("st_touches", st_touches_impl()), ("st_union", st_union_impl()), ("st_within", st_within_impl()), + ("st_crosses", st_crosses_impl()), + ("st_overlaps", st_overlaps_impl()), ] } From 632a2c09d981cbd87801b0b423ab4a378fb7df1e Mon Sep 17 00:00:00 2001 From: Abeeujah Date: Sun, 12 Oct 2025 00:14:57 +0100 Subject: [PATCH 2/5] dev: fix doc comments --- c/sedona-geos/src/geos.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/c/sedona-geos/src/geos.rs b/c/sedona-geos/src/geos.rs index fccf5654..eeaed350 100644 --- a/c/sedona-geos/src/geos.rs +++ b/c/sedona-geos/src/geos.rs @@ -97,7 +97,7 @@ impl BinaryPredicate for Touches { } } -/// Check if the geometries crosses +/// Check if the geometries cross #[derive(Debug, Default)] pub struct Crosses {} impl BinaryPredicate for Crosses { @@ -106,7 +106,7 @@ impl BinaryPredicate for Crosses { } } -/// Check if the geometries overlaps +/// Check if the geometries overlap #[derive(Debug, Default)] pub struct Overlaps {} impl BinaryPredicate for Overlaps { From a49715265abd6e360873ab25464ce60ebebfb3ad Mon Sep 17 00:00:00 2001 From: Abeeujah Date: Sun, 12 Oct 2025 00:20:31 +0100 Subject: [PATCH 3/5] dev: Fix nitpick suggestion for overlaps_udf test --- c/sedona-geos/src/binary_predicates.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/c/sedona-geos/src/binary_predicates.rs b/c/sedona-geos/src/binary_predicates.rs index 04255b3e..90ece9ba 100644 --- a/c/sedona-geos/src/binary_predicates.rs +++ b/c/sedona-geos/src/binary_predicates.rs @@ -454,12 +454,12 @@ mod tests { let arg2 = create_array( &[ Some("POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))"), - Some("LINESTRING (1 0, 3 0)"), + Some("LINESTRING (2 0, 3 0)"), Some("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"), ], &WKB_GEOMETRY, ); - let expected: ArrayRef = arrow_array!(Boolean, [Some(true), Some(true), None]); + let expected: ArrayRef = arrow_array!(Boolean, [Some(true), Some(false), None]); assert_array_equal(&tester.invoke_array_array(arg1, arg2).unwrap(), &expected); } } From b46130ff9ee63dc8d5df7baf1601d2f2830f75f4 Mon Sep 17 00:00:00 2001 From: Abeeujah Date: Sun, 12 Oct 2025 01:12:34 +0100 Subject: [PATCH 4/5] dev: Add Python integration test for crosses and overlap --- .../tests/functions/test_predicates.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/python/sedonadb/tests/functions/test_predicates.py b/python/sedonadb/tests/functions/test_predicates.py index 77b3f031..d91e3f20 100644 --- a/python/sedonadb/tests/functions/test_predicates.py +++ b/python/sedonadb/tests/functions/test_predicates.py @@ -357,3 +357,86 @@ def test_st_within_skipped(eng, geom1, geom2, expected): f"SELECT ST_Within({geom_or_null(geom1)}, {geom_or_null(geom2)})", expected, ) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom1", "geom2", "expected"), + [ + (None, None, None), + ("POINT (0 0)", None, None), + (None, "POINT (0 0)", None), + ("POINT (0 0)", "POINT (0 0)", False), + ("POINT (0.5 0.5)", "LINESTRING (0 0, 1 1)", False), + ("POINT (0 0)", "LINESTRING (0 0, 1 1)", False), + ("POINT (0.5 0.5)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", False), + ("POINT (0 0)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", False), + ("LINESTRING (0 0, 1 1)", "LINESTRING (0 1, 1 0)", True), + ("LINESTRING (0 0, 1 1)", "LINESTRING (1 1, 2 2)", False), + ("LINESTRING (0 0, 2 2)", "LINESTRING (1 1, 3 3)", False), + ("LINESTRING (-1 -1, 1 1)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", True), + ("LINESTRING (-1 0, 0 0)", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", False), + ( + "LINESTRING (0.1 0.1, 0.5 0.5)", + "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", + False, + ), + ( + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", + "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))", + False, + ), + ], +) +def test_st_crosses(eng, geom1, geom2, expected): + eng = eng.create_or_skip() + eng.assert_query_result( + f"SELECT ST_Crosses({geom_or_null(geom1)}, {geom_or_null(geom2)})", + expected, + ) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom1", "geom2", "expected"), + [ + (None, None, None), + ("POINT (0 0)", None, None), + (None, "POINT (0 0)", None), + ("POINT (0 0)", "LINESTRING (0 0, 1 1)", False), + ("LINESTRING (0 0, 2 2)", "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))", False), + ("MULTIPOINT ((0 0), (1 1))", "MULTIPOINT ((1 1), (2 2))", True), + ("MULTIPOINT ((0 0), (1 1))", "MULTIPOINT ((0 0), (1 1))", False), + ("POINT (0 0)", "POINT (0 0)", False), + ("LINESTRING (0 0, 2 2)", "LINESTRING (1 1, 3 3)", True), + ("LINESTRING (0 0, 1 1)", "LINESTRING (0 1, 1 0)", False), + ("LINESTRING (0 0, 1 1)", "LINESTRING (1 1, 2 2)", False), + ("LINESTRING (0 0, 1 1)", "LINESTRING (0 0, 1 1)", False), + ( + "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", + "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))", + True, + ), + ( + "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", + "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))", + False, + ), + ( + "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", + "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", + False, + ), + ( + "POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0))", + "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))", + False, + ), + ], +) +def test_st_overlaps(eng, geom1, geom2, expected): + eng = eng.create_or_skip() + eng.assert_query_result( + f"SELECT ST_Overlaps({geom_or_null(geom1)}, {geom_or_null(geom2)})", + expected, + ) From c926c2a4d4dda2e0fc00e04828b033c3c8fe4855 Mon Sep 17 00:00:00 2001 From: Abeeujah Date: Sun, 12 Oct 2025 02:33:13 +0100 Subject: [PATCH 5/5] dev: Add Edge case for identical and touching geometries --- python/sedonadb/tests/functions/test_predicates.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/sedonadb/tests/functions/test_predicates.py b/python/sedonadb/tests/functions/test_predicates.py index d91e3f20..9760ddc2 100644 --- a/python/sedonadb/tests/functions/test_predicates.py +++ b/python/sedonadb/tests/functions/test_predicates.py @@ -366,6 +366,7 @@ def test_st_within_skipped(eng, geom1, geom2, expected): (None, None, None), ("POINT (0 0)", None, None), (None, "POINT (0 0)", None), + ("POINT (0 0)", "POINT EMPTY", False), ("POINT (0 0)", "POINT (0 0)", False), ("POINT (0.5 0.5)", "LINESTRING (0 0, 1 1)", False), ("POINT (0 0)", "LINESTRING (0 0, 1 1)", False), @@ -403,6 +404,7 @@ def test_st_crosses(eng, geom1, geom2, expected): (None, None, None), ("POINT (0 0)", None, None), (None, "POINT (0 0)", None), + ("POINT (0 0)", "POINT EMPTY", False), ("POINT (0 0)", "LINESTRING (0 0, 1 1)", False), ("LINESTRING (0 0, 2 2)", "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))", False), ("MULTIPOINT ((0 0), (1 1))", "MULTIPOINT ((1 1), (2 2))", True),