Skip to content

Commit 3e6cfe3

Browse files
authored
feat(c/sedona-geos): Implement ST_Reverse using geos (#288)
1 parent 943d149 commit 3e6cfe3

File tree

4 files changed

+163
-0
lines changed

4 files changed

+163
-0
lines changed

c/sedona-geos/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mod st_isvalid;
3131
mod st_isvalidreason;
3232
mod st_length;
3333
mod st_perimeter;
34+
mod st_reverse;
3435
mod st_simplifypreservetopology;
3536
mod st_unaryunion;
3637
pub mod wkb_to_geos;

c/sedona-geos/src/register.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::{
2929
st_isvalidreason::st_is_valid_reason_impl,
3030
st_length::st_length_impl,
3131
st_perimeter::st_perimeter_impl,
32+
st_reverse::st_reverse_impl,
3233
st_simplifypreservetopology::st_simplify_preserve_topology_impl,
3334
st_unaryunion::st_unary_union_impl,
3435
};
@@ -67,6 +68,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> {
6768
("st_length", st_length_impl()),
6869
("st_overlaps", st_overlaps_impl()),
6970
("st_perimeter", st_perimeter_impl()),
71+
("st_reverse", st_reverse_impl()),
7072
(
7173
"st_simplifypreservetopology",
7274
st_simplify_preserve_topology_impl(),

c/sedona-geos/src/st_reverse.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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+
use std::sync::Arc;
18+
19+
use arrow_array::builder::BinaryBuilder;
20+
use datafusion_common::{error::Result, DataFusionError};
21+
use datafusion_expr::ColumnarValue;
22+
use geos::Geom;
23+
use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
24+
use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES;
25+
use sedona_schema::{
26+
datatypes::{SedonaType, WKB_GEOMETRY},
27+
matchers::ArgMatcher,
28+
};
29+
30+
use crate::executor::GeosExecutor;
31+
32+
/// ST_Reverse() implementation using the geos crate
33+
pub fn st_reverse_impl() -> ScalarKernelRef {
34+
Arc::new(STReverse {})
35+
}
36+
37+
#[derive(Debug)]
38+
struct STReverse {}
39+
40+
impl SedonaScalarKernel for STReverse {
41+
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
42+
let matcher = ArgMatcher::new(vec![ArgMatcher::is_geometry()], WKB_GEOMETRY);
43+
44+
matcher.match_args(args)
45+
}
46+
47+
fn invoke_batch(
48+
&self,
49+
arg_types: &[SedonaType],
50+
args: &[ColumnarValue],
51+
) -> Result<ColumnarValue> {
52+
let executor = GeosExecutor::new(arg_types, args);
53+
let mut builder = BinaryBuilder::with_capacity(
54+
executor.num_iterations(),
55+
WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
56+
);
57+
executor.execute_wkb_void(|maybe_wkb| {
58+
match maybe_wkb {
59+
Some(wkb) => {
60+
invoke_scalar(&wkb, &mut builder)?;
61+
builder.append_value([]);
62+
}
63+
_ => builder.append_null(),
64+
}
65+
66+
Ok(())
67+
})?;
68+
69+
executor.finish(Arc::new(builder.finish()))
70+
}
71+
}
72+
73+
fn invoke_scalar(geos_geom: &geos::Geometry, writer: &mut impl std::io::Write) -> Result<()> {
74+
let geometry = geos_geom
75+
.reverse()
76+
.map_err(|e| DataFusionError::Execution(format!("Failed to calculate reverse: {e}")))?;
77+
78+
let wkb = geometry
79+
.to_wkb()
80+
.map_err(|e| DataFusionError::Execution(format!("Failed to convert to wkb: {e}")))?;
81+
82+
writer.write_all(wkb.as_ref())?;
83+
Ok(())
84+
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use datafusion_common::ScalarValue;
89+
use rstest::rstest;
90+
use sedona_expr::scalar_udf::SedonaScalarUDF;
91+
use sedona_schema::datatypes::{WKB_GEOMETRY, WKB_VIEW_GEOMETRY};
92+
use sedona_testing::compare::assert_array_equal;
93+
use sedona_testing::create::create_array;
94+
use sedona_testing::testers::ScalarUdfTester;
95+
96+
use super::*;
97+
98+
#[rstest]
99+
fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) {
100+
let udf = SedonaScalarUDF::from_kernel("st_reverse", st_reverse_impl());
101+
let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type]);
102+
tester.assert_return_type(WKB_GEOMETRY);
103+
104+
let result = tester
105+
.invoke_scalar("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
106+
.unwrap();
107+
tester.assert_scalar_result_equals(result, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))");
108+
109+
let result = tester.invoke_scalar(ScalarValue::Null).unwrap();
110+
assert!(result.is_null());
111+
112+
let input_wkt = vec![
113+
Some("POLYGON ((2 2, 2 3, 3 3, 3 2, 2 2))"),
114+
Some("POINT EMPTY"),
115+
Some("POINT (1 2)"),
116+
Some("LINESTRING (1 2, 1 10)"),
117+
Some("GEOMETRYCOLLECTION (MULTIPOINT (3 4, 1 2, 7 8, 5 6), LINESTRING (1 10, 1 2))"),
118+
None,
119+
];
120+
121+
let expected = create_array(
122+
&[
123+
Some("POLYGON ((2 2, 3 2, 3 3, 2 3, 2 2))"),
124+
Some("POINT EMPTY"),
125+
Some("POINT (1 2)"),
126+
Some("LINESTRING (1 10, 1 2)"),
127+
Some("GEOMETRYCOLLECTION (MULTIPOINT (3 4, 1 2, 7 8, 5 6), LINESTRING(1 2, 1 10))"),
128+
None,
129+
],
130+
&WKB_GEOMETRY,
131+
);
132+
assert_array_equal(&tester.invoke_wkb_array(input_wkt).unwrap(), &expected);
133+
}
134+
}

