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 7f26ab65 feat(rust/sedona-functions): Implement ST_Reverse using 
geo-traits (#363)
7f26ab65 is described below

commit 7f26ab659173f9286fdb35ef51a648c8840447ba
Author: Abeeujah <[email protected]>
AuthorDate: Thu Nov 27 06:24:53 2025 +0100

    feat(rust/sedona-functions): Implement ST_Reverse using geo-traits (#363)
---
 benchmarks/test_functions.py                      |  18 +
 c/sedona-geos/src/lib.rs                          |   1 -
 c/sedona-geos/src/register.rs                     |   2 -
 c/sedona-geos/src/st_reverse.rs                   | 134 -------
 rust/sedona-functions/benches/native-functions.rs |   3 +
 rust/sedona-functions/src/lib.rs                  |   1 +
 rust/sedona-functions/src/register.rs             |   1 +
 rust/sedona-functions/src/st_reverse.rs           | 442 ++++++++++++++++++++++
 8 files changed, 465 insertions(+), 137 deletions(-)

diff --git a/benchmarks/test_functions.py b/benchmarks/test_functions.py
index aabae02f..27522680 100644
--- a/benchmarks/test_functions.py
+++ b/benchmarks/test_functions.py
@@ -329,6 +329,24 @@ class TestBenchFunctions(TestBenchBase):
 
         benchmark(queries)
 
+    @pytest.mark.parametrize(
+        "eng", [SedonaDBSingleThread, PostGISSingleThread, DuckDBSingleThread]
+    )
+    @pytest.mark.parametrize(
+        "table",
+        [
+            "collections_simple",
+            "segments_large",
+        ],
+    )
+    def test_st_reverse(self, benchmark, eng, table):
+        eng = self._get_eng(eng)
+
+        def queries():
+            eng.execute_and_collect(f"SELECT ST_Reverse(geom1) from {table}")
+
+        benchmark(queries)
+
     @pytest.mark.parametrize(
         "eng", [SedonaDBSingleThread, PostGISSingleThread, DuckDBSingleThread]
     )
diff --git a/c/sedona-geos/src/lib.rs b/c/sedona-geos/src/lib.rs
index f76f8169..921e7875 100644
--- a/c/sedona-geos/src/lib.rs
+++ b/c/sedona-geos/src/lib.rs
@@ -37,7 +37,6 @@ mod st_minimumclearance_line;
 mod st_perimeter;
 mod st_polygonize;
 mod st_polygonize_agg;
-mod st_reverse;
 mod st_simplify;
 mod st_simplifypreservetopology;
 mod st_snap;
diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs
index f1728522..4200ce4c 100644
--- a/c/sedona-geos/src/register.rs
+++ b/c/sedona-geos/src/register.rs
@@ -36,7 +36,6 @@ use crate::{
     st_perimeter::st_perimeter_impl,
     st_polygonize::st_polygonize_impl,
     st_polygonize_agg::st_polygonize_agg_impl,
-    st_reverse::st_reverse_impl,
     st_simplify::st_simplify_impl,
     st_simplifypreservetopology::st_simplify_preserve_topology_impl,
     st_snap::st_snap_impl,
@@ -82,7 +81,6 @@ pub fn scalar_kernels() -> Vec<(&'static str, 
ScalarKernelRef)> {
         ("st_overlaps", st_overlaps_impl()),
         ("st_perimeter", st_perimeter_impl()),
         ("st_polygonize", st_polygonize_impl()),
-        ("st_reverse", st_reverse_impl()),
         ("st_simplify", st_simplify_impl()),
         (
             "st_simplifypreservetopology",
diff --git a/c/sedona-geos/src/st_reverse.rs b/c/sedona-geos/src/st_reverse.rs
deleted file mode 100644
index 1ba799cd..00000000
--- a/c/sedona-geos/src/st_reverse.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-// 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/rust/sedona-functions/benches/native-functions.rs 
b/rust/sedona-functions/benches/native-functions.rs
index 2a68dc48..6f30fd81 100644
--- a/rust/sedona-functions/benches/native-functions.rs
+++ b/rust/sedona-functions/benches/native-functions.rs
@@ -147,6 +147,9 @@ fn criterion_benchmark(c: &mut Criterion) {
         ),
     );
 
+    benchmark::scalar(c, &f, "native", "st_reverse", Polygon(10));
+    benchmark::scalar(c, &f, "native", "st_reverse", MultiPoint(10));
+
     benchmark::scalar(
         c,
         &f,
diff --git a/rust/sedona-functions/src/lib.rs b/rust/sedona-functions/src/lib.rs
index a7488716..ba0ef210 100644
--- a/rust/sedona-functions/src/lib.rs
+++ b/rust/sedona-functions/src/lib.rs
@@ -55,6 +55,7 @@ mod st_pointn;
 mod st_points;
 mod st_pointzm;
 mod st_polygonize_agg;
+mod st_reverse;
 mod st_setsrid;
 mod st_srid;
 mod st_start_point;
diff --git a/rust/sedona-functions/src/register.rs 
b/rust/sedona-functions/src/register.rs
index 2b9ecb39..1e7a0e8a 100644
--- a/rust/sedona-functions/src/register.rs
+++ b/rust/sedona-functions/src/register.rs
@@ -96,6 +96,7 @@ pub fn default_function_set() -> FunctionSet {
         crate::st_pointzm::st_pointm_udf,
         crate::st_pointzm::st_pointz_udf,
         crate::st_pointzm::st_pointzm_udf,
+        crate::st_reverse::st_reverse_udf,
         crate::st_setsrid::st_set_crs_udf,
         crate::st_setsrid::st_set_srid_udf,
         crate::st_srid::st_crs_udf,
diff --git a/rust/sedona-functions/src/st_reverse.rs 
b/rust/sedona-functions/src/st_reverse.rs
new file mode 100644
index 00000000..94afdf2f
--- /dev/null
+++ b/rust/sedona-functions/src/st_reverse.rs
@@ -0,0 +1,442 @@
+// 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::io::Write;
+use std::sync::Arc;
+
+use arrow_array::builder::BinaryBuilder;
+use datafusion_common::{DataFusionError, Result};
+use datafusion_expr::ColumnarValue;
+use datafusion_expr::{scalar_doc_sections::DOC_SECTION_OTHER, Documentation, 
Volatility};
+use geo_traits::{
+    CoordTrait, GeometryCollectionTrait, GeometryTrait, LineStringTrait, 
MultiLineStringTrait,
+    MultiPointTrait, MultiPolygonTrait, PointTrait, PolygonTrait,
+};
+use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_geometry::wkb_factory::{
+    write_wkb_coord_trait, write_wkb_empty_point, 
write_wkb_geometrycollection_header,
+    write_wkb_linestring_header, write_wkb_multilinestring_header, 
write_wkb_multipoint_header,
+    write_wkb_multipolygon_header, write_wkb_point_header, 
write_wkb_polygon_header,
+    write_wkb_polygon_ring_header, WKB_MIN_PROBABLE_BYTES,
+};
+use sedona_schema::{
+    datatypes::{SedonaType, WKB_GEOMETRY},
+    matchers::ArgMatcher,
+};
+
+use crate::executor::WkbExecutor;
+
+/// ST_Reverse() scalar UDF
+///
+/// Native implementation to reverse the vertices in a geometry
+pub fn st_reverse_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "st_reverse",
+        vec![Arc::new(STReverse)],
+        Volatility::Immutable,
+        Some(st_reverse_doc()),
+    )
+}
+
+fn st_reverse_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Can be used on any geometry and reverses the order of the vertices.",
+        "ST_Reverse (geom: Geometry)",
+    )
+    .with_argument("geom", "geometry: Input geometry")
+    .with_sql_example("SELECT ST_AsText(ST_Reverse('POLYGON ((2 2, 2 3, 3 3, 3 
2, 2 2))'))")
+    .build()
+}
+
+#[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 = WkbExecutor::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(geom: &impl GeometryTrait<T = f64>, writer: &mut impl Write) 
-> Result<()> {
+    let dims = geom.dim();
+    match geom.as_type() {
+        geo_traits::GeometryType::Point(pt) => {
+            if pt.coord().is_some() {
+                write_wkb_point_header(writer, dims)
+                    .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+                write_wkb_coord_trait(writer, &pt.coord().unwrap())
+                    .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+            } else {
+                write_wkb_empty_point(writer, dims)
+                    .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+            }
+        }
+
+        geo_traits::GeometryType::MultiPoint(multi_point) => {
+            write_wkb_multipoint_header(writer, dims, 
multi_point.points().count())
+                .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+            for pt in multi_point.points() {
+                invoke_scalar(&pt, writer)?;
+            }
+        }
+
+        geo_traits::GeometryType::LineString(ls) => {
+            write_wkb_linestring_header(writer, dims, ls.coords().count())
+                .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+            write_reversed_coords(writer, ls.coords())?;
+        }
+
+        geo_traits::GeometryType::Polygon(pgn) => {
+            let num_rings = pgn.interiors().count() + pgn.exterior().is_some() 
as usize;
+            write_wkb_polygon_header(writer, dims, num_rings)
+                .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+
+            if let Some(exterior) = pgn.exterior() {
+                write_reversed_ring(writer, exterior)?;
+            }
+
+            for interior in pgn.interiors() {
+                write_reversed_ring(writer, interior)?;
+            }
+        }
+
+        geo_traits::GeometryType::MultiLineString(mls) => {
+            write_wkb_multilinestring_header(writer, dims, 
mls.line_strings().count())
+                .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+            for ls in mls.line_strings() {
+                invoke_scalar(&ls, writer)?;
+            }
+        }
+
+        geo_traits::GeometryType::MultiPolygon(mpgn) => {
+            write_wkb_multipolygon_header(writer, dims, 
mpgn.polygons().count())
+                .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+            for pgn in mpgn.polygons() {
+                invoke_scalar(&pgn, writer)?;
+            }
+        }
+
+        geo_traits::GeometryType::GeometryCollection(gcn) => {
+            write_wkb_geometrycollection_header(writer, dims, 
gcn.geometries().count())
+                .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+            for geom in gcn.geometries() {
+                invoke_scalar(&geom, writer)?;
+            }
+        }
+
+        _ => {
+            return Err(DataFusionError::Execution(
+                "Unsupported geometry type for reversal operation".to_string(),
+            ));
+        }
+    }
+    Ok(())
+}
+
+fn write_reversed_ring(writer: &mut impl Write, ring: impl LineStringTrait<T = 
f64>) -> Result<()> {
+    write_wkb_polygon_ring_header(writer, ring.coords().count())
+        .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+    write_reversed_coords(writer, ring.coords())
+}
+
+fn write_reversed_coords<I>(writer: &mut impl Write, coords: I) -> Result<()>
+where
+    I: DoubleEndedIterator,
+    I::Item: CoordTrait<T = f64>,
+{
+    coords.rev().try_for_each(|coord| {
+        write_wkb_coord_trait(writer, &coord).map_err(|e| 
DataFusionError::Execution(e.to_string()))
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use datafusion_common::ScalarValue;
+    use rstest::rstest;
+    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 tester = ScalarUdfTester::new(st_reverse_udf().into(), 
vec![sedona_type]);
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let result = tester.invoke_scalar("POINT EMPTY").unwrap();
+        tester.assert_scalar_result_equals(result, "POINT EMPTY");
+
+        let result = tester.invoke_scalar("POINT (30 10)").unwrap();
+        tester.assert_scalar_result_equals(result, "POINT (30 10)");
+
+        let result = tester
+            .invoke_scalar("LINESTRING (30 10, 10 30, 40 40)")
+            .unwrap();
+        tester.assert_scalar_result_equals(result, "LINESTRING (40 40, 10 30, 
30 10)");
+
+        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("MULTIPOINT ((10 40), (40 30), (20 20), (30 10))")
+            .unwrap();
+        tester
+            .assert_scalar_result_equals(result, "MULTIPOINT ((10 40), (40 
30), (20 20), (30 10))");
+
+        let result = tester
+            .invoke_scalar("MULTILINESTRING ((10 10, 20 20), (15 15, 30 15))")
+            .unwrap();
+        tester.assert_scalar_result_equals(
+            result,
+            "MULTILINESTRING ((20 20, 10 10), (30 15, 15 15))",
+        );
+
+        let result = tester
+            .invoke_scalar("MULTIPOLYGON (((10 10, 10 20, 20 20, 20 15, 10 
10)), ((60 60, 70 70, 80 60, 60 60)))")
+            .unwrap();
+        tester.assert_scalar_result_equals(
+            result,
+            "MULTIPOLYGON (((10 10, 20 15, 20 20, 10 20, 10 10)), ((60 60, 80 
60, 70 70, 60 60)))",
+        );
+
+        let result = tester
+            .invoke_scalar(
+                "GEOMETRYCOLLECTION (MULTIPOINT (3 4, 1 2, 7 8, 5 6), 
LINESTRING (1 10, 1 2))",
+            )
+            .unwrap();
+        tester.assert_scalar_result_equals(
+            result,
+            "GEOMETRYCOLLECTION (MULTIPOINT ((3 4), (1 2), (7 8), (5 6)), 
LINESTRING (1 2, 1 10))",
+        );
+
+        let result = tester
+            .invoke_scalar("GEOMETRYCOLLECTION (POINT (10 40), LINESTRING (30 
10, 10 30, 40 40), POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10)))")
+            .unwrap();
+        tester.assert_scalar_result_equals(
+            result,
+            "GEOMETRYCOLLECTION (POINT (10 40), LINESTRING (40 40, 10 30, 30 
10), POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10)))",
+        );
+
+        let result = tester
+            .invoke_scalar(
+                "GEOMETRYCOLLECTION (
+                POINT (10 10),
+                LINESTRING (10 20, 20 20, 20 30),
+                GEOMETRYCOLLECTION (
+                    POLYGON ((40 40, 50 50, 60 40, 40 40)),
+                    MULTIPOINT (70 70, 80 80)
+                ),
+                GEOMETRYCOLLECTION (
+                    LINESTRING (90 90, 100 100),
+                    POINT (95 95)
+                )
+            )",
+            )
+            .unwrap();
+        tester.assert_scalar_result_equals(
+            result,
+            "GEOMETRYCOLLECTION(
+            POINT(10 10),
+            LINESTRING(20 30,20 20,10 20),
+            GEOMETRYCOLLECTION(
+                POLYGON((40 40,60 40,50 50,40 40)),
+                MULTIPOINT((70 70),(80 80))
+            ),
+            GEOMETRYCOLLECTION(
+                LINESTRING(100 100,90 90),
+                POINT(95 95)
+            )
+            )",
+        );
+
+        let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+        assert!(result.is_null());
+
+        let input_wkt = vec![
+            // Null case
+            None,
+            // POINT types
+            Some("POINT EMPTY"),
+            Some("POINT (1 2)"),
+            Some("POINT Z EMPTY"),
+            Some("POINT Z (1 2 3)"),
+            Some("POINT M EMPTY"),
+            Some("POINT M (1 2 3)"),
+            Some("POINT ZM EMPTY"),
+            Some("POINT ZM (1 2 3 4)"),
+            // LINESTRING types
+            Some("LINESTRING EMPTY"),
+            Some("LINESTRING (1 2, 1 10)"),
+            Some("LINESTRING (0 0, 1 1, 2 2)"),
+            Some("LINESTRING (10 20, 30 40)"),
+            Some("LINESTRING Z EMPTY"),
+            Some("LINESTRING Z (1 2 3, 4 5 6)"),
+            Some("LINESTRING M EMPTY"),
+            Some("LINESTRING M (1 2 3, 4 5 6)"),
+            Some("LINESTRING ZM EMPTY"),
+            Some("LINESTRING ZM (1 2 3 4, 5 6 7 8)"),
+            // POLYGON types
+            Some("POLYGON EMPTY"),
+            Some("POLYGON ((2 2, 2 3, 3 3, 3 2, 2 2))"),
+            Some("POLYGON Z EMPTY"),
+            Some("POLYGON Z ((0 0 0, 0 1 0, 1 1 0, 1 0 0, 0 0 0))"),
+            Some("POLYGON M EMPTY"),
+            Some("POLYGON M ((0 0 0, 0 1 0, 1 1 0, 1 0 0, 0 0 0))"),
+            Some("POLYGON ZM EMPTY"),
+            Some("POLYGON ZM ((0 0 0 0, 0 1 0 0, 1 1 0 0, 1 0 0 0, 0 0 0 0))"),
+            // MULTIPOINT types
+            Some("MULTIPOINT EMPTY"),
+            Some("MULTIPOINT((3 4),(1 2),(7 8),(5 6))"),
+            Some("MULTIPOINT Z EMPTY"),
+            Some("MULTIPOINT Z ((1 2 3), (4 5 6))"),
+            Some("MULTIPOINT M EMPTY"),
+            Some("MULTIPOINT M ((1 2 3), (4 5 6))"),
+            Some("MULTIPOINT ZM EMPTY"),
+            Some("MULTIPOINT ZM ((1 2 3 4), (5 6 7 8))"),
+            // MULTILINESTRING types
+            Some("MULTILINESTRING EMPTY"),
+            Some("MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))"),
+            Some("MULTILINESTRING Z EMPTY"),
+            Some("MULTILINESTRING Z ((1 2 3, 4 5 6), (7 8 9, 10 11 12))"),
+            Some("MULTILINESTRING M EMPTY"),
+            Some("MULTILINESTRING M ((1 2 3, 4 5 6), (7 8 9, 10 11 12))"),
+            Some("MULTILINESTRING ZM EMPTY"),
+            Some("MULTILINESTRING ZM ((1 2 3 4, 5 6 7 8), (9 10 11 12, 13 14 
15 16))"),
+            // MULTIPOLYGON types
+            Some("MULTIPOLYGON EMPTY"),
+            Some("MULTIPOLYGON (((0 0, 0 1, 1 1, 1 0, 0 0)), ((2 2, 2 3, 3 3, 
3 2, 2 2)))"),
+            Some("MULTIPOLYGON Z EMPTY"),
+            Some("MULTIPOLYGON Z (((0 0 0, 0 1 0, 1 1 0, 1 0 0, 0 0 0)))"),
+            Some("MULTIPOLYGON M EMPTY"),
+            Some("MULTIPOLYGON M (((0 0 0, 0 1 0, 1 1 0, 1 0 0, 0 0 0)))"),
+            Some("MULTIPOLYGON ZM EMPTY"),
+            Some("MULTIPOLYGON ZM (((0 0 0 0, 0 1 0 0, 1 1 0 0, 1 0 0 0, 0 0 0 
0)))"),
+            // GEOMETRYCOLLECTION types
+            Some("GEOMETRYCOLLECTION EMPTY"),
+            Some(
+                "GEOMETRYCOLLECTION (MULTIPOINT((3 4),(1 2),(7 8),(5 6)), 
LINESTRING (1 10, 1 2))",
+            ),
+            Some("GEOMETRYCOLLECTION (POINT Z (1 2 3), LINESTRING Z (1 2 3, 4 
5 6))"),
+            Some("GEOMETRYCOLLECTION (POINT M (1 2 3), LINESTRING M (1 2 3, 4 
5 6))"),
+            Some("GEOMETRYCOLLECTION (POINT ZM (1 2 3 4), LINESTRING ZM (1 2 3 
4, 5 6 7 8))"),
+        ];
+
+        let expected = create_array(
+            &[
+                // Null case
+                None,
+                // POINT types (unchanged - points have no direction)
+                Some("POINT EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT Z EMPTY"),
+                Some("POINT Z (1 2 3)"),
+                Some("POINT M EMPTY"),
+                Some("POINT M (1 2 3)"),
+                Some("POINT ZM EMPTY"),
+                Some("POINT ZM (1 2 3 4)"),
+                // LINESTRING types (vertex order reversed)
+                Some("LINESTRING EMPTY"),
+                Some("LINESTRING (1 10, 1 2)"),
+                Some("LINESTRING (2 2, 1 1, 0 0)"),
+                Some("LINESTRING (30 40, 10 20)"),
+                Some("LINESTRING Z EMPTY"),
+                Some("LINESTRING Z (4 5 6, 1 2 3)"),
+                Some("LINESTRING M EMPTY"),
+                Some("LINESTRING M (4 5 6, 1 2 3)"),
+                Some("LINESTRING ZM EMPTY"),
+                Some("LINESTRING ZM (5 6 7 8, 1 2 3 4)"),
+                // POLYGON types (ring vertex order reversed)
+                Some("POLYGON EMPTY"),
+                Some("POLYGON ((2 2, 3 2, 3 3, 2 3, 2 2))"),
+                Some("POLYGON Z EMPTY"),
+                Some("POLYGON Z ((0 0 0, 1 0 0, 1 1 0, 0 1 0, 0 0 0))"),
+                Some("POLYGON M EMPTY"),
+                Some("POLYGON M ((0 0 0, 1 0 0, 1 1 0, 0 1 0, 0 0 0))"),
+                Some("POLYGON ZM EMPTY"),
+                Some("POLYGON ZM ((0 0 0 0, 1 0 0 0, 1 1 0 0, 0 1 0 0, 0 0 0 
0))"),
+                // MULTIPOINT types (no change)
+                Some("MULTIPOINT EMPTY"),
+                Some("MULTIPOINT((3 4),(1 2),(7 8),(5 6))"),
+                Some("MULTIPOINT Z EMPTY"),
+                Some("MULTIPOINT Z ((1 2 3), (4 5 6))"),
+                Some("MULTIPOINT M EMPTY"),
+                Some("MULTIPOINT M ((1 2 3), (4 5 6))"),
+                Some("MULTIPOINT ZM EMPTY"),
+                Some("MULTIPOINT ZM ((1 2 3 4), (5 6 7 8))"),
+                // MULTILINESTRING types (each linestring reversed 
individually)
+                Some("MULTILINESTRING EMPTY"),
+                Some("MULTILINESTRING ((3 4, 1 2), (7 8, 5 6))"),
+                Some("MULTILINESTRING Z EMPTY"),
+                Some("MULTILINESTRING Z ((4 5 6, 1 2 3), (10 11 12, 7 8 9))"),
+                Some("MULTILINESTRING M EMPTY"),
+                Some("MULTILINESTRING M ((4 5 6, 1 2 3), (10 11 12, 7 8 9))"),
+                Some("MULTILINESTRING ZM EMPTY"),
+                Some("MULTILINESTRING ZM ((5 6 7 8, 1 2 3 4), (13 14 15 16, 9 
10 11 12))"),
+                // MULTIPOLYGON types (each polygon reversed individually)
+                Some("MULTIPOLYGON EMPTY"),
+                Some("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 
3, 2 3, 2 2)))"),
+                Some("MULTIPOLYGON Z EMPTY"),
+                Some("MULTIPOLYGON Z (((0 0 0, 1 0 0, 1 1 0, 0 1 0, 0 0 0)))"),
+                Some("MULTIPOLYGON M EMPTY"),
+                Some("MULTIPOLYGON M (((0 0 0, 1 0 0, 1 1 0, 0 1 0, 0 0 0)))"),
+                Some("MULTIPOLYGON ZM EMPTY"),
+                Some("MULTIPOLYGON ZM (((0 0 0 0, 1 0 0 0, 1 1 0 0, 0 1 0 0, 0 
0 0 0)))"),
+                // GEOMETRYCOLLECTION types (each member geometry reversed)
+                Some("GEOMETRYCOLLECTION EMPTY"),
+                Some("GEOMETRYCOLLECTION (MULTIPOINT((3 4),(1 2),(7 8),(5 6)), 
LINESTRING (1 2, 1 10))"),
+                Some("GEOMETRYCOLLECTION (POINT Z (1 2 3), LINESTRING Z (4 5 
6, 1 2 3))"),
+                Some("GEOMETRYCOLLECTION (POINT M (1 2 3), LINESTRING M (4 5 
6, 1 2 3))"),
+                Some("GEOMETRYCOLLECTION (POINT ZM (1 2 3 4), LINESTRING ZM (5 
6 7 8, 1 2 3 4))"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        assert_array_equal(&tester.invoke_wkb_array(input_wkt).unwrap(), 
&expected);
+    }
+}

Reply via email to