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 4e28de1f feat(c/sedona-geos): Implement ST_NRings (#387)
4e28de1f is described below
commit 4e28de1f7dc30881f91af4c614cdaca93076a6e6
Author: L_Sowmya <[email protected]>
AuthorDate: Tue Dec 2 21:04:52 2025 +0530
feat(c/sedona-geos): Implement ST_NRings (#387)
---
c/sedona-geos/src/lib.rs | 1 +
c/sedona-geos/src/register.rs | 2 +
c/sedona-geos/src/st_nrings.rs | 171 ++++++++++++++++++++++
python/sedonadb/tests/functions/test_functions.py | 46 ++++++
4 files changed, 220 insertions(+)
diff --git a/c/sedona-geos/src/lib.rs b/c/sedona-geos/src/lib.rs
index a00bd69d..ef7dd35f 100644
--- a/c/sedona-geos/src/lib.rs
+++ b/c/sedona-geos/src/lib.rs
@@ -34,6 +34,7 @@ mod st_length;
mod st_makevalid;
mod st_minimumclearance;
mod st_minimumclearance_line;
+mod st_nrings;
mod st_numinteriorrings;
mod st_numpoints;
mod st_perimeter;
diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs
index 76f63ce6..6ed54e7e 100644
--- a/c/sedona-geos/src/register.rs
+++ b/c/sedona-geos/src/register.rs
@@ -33,6 +33,7 @@ use crate::{
st_makevalid::st_make_valid_impl,
st_minimumclearance::st_minimum_clearance_impl,
st_minimumclearance_line::st_minimum_clearance_line_impl,
+ st_nrings::st_nrings_impl,
st_numinteriorrings::st_num_interior_rings_impl,
st_numpoints::st_num_points_impl,
st_perimeter::st_perimeter_impl,
@@ -79,6 +80,7 @@ pub fn scalar_kernels() -> Vec<(&'static str,
ScalarKernelRef)> {
("st_length", st_length_impl()),
("st_numinteriorrings", st_num_interior_rings_impl()),
("st_numpoints", st_num_points_impl()),
+ ("st_nrings", st_nrings_impl()),
("st_makevalid", st_make_valid_impl()),
("st_minimumclearance", st_minimum_clearance_impl()),
("st_minimumclearanceline", st_minimum_clearance_line_impl()),
diff --git a/c/sedona-geos/src/st_nrings.rs b/c/sedona-geos/src/st_nrings.rs
new file mode 100644
index 00000000..5a02ca15
--- /dev/null
+++ b/c/sedona-geos/src/st_nrings.rs
@@ -0,0 +1,171 @@
+// 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 crate::executor::GeosExecutor;
+use arrow_array::builder::Int32Builder;
+use arrow_schema::DataType;
+use datafusion_common::{error::Result, DataFusionError};
+use datafusion_expr::ColumnarValue;
+use geos::{Geom, GeometryTypes};
+use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
+use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+
+pub fn st_nrings_impl() -> ScalarKernelRef {
+ Arc::new(STNRings {})
+}
+
+#[derive(Debug)]
+struct STNRings {}
+
+impl SedonaScalarKernel for STNRings {
+ fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+ let matcher = ArgMatcher::new(
+ vec![ArgMatcher::is_geometry()],
+ SedonaType::Arrow(DataType::Int32),
+ );
+ 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 =
Int32Builder::with_capacity(executor.num_iterations());
+ executor.execute_wkb_void(|maybe_geom| {
+ match maybe_geom {
+ None => builder.append_null(),
+ Some(geom) => {
+ let val = invoke_scalar(&geom)?;
+ builder.append_value(val);
+ }
+ }
+ Ok(())
+ })?;
+ executor.finish(Arc::new(builder.finish()))
+ }
+}
+
+fn invoke_scalar<G: Geom>(geom: &G) -> Result<i32> {
+ match geom.geometry_type() {
+ GeometryTypes::Polygon => {
+ if geom
+ .is_empty()
+ .map_err(|e| DataFusionError::Execution(format!("{e}")))?
+ {
+ return Ok(0);
+ }
+ let num_interior = geom
+ .get_num_interior_rings()
+ .map_err(|e| DataFusionError::Execution(format!("{e}")))?;
+ Ok((num_interior + 1) as i32)
+ }
+ GeometryTypes::MultiPolygon | GeometryTypes::GeometryCollection => {
+ if geom
+ .is_empty()
+ .map_err(|e| DataFusionError::Execution(format!("{e}")))?
+ {
+ return Ok(0);
+ }
+ let num_geoms = geom
+ .get_num_geometries()
+ .map_err(|e| DataFusionError::Execution(format!("{e}")))?;
+ let mut total_rings = 0;
+ for i in 0..num_geoms {
+ let sub_geom = geom
+ .get_geometry_n(i)
+ .map_err(|e| DataFusionError::Execution(format!("{e}")))?;
+ total_rings += invoke_scalar(&sub_geom)?;
+ }
+ Ok(total_rings)
+ }
+ _ => Ok(0),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Arc;
+
+ use arrow_array::{ArrayRef, Int32Array};
+ use arrow_schema::DataType;
+ use datafusion_common::ScalarValue;
+ use rstest::rstest;
+ use sedona_expr::scalar_udf::SedonaScalarUDF;
+ use sedona_schema::datatypes::{SedonaType, WKB_GEOMETRY,
WKB_VIEW_GEOMETRY};
+ use sedona_testing::compare::assert_array_equal;
+ 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_nrings", st_nrings_impl());
+ let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type]);
+ tester.assert_return_type(DataType::Int32);
+
+ let result = tester
+ .invoke_scalar(
+ "POLYGON((0 0,10 0,10 6,0 6,0 0),(1 1,2 1,2 5,1 5,1 1),(8 5,8
4,9 4,9 5,8 5))",
+ )
+ .unwrap();
+ tester.assert_scalar_result_equals(result, 3_i32);
+
+ let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
+ assert!(result.is_null());
+
+ let input_wkt = vec![
+ None,
+ Some("POINT (1 2)"),
+ Some("LINESTRING (0 0, 1 1, 2 2)"),
+ Some("POLYGON EMPTY"),
+ Some("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))"),
+ Some("POLYGON ((0 0,6 0,6 6,0 6,0 0),(2 2,4 2,4 4,2 4,2 2))"),
+ Some(
+ "POLYGON ((0 0,10 0,10 6,0 6,0 0),(1 1,2 1,2 5,1 5,1 1),(8 5,8
4,9 4,9 5,8 5))",
+ ),
+ Some(
+ "MULTIPOLYGON (((0 0,5 0,5 5,0 5,0 0),(1 1,2 1,2 2,1 2,1
1)),((10 10,14 10,14 14,10 14,10 10)))",
+ ),
+ Some(
+ "GEOMETRYCOLLECTION (POINT (1 2),POLYGON ((0 0,3 0,3 3,0 3,0
0)))",
+ ),
+ Some("POLYGON Z ((0 0 1, 1 0 1, 1 1 1, 0 1 1, 0 0 1))"),
+ Some("GEOMETRYCOLLECTION(POINT(2 3), LINESTRING(0 0, 1 1, 2 2),
POLYGON((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1)), MULTIPOLYGON(((5
5, 6 5, 6 6, 5 6, 5 5)), ((10 10, 12 10, 12 12, 10 12, 10 10), (10.5 10.5, 11
10.5, 11 11, 10.5 11, 10.5 10.5))), GEOMETRYCOLLECTION(POLYGON((20 20, 22 20,
22 22, 20 22, 20 20)), POINT(30 30)))"),
+ ];
+
+ let expected: ArrayRef = Arc::new(Int32Array::from(vec![
+ None,
+ Some(0),
+ Some(0),
+ Some(0),
+ Some(1),
+ Some(2),
+ Some(3),
+ Some(3),
+ Some(1),
+ Some(1),
+ Some(6),
+ ]));
+
+ let result = tester.invoke_wkb_array(input_wkt).unwrap();
+ assert_array_equal(&result, &expected);
+ }
+}
diff --git a/python/sedonadb/tests/functions/test_functions.py
b/python/sedonadb/tests/functions/test_functions.py
index 09223eca..98c27c53 100644
--- a/python/sedonadb/tests/functions/test_functions.py
+++ b/python/sedonadb/tests/functions/test_functions.py
@@ -2889,3 +2889,49 @@ def test_st_numpoints(eng, geom, expected):
f"SELECT ST_NumPoints({geom_or_null(geom)})",
expected,
)
+
+
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+ ("geom", "expected"),
+ [
+ (None, None),
+ ("POINT (1 2)", 0),
+ ("LINESTRING (0 0, 1 1, 2 2)", 0),
+ ("MULTIPOINT ((0 0), (1 1))", 0),
+ ("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", 0),
+ ("POINT EMPTY", 0),
+ ("MULTIPOINT EMPTY", 0),
+ ("LINESTRING EMPTY", 0),
+ ("MULTILINESTRING EMPTY", 0),
+ ("MULTIPOINT ((0 0), (1 1))", 0),
+ ("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", 0),
+ ("POINT EMPTY", 0),
+ ("MULTIPOINT EMPTY", 0),
+ ("LINESTRING EMPTY", 0),
+ ("MULTILINESTRING EMPTY", 0),
+ ("GEOMETRYCOLLECTION EMPTY", 0),
+ ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1),
+ ("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))",
2),
+ (
+ "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1
1), (5 5, 5 6, 6 6, 6 5, 5 5))",
+ 3,
+ ),
+ (
+ "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((10 10, 20 10, 20 20,
10 20, 10 10), (12 12, 12 14, 14 14, 14 12, 12 12)))",
+ 3,
+ ),
+ ("POLYGON Z ((0 0 1, 1 0 1, 1 1 1, 0 1 1, 0 0 1))", 1),
+ ("GEOMETRYCOLLECTION(POINT(1 1), POLYGON((0 0, 1 0, 1 1, 0 0)))", 1),
+ (
+ "GEOMETRYCOLLECTION(POINT(2 3), LINESTRING(0 0, 1 1, 2 2),
POLYGON((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1)), MULTIPOLYGON(((5
5, 6 5, 6 6, 5 6, 5 5)), ((10 10, 12 10, 12 12, 10 12, 10 10), (10.5 10.5, 11
10.5, 11 11, 10.5 11, 10.5 10.5))), GEOMETRYCOLLECTION(POLYGON((20 20, 22 20,
22 22, 20 22, 20 20)), POINT(30 30)))",
+ 6,
+ ),
+ ],
+)
+def test_st_NRings(eng, geom, expected):
+ eng = eng.create_or_skip()
+ eng.assert_query_result(
+ f"SELECT ST_NRings({geom_or_null(geom)})",
+ expected,
+ )