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 5493b8e feat: Implement ST_Crosses and ST_Overlaps predicates (#204)
5493b8e is described below
commit 5493b8e48641a5f48d32a56b1c7aaeb0a20d8bb5
Author: Abeeujah <[email protected]>
AuthorDate: Sun Oct 12 03:55:28 2025 +0100
feat: Implement ST_Crosses and ST_Overlaps predicates (#204)
---
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 +-
python/sedonadb/tests/functions/test_predicates.py | 85 +++++++++++++++++++++
5 files changed, 222 insertions(+), 4 deletions(-)
diff --git a/c/sedona-geos/benches/geos-functions.rs
b/c/sedona-geos/benches/geos-functions.rs
index 74152c5..34382bd 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 d66a09b..90ece9b 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::<Within>::default())
}
+pub fn st_crosses_impl() -> ScalarKernelRef {
+ Arc::new(GeosPredicate::<Crosses>::default())
+}
+
+pub fn st_overlaps_impl() -> ScalarKernelRef {
+ Arc::new(GeosPredicate::<Overlaps>::default())
+}
+
impl<Op: BinaryPredicate> SedonaScalarKernel for GeosPredicate<Op> {
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
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 (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(false), 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 6a96dd1..eeaed35 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 cross
+#[derive(Debug, Default)]
+pub struct Crosses {}
+impl BinaryPredicate for Crosses {
+ fn evaluate(lhs: &Geometry, rhs: &Geometry) -> GResult<bool> {
+ lhs.crosses(rhs)
+ }
+}
+
+/// Check if the geometries overlap
+#[derive(Debug, Default)]
+pub struct Overlaps {}
+impl BinaryPredicate for Overlaps {
+ fn evaluate(lhs: &Geometry, rhs: &Geometry) -> GResult<bool> {
+ lhs.overlaps(rhs)
+ }
+}
diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs
index 15e18c6..f349f7e 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()),
]
}
diff --git a/python/sedonadb/tests/functions/test_predicates.py
b/python/sedonadb/tests/functions/test_predicates.py
index 77b3f03..9760ddc 100644
--- a/python/sedonadb/tests/functions/test_predicates.py
+++ b/python/sedonadb/tests/functions/test_predicates.py
@@ -357,3 +357,88 @@ def test_st_within_skipped(eng, geom1, geom2, expected):
f"SELECT ST_Within({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 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),
+ ("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,
+ )
+
+
[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 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),
+ ("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,
+ )