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);