Skip to content

Commit 3d01227

Browse files
authored
Add ST_FlipCoordinates (#67)
1 parent 4948319 commit 3d01227

File tree

6 files changed

+266
-0
lines changed

6 files changed

+266
-0
lines changed

benchmarks/test_functions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ def queries():
100100

101101
benchmark(queries)
102102

103+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS, DuckDB])
104+
@pytest.mark.parametrize(
105+
"table",
106+
[
107+
"collections_simple",
108+
"collections_complex",
109+
],
110+
)
111+
def test_st_flipcoordinates(self, benchmark, eng, table):
112+
eng = self._get_eng(eng)
113+
114+
def queries():
115+
eng.execute_and_collect(f"SELECT ST_FlipCoordinates(geom1) from {table}")
116+
117+
benchmark(queries)
118+
103119
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS, DuckDB])
104120
@pytest.mark.parametrize(
105121
"table",

python/sedonadb/tests/functions/test_functions.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,34 @@ def test_st_envelope(eng, geom, expected):
290290
eng.assert_query_result(f"SELECT ST_Envelope({geom_or_null(geom)})", expected)
291291

292292

293+
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
294+
@pytest.mark.parametrize(
295+
("geom", "expected"),
296+
[
297+
# Failing on None for SedonaDB: with datafusion optimizer exception
298+
("POINT EMPTY", "POINT (nan nan)"),
299+
("POLYGON EMPTY", "POLYGON EMPTY"),
300+
("LINESTRING EMPTY", "LINESTRING EMPTY"),
301+
("MULTIPOINT EMPTY", "MULTIPOINT EMPTY"),
302+
("MULTILINESTRING EMPTY", "MULTILINESTRING EMPTY"),
303+
("MULTIPOLYGON EMPTY", "MULTIPOLYGON EMPTY"),
304+
("GEOMETRYCOLLECTION EMPTY", "GEOMETRYCOLLECTION EMPTY"),
305+
("POINT (0 1)", "POINT (1 0)"),
306+
("LINESTRING (0 1, 2 3)", "LINESTRING (1 0, 3 2)"),
307+
("MULTIPOINT (0 1, 2 3)", "MULTIPOINT (1 0, 3 2)"),
308+
(
309+
"GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (3 4, 5 6), POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)))",
310+
"GEOMETRYCOLLECTION (POINT (2 1), LINESTRING (4 3, 6 5), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))",
311+
),
312+
],
313+
)
314+
def test_st_flipcoordinates(eng, geom, expected):
315+
eng = eng.create_or_skip()
316+
eng.assert_query_result(
317+
f"SELECT ST_FlipCoordinates({geom_or_null(geom)})", expected
318+
)
319+
320+
293321
@pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
294322
@pytest.mark.parametrize(
295323
("geom", "expected"),

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ fn criterion_benchmark(c: &mut Criterion) {
3333
benchmark::scalar(c, &f, "native", "st_envelope", Point);
3434
benchmark::scalar(c, &f, "native", "st_envelope", LineString(10));
3535

36+
benchmark::scalar(c, &f, "native", "st_flipcoordinates", Point);
37+
benchmark::scalar(c, &f, "native", "st_flipcoordinates", LineString(10));
38+
3639
benchmark::scalar(c, &f, "native", "st_geometrytype", Point);
3740
benchmark::scalar(c, &f, "native", "st_geometrytype", LineString(10));
3841

rust/sedona-functions/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ mod st_dimension;
3333
mod st_dwithin;
3434
pub mod st_envelope;
3535
pub mod st_envelope_aggr;
36+
pub mod st_flipcoordinates;
3637
mod st_geometrytype;
3738
mod st_geomfromwkb;
3839
mod st_geomfromwkt;

rust/sedona-functions/src/register.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub fn default_function_set() -> FunctionSet {
6969
crate::st_dimension::st_dimension_udf,
7070
crate::st_dwithin::st_dwithin_udf,
7171
crate::st_envelope::st_envelope_udf,
72+
crate::st_flipcoordinates::st_flipcoordinates_udf,
7273
crate::st_geometrytype::st_geometry_type_udf,
7374
crate::st_geomfromwkb::st_geogfromwkb_udf,
7475
crate::st_geomfromwkb::st_geomfromwkb_udf,
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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, vec};
18+
19+
use crate::executor::WkbExecutor;
20+
use arrow_array::builder::BinaryBuilder;
21+
use datafusion_common::error::{DataFusionError, Result};
22+
use datafusion_expr::{
23+
scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation, Volatility,
24+
};
25+
use sedona_expr::scalar_udf::{SedonaScalarKernel, SedonaScalarUDF};
26+
use sedona_geometry::{
27+
error::SedonaGeometryError,
28+
transform::{transform, CrsTransform},
29+
wkb_factory::WKB_MIN_PROBABLE_BYTES,
30+
};
31+
32+
use sedona_schema::datatypes::WKB_GEOGRAPHY;
33+
use sedona_schema::{
34+
datatypes::{SedonaType, WKB_GEOMETRY},
35+
matchers::ArgMatcher,
36+
};
37+
use wkb::reader::Wkb;
38+
39+
/// ST_FlipCoordinates() scalar UDF implementation
40+
///
41+
/// An implementation of flip coordinates
42+
pub fn st_flipcoordinates_udf() -> SedonaScalarUDF {
43+
SedonaScalarUDF::new(
44+
"st_flipcoordinates",
45+
vec![Arc::new(STFlipCoordinates {})],
46+
Volatility::Immutable,
47+
Some(st_flipcoordinates_doc()),
48+
)
49+
}
50+
51+
fn st_flipcoordinates_doc() -> Documentation {
52+
Documentation::builder(
53+
DOC_SECTION_OTHER,
54+
"Returns a version of the given geometry with X and Y axis flipped.",
55+
"ST_FlipCoordinates(A:geometry)",
56+
)
57+
.with_argument("geom", "geometry: Input geometry")
58+
.with_sql_example("SELECT ST_FlipCoordinates(df.geometry)")
59+
.build()
60+
}
61+
62+
#[derive(Debug)]
63+
struct STFlipCoordinates {}
64+
65+
impl SedonaScalarKernel for STFlipCoordinates {
66+
fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
67+
let geom_matcher = ArgMatcher::new(vec![ArgMatcher::is_geometry()], WKB_GEOMETRY);
68+
let geog_matcher = ArgMatcher::new(vec![ArgMatcher::is_geography()], WKB_GEOGRAPHY);
69+
let matched_geom = geom_matcher.match_args(args)?;
70+
let matched_geog = geog_matcher.match_args(args)?;
71+
72+
match (matched_geom, matched_geog) {
73+
(Some(geom_result), _) => Ok(Some(geom_result)),
74+
(_, Some(geog_result)) => Ok(Some(geog_result)),
75+
_ => Ok(None),
76+
}
77+
}
78+
79+
fn invoke_batch(
80+
&self,
81+
arg_types: &[SedonaType],
82+
args: &[ColumnarValue],
83+
) -> Result<ColumnarValue> {
84+
let executor = WkbExecutor::new(arg_types, args);
85+
let mut builder = BinaryBuilder::with_capacity(
86+
executor.num_iterations(),
87+
WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
88+
);
89+
90+
let mut transform = SwapXy {};
91+
92+
executor.execute_wkb_void(|maybe_item| {
93+
match maybe_item {
94+
Some(item) => {
95+
invoke_scalar(&item, &mut transform, &mut builder)?;
96+
builder.append_value([]);
97+
}
98+
None => builder.append_null(),
99+
}
100+
Ok(())
101+
})?;
102+
103+
executor.finish(Arc::new(builder.finish()))
104+
}
105+
}
106+
107+
fn invoke_scalar(
108+
wkb: &Wkb,
109+
swap_transform: &mut SwapXy,
110+
writer: &mut impl std::io::Write,
111+
) -> Result<(), DataFusionError> {
112+
transform(wkb, swap_transform, writer).map_err(|e| DataFusionError::External(e.into()))?;
113+
Ok(())
114+
}
115+
116+
#[derive(Debug)]
117+
struct SwapXy {}
118+
impl CrsTransform for SwapXy {
119+
fn transform_coord(
120+
&self,
121+
coord: &mut (f64, f64),
122+
) -> std::result::Result<(), SedonaGeometryError> {
123+
let (x, y) = *coord;
124+
*coord = (y, x);
125+
Ok(())
126+
}
127+
}
128+
129+
#[cfg(test)]
130+
mod tests {
131+
use super::*;
132+
use datafusion_expr::ScalarUDF;
133+
use rstest::rstest;
134+
use sedona_schema::crs::lnglat;
135+
use sedona_schema::datatypes::SedonaType::Wkb;
136+
use sedona_schema::datatypes::{Edges, WKB_VIEW_GEOMETRY};
137+
use sedona_testing::{
138+
compare::assert_array_equal, create::create_array, testers::ScalarUdfTester,
139+
};
140+
141+
#[test]
142+
fn udf_metadata() {
143+
let udf: ScalarUDF = st_flipcoordinates_udf().into();
144+
assert_eq!(udf.name(), "st_flipcoordinates");
145+
assert!(udf.documentation().is_some());
146+
}
147+
148+
#[test]
149+
fn udf_return_type() {
150+
let tester = ScalarUdfTester::new(st_flipcoordinates_udf().into(), vec![WKB_GEOGRAPHY]);
151+
tester.assert_return_type(WKB_GEOGRAPHY);
152+
153+
let tester = ScalarUdfTester::new(st_flipcoordinates_udf().into(), vec![WKB_GEOMETRY]);
154+
tester.assert_return_type(WKB_GEOMETRY);
155+
156+
let tester = ScalarUdfTester::new(st_flipcoordinates_udf().into(), vec![WKB_VIEW_GEOMETRY]);
157+
tester.assert_return_type(WKB_GEOMETRY);
158+
159+
let tester = ScalarUdfTester::new(
160+
st_flipcoordinates_udf().into(),
161+
vec![Wkb(Edges::Planar, lnglat())],
162+
);
163+
tester.assert_return_type(Wkb(Edges::Planar, lnglat()));
164+
}
165+
166+
#[rstest]
167+
fn udf_invoke(
168+
#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY, WKB_GEOGRAPHY)] sedona_type: SedonaType,
169+
) {
170+
let tester =
171+
ScalarUdfTester::new(st_flipcoordinates_udf().into(), vec![sedona_type.clone()]);
172+
173+
let result = tester.invoke_scalar("POINT (1 3)").unwrap();
174+
tester.assert_scalar_result_equals(result, "POINT (3 1)");
175+
176+
let input_wkt = vec![
177+
None,
178+
Some("POINT (1 2)"),
179+
Some("POINT Z(1 2 3)"),
180+
Some("LINESTRING (10 0, 1 3)"),
181+
Some("LINESTRING M(10 0 5, 1 3 6)"),
182+
Some("POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0), (0 1, 1 1, 1 0, 0 1))"),
183+
Some("GEOMETRYCOLLECTION (POINT (7 5), LINESTRING (-1 -3, 1 2))"),
184+
Some("MULTIPOINT ZM(1 2 3 4, 5 6 7 8)"),
185+
Some("MULTILINESTRING ((0 0, 1 3), (10 0, 1 3))"),
186+
Some("POINT EMPTY"),
187+
Some("LINESTRING EMPTY"),
188+
Some("POLYGON EMPTY"),
189+
Some("MULTIPOINT EMPTY"),
190+
Some("MULTILINESTRING EMPTY"),
191+
Some("MULTIPOLYGON EMPTY"),
192+
Some("GEOMETRYCOLLECTION EMPTY"),
193+
];
194+
let expected = create_array(
195+
&[
196+
None,
197+
Some("POINT (2 1)"),
198+
Some("POINT Z(2 1 3)"),
199+
Some("LINESTRING (0 10, 3 1)"),
200+
Some("LINESTRING M(0 10 5, 3 1 6)"),
201+
Some("POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0), (1 0, 1 1, 0 1, 1 0))"),
202+
Some("GEOMETRYCOLLECTION (POINT (5 7), LINESTRING (-3 -1, 2 1))"),
203+
Some("MULTIPOINT ZM(2 1 3 4, 6 5 7 8)"),
204+
Some("MULTILINESTRING ((0 0, 3 1), (0 10, 3 1))"),
205+
Some("POINT EMPTY"),
206+
Some("LINESTRING EMPTY"),
207+
Some("POLYGON EMPTY"),
208+
Some("MULTIPOINT EMPTY"),
209+
Some("MULTILINESTRING EMPTY"),
210+
Some("MULTIPOLYGON EMPTY"),
211+
Some("GEOMETRYCOLLECTION EMPTY"),
212+
],
213+
&WKB_GEOMETRY,
214+
);
215+
assert_array_equal(&tester.invoke_wkb_array(input_wkt).unwrap(), &expected);
216+
}
217+
}

0 commit comments

Comments
 (0)