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 3e6cfe3 feat(c/sedona-geos): Implement ST_Reverse using `geos` (#288)
3e6cfe3 is described below
commit 3e6cfe33e7faa4d237542abbb9c09df994c57ec0
Author: Peter Nguyen <[email protected]>
AuthorDate: Sat Nov 8 20:09:11 2025 -0800
feat(c/sedona-geos): Implement ST_Reverse using `geos` (#288)
---
c/sedona-geos/src/lib.rs | 1 +
c/sedona-geos/src/register.rs | 2 +
c/sedona-geos/src/st_reverse.rs | 134 ++++++++++++++++++++++
python/sedonadb/tests/functions/test_functions.py | 26 +++++
4 files changed, 163 insertions(+)
diff --git a/c/sedona-geos/src/lib.rs b/c/sedona-geos/src/lib.rs
index 6aca11e..fc589a1 100644
--- a/c/sedona-geos/src/lib.rs
+++ b/c/sedona-geos/src/lib.rs
@@ -31,6 +31,7 @@ mod st_isvalid;
mod st_isvalidreason;
mod st_length;
mod st_perimeter;
+mod st_reverse;
mod st_simplifypreservetopology;
mod st_unaryunion;
pub mod wkb_to_geos;
diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs
index c4c5e3a..f24404e 100644
--- a/c/sedona-geos/src/register.rs
+++ b/c/sedona-geos/src/register.rs
@@ -29,6 +29,7 @@ use crate::{
st_isvalidreason::st_is_valid_reason_impl,
st_length::st_length_impl,
st_perimeter::st_perimeter_impl,
+ st_reverse::st_reverse_impl,
st_simplifypreservetopology::st_simplify_preserve_topology_impl,
st_unaryunion::st_unary_union_impl,
};
@@ -67,6 +68,7 @@ pub fn scalar_kernels() -> Vec<(&'static str,
ScalarKernelRef)> {
("st_length", st_length_impl()),
("st_overlaps", st_overlaps_impl()),
("st_perimeter", st_perimeter_impl()),
+ ("st_reverse", st_reverse_impl()),
(
"st_simplifypreservetopology",
st_simplify_preserve_topology_impl(),
diff --git a/c/sedona-geos/src/st_reverse.rs b/c/sedona-geos/src/st_reverse.rs
new file mode 100644
index 0000000..1ba799c
--- /dev/null
+++ b/c/sedona-geos/src/st_reverse.rs
@@ -0,0 +1,134 @@
+// 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::BinaryBuilder;
+use datafusion_common::{error::Result, DataFusionError};
+use datafusion_expr::ColumnarValue;
+use geos::Geom;
+use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
+use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
+use sedona_schema::{
+ datatypes::{SedonaType, WKB_GEOMETRY},
+ matchers::ArgMatcher,
+};
+
+use crate::executor::GeosExecutor;
+
+/// ST_Reverse() implementation using the geos crate
+pub fn st_reverse_impl() -> ScalarKernelRef {
+ Arc::new(STReverse {})
+}
+
+#[derive(Debug)]
+struct STReverse {}
+
+impl SedonaScalarKernel for STReverse {
+ fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+ let matcher = ArgMatcher::new(vec![ArgMatcher::is_geometry()],
WKB_GEOMETRY);
+
+ matcher.match_args(args)
+ }
+
+ fn invoke_batch(
+ &self,
+ arg_types: &[SedonaType],
+ args: &[ColumnarValue],
+ ) -> Result<ColumnarValue> {
+ let executor = GeosExecutor::new(arg_types, args);
+ let mut builder = BinaryBuilder::with_capacity(
+ executor.num_iterations(),
+ WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
+ );
+ executor.execute_wkb_void(|maybe_wkb| {
+ match maybe_wkb {
+ Some(wkb) => {
+ invoke_scalar(&wkb, &mut builder)?;
+ builder.append_value([]);
+ }
+ _ => builder.append_null(),
+ }
+
+ Ok(())
+ })?;
+
+ executor.finish(Arc::new(builder.finish()))
+ }
+}
+
+fn invoke_scalar(geos_geom: &geos::Geometry, writer: &mut impl std::io::Write)
-> Result<()> {
+ let geometry = geos_geom
+ .reverse()
+ .map_err(|e| DataFusionError::Execution(format!("Failed to calculate
reverse: {e}")))?;
+
+ let wkb = geometry
+ .to_wkb()
+ .map_err(|e| DataFusionError::Execution(format!("Failed to convert to
wkb: {e}")))?;
+
+ writer.write_all(wkb.as_ref())?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ 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_kernel("st_reverse",
st_reverse_impl());
+ let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type]);
+ tester.assert_return_type(WKB_GEOMETRY);
+
+ let result = tester
+ .invoke_scalar("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
+ .unwrap();
+ tester.assert_scalar_result_equals(result, "POLYGON ((0 0, 1 0, 1 1, 0
1, 0 0))");
+
+ let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+ assert!(result.is_null());
+
+ let input_wkt = vec![
+ Some("POLYGON ((2 2, 2 3, 3 3, 3 2, 2 2))"),
+ Some("POINT EMPTY"),
+ Some("POINT (1 2)"),
+ Some("LINESTRING (1 2, 1 10)"),
+ Some("GEOMETRYCOLLECTION (MULTIPOINT (3 4, 1 2, 7 8, 5 6),
LINESTRING (1 10, 1 2))"),
+ None,
+ ];
+
+ let expected = create_array(
+ &[
+ Some("POLYGON ((2 2, 3 2, 3 3, 2 3, 2 2))"),
+ Some("POINT EMPTY"),
+ Some("POINT (1 2)"),
+ Some("LINESTRING (1 10, 1 2)"),
+ Some("GEOMETRYCOLLECTION (MULTIPOINT (3 4, 1 2, 7 8, 5 6),
LINESTRING(1 2, 1 10))"),
+ None,
+ ],
+ &WKB_GEOMETRY,
+ );
+ assert_array_equal(&tester.invoke_wkb_array(input_wkt).unwrap(),
&expected);
+ }
+}
diff --git a/python/sedonadb/tests/functions/test_functions.py
b/python/sedonadb/tests/functions/test_functions.py
index 6b5a42e..f7bc673 100644
--- a/python/sedonadb/tests/functions/test_functions.py
+++ b/python/sedonadb/tests/functions/test_functions.py
@@ -1269,6 +1269,32 @@ def test_st_perimeter(eng, geom, expected):
eng.assert_query_result(f"SELECT ST_Perimeter({arg})", expected)
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+ ("geom", "expected"),
+ [
+ (None, None),
+ ("LINESTRING EMPTY", "LINESTRING EMPTY"),
+ ("LINESTRING(0 0, 1 1, 2 2)", "LINESTRING (2 2, 1 1, 0 0)"),
+ ("POINT (1 2)", "POINT (1 2)"),
+ ("POLYGON ((0 0, 1 0, 2 2, 1 2, 0 0))", "POLYGON ((0 0, 1 2, 2 2, 1 0,
0 0))"),
+ # Note MultiPoints don't change since each point is separate (e.g not
a line string)
+ ("MULTIPOINT (1 2, 3 4)", "MULTIPOINT (1 2, 3 4)"),
+ (
+ "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 2, 0 0)), ((5 5, 6 0, 7 1, 0 1,
5 5)))",
+ "MULTIPOLYGON (((0 0, 0 2, 1 1, 1 0, 0 0)), ((5 5, 0 1, 7 1, 6 0,
5 5)))",
+ ),
+ (
+ "GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), POLYGON
((0 0, 0 1, 1 1, 1 0, 0 0)))",
+ "GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (5 6, 3 4), POLYGON
((0 0, 1 0, 1 1, 0 1, 0 0)))",
+ ),
+ ],
+)
+def test_st_reverse(eng, geom, expected):
+ eng = eng.create_or_skip()
+ eng.assert_query_result(f"SELECT ST_Reverse({geom_or_null(geom)})",
expected)
+
+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
@pytest.mark.parametrize(
("x", "y", "expected"),