Skip to content

Commit 4e28de1

Browse files
feat(c/sedona-geos): Implement ST_NRings (#387)
1 parent 27ba476 commit 4e28de1

File tree

4 files changed

+220
-0
lines changed

4 files changed

+220
-0
lines changed

c/sedona-geos/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ mod st_length;
3434
mod st_makevalid;
3535
mod st_minimumclearance;
3636
mod st_minimumclearance_line;
37+
mod st_nrings;
3738
mod st_numinteriorrings;
3839
mod st_numpoints;
3940
mod st_perimeter;

c/sedona-geos/src/register.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use crate::{
3333
st_makevalid::st_make_valid_impl,
3434
st_minimumclearance::st_minimum_clearance_impl,
3535
st_minimumclearance_line::st_minimum_clearance_line_impl,
36+
st_nrings::st_nrings_impl,
3637
st_numinteriorrings::st_num_interior_rings_impl,
3738
st_numpoints::st_num_points_impl,
3839
st_perimeter::st_perimeter_impl,
@@ -79,6 +80,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> {
7980
("st_length", st_length_impl()),
8081
("st_numinteriorrings", st_num_interior_rings_impl()),
8182
("st_numpoints", st_num_points_impl()),
83+
("st_nrings", st_nrings_impl()),
8284
("st_makevalid", st_make_valid_impl()),
8385
("st_minimumclearance", st_minimum_clearance_impl()),
8486
("st_minimumclearanceline", st_minimum_clearance_line_impl()),

c/sedona-geos/src/st_nrings.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::sync::Arc;
19+
20+
use crate::executor::GeosExecutor;
21+
use arrow_array::builder::Int32Builder;
22+
use arrow_schema::DataType;
23+
use datafusion_common::{error::Result, DataFusionError};
24+
use datafusion_expr::ColumnarValue;
25+
use geos::{Geom, GeometryTypes};
26+
use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
27+
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
28+
29+
pub fn st_nrings_impl() -> ScalarKernelRef {
30+
Arc::new(STNRings {})
31+
}
32+
33+
#[derive(Debug)]
34+
struct STNRings {}
35+
36+
impl SedonaScalarKernel for STNRings {
37+
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
38+
let matcher = ArgMatcher::new(
39+
vec![ArgMatcher::is_geometry()],
40+
SedonaType::Arrow(DataType::Int32),
41+
);
42+
matcher.match_args(args)
43+
}
44+
45+
fn invoke_batch(
46+
&self,
47+
arg_types: &[SedonaType],
48+
args: &[ColumnarValue],
49+
) -> Result<ColumnarValue> {
50+
let executor = GeosExecutor::new(arg_types, args);
51+
let mut builder = Int32Builder::with_capacity(executor.num_iterations());
52+
executor.execute_wkb_void(|maybe_geom| {
53+
match maybe_geom {
54+
None => builder.append_null(),
55+
Some(geom) => {
56+
let val = invoke_scalar(&geom)?;
57+
builder.append_value(val);
58+
}
59+
}
60+
Ok(())
61+
})?;
62+
executor.finish(Arc::new(builder.finish()))
63+
}
64+
}
65+
66+
fn invoke_scalar<G: Geom>(geom: &G) -> Result<i32> {
67+
match geom.geometry_type() {
68+
GeometryTypes::Polygon => {
69+
if geom
70+
.is_empty()
71+
.map_err(|e| DataFusionError::Execution(format!("{e}")))?
72+
{
73+
return Ok(0);
74+
}
75+
let num_interior = geom
76+
.get_num_interior_rings()
77+
.map_err(|e| DataFusionError::Execution(format!("{e}")))?;
78+
Ok((num_interior + 1) as i32)
79+
}
80+
GeometryTypes::MultiPolygon | GeometryTypes::GeometryCollection => {
81+
if geom
82+
.is_empty()
83+
.map_err(|e| DataFusionError::Execution(format!("{e}")))?
84+
{
85+
return Ok(0);
86+
}
87+
let num_geoms = geom
88+
.get_num_geometries()
89+
.map_err(|e| DataFusionError::Execution(format!("{e}")))?;
90+
let mut total_rings = 0;
91+
for i in 0..num_geoms {
92+
let sub_geom = geom
93+
.get_geometry_n(i)
94+
.map_err(|e| DataFusionError::Execution(format!("{e}")))?;
95+
total_rings += invoke_scalar(&sub_geom)?;
96+
}
97+
Ok(total_rings)
98+
}
99+
_ => Ok(0),
100+
}
101+
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use std::sync::Arc;
106+
107+
use arrow_array::{ArrayRef, Int32Array};
108+
use arrow_schema::DataType;
109+
use datafusion_common::ScalarValue;
110+
use rstest::rstest;
111+
use sedona_expr::scalar_udf::SedonaScalarUDF;
112+
use sedona_schema::datatypes::{SedonaType, WKB_GEOMETRY, WKB_VIEW_GEOMETRY};
113+
use sedona_testing::compare::assert_array_equal;
114+
use sedona_testing::testers::ScalarUdfTester;
115+
116+
use super::*;
117+
118+
#[rstest]
119+
fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) {
120+
let udf = SedonaScalarUDF::from_kernel("st_nrings", st_nrings_impl());
121+
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type]);
122+
tester.assert_return_type(DataType::Int32);
123+
124+
let result = tester
125+
.invoke_scalar(
126+
"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))",
127+
)
128+
.unwrap();
129+
tester.assert_scalar_result_equals(result, 3_i32);
130+
131+
let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
132+
assert!(result.is_null());
133+
134+
let input_wkt = vec![
135+
None,
136+
Some("POINT (1 2)"),
137+
Some("LINESTRING (0 0, 1 1, 2 2)"),
138+
Some("POLYGON EMPTY"),
139+
Some("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))"),
140+
Some("POLYGON ((0 0,6 0,6 6,0 6,0 0),(2 2,4 2,4 4,2 4,2 2))"),
141+
Some(
142+
"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))",
143+
),
144+
Some(
145+
"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)))",
146+
),
147+
Some(
148+
"GEOMETRYCOLLECTION (POINT (1 2),POLYGON ((0 0,3 0,3 3,0 3,0 0)))",
149+
),
150+
Some("POLYGON Z ((0 0 1, 1 0 1, 1 1 1, 0 1 1, 0 0 1))"),
151+
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)))"),
152+
];
153+
154+
let expected: ArrayRef = Arc::new(Int32Array::from(vec![
155+
None,
156+
Some(0),
157+
Some(0),
158+
Some(0),
159+
Some(1),
160+
Some(2),
161+
Some(3),
162+
Some(3),
163+
Some(1),
164+
Some(1),
165+
Some(6),
166+
]));
167+
168+
let result = tester.invoke_wkb_array(input_wkt).unwrap();
169+
assert_array_equal(&result, &expected);
170+
}
171+
}

