From 493c6ee83676e91a88cf7e4a0f877e59eed2ec9e Mon Sep 17 00:00:00 2001 From: Ajay Padwal Date: Sun, 21 Jun 2026 22:05:49 +0530 Subject: [PATCH 1/3] feat: implement ST_XxxFromText typed geometry constructors (issue #205) Add ST_GeomCollFromText, ST_LineFromText, ST_LineStringFromText (alias), ST_MLineFromText, ST_MPointFromText, ST_MPolyFromText, ST_PointFromText, and ST_PolygonFromText. Each function parses WKT and errors if the result does not match the expected geometry type, providing OGC/PostGIS compatibility. Implemented by adding an optional ExpectedGeomType to STGeoFromWKT, sharing all parsing and SRID logic with ST_GeomFromWKT. --- docs/reference/sql/st_geomcollfromtext.qmd | 48 +++++ docs/reference/sql/st_linefromtext.qmd | 50 +++++ docs/reference/sql/st_mlinefromtext.qmd | 48 +++++ docs/reference/sql/st_mpointfromtext.qmd | 48 +++++ docs/reference/sql/st_mpolyfromtext.qmd | 48 +++++ docs/reference/sql/st_pointfromtext.qmd | 48 +++++ docs/reference/sql/st_polygonfromtext.qmd | 48 +++++ rust/sedona-functions/src/register.rs | 7 + rust/sedona-functions/src/st_geomfromwkt.rs | 195 +++++++++++++++++++- 9 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 docs/reference/sql/st_geomcollfromtext.qmd create mode 100644 docs/reference/sql/st_linefromtext.qmd create mode 100644 docs/reference/sql/st_mlinefromtext.qmd create mode 100644 docs/reference/sql/st_mpointfromtext.qmd create mode 100644 docs/reference/sql/st_mpolyfromtext.qmd create mode 100644 docs/reference/sql/st_pointfromtext.qmd create mode 100644 docs/reference/sql/st_polygonfromtext.qmd diff --git a/docs/reference/sql/st_geomcollfromtext.qmd b/docs/reference/sql/st_geomcollfromtext.qmd new file mode 100644 index 000000000..b38ba542d --- /dev/null +++ b/docs/reference/sql/st_geomcollfromtext.qmd @@ -0,0 +1,48 @@ +--- +# 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. + +title: ST_GeomCollFromText +description: Constructs a GeometryCollection from Well-Known Text (WKT), erroring if the input is not a GeometryCollection. +kernels: + - returns: geometry + args: + - name: wkt + type: string + - returns: geometry + args: + - name: wkt + type: string + - name: srid + type: crs +--- + +## Description + +Parses a WKT string and returns a geometry. Raises an error if the WKT does not represent a GeometryCollection. + +## Examples + +```sql +SELECT ST_GeomCollFromText('GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1))'); +``` + +With an SRID: + +```sql +SELECT ST_GeomCollFromText('GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1))', 4326); +``` diff --git a/docs/reference/sql/st_linefromtext.qmd b/docs/reference/sql/st_linefromtext.qmd new file mode 100644 index 000000000..06fc9e738 --- /dev/null +++ b/docs/reference/sql/st_linefromtext.qmd @@ -0,0 +1,50 @@ +--- +# 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. + +title: ST_LineFromText +description: Constructs a LineString from Well-Known Text (WKT), erroring if the input is not a LineString. +kernels: + - returns: geometry + args: + - name: wkt + type: string + - returns: geometry + args: + - name: wkt + type: string + - name: srid + type: crs +--- + +## Description + +Parses a WKT string and returns a geometry. Raises an error if the WKT does not represent a LineString. + +This function also has the alias `ST_LineStringFromText`. + +## Examples + +```sql +SELECT ST_LineFromText('LINESTRING (0 0, 1 1, 2 2)'); +``` + +With an SRID: + +```sql +SELECT ST_LineFromText('LINESTRING (0 0, 1 1, 2 2)', 4326); +``` diff --git a/docs/reference/sql/st_mlinefromtext.qmd b/docs/reference/sql/st_mlinefromtext.qmd new file mode 100644 index 000000000..e3ac4015d --- /dev/null +++ b/docs/reference/sql/st_mlinefromtext.qmd @@ -0,0 +1,48 @@ +--- +# 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. + +title: ST_MLineFromText +description: Constructs a MultiLineString from Well-Known Text (WKT), erroring if the input is not a MultiLineString. +kernels: + - returns: geometry + args: + - name: wkt + type: string + - returns: geometry + args: + - name: wkt + type: string + - name: srid + type: crs +--- + +## Description + +Parses a WKT string and returns a geometry. Raises an error if the WKT does not represent a MultiLineString. + +## Examples + +```sql +SELECT ST_MLineFromText('MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))'); +``` + +With an SRID: + +```sql +SELECT ST_MLineFromText('MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))', 4326); +``` diff --git a/docs/reference/sql/st_mpointfromtext.qmd b/docs/reference/sql/st_mpointfromtext.qmd new file mode 100644 index 000000000..34e9f577f --- /dev/null +++ b/docs/reference/sql/st_mpointfromtext.qmd @@ -0,0 +1,48 @@ +--- +# 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. + +title: ST_MPointFromText +description: Constructs a MultiPoint from Well-Known Text (WKT), erroring if the input is not a MultiPoint. +kernels: + - returns: geometry + args: + - name: wkt + type: string + - returns: geometry + args: + - name: wkt + type: string + - name: srid + type: crs +--- + +## Description + +Parses a WKT string and returns a geometry. Raises an error if the WKT does not represent a MultiPoint. + +## Examples + +```sql +SELECT ST_MPointFromText('MULTIPOINT ((0 0), (1 1), (2 2))'); +``` + +With an SRID: + +```sql +SELECT ST_MPointFromText('MULTIPOINT ((0 0), (1 1), (2 2))', 4326); +``` diff --git a/docs/reference/sql/st_mpolyfromtext.qmd b/docs/reference/sql/st_mpolyfromtext.qmd new file mode 100644 index 000000000..413846067 --- /dev/null +++ b/docs/reference/sql/st_mpolyfromtext.qmd @@ -0,0 +1,48 @@ +--- +# 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. + +title: ST_MPolyFromText +description: Constructs a MultiPolygon from Well-Known Text (WKT), erroring if the input is not a MultiPolygon. +kernels: + - returns: geometry + args: + - name: wkt + type: string + - returns: geometry + args: + - name: wkt + type: string + - name: srid + type: crs +--- + +## Description + +Parses a WKT string and returns a geometry. Raises an error if the WKT does not represent a MultiPolygon. + +## Examples + +```sql +SELECT ST_MPolyFromText('MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((2 2, 3 2, 3 3, 2 2)))'); +``` + +With an SRID: + +```sql +SELECT ST_MPolyFromText('MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((2 2, 3 2, 3 3, 2 2)))', 4326); +``` diff --git a/docs/reference/sql/st_pointfromtext.qmd b/docs/reference/sql/st_pointfromtext.qmd new file mode 100644 index 000000000..cb3c03475 --- /dev/null +++ b/docs/reference/sql/st_pointfromtext.qmd @@ -0,0 +1,48 @@ +--- +# 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. + +title: ST_PointFromText +description: Constructs a Point from Well-Known Text (WKT), erroring if the input is not a Point. +kernels: + - returns: geometry + args: + - name: wkt + type: string + - returns: geometry + args: + - name: wkt + type: string + - name: srid + type: crs +--- + +## Description + +Parses a WKT string and returns a geometry. Raises an error if the WKT does not represent a Point. + +## Examples + +```sql +SELECT ST_PointFromText('POINT (30 10)'); +``` + +With an SRID: + +```sql +SELECT ST_PointFromText('POINT (30 10)', 4326); +``` diff --git a/docs/reference/sql/st_polygonfromtext.qmd b/docs/reference/sql/st_polygonfromtext.qmd new file mode 100644 index 000000000..a775117f3 --- /dev/null +++ b/docs/reference/sql/st_polygonfromtext.qmd @@ -0,0 +1,48 @@ +--- +# 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. + +title: ST_PolygonFromText +description: Constructs a Polygon from Well-Known Text (WKT), erroring if the input is not a Polygon. +kernels: + - returns: geometry + args: + - name: wkt + type: string + - returns: geometry + args: + - name: wkt + type: string + - name: srid + type: crs +--- + +## Description + +Parses a WKT string and returns a geometry. Raises an error if the WKT does not represent a Polygon. + +## Examples + +```sql +SELECT ST_PolygonFromText('POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))'); +``` + +With an SRID: + +```sql +SELECT ST_PolygonFromText('POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))', 4326); +``` diff --git a/rust/sedona-functions/src/register.rs b/rust/sedona-functions/src/register.rs index 1ffee6e96..f45cc0c7b 100644 --- a/rust/sedona-functions/src/register.rs +++ b/rust/sedona-functions/src/register.rs @@ -61,8 +61,15 @@ pub fn default_function_set() -> FunctionSet { crate::st_geomfromwkb::st_geomfromwkb_udf, crate::st_geomfromwkb::st_geomfromwkbunchecked_udf, crate::st_geomfromwkt::st_geogfromwkt_udf, + crate::st_geomfromwkt::st_geomcollfromtext_udf, crate::st_geomfromwkt::st_geomfromewkt_udf, crate::st_geomfromwkt::st_geomfromwkt_udf, + crate::st_geomfromwkt::st_linefromtext_udf, + crate::st_geomfromwkt::st_mlinefromtext_udf, + crate::st_geomfromwkt::st_mpointfromtext_udf, + crate::st_geomfromwkt::st_mpolyfromtext_udf, + crate::st_geomfromwkt::st_pointfromtext_udf, + crate::st_geomfromwkt::st_polygonfromtext_udf, crate::st_haszm::st_hasm_udf, crate::st_haszm::st_hasz_udf, crate::st_interiorringn::st_interiorringn_udf, diff --git a/rust/sedona-functions/src/st_geomfromwkt.rs b/rust/sedona-functions/src/st_geomfromwkt.rs index ca3d1539a..08308135a 100644 --- a/rust/sedona-functions/src/st_geomfromwkt.rs +++ b/rust/sedona-functions/src/st_geomfromwkt.rs @@ -38,6 +38,18 @@ use wkt::Wkt; use crate::executor::WkbExecutor; use crate::st_setsrid::SRIDifiedKernel; +/// Geometry type constraint used by ST_XxxFromText functions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ExpectedGeomType { + Point, + LineString, + Polygon, + MultiPoint, + MultiLineString, + MultiPolygon, + GeometryCollection, +} + /// ST_GeomFromWKT() UDF implementation /// /// An implementation of WKT reading using GeoRust's wkt crate. @@ -45,6 +57,7 @@ use crate::st_setsrid::SRIDifiedKernel; pub fn st_geomfromwkt_udf() -> SedonaScalarUDF { let kernel = Arc::new(STGeoFromWKT { out_type: WKB_GEOMETRY, + expected_geom_type: None, }); let sridified_kernel = Arc::new(SRIDifiedKernel::new(kernel.clone())); @@ -67,12 +80,14 @@ pub fn st_geogfromwkt_udf() -> SedonaScalarUDF { // Inner kernel for SRIDified has no CRS - the SRID argument sets it let inner_kernel = Arc::new(STGeoFromWKT { out_type: WKB_GEOGRAPHY, + expected_geom_type: None, }); let sridified_kernel = Arc::new(SRIDifiedKernel::new(inner_kernel)); // Standalone kernel returns WGS84 CRS by default let standalone_kernel = Arc::new(STGeoFromWKT { out_type: WKB_GEOGRAPHY_WGS84.clone(), + expected_geom_type: None, }); let udf = SedonaScalarUDF::new( @@ -83,9 +98,48 @@ pub fn st_geogfromwkt_udf() -> SedonaScalarUDF { udf.with_aliases(vec!["st_geogfromtext".to_string()]) } +fn make_typed_geom_udf(name: &'static str, expected: ExpectedGeomType) -> SedonaScalarUDF { + let kernel = Arc::new(STGeoFromWKT { + out_type: WKB_GEOMETRY, + expected_geom_type: Some(expected), + }); + let sridified_kernel = Arc::new(SRIDifiedKernel::new(kernel.clone())); + SedonaScalarUDF::new(name, vec![sridified_kernel, kernel], Volatility::Immutable) +} + +pub fn st_geomcollfromtext_udf() -> SedonaScalarUDF { + make_typed_geom_udf("st_geomcollfromtext", ExpectedGeomType::GeometryCollection) +} + +pub fn st_linefromtext_udf() -> SedonaScalarUDF { + make_typed_geom_udf("st_linefromtext", ExpectedGeomType::LineString) + .with_aliases(vec!["st_linestringfromtext".to_string()]) +} + +pub fn st_mlinefromtext_udf() -> SedonaScalarUDF { + make_typed_geom_udf("st_mlinefromtext", ExpectedGeomType::MultiLineString) +} + +pub fn st_mpointfromtext_udf() -> SedonaScalarUDF { + make_typed_geom_udf("st_mpointfromtext", ExpectedGeomType::MultiPoint) +} + +pub fn st_mpolyfromtext_udf() -> SedonaScalarUDF { + make_typed_geom_udf("st_mpolyfromtext", ExpectedGeomType::MultiPolygon) +} + +pub fn st_pointfromtext_udf() -> SedonaScalarUDF { + make_typed_geom_udf("st_pointfromtext", ExpectedGeomType::Point) +} + +pub fn st_polygonfromtext_udf() -> SedonaScalarUDF { + make_typed_geom_udf("st_polygonfromtext", ExpectedGeomType::Polygon) +} + #[derive(Debug)] struct STGeoFromWKT { out_type: SedonaType, + expected_geom_type: Option, } impl SedonaScalarKernel for STGeoFromWKT { @@ -111,7 +165,7 @@ impl SedonaScalarKernel for STGeoFromWKT { for item in as_string_view_array(&arg_array)? { if let Some(wkt_bytes) = item { - invoke_scalar(wkt_bytes, &mut builder)?; + invoke_scalar(wkt_bytes, self.expected_geom_type, &mut builder)?; builder.append_value(vec![]); } else { builder.append_null(); @@ -123,10 +177,37 @@ impl SedonaScalarKernel for STGeoFromWKT { } } -fn invoke_scalar(wkt_bytes: &str, builder: &mut BinaryBuilder) -> Result<()> { +fn invoke_scalar( + wkt_bytes: &str, + expected_geom_type: Option, + builder: &mut BinaryBuilder, +) -> Result<()> { let geometry: Wkt = Wkt::from_str(wkt_bytes).map_err(|err| exec_datafusion_err!("WKT parse error: {err}"))?; + if let Some(expected) = expected_geom_type { + let matches = matches!( + (&geometry, expected), + (Wkt::Point(_), ExpectedGeomType::Point) + | (Wkt::LineString(_), ExpectedGeomType::LineString) + | (Wkt::Polygon(_), ExpectedGeomType::Polygon) + | (Wkt::MultiPoint(_), ExpectedGeomType::MultiPoint) + | (Wkt::MultiLineString(_), ExpectedGeomType::MultiLineString) + | (Wkt::MultiPolygon(_), ExpectedGeomType::MultiPolygon) + | ( + Wkt::GeometryCollection(_), + ExpectedGeomType::GeometryCollection + ) + ); + if !matches { + let actual = geom_type_name(&geometry); + let expected_name = expected_geom_type_name(expected); + return Err(exec_datafusion_err!( + "Expected {expected_name} but got {actual}" + )); + } + } + write_geometry( builder, &geometry, @@ -137,6 +218,30 @@ fn invoke_scalar(wkt_bytes: &str, builder: &mut BinaryBuilder) -> Result<()> { .map_err(|err| sedona_internal_datafusion_err!("WKB write error: {err}")) } +fn geom_type_name(geom: &Wkt) -> &'static str { + match geom { + Wkt::Point(_) => "Point", + Wkt::LineString(_) => "LineString", + Wkt::Polygon(_) => "Polygon", + Wkt::MultiPoint(_) => "MultiPoint", + Wkt::MultiLineString(_) => "MultiLineString", + Wkt::MultiPolygon(_) => "MultiPolygon", + Wkt::GeometryCollection(_) => "GeometryCollection", + } +} + +fn expected_geom_type_name(expected: ExpectedGeomType) -> &'static str { + match expected { + ExpectedGeomType::Point => "Point", + ExpectedGeomType::LineString => "LineString", + ExpectedGeomType::Polygon => "Polygon", + ExpectedGeomType::MultiPoint => "MultiPoint", + ExpectedGeomType::MultiLineString => "MultiLineString", + ExpectedGeomType::MultiPolygon => "MultiPolygon", + ExpectedGeomType::GeometryCollection => "GeometryCollection", + } +} + /// ST_GeomFromEWKT() UDF implementation /// /// An implementation of EWKT reading using GeoRust's wkt crate. @@ -242,7 +347,7 @@ fn invoke_scalar_with_srid( geom_builder: &mut BinaryBuilder, srid_builder: &mut StringViewBuilder, ) -> Result<()> { - invoke_scalar(wkt_bytes, geom_builder)?; + invoke_scalar(wkt_bytes, None, geom_builder)?; srid_builder.append_option(srid); Ok(()) } @@ -468,5 +573,89 @@ mod tests { let udf: ScalarUDF = st_geogfromwkt_udf().into(); assert!(udf.aliases().contains(&"st_geogfromtext".to_string())); + + let udf: ScalarUDF = st_linefromtext_udf().into(); + assert!(udf.aliases().contains(&"st_linestringfromtext".to_string())); + } + + #[test] + fn typed_constructors_accept_correct_type() { + let cases: &[(&str, SedonaScalarUDF)] = &[ + ("POINT (1 2)", st_pointfromtext_udf()), + ("LINESTRING (0 0, 1 1)", st_linefromtext_udf()), + ("POLYGON ((0 0, 1 0, 1 1, 0 0))", st_polygonfromtext_udf()), + ("MULTIPOINT ((0 0), (1 1))", st_mpointfromtext_udf()), + ( + "MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", + st_mlinefromtext_udf(), + ), + ( + "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)))", + st_mpolyfromtext_udf(), + ), + ( + "GEOMETRYCOLLECTION (POINT (0 0))", + st_geomcollfromtext_udf(), + ), + ]; + for (wkt, udf) in cases { + let tester = + ScalarUdfTester::new(udf.clone().into(), vec![SedonaType::Arrow(DataType::Utf8)]); + assert!(tester.invoke_scalar(*wkt).is_ok(), "expected Ok for {wkt}"); + } + } + + #[test] + fn typed_constructors_reject_wrong_type() { + let udf = st_pointfromtext_udf(); + let tester = ScalarUdfTester::new(udf.into(), vec![SedonaType::Arrow(DataType::Utf8)]); + let err = tester.invoke_scalar("LINESTRING (0 0, 1 1)").unwrap_err(); + assert!( + err.message().contains("Expected Point"), + "unexpected error: {err}" + ); + } + + #[test] + fn typed_constructors_accept_srid() { + let cases: &[(&str, SedonaScalarUDF)] = &[ + ("POINT (1 2)", st_pointfromtext_udf()), + ("LINESTRING (0 0, 1 1)", st_linefromtext_udf()), + ("POLYGON ((0 0, 1 0, 1 1, 0 0))", st_polygonfromtext_udf()), + ("MULTIPOINT ((0 0), (1 1))", st_mpointfromtext_udf()), + ( + "MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", + st_mlinefromtext_udf(), + ), + ( + "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)))", + st_mpolyfromtext_udf(), + ), + ( + "GEOMETRYCOLLECTION (POINT (0 0))", + st_geomcollfromtext_udf(), + ), + ]; + for (wkt, udf) in cases { + let tester = ScalarUdfTester::new( + udf.clone().into(), + vec![ + SedonaType::Arrow(DataType::Utf8), + SedonaType::Arrow(DataType::UInt32), + ], + ); + let return_type = tester + .return_type_with_scalar_scalar(Some(*wkt), Some(4326u32)) + .unwrap(); + assert_eq!( + return_type, + SedonaType::Wkb(Edges::Planar, lnglat()), + "wrong return type for {wkt}" + ); + assert_scalar_equal_wkb_geometry( + &tester.invoke_scalar_scalar(*wkt, 4326u32).unwrap(), + Some(wkt), + ); + } } } From 183287e398197ca9dfb64958e9a2abfa7aa88043 Mon Sep 17 00:00:00 2001 From: Ajay Padwal Date: Tue, 23 Jun 2026 03:00:14 +0530 Subject: [PATCH 2/3] refactor: use GeometryTypeId for typed constructors, add Python tests, fix docs - Replace custom ExpectedGeomType enum with sedona_geometry::types::GeometryTypeId to avoid duplication; use .geojson_id() for error messages - Convert typed_constructors_accept_correct_type, accept_srid, and reject_wrong_type Rust tests to rstest #[case] parametrize covering all 7 constructors - Add Python integration tests: accept correct type, reject wrong type, accept SRID, accept matching EMPTY, reject wrong EMPTY (all 7), null input, ST_LineStringFromText alias - Fix ST_LineFromText doc: 'This function also has the alias' -> 'Aliases:' - Fix all 7 typed constructor docs: 'With an SRID:' -> 'With an SRID or CRS:' --- docs/reference/sql/st_geomcollfromtext.qmd | 2 +- docs/reference/sql/st_linefromtext.qmd | 4 +- docs/reference/sql/st_mlinefromtext.qmd | 2 +- docs/reference/sql/st_mpointfromtext.qmd | 2 +- docs/reference/sql/st_mpolyfromtext.qmd | 2 +- docs/reference/sql/st_pointfromtext.qmd | 2 +- docs/reference/sql/st_polygonfromtext.qmd | 2 +- .../tests/functions/test_functions.py | 103 +++++++++ rust/sedona-functions/src/st_geomfromwkt.rs | 214 +++++++----------- 9 files changed, 195 insertions(+), 138 deletions(-) diff --git a/docs/reference/sql/st_geomcollfromtext.qmd b/docs/reference/sql/st_geomcollfromtext.qmd index b38ba542d..b4b4900c8 100644 --- a/docs/reference/sql/st_geomcollfromtext.qmd +++ b/docs/reference/sql/st_geomcollfromtext.qmd @@ -41,7 +41,7 @@ Parses a WKT string and returns a geometry. Raises an error if the WKT does not SELECT ST_GeomCollFromText('GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1))'); ``` -With an SRID: +With an SRID or CRS: ```sql SELECT ST_GeomCollFromText('GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1))', 4326); diff --git a/docs/reference/sql/st_linefromtext.qmd b/docs/reference/sql/st_linefromtext.qmd index 06fc9e738..7a745dc00 100644 --- a/docs/reference/sql/st_linefromtext.qmd +++ b/docs/reference/sql/st_linefromtext.qmd @@ -35,7 +35,7 @@ kernels: Parses a WKT string and returns a geometry. Raises an error if the WKT does not represent a LineString. -This function also has the alias `ST_LineStringFromText`. +Aliases: `ST_LineStringFromText`. ## Examples @@ -43,7 +43,7 @@ This function also has the alias `ST_LineStringFromText`. SELECT ST_LineFromText('LINESTRING (0 0, 1 1, 2 2)'); ``` -With an SRID: +With an SRID or CRS: ```sql SELECT ST_LineFromText('LINESTRING (0 0, 1 1, 2 2)', 4326); diff --git a/docs/reference/sql/st_mlinefromtext.qmd b/docs/reference/sql/st_mlinefromtext.qmd index e3ac4015d..4661c1f05 100644 --- a/docs/reference/sql/st_mlinefromtext.qmd +++ b/docs/reference/sql/st_mlinefromtext.qmd @@ -41,7 +41,7 @@ Parses a WKT string and returns a geometry. Raises an error if the WKT does not SELECT ST_MLineFromText('MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))'); ``` -With an SRID: +With an SRID or CRS: ```sql SELECT ST_MLineFromText('MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))', 4326); diff --git a/docs/reference/sql/st_mpointfromtext.qmd b/docs/reference/sql/st_mpointfromtext.qmd index 34e9f577f..1773354e6 100644 --- a/docs/reference/sql/st_mpointfromtext.qmd +++ b/docs/reference/sql/st_mpointfromtext.qmd @@ -41,7 +41,7 @@ Parses a WKT string and returns a geometry. Raises an error if the WKT does not SELECT ST_MPointFromText('MULTIPOINT ((0 0), (1 1), (2 2))'); ``` -With an SRID: +With an SRID or CRS: ```sql SELECT ST_MPointFromText('MULTIPOINT ((0 0), (1 1), (2 2))', 4326); diff --git a/docs/reference/sql/st_mpolyfromtext.qmd b/docs/reference/sql/st_mpolyfromtext.qmd index 413846067..03057a0a4 100644 --- a/docs/reference/sql/st_mpolyfromtext.qmd +++ b/docs/reference/sql/st_mpolyfromtext.qmd @@ -41,7 +41,7 @@ Parses a WKT string and returns a geometry. Raises an error if the WKT does not SELECT ST_MPolyFromText('MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((2 2, 3 2, 3 3, 2 2)))'); ``` -With an SRID: +With an SRID or CRS: ```sql SELECT ST_MPolyFromText('MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)), ((2 2, 3 2, 3 3, 2 2)))', 4326); diff --git a/docs/reference/sql/st_pointfromtext.qmd b/docs/reference/sql/st_pointfromtext.qmd index cb3c03475..937816676 100644 --- a/docs/reference/sql/st_pointfromtext.qmd +++ b/docs/reference/sql/st_pointfromtext.qmd @@ -41,7 +41,7 @@ Parses a WKT string and returns a geometry. Raises an error if the WKT does not SELECT ST_PointFromText('POINT (30 10)'); ``` -With an SRID: +With an SRID or CRS: ```sql SELECT ST_PointFromText('POINT (30 10)', 4326); diff --git a/docs/reference/sql/st_polygonfromtext.qmd b/docs/reference/sql/st_polygonfromtext.qmd index a775117f3..3d329a74b 100644 --- a/docs/reference/sql/st_polygonfromtext.qmd +++ b/docs/reference/sql/st_polygonfromtext.qmd @@ -41,7 +41,7 @@ Parses a WKT string and returns a geometry. Raises an error if the WKT does not SELECT ST_PolygonFromText('POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))'); ``` -With an SRID: +With an SRID or CRS: ```sql SELECT ST_PolygonFromText('POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))', 4326); diff --git a/python/sedonadb/tests/functions/test_functions.py b/python/sedonadb/tests/functions/test_functions.py index 95c36c0ae..ec78aaa11 100644 --- a/python/sedonadb/tests/functions/test_functions.py +++ b/python/sedonadb/tests/functions/test_functions.py @@ -1697,6 +1697,109 @@ def test_st_geomfromewkt(eng, ewkt, expected, expected_srid): ) +# --- ST_XxxFromText typed constructors --- + +# (fn_name, matching_wkt, wrong_wkt) +_TYPED_CONSTRUCTOR_CASES = [ + ("ST_PointFromText", "POINT (1 2)", "LINESTRING (0 0, 1 1)"), + ("ST_LineFromText", "LINESTRING (0 0, 1 1)", "POINT (1 2)"), + ("ST_PolygonFromText", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POINT (1 2)"), + ("ST_MPointFromText", "MULTIPOINT ((0 0), (1 1))", "POINT (1 2)"), + ("ST_MLineFromText", "MULTILINESTRING ((0 0, 1 1))", "POINT (1 2)"), + ("ST_MPolyFromText", "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))", "POINT (1 2)"), + ( + "ST_GeomCollFromText", + "GEOMETRYCOLLECTION (POINT (0 0))", + "POINT (1 2)", + ), +] + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize(("fn_name", "wkt", "_wrong"), _TYPED_CONSTRUCTOR_CASES) +def test_typed_geom_constructors_accept_correct_type(eng, fn_name, wkt, _wrong): + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT {fn_name}('{wkt}')", wkt) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("fn_name", "_matching", "wrong_wkt"), _TYPED_CONSTRUCTOR_CASES +) +def test_typed_geom_constructors_reject_wrong_type(eng, fn_name, _matching, wrong_wkt): + eng = eng.create_or_skip() + with pytest.raises(Exception): + eng.assert_query_result(f"SELECT {fn_name}('{wrong_wkt}')", None) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize(("fn_name", "wkt", "_wrong"), _TYPED_CONSTRUCTOR_CASES) +def test_typed_geom_constructors_accept_srid(eng, fn_name, wkt, _wrong): + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT ST_SRID({fn_name}('{wkt}', 4326))", 4326) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("fn_name", "empty_wkt"), + [ + ("ST_PointFromText", "POINT EMPTY"), + ("ST_LineFromText", "LINESTRING EMPTY"), + ("ST_PolygonFromText", "POLYGON EMPTY"), + ("ST_MPointFromText", "MULTIPOINT EMPTY"), + ("ST_MLineFromText", "MULTILINESTRING EMPTY"), + ("ST_MPolyFromText", "MULTIPOLYGON EMPTY"), + ("ST_GeomCollFromText", "GEOMETRYCOLLECTION EMPTY"), + ], +) +def test_typed_geom_constructors_accept_matching_empty(eng, fn_name, empty_wkt): + """Each constructor accepts its own EMPTY type (correct type, empty geometry).""" + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT {fn_name}('{empty_wkt}')", empty_wkt) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + "fn_name", + [fn for fn, _, _ in _TYPED_CONSTRUCTOR_CASES], +) +def test_typed_geom_constructors_null_input(eng, fn_name): + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT {fn_name}(NULL)", None) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +def test_st_linestringfromtext_alias(eng): + eng = eng.create_or_skip() + eng.assert_query_result( + "SELECT ST_LineStringFromText('LINESTRING (0 0, 1 1)')", "LINESTRING (0 0, 1 1)" + ) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("fn_name", "wkt", "wrong_empty"), + [ + ("ST_PointFromText", "POINT (1 2)", "LINESTRING EMPTY"), + ("ST_LineFromText", "LINESTRING (0 0, 1 1)", "POINT EMPTY"), + ("ST_PolygonFromText", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POINT EMPTY"), + ("ST_MPointFromText", "MULTIPOINT ((0 0))", "LINESTRING EMPTY"), + ("ST_MLineFromText", "MULTILINESTRING ((0 0, 1 1))", "POINT EMPTY"), + ( + "ST_MPolyFromText", + "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))", + "POINT EMPTY", + ), + ("ST_GeomCollFromText", "GEOMETRYCOLLECTION (POINT (0 0))", "LINESTRING EMPTY"), + ], +) +def test_typed_geom_constructors_reject_wrong_empty(eng, fn_name, wkt, wrong_empty): + """EMPTY of wrong type is rejected just like non-empty wrong type.""" + eng = eng.create_or_skip() + with pytest.raises(Exception): + eng.assert_query_result(f"SELECT {fn_name}('{wrong_empty}')", None) + + @pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) @pytest.mark.parametrize( ("geom"), diff --git a/rust/sedona-functions/src/st_geomfromwkt.rs b/rust/sedona-functions/src/st_geomfromwkt.rs index 08308135a..161d63773 100644 --- a/rust/sedona-functions/src/st_geomfromwkt.rs +++ b/rust/sedona-functions/src/st_geomfromwkt.rs @@ -35,21 +35,11 @@ use wkb::writer::{write_geometry, WriteOptions}; use wkb::Endianness; use wkt::Wkt; +use sedona_geometry::types::GeometryTypeId; + use crate::executor::WkbExecutor; use crate::st_setsrid::SRIDifiedKernel; -/// Geometry type constraint used by ST_XxxFromText functions. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ExpectedGeomType { - Point, - LineString, - Polygon, - MultiPoint, - MultiLineString, - MultiPolygon, - GeometryCollection, -} - /// ST_GeomFromWKT() UDF implementation /// /// An implementation of WKT reading using GeoRust's wkt crate. @@ -98,7 +88,7 @@ pub fn st_geogfromwkt_udf() -> SedonaScalarUDF { udf.with_aliases(vec!["st_geogfromtext".to_string()]) } -fn make_typed_geom_udf(name: &'static str, expected: ExpectedGeomType) -> SedonaScalarUDF { +fn make_typed_geom_udf(name: &'static str, expected: GeometryTypeId) -> SedonaScalarUDF { let kernel = Arc::new(STGeoFromWKT { out_type: WKB_GEOMETRY, expected_geom_type: Some(expected), @@ -108,38 +98,38 @@ fn make_typed_geom_udf(name: &'static str, expected: ExpectedGeomType) -> Sedona } pub fn st_geomcollfromtext_udf() -> SedonaScalarUDF { - make_typed_geom_udf("st_geomcollfromtext", ExpectedGeomType::GeometryCollection) + make_typed_geom_udf("st_geomcollfromtext", GeometryTypeId::GeometryCollection) } pub fn st_linefromtext_udf() -> SedonaScalarUDF { - make_typed_geom_udf("st_linefromtext", ExpectedGeomType::LineString) + make_typed_geom_udf("st_linefromtext", GeometryTypeId::LineString) .with_aliases(vec!["st_linestringfromtext".to_string()]) } pub fn st_mlinefromtext_udf() -> SedonaScalarUDF { - make_typed_geom_udf("st_mlinefromtext", ExpectedGeomType::MultiLineString) + make_typed_geom_udf("st_mlinefromtext", GeometryTypeId::MultiLineString) } pub fn st_mpointfromtext_udf() -> SedonaScalarUDF { - make_typed_geom_udf("st_mpointfromtext", ExpectedGeomType::MultiPoint) + make_typed_geom_udf("st_mpointfromtext", GeometryTypeId::MultiPoint) } pub fn st_mpolyfromtext_udf() -> SedonaScalarUDF { - make_typed_geom_udf("st_mpolyfromtext", ExpectedGeomType::MultiPolygon) + make_typed_geom_udf("st_mpolyfromtext", GeometryTypeId::MultiPolygon) } pub fn st_pointfromtext_udf() -> SedonaScalarUDF { - make_typed_geom_udf("st_pointfromtext", ExpectedGeomType::Point) + make_typed_geom_udf("st_pointfromtext", GeometryTypeId::Point) } pub fn st_polygonfromtext_udf() -> SedonaScalarUDF { - make_typed_geom_udf("st_polygonfromtext", ExpectedGeomType::Polygon) + make_typed_geom_udf("st_polygonfromtext", GeometryTypeId::Polygon) } #[derive(Debug)] struct STGeoFromWKT { out_type: SedonaType, - expected_geom_type: Option, + expected_geom_type: Option, } impl SedonaScalarKernel for STGeoFromWKT { @@ -179,31 +169,19 @@ impl SedonaScalarKernel for STGeoFromWKT { fn invoke_scalar( wkt_bytes: &str, - expected_geom_type: Option, + expected_geom_type: Option, builder: &mut BinaryBuilder, ) -> Result<()> { let geometry: Wkt = Wkt::from_str(wkt_bytes).map_err(|err| exec_datafusion_err!("WKT parse error: {err}"))?; if let Some(expected) = expected_geom_type { - let matches = matches!( - (&geometry, expected), - (Wkt::Point(_), ExpectedGeomType::Point) - | (Wkt::LineString(_), ExpectedGeomType::LineString) - | (Wkt::Polygon(_), ExpectedGeomType::Polygon) - | (Wkt::MultiPoint(_), ExpectedGeomType::MultiPoint) - | (Wkt::MultiLineString(_), ExpectedGeomType::MultiLineString) - | (Wkt::MultiPolygon(_), ExpectedGeomType::MultiPolygon) - | ( - Wkt::GeometryCollection(_), - ExpectedGeomType::GeometryCollection - ) - ); - if !matches { - let actual = geom_type_name(&geometry); - let expected_name = expected_geom_type_name(expected); + let actual_id = wkt_geometry_type_id(&geometry); + if actual_id != expected { return Err(exec_datafusion_err!( - "Expected {expected_name} but got {actual}" + "Expected {} but got {}", + expected.geojson_id(), + actual_id.geojson_id() )); } } @@ -218,27 +196,15 @@ fn invoke_scalar( .map_err(|err| sedona_internal_datafusion_err!("WKB write error: {err}")) } -fn geom_type_name(geom: &Wkt) -> &'static str { +fn wkt_geometry_type_id(geom: &Wkt) -> GeometryTypeId { match geom { - Wkt::Point(_) => "Point", - Wkt::LineString(_) => "LineString", - Wkt::Polygon(_) => "Polygon", - Wkt::MultiPoint(_) => "MultiPoint", - Wkt::MultiLineString(_) => "MultiLineString", - Wkt::MultiPolygon(_) => "MultiPolygon", - Wkt::GeometryCollection(_) => "GeometryCollection", - } -} - -fn expected_geom_type_name(expected: ExpectedGeomType) -> &'static str { - match expected { - ExpectedGeomType::Point => "Point", - ExpectedGeomType::LineString => "LineString", - ExpectedGeomType::Polygon => "Polygon", - ExpectedGeomType::MultiPoint => "MultiPoint", - ExpectedGeomType::MultiLineString => "MultiLineString", - ExpectedGeomType::MultiPolygon => "MultiPolygon", - ExpectedGeomType::GeometryCollection => "GeometryCollection", + Wkt::Point(_) => GeometryTypeId::Point, + Wkt::LineString(_) => GeometryTypeId::LineString, + Wkt::Polygon(_) => GeometryTypeId::Polygon, + Wkt::MultiPoint(_) => GeometryTypeId::MultiPoint, + Wkt::MultiLineString(_) => GeometryTypeId::MultiLineString, + Wkt::MultiPolygon(_) => GeometryTypeId::MultiPolygon, + Wkt::GeometryCollection(_) => GeometryTypeId::GeometryCollection, } } @@ -578,84 +544,72 @@ mod tests { assert!(udf.aliases().contains(&"st_linestringfromtext".to_string())); } - #[test] - fn typed_constructors_accept_correct_type() { - let cases: &[(&str, SedonaScalarUDF)] = &[ - ("POINT (1 2)", st_pointfromtext_udf()), - ("LINESTRING (0 0, 1 1)", st_linefromtext_udf()), - ("POLYGON ((0 0, 1 0, 1 1, 0 0))", st_polygonfromtext_udf()), - ("MULTIPOINT ((0 0), (1 1))", st_mpointfromtext_udf()), - ( - "MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", - st_mlinefromtext_udf(), - ), - ( - "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)))", - st_mpolyfromtext_udf(), - ), - ( - "GEOMETRYCOLLECTION (POINT (0 0))", - st_geomcollfromtext_udf(), - ), - ]; - for (wkt, udf) in cases { - let tester = - ScalarUdfTester::new(udf.clone().into(), vec![SedonaType::Arrow(DataType::Utf8)]); - assert!(tester.invoke_scalar(*wkt).is_ok(), "expected Ok for {wkt}"); - } + #[rstest] + #[case("POINT (1 2)", st_pointfromtext_udf())] + #[case("LINESTRING (0 0, 1 1)", st_linefromtext_udf())] + #[case("POLYGON ((0 0, 1 0, 1 1, 0 0))", st_polygonfromtext_udf())] + #[case("MULTIPOINT ((0 0), (1 1))", st_mpointfromtext_udf())] + #[case("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", st_mlinefromtext_udf())] + #[case("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)))", st_mpolyfromtext_udf())] + #[case("GEOMETRYCOLLECTION (POINT (0 0))", st_geomcollfromtext_udf())] + fn typed_constructors_accept_correct_type(#[case] wkt: &str, #[case] udf: SedonaScalarUDF) { + let tester = ScalarUdfTester::new(udf.into(), vec![SedonaType::Arrow(DataType::Utf8)]); + assert!(tester.invoke_scalar(wkt).is_ok(), "expected Ok for {wkt}"); } - #[test] - fn typed_constructors_reject_wrong_type() { - let udf = st_pointfromtext_udf(); + #[rstest] + #[case(st_pointfromtext_udf(), "LINESTRING (0 0, 1 1)", "Point")] + #[case(st_linefromtext_udf(), "POINT (1 2)", "LineString")] + #[case(st_polygonfromtext_udf(), "POINT (1 2)", "Polygon")] + #[case(st_mpointfromtext_udf(), "POINT (1 2)", "MultiPoint")] + #[case(st_mlinefromtext_udf(), "POINT (1 2)", "MultiLineString")] + #[case(st_mpolyfromtext_udf(), "POINT (1 2)", "MultiPolygon")] + #[case( + st_geomcollfromtext_udf(), + "LINESTRING (0 0, 1 1)", + "GeometryCollection" + )] + fn typed_constructors_reject_wrong_type( + #[case] udf: SedonaScalarUDF, + #[case] wrong_wkt: &str, + #[case] expected_type_name: &str, + ) { let tester = ScalarUdfTester::new(udf.into(), vec![SedonaType::Arrow(DataType::Utf8)]); - let err = tester.invoke_scalar("LINESTRING (0 0, 1 1)").unwrap_err(); + let err = tester.invoke_scalar(wrong_wkt).unwrap_err(); assert!( - err.message().contains("Expected Point"), + err.message() + .contains(&format!("Expected {expected_type_name}")), "unexpected error: {err}" ); } - #[test] - fn typed_constructors_accept_srid() { - let cases: &[(&str, SedonaScalarUDF)] = &[ - ("POINT (1 2)", st_pointfromtext_udf()), - ("LINESTRING (0 0, 1 1)", st_linefromtext_udf()), - ("POLYGON ((0 0, 1 0, 1 1, 0 0))", st_polygonfromtext_udf()), - ("MULTIPOINT ((0 0), (1 1))", st_mpointfromtext_udf()), - ( - "MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", - st_mlinefromtext_udf(), - ), - ( - "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)))", - st_mpolyfromtext_udf(), - ), - ( - "GEOMETRYCOLLECTION (POINT (0 0))", - st_geomcollfromtext_udf(), - ), - ]; - for (wkt, udf) in cases { - let tester = ScalarUdfTester::new( - udf.clone().into(), - vec![ - SedonaType::Arrow(DataType::Utf8), - SedonaType::Arrow(DataType::UInt32), - ], - ); - let return_type = tester - .return_type_with_scalar_scalar(Some(*wkt), Some(4326u32)) - .unwrap(); - assert_eq!( - return_type, - SedonaType::Wkb(Edges::Planar, lnglat()), - "wrong return type for {wkt}" - ); - assert_scalar_equal_wkb_geometry( - &tester.invoke_scalar_scalar(*wkt, 4326u32).unwrap(), - Some(wkt), - ); - } + #[rstest] + #[case("POINT (1 2)", st_pointfromtext_udf())] + #[case("LINESTRING (0 0, 1 1)", st_linefromtext_udf())] + #[case("POLYGON ((0 0, 1 0, 1 1, 0 0))", st_polygonfromtext_udf())] + #[case("MULTIPOINT ((0 0), (1 1))", st_mpointfromtext_udf())] + #[case("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", st_mlinefromtext_udf())] + #[case("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)))", st_mpolyfromtext_udf())] + #[case("GEOMETRYCOLLECTION (POINT (0 0))", st_geomcollfromtext_udf())] + fn typed_constructors_accept_srid(#[case] wkt: &str, #[case] udf: SedonaScalarUDF) { + let tester = ScalarUdfTester::new( + udf.into(), + vec![ + SedonaType::Arrow(DataType::Utf8), + SedonaType::Arrow(DataType::UInt32), + ], + ); + let return_type = tester + .return_type_with_scalar_scalar(Some(wkt), Some(4326u32)) + .unwrap(); + assert_eq!( + return_type, + SedonaType::Wkb(Edges::Planar, lnglat()), + "wrong return type for {wkt}" + ); + assert_scalar_equal_wkb_geometry( + &tester.invoke_scalar_scalar(wkt, 4326u32).unwrap(), + Some(wkt), + ); } } From fc5c0b512b60bbf835a4c6bcd56052983db48a35 Mon Sep 17 00:00:00 2001 From: Ajay Padwal Date: Tue, 23 Jun 2026 04:00:22 +0530 Subject: [PATCH 3/3] fix(tests): correct PostGIS behavioral differences in typed constructor tests - ST_MPointFromText: use geoarrow-c canonical form (no per-point parens) - reject_wrong_type: SedonaDB-only; PostGIS typed constructors are aliases for ST_GeomFromText and do not validate geometry type - accept_matching_empty: POINT EMPTY renders as POINT (nan nan) via geoarrow-c - ST_LineStringFromText alias: SedonaDB-only; function does not exist in PostGIS - reject_wrong_empty: SedonaDB-only; same reason as reject_wrong_type --- .../sedonadb/tests/functions/test_functions.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/python/sedonadb/tests/functions/test_functions.py b/python/sedonadb/tests/functions/test_functions.py index ec78aaa11..cc3d9a6ad 100644 --- a/python/sedonadb/tests/functions/test_functions.py +++ b/python/sedonadb/tests/functions/test_functions.py @@ -1704,7 +1704,8 @@ def test_st_geomfromewkt(eng, ewkt, expected, expected_srid): ("ST_PointFromText", "POINT (1 2)", "LINESTRING (0 0, 1 1)"), ("ST_LineFromText", "LINESTRING (0 0, 1 1)", "POINT (1 2)"), ("ST_PolygonFromText", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POINT (1 2)"), - ("ST_MPointFromText", "MULTIPOINT ((0 0), (1 1))", "POINT (1 2)"), + # geoarrow-c renders MULTIPOINT without per-point parens + ("ST_MPointFromText", "MULTIPOINT (0 0, 1 1)", "POINT (1 2)"), ("ST_MLineFromText", "MULTILINESTRING ((0 0, 1 1))", "POINT (1 2)"), ("ST_MPolyFromText", "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))", "POINT (1 2)"), ( @@ -1722,11 +1723,12 @@ def test_typed_geom_constructors_accept_correct_type(eng, fn_name, wkt, _wrong): eng.assert_query_result(f"SELECT {fn_name}('{wkt}')", wkt) -@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize("eng", [SedonaDB]) @pytest.mark.parametrize( ("fn_name", "_matching", "wrong_wkt"), _TYPED_CONSTRUCTOR_CASES ) def test_typed_geom_constructors_reject_wrong_type(eng, fn_name, _matching, wrong_wkt): + # PostGIS typed constructors are aliases for ST_GeomFromText and do not validate type eng = eng.create_or_skip() with pytest.raises(Exception): eng.assert_query_result(f"SELECT {fn_name}('{wrong_wkt}')", None) @@ -1755,7 +1757,9 @@ def test_typed_geom_constructors_accept_srid(eng, fn_name, wkt, _wrong): def test_typed_geom_constructors_accept_matching_empty(eng, fn_name, empty_wkt): """Each constructor accepts its own EMPTY type (correct type, empty geometry).""" eng = eng.create_or_skip() - eng.assert_query_result(f"SELECT {fn_name}('{empty_wkt}')", empty_wkt) + # geoarrow-c renders POINT EMPTY as POINT (nan nan) + expected = "POINT (nan nan)" if empty_wkt == "POINT EMPTY" else empty_wkt + eng.assert_query_result(f"SELECT {fn_name}('{empty_wkt}')", expected) @pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) @@ -1768,15 +1772,16 @@ def test_typed_geom_constructors_null_input(eng, fn_name): eng.assert_query_result(f"SELECT {fn_name}(NULL)", None) -@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize("eng", [SedonaDB]) def test_st_linestringfromtext_alias(eng): + # PostGIS does not have ST_LineStringFromText eng = eng.create_or_skip() eng.assert_query_result( "SELECT ST_LineStringFromText('LINESTRING (0 0, 1 1)')", "LINESTRING (0 0, 1 1)" ) -@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize("eng", [SedonaDB]) @pytest.mark.parametrize( ("fn_name", "wkt", "wrong_empty"), [ @@ -1795,6 +1800,7 @@ def test_st_linestringfromtext_alias(eng): ) def test_typed_geom_constructors_reject_wrong_empty(eng, fn_name, wkt, wrong_empty): """EMPTY of wrong type is rejected just like non-empty wrong type.""" + # PostGIS typed constructors are aliases for ST_GeomFromText and do not validate type eng = eng.create_or_skip() with pytest.raises(Exception): eng.assert_query_result(f"SELECT {fn_name}('{wrong_empty}')", None)