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 689da4fc feat(rust/sedona-functions): Implement ST_InteriorRingN (#381)
689da4fc is described below

commit 689da4fc362e92f457392c32bf3efbd09a4e02f3
Author: Abeeujah <[email protected]>
AuthorDate: Mon Dec 1 23:13:46 2025 +0100

    feat(rust/sedona-functions): Implement ST_InteriorRingN (#381)
---
 python/sedonadb/tests/functions/test_functions.py | 115 +++++++
 rust/sedona-functions/benches/native-functions.rs |  15 +
 rust/sedona-functions/src/lib.rs                  |   1 +
 rust/sedona-functions/src/register.rs             |   1 +
 rust/sedona-functions/src/st_geometryn.rs         |   3 +-
 rust/sedona-functions/src/st_interiorringn.rs     | 395 ++++++++++++++++++++++
 rust/sedona-testing/src/benchmark_util.rs         |  49 ++-
 7 files changed, 573 insertions(+), 6 deletions(-)

diff --git a/python/sedonadb/tests/functions/test_functions.py 
b/python/sedonadb/tests/functions/test_functions.py
index e956eaff..09223eca 100644
--- a/python/sedonadb/tests/functions/test_functions.py
+++ b/python/sedonadb/tests/functions/test_functions.py
@@ -1178,6 +1178,121 @@ def test_st_hasz(eng, geom, expected):
     eng.assert_query_result(f"SELECT ST_HasZ({geom_or_null(geom)})", expected)
 
 
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+    ("geom", "index", "expected"),
+    [
+        # I. Null/Empty/Non-Polygon Inputs
+        # NULL input
+        (None, 1, None),
+        # POINT
+        ("POINT (0 0)", 1, None),
+        # POINT EMPTY
+        ("POINT EMPTY", 1, None),
+        # LINESTRING
+        ("LINESTRING (0 0, 0 1, 1 2)", 1, None),
+        # LINESTRING EMPTY
+        ("LINESTRING EMPTY", 1, None),
+        # MULTIPOINT
+        ("MULTIPOINT ((0 0), (1 1))", 1, None),
+        # MULTIPOLYGON (Interior rings are within constituent Polygons, not 
the MultiPolygon itself)
+        ("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1, 1 1)))", 1, None),
+        # GEOMETRYCOLLECTION
+        ("GEOMETRYCOLLECTION (POINT(1 1))", 1, None),
+        # II. Polygon Edge Cases
+        # POLYGON EMPTY
+        ("POLYGON EMPTY", 1, None),
+        # Polygon with NO interior rings, index=1
+        ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1, None),
+        # Invalid index n=0 (Assuming 1-based indexing means n=0 is 
invalid/out of range)
+        ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 0, None),
+        # Index n too high (index=2, but 0 holes)
+        ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 2, None),
+        # III. Valid Polygon with Interior Ring(s)
+        # Polygon: ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))
+        # Single hole, index=1
+        (
+            "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))",
+            1,
+            "LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)",
+        ),
+        # Single hole, negative index=-1
+        (
+            "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))",
+            -1,
+            None,
+        ),
+        # Single hole, index=2 (index too high)
+        ("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", 2, 
None),
+        # Polygon: ((0 0, 6 0, 6 6, 0 6, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (4 
4, 4 5, 5 5, 5 4, 4 4))
+        # Two holes, index=1 (first hole)
+        (
+            "POLYGON ((0 0, 6 0, 6 6, 0 6, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (4 
4, 4 5, 5 5, 5 4, 4 4))",
+            1,
+            "LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)",
+        ),
+        # Two holes, index=2 (second hole)
+        (
+            "POLYGON ((0 0, 6 0, 6 6, 0 6, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (4 
4, 4 5, 5 5, 5 4, 4 4))",
+            2,
+            "LINESTRING (4 4, 4 5, 5 5, 5 4, 4 4)",
+        ),
+        # Two holes, index=3 (index too high)
+        (
+            "POLYGON ((0 0, 6 0, 6 6, 0 6, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (4 
4, 4 5, 5 5, 5 4, 4 4))",
+            3,
+            None,
+        ),
+        # IV. Invalid/Malformed Polygon Input
+        #  External hole (WKT is syntactically valid, second ring is usually 
treated as a hole by parsers regardless of validity)
+        (
+            "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (5 5, 5 6, 6 6, 6 5, 5 5))",
+            1,
+            "LINESTRING (5 5, 5 6, 6 6, 6 5, 5 5)",
+        ),
+        # Intersecting holes (WKT is syntactically valid)
+        (
+            "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 3, 3 3, 3 1, 1 1), (2 
2, 2 2.5, 2.5 2.5, 2.5 2, 2 2))",
+            2,
+            "LINESTRING (2 2, 2 2.5, 2.5 2.5, 2.5 2, 2 2)",
+        ),
+        # Z Dimensions
+        ("POINT Z (1 1 5)", 1, None),
+        (
+            "POLYGON Z ((0 0 10, 4 0 10, 4 4 10, 0 4 10, 0 0 10), (1 1 5, 1 2 
5, 2 2 5, 2 1 5, 1 1 5))",
+            1,
+            "LINESTRING Z (1 1 5, 1 2 5, 2 2 5, 2 1 5, 1 1 5)",
+        ),
+        ("POLYGON Z ((0 0 10, 4 0 10, 4 4 10, 0 4 10, 0 0 10))", 1, None),
+        # M Dimensions
+        ("LINESTRING M (0 0 1, 1 1 2)", 1, None),
+        ("POLYGON M ((0 0 1, 4 0 2, 4 4 3, 0 4 4, 0 0 5))", 1, None),
+        (
+            "POLYGON M ((0 0 1, 4 0 2, 4 4 3, 0 4 4, 0 0 5), (1 1 6, 1 2 7, 2 
2 8, 2 1 9, 1 1 10))",
+            1,
+            "LINESTRING M (1 1 6, 1 2 7, 2 2 8, 2 1 9, 1 1 10)",
+        ),
+        # ZM Dimensions
+        ("POLYGON ZM EMPTY", 1, None),
+        (
+            "POLYGON ZM ((0 0 10 1, 4 0 10 2, 4 4 10 3, 0 4 10 4, 0 0 10 5), 
(1 1 5 6, 1 2 5 7, 2 2 5 8, 2 1 5 9, 1 1 5 10))",
+            2,
+            None,
+        ),
+        (
+            "POLYGON ZM ((0 0 10 1, 4 0 10 2, 4 4 10 3, 0 4 10 4, 0 0 10 5), 
(1 1 5 6, 1 2 5 7, 2 2 5 8, 2 1 5 9, 1 1 5 10))",
+            1,
+            "LINESTRING ZM (1 1 5 6, 1 2 5 7, 2 2 5 8, 2 1 5 9, 1 1 5 10)",
+        ),
+    ],
+)
+def test_st_interiorringn(eng, geom, index, expected):
+    eng = eng.create_or_skip()
+    eng.assert_query_result(
+        f"SELECT ST_InteriorRingN({geom_or_null(geom)}, 
{val_or_null(index)})", expected
+    )
+
+
 @pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
 @pytest.mark.parametrize(
     ("geom", "expected"),
diff --git a/rust/sedona-functions/benches/native-functions.rs 
b/rust/sedona-functions/benches/native-functions.rs
index 6f30fd81..6607e7ba 100644
--- a/rust/sedona-functions/benches/native-functions.rs
+++ b/rust/sedona-functions/benches/native-functions.rs
@@ -82,6 +82,21 @@ fn criterion_benchmark(c: &mut Criterion) {
     benchmark::scalar(c, &f, "native", "st_hasm", Point);
     benchmark::scalar(c, &f, "native", "st_hasm", LineString(10));
 
+    benchmark::scalar(
+        c,
+        &f,
+        "native",
+        "st_interiorringn",
+        BenchmarkArgs::ArrayArray(PolygonWithHole(10), Int64(1, 10)),
+    );
+    benchmark::scalar(
+        c,
+        &f,
+        "native",
+        "st_interiorringn",
+        BenchmarkArgs::ArrayArray(PolygonWithHole(500), Int64(1, 10)),
+    );
+
     benchmark::scalar(c, &f, "native", "st_isempty", Point);
     benchmark::scalar(c, &f, "native", "st_isempty", LineString(10));
 
diff --git a/rust/sedona-functions/src/lib.rs b/rust/sedona-functions/src/lib.rs
index ba0ef210..26b5a8be 100644
--- a/rust/sedona-functions/src/lib.rs
+++ b/rust/sedona-functions/src/lib.rs
@@ -42,6 +42,7 @@ mod st_geometrytype;
 mod st_geomfromwkb;
 mod st_geomfromwkt;
 mod st_haszm;
+mod st_interiorringn;
 pub mod st_intersection_agg;
 pub mod st_isclosed;
 mod st_iscollection;
diff --git a/rust/sedona-functions/src/register.rs 
b/rust/sedona-functions/src/register.rs
index 1e7a0e8a..dc7366f5 100644
--- a/rust/sedona-functions/src/register.rs
+++ b/rust/sedona-functions/src/register.rs
@@ -81,6 +81,7 @@ pub fn default_function_set() -> FunctionSet {
         crate::st_geomfromwkt::st_geomfromwkt_udf,
         crate::st_haszm::st_hasm_udf,
         crate::st_haszm::st_hasz_udf,
+        crate::st_interiorringn::st_interiorringn_udf,
         crate::st_isclosed::st_isclosed_udf,
         crate::st_iscollection::st_iscollection_udf,
         crate::st_isempty::st_isempty_udf,
diff --git a/rust/sedona-functions/src/st_geometryn.rs 
b/rust/sedona-functions/src/st_geometryn.rs
index 74ed3953..a79498f2 100644
--- a/rust/sedona-functions/src/st_geometryn.rs
+++ b/rust/sedona-functions/src/st_geometryn.rs
@@ -139,13 +139,12 @@ mod tests {
     use rstest::rstest;
     use sedona_schema::datatypes::WKB_VIEW_GEOMETRY;
     use sedona_testing::testers::ScalarUdfTester;
+    use sedona_testing::{compare::assert_array_equal, create::create_array};
 
     use super::*;
 
     #[rstest]
     fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) 
{
-        use sedona_testing::{compare::assert_array_equal, 
create::create_array};
-
         let tester = ScalarUdfTester::new(
             st_geometryn_udf().into(),
             vec![
diff --git a/rust/sedona-functions/src/st_interiorringn.rs 
b/rust/sedona-functions/src/st_interiorringn.rs
new file mode 100644
index 00000000..22070275
--- /dev/null
+++ b/rust/sedona-functions/src/st_interiorringn.rs
@@ -0,0 +1,395 @@
+// 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::cast::as_int64_array;
+use datafusion_common::{DataFusionError, Result};
+use datafusion_expr::{scalar_doc_sections::DOC_SECTION_OTHER, Documentation};
+use geo_traits::{GeometryTrait, LineStringTrait, PolygonTrait};
+use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
+use sedona_geometry::wkb_factory::{
+    write_wkb_coord_trait, write_wkb_linestring_header, WKB_MIN_PROBABLE_BYTES,
+};
+use sedona_schema::datatypes::SedonaType;
+use sedona_schema::{datatypes::WKB_GEOMETRY, matchers::ArgMatcher};
+use wkb::reader::Wkb;
+
+use crate::executor::WkbExecutor;
+
+/// ST_InteriorRingN() scalar UDF
+///
+/// Native implementation to get the nth interior ring (hole) of a Polygon
+pub fn st_interiorringn_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "st_interiorringn",
+        vec![Arc::new(STInteriorRingN)],
+        datafusion_expr::Volatility::Immutable,
+        Some(st_interiorringn_doc()),
+    )
+}
+
+fn st_interiorringn_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Returns the Nth interior ring (hole) of a POLYGON geometry as a 
LINESTRING. \
+        The index starts at 1. Returns NULL if the geometry is not a polygon 
or the index is out of range.",
+        "ST_GeometryN (geom: Geometry, n: integer)")
+    .with_argument("geom", "geometry: Input Polygon")
+    .with_argument("n", "n: Index")
+    .with_sql_example("SELECT ST_InteriorRingN('POLYGON ((0 0, 4 0, 4 4, 0 4, 
0 0), (1 1, 1 2, 2 2, 2 1, 1 1))', 1)")
+    .build()
+}
+
+#[derive(Debug)]
+struct STInteriorRingN;
+
+impl SedonaScalarKernel for STInteriorRingN {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(
+            vec![ArgMatcher::is_geometry(), ArgMatcher::is_integer()],
+            WKB_GEOMETRY,
+        );
+
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[datafusion_expr::ColumnarValue],
+    ) -> Result<datafusion_expr::ColumnarValue> {
+        let executor = WkbExecutor::new(arg_types, args);
+        let mut builder = BinaryBuilder::with_capacity(
+            executor.num_iterations(),
+            WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
+        );
+
+        let integer_value = args[1]
+            .cast_to(&arrow_schema::DataType::Int64, None)?
+            .to_array(executor.num_iterations())?;
+        let index_array = as_int64_array(&integer_value)?;
+        let mut index_iter = index_array.iter();
+
+        executor.execute_wkb_void(|maybe_wkb| {
+            match (maybe_wkb, index_iter.next().unwrap()) {
+                (Some(wkb), Some(index)) => {
+                    if invoke_scalar(&wkb, (index - 1) as usize, &mut 
builder)? {
+                        builder.append_value([]);
+                    } else {
+                        // Unsupported Geometry Type, Invalid index encountered
+                        builder.append_null();
+                    }
+                }
+                _ => builder.append_null(),
+            }
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+fn invoke_scalar(geom: &Wkb, index: usize, writer: &mut impl std::io::Write) 
-> Result<bool> {
+    let geometry = match geom.as_type() {
+        geo_traits::GeometryType::Polygon(pgn) => pgn.interior(index),
+        _ => None,
+    };
+
+    if let Some(wkb) = geometry {
+        write_wkb_linestring_header(writer, wkb.dim(), wkb.num_coords())
+            .map_err(|e| DataFusionError::Execution(e.to_string()))?;
+        wkb.coords().try_for_each(|coord| {
+            write_wkb_coord_trait(writer, &coord)
+                .map_err(|e| DataFusionError::Execution(e.to_string()))
+        })?;
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use rstest::rstest;
+    use sedona_schema::datatypes::WKB_VIEW_GEOMETRY;
+    use sedona_testing::{
+        compare::assert_array_equal, create::create_array, 
testers::ScalarUdfTester,
+    };
+
+    use super::*;
+
+    fn setup_tester(sedona_type: SedonaType) -> ScalarUdfTester {
+        let tester = ScalarUdfTester::new(
+            st_interiorringn_udf().into(),
+            vec![
+                sedona_type,
+                SedonaType::Arrow(arrow_schema::DataType::Int64),
+            ],
+        );
+        tester.assert_return_type(WKB_GEOMETRY);
+        tester
+    }
+
+    // 1. Tests for Non-Polygon Geometries (Should return NULL)
+    #[rstest]
+    fn test_st_interiorringn_non_polygons(
+        #[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType,
+    ) {
+        let tester = setup_tester(sedona_type);
+
+        let input_wkt = create_array(
+            &[
+                None,                                               // NULL 
input
+                Some("POINT (0 0)"),                                // POINT
+                Some("POINT EMPTY"),                                // POINT 
EMPTY
+                Some("LINESTRING (0 0, 0 1, 1 2)"),                 // 
LINESTRING
+                Some("LINESTRING EMPTY"),                           // 
LINESTRING EMPTY
+                Some("MULTIPOINT ((0 0), (1 1))"),                  // 
MULTIPOINT
+                Some("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1, 1 1)))"), // 
MULTIPOLYGON
+                Some("GEOMETRYCOLLECTION (POINT(1 1))"),            // 
GEOMETRYCOLLECTION
+            ],
+            &WKB_GEOMETRY,
+        );
+        let integers = arrow_array::create_array!(
+            Int64,
+            [
+                Some(1),
+                Some(1),
+                Some(1),
+                Some(1),
+                Some(1),
+                Some(1),
+                Some(1),
+                Some(1)
+            ]
+        );
+        let expected = create_array(
+            &[None, None, None, None, None, None, None, None],
+            &WKB_GEOMETRY,
+        );
+
+        assert_array_equal(
+            &tester.invoke_arrays(vec![input_wkt, integers]).unwrap(),
+            &expected,
+        );
+    }
+
+    // 2. Tests for Polygon Edge Cases (No holes, Invalid index)
+    #[rstest]
+    fn test_st_interiorringn_polygon_edge_cases(
+        #[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType,
+    ) {
+        let tester = setup_tester(sedona_type);
+
+        let input_wkt = create_array(
+            &[
+                Some("POLYGON EMPTY"),                       // POLYGON EMPTY
+                Some("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"), // Polygon with 
NO interior rings (n=1)
+                Some("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"), // Invalid index 
n=0
+                Some("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"), // Index n too 
high (n=2)
+            ],
+            &WKB_GEOMETRY,
+        );
+        let integers = arrow_array::create_array!(Int64, [Some(1), Some(1), 
Some(0), Some(2)]);
+        let expected = create_array(
+            &[
+                None, // POLYGON EMPTY
+                None, // Polygon with NO interior rings
+                None, // Invalid index n=0 (Assuming NULL/None on invalid 
index)
+                None, // Index n too high
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        assert_array_equal(
+            &tester.invoke_arrays(vec![input_wkt, integers]).unwrap(),
+            &expected,
+        );
+    }
+
+    // 3. Tests for Valid Polygons (Correct Extraction)
+    #[rstest]
+    fn test_st_interiorringn_valid_polygons(
+        #[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType,
+    ) {
+        let tester = setup_tester(sedona_type);
+
+        let input_wkt = create_array(
+            &[
+                Some("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 
1 1))"),                                  // Single hole, n=1
+                Some("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 
1 1))"),                                  // Single hole, n=1
+                Some("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 
1 1))"),                                  // Single hole, n=2 (too high)
+                Some("POLYGON ((0 0, 6 0, 6 6, 0 6, 0 0), (1 1, 1 2, 2 2, 2 1, 
1 1), (4 4, 4 5, 5 5, 5 4, 4 4))"),       // Two holes, n=1
+                Some("POLYGON ((0 0, 6 0, 6 6, 0 6, 0 0), (1 1, 1 2, 2 2, 2 1, 
1 1), (4 4, 4 5, 5 5, 5 4, 4 4))"),       // Two holes, n=2
+                Some("POLYGON ((0 0, 6 0, 6 6, 0 6, 0 0), (1 1, 1 2, 2 2, 2 1, 
1 1), (4 4, 4 5, 5 5, 5 4, 4 4))"),       // Two holes, n=3 (too high)
+            ],
+            &WKB_GEOMETRY,
+        );
+        let integers = arrow_array::create_array!(
+            Int64,
+            [Some(1), Some(-1), Some(2), Some(1), Some(2), Some(3)]
+        );
+        let expected = create_array(
+            &[
+                Some("LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)"),
+                None,
+                None,
+                Some("LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)"),
+                Some("LINESTRING (4 4, 4 5, 5 5, 5 4, 4 4)"),
+                None,
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        assert_array_equal(
+            &tester.invoke_arrays(vec![input_wkt, integers]).unwrap(),
+            &expected,
+        );
+    }
+
+    // 4. Tests for Invalid/Malformed Polygons (Checking for error/extraction)
+    #[rstest]
+    fn test_st_interiorringn_invalid_polygons(
+        #[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType,
+    ) {
+        let tester = setup_tester(sedona_type);
+
+        let input_wkt = create_array(
+            &[
+                Some("POLYGON ((0 0, 1 0, 1 1))"),                             
                               // Unclosed/Malformed WKT
+                Some("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (5 5, 5 6, 6 6, 6 5, 
5 5))"),                       // External hole
+                Some("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 3, 3 3, 3 1, 
1 1), (2 2, 2 2.5, 2.5 2.5, 2.5 2, 2 2))"), // Intersecting holes
+            ],
+            &WKB_GEOMETRY,
+        );
+        let integers = arrow_array::create_array!(Int64, [Some(1), Some(1), 
Some(2)]);
+        let expected = create_array(
+            &[
+                None, // parsing/validation returns None/NULL for invalid 
geometry (Unclosed)
+                Some("LINESTRING (5 5, 5 6, 6 6, 6 5, 5 5)"), // Extraction 
works even if topologically invalid (external)
+                Some("LINESTRING (2 2, 2 2.5, 2.5 2.5, 2.5 2, 2 2)"), // 
Extraction works even if topologically invalid (intersecting)
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        assert_array_equal(
+            &tester.invoke_arrays(vec![input_wkt, integers]).unwrap(),
+            &expected,
+        );
+    }
+
+    #[rstest]
+    fn test_st_interiorringn_z_dimensions(
+        #[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType,
+    ) {
+        let tester = setup_tester(sedona_type);
+
+        let input_wkt = create_array(
+            &[
+                // Valid Polygon Z extraction
+                Some("POLYGON Z ((0 0 10, 4 0 10, 4 4 10, 0 4 10, 0 0 10), (1 
1 5, 1 2 5, 2 2 5, 2 1 5, 1 1 5))"),
+                // Non-Polygon Z (Should be NULL)
+                Some("POINT Z (1 1 5)"),
+                // Polygon Z with no hole (Should be NULL)
+                Some("POLYGON Z ((0 0 10, 4 0 10, 4 4 10, 0 4 10, 0 0 10))"),
+            ],
+            &WKB_GEOMETRY
+        );
+        let integers = arrow_array::create_array!(Int64, [Some(1), Some(1), 
Some(1)]);
+        let expected = create_array(
+            &[
+                Some("LINESTRING Z (1 1 5, 1 2 5, 2 2 5, 2 1 5, 1 1 5)"),
+                None,
+                None,
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        assert_array_equal(
+            &tester.invoke_arrays(vec![input_wkt, integers]).unwrap(),
+            &expected,
+        );
+    }
+
+    #[rstest]
+    fn test_st_interiorringn_m_dimensions(
+        #[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType,
+    ) {
+        let tester = setup_tester(sedona_type);
+
+        let input_wkt = create_array(
+            &[
+                // Valid Polygon M extraction
+                Some("POLYGON M ((0 0 1, 4 0 2, 4 4 3, 0 4 4, 0 0 5), (1 1 6, 
1 2 7, 2 2 8, 2 1 9, 1 1 10))"),
+                // Non-Polygon M (Should be NULL)
+                Some("LINESTRING M (0 0 1, 1 1 2)"),
+                // Polygon M with no hole (Should be NULL)
+                Some("POLYGON M ((0 0 1, 4 0 2, 4 4 3, 0 4 4, 0 0 5))"),
+            ],
+            &WKB_GEOMETRY
+        );
+        let integers = arrow_array::create_array!(Int64, [Some(1), Some(1), 
Some(1)]);
+        let expected = create_array(
+            &[
+                Some("LINESTRING M (1 1 6, 1 2 7, 2 2 8, 2 1 9, 1 1 10)"),
+                None,
+                None,
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        assert_array_equal(
+            &tester.invoke_arrays(vec![input_wkt, integers]).unwrap(),
+            &expected,
+        );
+    }
+
+    #[rstest]
+    fn test_st_interiorringn_zm_dimensions(
+        #[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType,
+    ) {
+        let tester = setup_tester(sedona_type);
+
+        let input_wkt = create_array(
+            &[
+                // Valid Polygon ZM extraction (n=1)
+                Some("POLYGON ZM ((0 0 10 1, 4 0 10 2, 4 4 10 3, 0 4 10 4, 0 0 
10 5), (1 1 5 6, 1 2 5 7, 2 2 5 8, 2 1 5 9, 1 1 5 10))"),
+                // Index too high (n=2)
+                Some("POLYGON ZM ((0 0 10 1, 4 0 10 2, 4 4 10 3, 0 4 10 4, 0 0 
10 5), (1 1 5 6, 1 2 5 7, 2 2 5 8, 2 1 5 9, 1 1 5 10))"),
+                // POLYGON ZM EMPTY (Should be NULL)
+                Some("POLYGON ZM EMPTY"),
+            ],
+            &WKB_GEOMETRY
+        );
+        let integers = arrow_array::create_array!(Int64, [Some(1), Some(2), 
Some(1)]);
+        let expected = create_array(
+            &[
+                Some("LINESTRING ZM (1 1 5 6, 1 2 5 7, 2 2 5 8, 2 1 5 9, 1 1 5 
10)"),
+                None,
+                None,
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        assert_array_equal(
+            &tester.invoke_arrays(vec![input_wkt, integers]).unwrap(),
+            &expected,
+        );
+    }
+}
diff --git a/rust/sedona-testing/src/benchmark_util.rs 
b/rust/sedona-testing/src/benchmark_util.rs
index 9e94748f..00f3c981 100644
--- a/rust/sedona-testing/src/benchmark_util.rs
+++ b/rust/sedona-testing/src/benchmark_util.rs
@@ -16,7 +16,7 @@
 // under the License.
 use std::{fmt::Debug, sync::Arc, vec};
 
-use arrow_array::{ArrayRef, Float64Array};
+use arrow_array::{ArrayRef, Float64Array, Int64Array};
 use arrow_schema::DataType;
 
 use datafusion_common::{Result, ScalarValue};
@@ -274,8 +274,12 @@ pub enum BenchmarkArgSpec {
     LineString(usize),
     /// Randomly generated polygon input with a specified number of vertices
     Polygon(usize),
+    /// Randomly generated polygon with hole input with a specified number of 
vertices
+    PolygonWithHole(usize),
     /// Randomly generated linestring input with a specified number of vertices
     MultiPoint(usize),
+    /// Randomly generated integer input with a given range of values
+    Int64(i64, i64),
     /// Randomly generated floating point input with a given range of values
     Float64(f64, f64),
     /// A transformation of any of the above based on a [ScalarUDF] accepting
@@ -295,7 +299,9 @@ impl Debug for BenchmarkArgSpec {
             Self::Point => write!(f, "Point"),
             Self::LineString(arg0) => 
f.debug_tuple("LineString").field(arg0).finish(),
             Self::Polygon(arg0) => 
f.debug_tuple("Polygon").field(arg0).finish(),
+            Self::PolygonWithHole(arg0) => 
f.debug_tuple("PolygonWithHole").field(arg0).finish(),
             Self::MultiPoint(arg0) => 
f.debug_tuple("MultiPoint").field(arg0).finish(),
+            Self::Int64(arg0, arg1) => 
f.debug_tuple("Int64").field(arg0).field(arg1).finish(),
             Self::Float64(arg0, arg1) => 
f.debug_tuple("Float64").field(arg0).field(arg1).finish(),
             Self::Transformed(inner, t) => write!(f, "{}({:?})", t.name(), 
inner),
             Self::String(s) => write!(f, "String({s})"),
@@ -310,8 +316,10 @@ impl BenchmarkArgSpec {
         match self {
             BenchmarkArgSpec::Point
             | BenchmarkArgSpec::Polygon(_)
+            | BenchmarkArgSpec::PolygonWithHole(_)
             | BenchmarkArgSpec::LineString(_)
             | BenchmarkArgSpec::MultiPoint(_) => WKB_GEOMETRY,
+            BenchmarkArgSpec::Int64(_, _) => 
SedonaType::Arrow(DataType::Int64),
             BenchmarkArgSpec::Float64(_, _) => 
SedonaType::Arrow(DataType::Float64),
             BenchmarkArgSpec::Transformed(inner, t) => {
                 let tester = ScalarUdfTester::new(t.clone(), 
vec![inner.sedona_type()]);
@@ -342,9 +350,15 @@ impl BenchmarkArgSpec {
         rows_per_batch: usize,
     ) -> Result<Vec<ArrayRef>> {
         match self {
-            BenchmarkArgSpec::Point => {
-                self.build_geometry(i, GeometryTypeId::Point, num_batches, 1, 
1, rows_per_batch)
-            }
+            BenchmarkArgSpec::Point => self.build_geometry(
+                i,
+                GeometryTypeId::Point,
+                num_batches,
+                1,
+                1,
+                rows_per_batch,
+                None,
+            ),
             BenchmarkArgSpec::LineString(vertex_count) => self.build_geometry(
                 i,
                 GeometryTypeId::LineString,
@@ -352,6 +366,7 @@ impl BenchmarkArgSpec {
                 *vertex_count,
                 1,
                 rows_per_batch,
+                None,
             ),
             BenchmarkArgSpec::Polygon(vertex_count) => self.build_geometry(
                 i,
@@ -360,6 +375,17 @@ impl BenchmarkArgSpec {
                 *vertex_count,
                 1,
                 rows_per_batch,
+                None,
+            ),
+            BenchmarkArgSpec::PolygonWithHole(vertex_count) => 
self.build_geometry(
+                i,
+                GeometryTypeId::Polygon,
+                num_batches,
+                *vertex_count,
+                1,
+                rows_per_batch,
+                // Currently only a single interior ring is possible.
+                Some(1.0),
             ),
             BenchmarkArgSpec::MultiPoint(part_count) => self.build_geometry(
                 i,
@@ -368,7 +394,19 @@ impl BenchmarkArgSpec {
                 1,
                 *part_count,
                 rows_per_batch,
+                None,
             ),
+            BenchmarkArgSpec::Int64(lo, hi) => {
+                let mut rng = self.rng(i);
+                let dist = Uniform::new(lo, hi);
+                (0..num_batches)
+                    .map(|_| -> Result<ArrayRef> {
+                        let int64_array: Int64Array =
+                            (0..rows_per_batch).map(|_| 
rng.sample(dist)).collect();
+                        Ok(Arc::new(int64_array))
+                    })
+                    .collect()
+            }
             BenchmarkArgSpec::Float64(lo, hi) => {
                 let mut rng = self.rng(i);
                 let dist = Uniform::new(lo, hi);
@@ -418,6 +456,7 @@ impl BenchmarkArgSpec {
         }
     }
 
+    #[allow(clippy::too_many_arguments)]
     fn build_geometry(
         &self,
         i: usize,
@@ -426,6 +465,7 @@ impl BenchmarkArgSpec {
         vertex_count: usize,
         num_parts_count: usize,
         rows_per_batch: usize,
+        polygon_hole_rate: Option<f64>,
     ) -> Result<Vec<ArrayRef>> {
         let builder = RandomPartitionedDataBuilder::new()
             .num_partitions(1)
@@ -437,6 +477,7 @@ impl BenchmarkArgSpec {
             .vertices_per_linestring_range((vertex_count, vertex_count))
             .num_parts_range((num_parts_count, num_parts_count))
             .geometry_type(geom_type)
+            .polygon_hole_rate(polygon_hole_rate.unwrap_or_default())
             // Currently just use WKB_GEOMETRY (we can generate a view type 
with
             // Transformed)
             .sedona_type(WKB_GEOMETRY);

Reply via email to