python/sedonadb/tests/functions/test_functions.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2889,3 +2889,49 @@ def test_st_numpoints(eng, geom, expected):
28892889
f"SELECT ST_NumPoints({geom_or_null(geom)})",
28902890
expected,
28912891
)
2892+
2893+
2894+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
2895+
@pytest.mark.parametrize(
2896+
("geom", "expected"),
2897+
[
2898+
(None, None),
2899+
("POINT (1 2)", 0),
2900+
("LINESTRING (0 0, 1 1, 2 2)", 0),
2901+
("MULTIPOINT ((0 0), (1 1))", 0),
2902+
("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", 0),
2903+
("POINT EMPTY", 0),
2904+
("MULTIPOINT EMPTY", 0),
2905+
("LINESTRING EMPTY", 0),
2906+
("MULTILINESTRING EMPTY", 0),
2907+
("MULTIPOINT ((0 0), (1 1))", 0),
2908+
("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", 0),
2909+
("POINT EMPTY", 0),
2910+
("MULTIPOINT EMPTY", 0),
2911+
("LINESTRING EMPTY", 0),
2912+
("MULTILINESTRING EMPTY", 0),
2913+
("GEOMETRYCOLLECTION EMPTY", 0),
2914+
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1),
2915+
("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", 2),
2916+
(
2917+
"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))",
2918+
3,
2919+
),
2920+
(
2921+
"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)))",
2922+
3,
2923+
),
2924+
("POLYGON Z ((0 0 1, 1 0 1, 1 1 1, 0 1 1, 0 0 1))", 1),
2925+
("GEOMETRYCOLLECTION(POINT(1 1), POLYGON((0 0, 1 0, 1 1, 0 0)))", 1),
2926+
(
2927+
"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)))",
2928+
6,
2929+
),
2930+
],
2931+
)
2932+
def test_st_NRings(eng, geom, expected):
2933+
eng = eng.create_or_skip()
2934+
eng.assert_query_result(
2935+
f"SELECT ST_NRings({geom_or_null(geom)})",
2936+
expected,
2937+
)

0 commit comments

Comments
 (0)