Skip to content

Commit 689da4f

Browse files
authored
feat(rust/sedona-functions): Implement ST_InteriorRingN (#381)
1 parent 7b20d0c commit 689da4f

File tree

7 files changed

+573
-6
lines changed

7 files changed

+573
-6
lines changed

python/sedonadb/tests/functions/test_functions.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,121 @@ def test_st_hasz(eng, geom, expected):
11781178
eng.assert_query_result(f"SELECT ST_HasZ({geom_or_null(geom)})", expected)
11791179

11801180

1181+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
1182+
@pytest.mark.parametrize(
1183+
("geom", "index", "expected"),
1184+
[
1185+
# I. Null/Empty/Non-Polygon Inputs
1186+
# NULL input
1187+
(None, 1, None),
1188+
# POINT
1189+
("POINT (0 0)", 1, None),
1190+
# POINT EMPTY
1191+
("POINT EMPTY", 1, None),
1192+
# LINESTRING
1193+
("LINESTRING (0 0, 0 1, 1 2)", 1, None),
1194+
# LINESTRING EMPTY
1195+
("LINESTRING EMPTY", 1, None),
1196+
# MULTIPOINT
1197+
("MULTIPOINT ((0 0), (1 1))", 1, None),
1198+
# MULTIPOLYGON (Interior rings are within constituent Polygons, not the MultiPolygon itself)
1199+
("MULTIPOLYGON (((1 1, 1 3, 3 3, 3 1, 1 1)))", 1, None),
1200+
# GEOMETRYCOLLECTION
1201+
("GEOMETRYCOLLECTION (POINT(1 1))", 1, None),
1202+
# II. Polygon Edge Cases
1203+
# POLYGON EMPTY
1204+
("POLYGON EMPTY", 1, None),
1205+
# Polygon with NO interior rings, index=1
1206+
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 1, None),
1207+
# Invalid index n=0 (Assuming 1-based indexing means n=0 is invalid/out of range)
1208+
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 0, None),
1209+
# Index n too high (index=2, but 0 holes)
1210+
("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 2, None),
1211+
# III. Valid Polygon with Interior Ring(s)
1212+
# Polygon: ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))
1213+
# Single hole, index=1
1214+
(
1215+
"POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))",
1216+
1,
1217+
"LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)",
1218+
),
1219+
# Single hole, negative index=-1
1220+
(
1221+
"POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))",
1222+
-1,
1223+
None,
1224+
),
1225+
# Single hole, index=2 (index too high)
1226+
("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", 2, None),
1227+
# 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))
1228+
# Two holes, index=1 (first hole)
1229+
(
1230+
"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))",
1231+
1,
1232+
"LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1)",
1233+
),
1234+
# Two holes, index=2 (second hole)
1235+
(
1236+
"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))",
1237+
2,
1238+
"LINESTRING (4 4, 4 5, 5 5, 5 4, 4 4)",
1239+
),
1240+
# Two holes, index=3 (index too high)
1241+
(
1242+
"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))",
1243+
3,
1244+
None,
1245+
),
1246+
# IV. Invalid/Malformed Polygon Input
1247+
# External hole (WKT is syntactically valid, second ring is usually treated as a hole by parsers regardless of validity)
1248+
(
1249+
"POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (5 5, 5 6, 6 6, 6 5, 5 5))",
1250+
1,
1251+
"LINESTRING (5 5, 5 6, 6 6, 6 5, 5 5)",
1252+
),
1253+
# Intersecting holes (WKT is syntactically valid)
1254+
(
1255+
"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))",
1256+
2,
1257+
"LINESTRING (2 2, 2 2.5, 2.5 2.5, 2.5 2, 2 2)",
1258+
),
1259+
# Z Dimensions
1260+
("POINT Z (1 1 5)", 1, None),
1261+
(
1262+
"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))",
1263+
1,
1264+
"LINESTRING Z (1 1 5, 1 2 5, 2 2 5, 2 1 5, 1 1 5)",
1265+
),
1266+
("POLYGON Z ((0 0 10, 4 0 10, 4 4 10, 0 4 10, 0 0 10))", 1, None),
1267+
# M Dimensions
1268+
("LINESTRING M (0 0 1, 1 1 2)", 1, None),
1269+
("POLYGON M ((0 0 1, 4 0 2, 4 4 3, 0 4 4, 0 0 5))", 1, None),
1270+
(
1271+
"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))",
1272+
1,
1273+
"LINESTRING M (1 1 6, 1 2 7, 2 2 8, 2 1 9, 1 1 10)",
1274+
),
1275+
# ZM Dimensions
1276+
("POLYGON ZM EMPTY", 1, None),
1277+
(
1278+
"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))",
1279+
2,
1280+
None,
1281+
),
1282+
(
1283+
"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))",
1284+
1,
1285+
"LINESTRING ZM (1 1 5 6, 1 2 5 7, 2 2 5 8, 2 1 5 9, 1 1 5 10)",
1286+
),
1287+
],
1288+
)
1289+
def test_st_interiorringn(eng, geom, index, expected):
1290+
eng = eng.create_or_skip()
1291+
eng.assert_query_result(
1292+
f"SELECT ST_InteriorRingN({geom_or_null(geom)}, {val_or_null(index)})", expected
1293+
)
1294+
1295+
11811296
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
11821297
@pytest.mark.parametrize(
11831298
("geom", "expected"),

rust/sedona-functions/benches/native-functions.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ fn criterion_benchmark(c: &mut Criterion) {
8282
benchmark::scalar(c, &f, "native", "st_hasm", Point);
8383
benchmark::scalar(c, &f, "native", "st_hasm", LineString(10));
8484

85+
benchmark::scalar(
86+
c,
87+
&f,
88+
"native",
89+
"st_interiorringn",
90+
BenchmarkArgs::ArrayArray(PolygonWithHole(10), Int64(1, 10)),
91+
);
92+
benchmark::scalar(
93+
c,
94+
&f,
95+
"native",
96+
"st_interiorringn",
97+
BenchmarkArgs::ArrayArray(PolygonWithHole(500), Int64(1, 10)),
98+
);
99+
85100
benchmark::scalar(c, &f, "native", "st_isempty", Point);
86101
benchmark::scalar(c, &f, "native", "st_isempty", LineString(10));
87102

rust/sedona-functions/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ mod st_geometrytype;
4242
mod st_geomfromwkb;
4343
mod st_geomfromwkt;
4444
mod st_haszm;
45+
mod st_interiorringn;
4546
pub mod st_intersection_agg;
4647
pub mod st_isclosed;
4748
mod st_iscollection;

rust/sedona-functions/src/register.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub fn default_function_set() -> FunctionSet {
8181
crate::st_geomfromwkt::st_geomfromwkt_udf,
8282
crate::st_haszm::st_hasm_udf,
8383
crate::st_haszm::st_hasz_udf,
84+
crate::st_interiorringn::st_interiorringn_udf,
8485
crate::st_isclosed::st_isclosed_udf,
8586
crate::st_iscollection::st_iscollection_udf,
8687
crate::st_isempty::st_isempty_udf,

rust/sedona-functions/src/st_geometryn.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,12 @@ mod tests {
139139
use rstest::rstest;
140140
use sedona_schema::datatypes::WKB_VIEW_GEOMETRY;
141141
use sedona_testing::testers::ScalarUdfTester;
142+
use sedona_testing::{compare::assert_array_equal, create::create_array};
142143

143144
use super::*;
144145

145146
#[rstest]
146147
fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) {
147-
use sedona_testing::{compare::assert_array_equal, create::create_array};
148-
149148
let tester = ScalarUdfTester::new(
150149
st_geometryn_udf().into(),
151150
vec![

0 commit comments

Comments
 (0)