python/sedonadb/tests/functions/test_functions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,32 @@ def test_st_perimeter(eng, geom, expected):
12691269
eng.assert_query_result(f"SELECT ST_Perimeter({arg})", expected)
12701270

12711271

1272+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
1273+
@pytest.mark.parametrize(
1274+
("geom", "expected"),
1275+
[
1276+
(None, None),
1277+
("LINESTRING EMPTY", "LINESTRING EMPTY"),
1278+
("LINESTRING(0 0, 1 1, 2 2)", "LINESTRING (2 2, 1 1, 0 0)"),
1279+
("POINT (1 2)", "POINT (1 2)"),
1280+
("POLYGON ((0 0, 1 0, 2 2, 1 2, 0 0))", "POLYGON ((0 0, 1 2, 2 2, 1 0, 0 0))"),
1281+
# Note MultiPoints don't change since each point is separate (e.g not a line string)
1282+
("MULTIPOINT (1 2, 3 4)", "MULTIPOINT (1 2, 3 4)"),
1283+
(
1284+
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 2, 0 0)), ((5 5, 6 0, 7 1, 0 1, 5 5)))",
1285+
"MULTIPOLYGON (((0 0, 0 2, 1 1, 1 0, 0 0)), ((5 5, 0 1, 7 1, 6 0, 5 5)))",
1286+
),
1287+
(
1288+
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
1289+
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (5 6, 3 4), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
1290+
),
1291+
],
1292+
)
1293+
def test_st_reverse(eng, geom, expected):
1294+
eng = eng.create_or_skip()
1295+
eng.assert_query_result(f"SELECT ST_Reverse({geom_or_null(geom)})", expected)
1296+
1297+
12721298
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
12731299
@pytest.mark.parametrize(
12741300
("x", "y", "expected"),

0 commit comments

Comments
 (0)