diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index 6702075c9..1e841dadf 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -145,6 +145,11 @@ jobs: # These two packages are quick to CMD check after sedonadb is installed, # so do that here - uses: r-lib/actions/check-r-package@v2 + env: + # The install/test step above already bootstraps generated sedonafns + # sources. Avoid running the same roxygen bootstrap again here, which + # can crash Rscript on Windows while building the check tarball. + R_SEDONADB_FNS_SKIP_BOOTSTRAP: "1" with: working-directory: r/sedonafns diff --git a/.gitignore b/.gitignore index 5a3e54362..606212086 100644 --- a/.gitignore +++ b/.gitignore @@ -43,12 +43,14 @@ site/ # Python cache files __pycache__ +.venv/ # R-related files .Rproj.user # .env file for release management dev/release/.env +.mcp.env /.luarc.json .positai diff --git a/c/sedona-geos/src/geos_to_wkb.rs b/c/sedona-geos/src/geos_to_wkb.rs index c8d260381..f5f36e40d 100644 --- a/c/sedona-geos/src/geos_to_wkb.rs +++ b/c/sedona-geos/src/geos_to_wkb.rs @@ -19,7 +19,7 @@ use std::io::Write; use byteorder::{LittleEndian, WriteBytesExt}; use datafusion_common::{error::Result, DataFusionError}; use geo_traits::Dimensions; -use geos::{CoordType, Geom, Geometry, GeometryTypes}; +use geos::{CoordType, Geom, GeometryTypes}; use sedona_common::sedona_internal_err; use sedona_geometry::wkb_factory::{ write_wkb_geometrycollection_header, write_wkb_linestring_header, @@ -31,7 +31,7 @@ use sedona_geometry::wkb_factory::{ /// /// This is a fast, custom implementation that directly extracts coordinates /// from GEOS geometries and writes them in WKB format into a buffer. -pub fn write_geos_geometry(geom: &Geometry, writer: &mut impl Write) -> Result<()> { +pub fn write_geos_geometry(geom: &impl Geom, writer: &mut impl Write) -> Result<()> { write_geometry(geom, writer) } diff --git a/c/sedona-geos/src/lib.rs b/c/sedona-geos/src/lib.rs index 9973f4437..05696998b 100644 --- a/c/sedona-geos/src/lib.rs +++ b/c/sedona-geos/src/lib.rs @@ -25,10 +25,13 @@ pub mod register; mod st_area; mod st_boundary; mod st_buffer; +mod st_buildarea; mod st_centroid; mod st_concavehull; mod st_convexhull; +mod st_delaunaytriangles; mod st_dwithin; +mod st_exteriorring; mod st_isring; mod st_issimple; mod st_isvalid; @@ -43,6 +46,7 @@ mod st_nrings; mod st_numinteriorrings; mod st_numpoints; mod st_perimeter; +mod st_pointonsurface; mod st_polygonize; mod st_polygonize_agg; mod st_relate; diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs index bc48a14eb..db2caad44 100644 --- a/c/sedona-geos/src/register.rs +++ b/c/sedona-geos/src/register.rs @@ -41,6 +41,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, Vec)> { define_scalar_kernels!( "st_area" => crate::st_area::st_area_impl, "st_boundary" => crate::st_boundary::st_boundary_impl, + "st_buildarea" => crate::st_buildarea::st_build_area_impl, "st_buffer" => crate::st_buffer::st_buffer_impl, "st_buffer" => crate::st_buffer::st_buffer_style_impl, "st_centroid" => crate::st_centroid::st_centroid_impl, @@ -51,11 +52,15 @@ pub fn scalar_kernels() -> Vec<(&'static str, Vec)> { "st_coveredby" => crate::binary_predicates::st_covered_by_impl, "st_covers" => crate::binary_predicates::st_covers_impl, "st_crosses" => crate::binary_predicates::st_crosses_impl, + "st_delaunaytriangles" => crate::st_delaunaytriangles::st_delaunay_triangles_impl, + "st_delaunaytriangles" => crate::st_delaunaytriangles::st_delaunay_triangles_tolerance_impl, + "st_delaunaytriangles" => crate::st_delaunaytriangles::st_delaunay_triangles_flags_impl, "st_difference" => crate::overlay::st_difference_impl, "st_disjoint" => crate::binary_predicates::st_disjoint_impl, "st_distance" => crate::distance::st_distance_impl, "st_dwithin" => crate::st_dwithin::st_dwithin_impl, "st_equals" => crate::binary_predicates::st_equals_impl, + "st_exteriorring" => crate::st_exteriorring::st_exterior_ring_impl, "st_intersection" => crate::overlay::st_intersection_impl, "st_intersects" => crate::binary_predicates::st_intersects_impl, "st_isring" => crate::st_isring::st_is_ring_impl, @@ -69,10 +74,12 @@ pub fn scalar_kernels() -> Vec<(&'static str, Vec)> { "st_minimumclearanceline" => crate::st_minimumclearance_line::st_minimum_clearance_line_impl, "st_normalize" => crate::st_normalize::st_normalize_impl, "st_nrings" => crate::st_nrings::st_nrings_impl, + "st_numinteriorring" => crate::st_numinteriorrings::st_num_interior_rings_impl, "st_numinteriorrings" => crate::st_numinteriorrings::st_num_interior_rings_impl, "st_numpoints" => crate::st_numpoints::st_num_points_impl, "st_overlaps" => crate::binary_predicates::st_overlaps_impl, "st_perimeter" => crate::st_perimeter::st_perimeter_impl, + "st_pointonsurface" => crate::st_pointonsurface::st_point_on_surface_impl, "st_polygonize" => crate::st_polygonize::st_polygonize_impl, "st_relate" => crate::st_relate::st_relate_impl, "st_relate" => crate::st_relate::st_relate_pattern_impl, diff --git a/c/sedona-geos/src/st_buildarea.rs b/c/sedona-geos/src/st_buildarea.rs new file mode 100644 index 000000000..d1c20dc04 --- /dev/null +++ b/c/sedona-geos/src/st_buildarea.rs @@ -0,0 +1,156 @@ +// 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. + +use std::sync::Arc; + +use arrow_array::builder::BinaryBuilder; +use datafusion_common::{error::Result, DataFusionError}; +use datafusion_expr::ColumnarValue; +use geos::{Geom, Geometry, GeometryTypes}; +use sedona_expr::{ + item_crs::ItemCrsKernel, + scalar_udf::{ScalarKernelRef, SedonaScalarKernel}, +}; +use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES; +use sedona_schema::{ + datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY}, + matchers::ArgMatcher, +}; + +use crate::executor::GeosExecutor; +use crate::geos_to_wkb::write_geos_geometry; + +/// ST_BuildArea() implementation using the geos crate +pub fn st_build_area_impl() -> Vec { + ItemCrsKernel::wrap_impl(vec![ + Arc::new(STBuildArea { + matcher: ArgMatcher::new(vec![ArgMatcher::is_geometry()], WKB_GEOMETRY), + }), + Arc::new(STBuildArea { + matcher: ArgMatcher::new(vec![ArgMatcher::is_geography()], WKB_GEOGRAPHY), + }), + ]) +} + +#[derive(Debug)] +struct STBuildArea { + matcher: ArgMatcher, +} + +impl SedonaScalarKernel for STBuildArea { + fn return_type(&self, args: &[SedonaType]) -> Result> { + self.matcher.match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = GeosExecutor::new(arg_types, args); + let mut builder = BinaryBuilder::with_capacity( + executor.num_iterations(), + WKB_MIN_PROBABLE_BYTES * executor.num_iterations(), + ); + executor.execute_wkb_void(|maybe_geom| { + match maybe_geom { + Some(geom) => { + if invoke_scalar(&geom, &mut builder)? { + builder.append_value([]); + } else { + builder.append_null(); + } + } + _ => builder.append_null(), + } + Ok(()) + })?; + + executor.finish(Arc::new(builder.finish())) + } +} + +fn invoke_scalar(geom: &Geometry, writer: &mut impl std::io::Write) -> Result { + let geom_type = geom + .geometry_type() + .map_err(|e| DataFusionError::Execution(format!("Failed to get geometry type: {e}")))?; + + match geom_type { + GeometryTypes::LineString + | GeometryTypes::MultiLineString + | GeometryTypes::GeometryCollection => {} + _ => return Ok(false), + } + + let result = geom + .build_area() + .map_err(|e| DataFusionError::Execution(format!("ST_BuildArea failed: {e}")))?; + write_geos_geometry(&result, writer)?; + Ok(true) +} + +#[cfg(test)] +mod tests { + use datafusion_common::ScalarValue; + use rstest::rstest; + use sedona_expr::scalar_udf::SedonaScalarUDF; + use sedona_schema::datatypes::{ + WKB_GEOGRAPHY, WKB_GEOGRAPHY_ITEM_CRS, WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS, + }; + use sedona_testing::testers::ScalarUdfTester; + + use super::*; + + #[rstest] + fn udf(#[values(WKB_GEOMETRY, WKB_GEOGRAPHY)] sedona_type: SedonaType) { + let udf = SedonaScalarUDF::from_impl("st_buildarea", st_build_area_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]); + + tester.assert_return_type(sedona_type.clone()); + + let result = tester + .invoke_scalar("LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)") + .unwrap(); + tester.assert_scalar_result_equals(result, "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"); + + let result = tester.invoke_scalar("POINT (0 0)").unwrap(); + assert!(result.is_null()); + + let result = tester + .invoke_scalar("POLYGON ((0 0, 1 0, 1 1, 0 0))") + .unwrap(); + assert!(result.is_null()); + + let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); + assert!(result.is_null()); + } + + #[rstest] + fn udf_invoke_item_crs( + #[values(WKB_GEOMETRY_ITEM_CRS.clone(), WKB_GEOGRAPHY_ITEM_CRS.clone())] + sedona_type: SedonaType, + ) { + let udf = SedonaScalarUDF::from_impl("st_buildarea", st_build_area_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]); + tester.assert_return_type(sedona_type); + + let result = tester + .invoke_scalar("LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)") + .unwrap(); + tester.assert_scalar_result_equals(result, "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"); + } +} diff --git a/c/sedona-geos/src/st_delaunaytriangles.rs b/c/sedona-geos/src/st_delaunaytriangles.rs new file mode 100644 index 000000000..2fe082001 --- /dev/null +++ b/c/sedona-geos/src/st_delaunaytriangles.rs @@ -0,0 +1,288 @@ +// 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. + +use std::sync::Arc; + +use arrow_array::builder::BinaryBuilder; +use arrow_schema::DataType; +use datafusion_common::{ + cast::{as_float64_array, as_int64_array}, + DataFusionError, Result, +}; +use datafusion_expr::ColumnarValue; +use geos::{Geom, Geometry}; +use sedona_expr::{ + item_crs::ItemCrsKernel, + scalar_udf::{ScalarKernelRef, SedonaScalarKernel}, +}; +use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES; +use sedona_schema::{ + datatypes::{SedonaType, WKB_GEOMETRY}, + matchers::ArgMatcher, +}; + +use crate::executor::GeosExecutor; +use crate::geos_to_wkb::write_geos_geometry; + +fn invoke_scalar( + geom: &Geometry, + tolerance: f64, + only_edges: bool, + writer: &mut impl std::io::Write, +) -> Result<()> { + let result = geom + .delaunay_triangulation(tolerance, only_edges) + .map_err(|e| DataFusionError::Execution(format!("ST_DelaunayTriangles failed: {e}")))?; + write_geos_geometry(&result, writer)?; + Ok(()) +} + +// ── 1-arg: ST_DelaunayTriangles(geom) ──────────────────────────────────────── + +pub fn st_delaunay_triangles_impl() -> Vec { + ItemCrsKernel::wrap_impl(STDelaunayTriangles) +} + +#[derive(Debug)] +struct STDelaunayTriangles; + +impl SedonaScalarKernel for STDelaunayTriangles { + fn return_type(&self, args: &[SedonaType]) -> Result> { + ArgMatcher::new(vec![ArgMatcher::is_geometry()], WKB_GEOMETRY).match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = GeosExecutor::new(arg_types, args); + let mut builder = BinaryBuilder::with_capacity( + executor.num_iterations(), + WKB_MIN_PROBABLE_BYTES * executor.num_iterations(), + ); + executor.execute_wkb_void(|maybe_geom| { + match maybe_geom { + Some(geom) => { + invoke_scalar(&geom, 0.0, false, &mut builder)?; + builder.append_value([]); + } + None => builder.append_null(), + } + Ok(()) + })?; + executor.finish(Arc::new(builder.finish())) + } +} + +// ── 2-arg: ST_DelaunayTriangles(geom, tolerance) ───────────────────────────── + +pub fn st_delaunay_triangles_tolerance_impl() -> Vec { + ItemCrsKernel::wrap_impl(STDelaunayTrianglesWithTolerance) +} + +#[derive(Debug)] +struct STDelaunayTrianglesWithTolerance; + +impl SedonaScalarKernel for STDelaunayTrianglesWithTolerance { + fn return_type(&self, args: &[SedonaType]) -> Result> { + ArgMatcher::new( + vec![ArgMatcher::is_geometry(), ArgMatcher::is_numeric()], + WKB_GEOMETRY, + ) + .match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = GeosExecutor::new(arg_types, args); + let mut builder = BinaryBuilder::with_capacity( + executor.num_iterations(), + WKB_MIN_PROBABLE_BYTES * executor.num_iterations(), + ); + let tol_value = args[1] + .cast_to(&DataType::Float64, None)? + .to_array(executor.num_iterations())?; + let tol_array = as_float64_array(&tol_value)?; + let mut tol_iter = tol_array.iter(); + executor.execute_wkb_void(|maybe_geom| { + match (maybe_geom, tol_iter.next().unwrap()) { + (Some(geom), Some(tol)) => { + invoke_scalar(&geom, tol, false, &mut builder)?; + builder.append_value([]); + } + _ => builder.append_null(), + } + Ok(()) + })?; + executor.finish(Arc::new(builder.finish())) + } +} + +// -- 3-arg: ST_DelaunayTriangles(geom, tolerance, flags) +// flags=0 -> polygon output (default), flags=1 -> multilinestring edges only + +pub fn st_delaunay_triangles_flags_impl() -> Vec { + ItemCrsKernel::wrap_impl(STDelaunayTrianglesWithFlags) +} + +#[derive(Debug)] +struct STDelaunayTrianglesWithFlags; + +impl SedonaScalarKernel for STDelaunayTrianglesWithFlags { + fn return_type(&self, args: &[SedonaType]) -> Result> { + ArgMatcher::new( + vec![ + ArgMatcher::is_geometry(), + ArgMatcher::is_numeric(), + ArgMatcher::is_integer(), + ], + WKB_GEOMETRY, + ) + .match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = GeosExecutor::new(arg_types, args); + let mut builder = BinaryBuilder::with_capacity( + executor.num_iterations(), + WKB_MIN_PROBABLE_BYTES * executor.num_iterations(), + ); + let tol_value = args[1] + .cast_to(&DataType::Float64, None)? + .to_array(executor.num_iterations())?; + let tol_array = as_float64_array(&tol_value)?; + let mut tol_iter = tol_array.iter(); + + let flags_value = args[2] + .cast_to(&DataType::Int64, None)? + .to_array(executor.num_iterations())?; + let flags_array = as_int64_array(&flags_value)?; + let mut flags_iter = flags_array.iter(); + + executor.execute_wkb_void(|maybe_geom| { + match ( + maybe_geom, + tol_iter.next().unwrap(), + flags_iter.next().unwrap(), + ) { + (Some(geom), Some(tol), Some(flag)) => { + let only_edges = match flag { + 0 => false, + 1 => true, + _ => { + return Err(DataFusionError::Execution(format!( + "ST_DelaunayTriangles flags must be 0 or 1, got {flag}" + ))) + } + }; + invoke_scalar(&geom, tol, only_edges, &mut builder)?; + builder.append_value([]); + } + _ => builder.append_null(), + } + Ok(()) + })?; + executor.finish(Arc::new(builder.finish())) + } +} + +#[cfg(test)] +mod tests { + use datafusion_common::ScalarValue; + use rstest::rstest; + use sedona_expr::scalar_udf::SedonaScalarUDF; + use sedona_schema::datatypes::{WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS}; + use sedona_testing::{create::create_scalar, testers::ScalarUdfTester}; + + use super::*; + + #[rstest] + fn udf_no_tolerance(#[values(WKB_GEOMETRY)] sedona_type: SedonaType) { + let udf = SedonaScalarUDF::from_impl("st_delaunaytriangles", st_delaunay_triangles_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]); + + tester.assert_return_type(WKB_GEOMETRY); + + let result = tester + .invoke_scalar("MULTIPOINT ((0 0), (1 1), (0 1))") + .unwrap(); + tester.assert_scalar_result_equals( + result, + "GEOMETRYCOLLECTION (POLYGON ((0 1, 0 0, 1 1, 0 1)))", + ); + + let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); + assert!(result.is_null()); + } + + #[rstest] + fn udf_invoke_item_crs(#[values(WKB_GEOMETRY_ITEM_CRS.clone())] sedona_type: SedonaType) { + let udf = SedonaScalarUDF::from_impl("st_delaunaytriangles", st_delaunay_triangles_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]); + tester.assert_return_type(sedona_type); + } + + #[rstest] + fn udf_flags(#[values(WKB_GEOMETRY)] sedona_type: SedonaType) { + let udf = + SedonaScalarUDF::from_impl("st_delaunaytriangles", st_delaunay_triangles_flags_impl()); + let tester = ScalarUdfTester::new( + udf.into(), + vec![ + sedona_type.clone(), + SedonaType::Arrow(DataType::Float64), + SedonaType::Arrow(DataType::Int64), + ], + ); + + tester.assert_return_type(WKB_GEOMETRY); + + let geom = create_scalar(Some("MULTIPOINT ((0 0), (1 1), (0 1))"), &sedona_type); + + let result = tester + .invoke_scalar_scalar_scalar( + geom.clone(), + ScalarValue::Float64(Some(0.0)), + ScalarValue::Int64(Some(1)), + ) + .unwrap(); + tester.assert_scalar_result_equals( + result, + "MULTILINESTRING ((0 1, 1 1), (0 0, 0 1), (0 0, 1 1))", + ); + + let result = tester + .invoke_scalar_scalar_scalar( + geom, + ScalarValue::Float64(Some(0.0)), + ScalarValue::Int64(Some(0)), + ) + .unwrap(); + tester.assert_scalar_result_equals( + result, + "GEOMETRYCOLLECTION (POLYGON ((0 1, 0 0, 1 1, 0 1)))", + ); + } +} diff --git a/c/sedona-geos/src/st_exteriorring.rs b/c/sedona-geos/src/st_exteriorring.rs new file mode 100644 index 000000000..af475a510 --- /dev/null +++ b/c/sedona-geos/src/st_exteriorring.rs @@ -0,0 +1,159 @@ +// 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. + +use std::sync::Arc; + +use arrow_array::builder::BinaryBuilder; +use datafusion_common::{error::Result, DataFusionError}; +use datafusion_expr::ColumnarValue; +use geos::{Geom, Geometry, GeometryTypes}; +use sedona_expr::{ + item_crs::ItemCrsKernel, + scalar_udf::{ScalarKernelRef, SedonaScalarKernel}, +}; +use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES; +use sedona_schema::{ + datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY}, + matchers::ArgMatcher, +}; + +use crate::executor::GeosExecutor; +use crate::geos_to_wkb::write_geos_geometry; + +/// ST_ExteriorRing() implementation using the geos crate +/// +/// Returns the exterior ring of a Polygon, or NULL for non-polygon geometries. +pub fn st_exterior_ring_impl() -> Vec { + ItemCrsKernel::wrap_impl(vec![ + Arc::new(STExteriorRing { + matcher: ArgMatcher::new(vec![ArgMatcher::is_geometry()], WKB_GEOMETRY), + }), + Arc::new(STExteriorRing { + matcher: ArgMatcher::new(vec![ArgMatcher::is_geography()], WKB_GEOGRAPHY), + }), + ]) +} + +#[derive(Debug)] +struct STExteriorRing { + matcher: ArgMatcher, +} + +impl SedonaScalarKernel for STExteriorRing { + fn return_type(&self, args: &[SedonaType]) -> Result> { + self.matcher.match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = GeosExecutor::new(arg_types, args); + let mut builder = BinaryBuilder::with_capacity( + executor.num_iterations(), + WKB_MIN_PROBABLE_BYTES * executor.num_iterations(), + ); + executor.execute_wkb_void(|maybe_geom| { + match maybe_geom { + Some(geom) => { + if invoke_scalar(&geom, &mut builder)? { + builder.append_value([]); + } else { + builder.append_null(); + } + } + _ => builder.append_null(), + } + Ok(()) + })?; + + executor.finish(Arc::new(builder.finish())) + } +} + +fn invoke_scalar(geom: &Geometry, writer: &mut impl std::io::Write) -> Result { + let geom_type = geom + .geometry_type() + .map_err(|e| DataFusionError::Execution(format!("Failed to get geometry type: {e}")))?; + + if geom_type != GeometryTypes::Polygon { + return Ok(false); + } + + let ring = geom + .get_exterior_ring() + .map_err(|e| DataFusionError::Execution(format!("ST_ExteriorRing failed: {e}")))?; + let line = + Geometry::create_line_string(ring.get_coord_seq().map_err(|e| { + DataFusionError::Execution(format!("Failed to get ring coordinates: {e}")) + })?) + .map_err(|e| { + DataFusionError::Execution(format!("Failed to create exterior linestring: {e}")) + })?; + + write_geos_geometry(&line, writer)?; + Ok(true) +} + +#[cfg(test)] +mod tests { + use datafusion_common::ScalarValue; + use rstest::rstest; + use sedona_expr::scalar_udf::SedonaScalarUDF; + use sedona_schema::datatypes::{ + WKB_GEOGRAPHY, WKB_GEOGRAPHY_ITEM_CRS, WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS, + }; + use sedona_testing::testers::ScalarUdfTester; + + use super::*; + + #[rstest] + fn udf(#[values(WKB_GEOMETRY, WKB_GEOGRAPHY)] sedona_type: SedonaType) { + let udf = SedonaScalarUDF::from_impl("st_exteriorring", st_exterior_ring_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]); + + tester.assert_return_type(sedona_type.clone()); + + let result = tester + .invoke_scalar("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))") + .unwrap(); + tester.assert_scalar_result_equals(result, "LINESTRING (0 0, 4 0, 4 4, 0 4, 0 0)"); + + // non-polygon returns null + let result = tester.invoke_scalar("POINT (1 2)").unwrap(); + assert!(result.is_null()); + + let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); + assert!(result.is_null()); + } + + #[rstest] + fn udf_invoke_item_crs( + #[values(WKB_GEOMETRY_ITEM_CRS.clone(), WKB_GEOGRAPHY_ITEM_CRS.clone())] + sedona_type: SedonaType, + ) { + let udf = SedonaScalarUDF::from_impl("st_exteriorring", st_exterior_ring_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]); + tester.assert_return_type(sedona_type); + + let result = tester + .invoke_scalar("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))") + .unwrap(); + tester.assert_scalar_result_equals(result, "LINESTRING (0 0, 4 0, 4 4, 0 4, 0 0)"); + } +} diff --git a/c/sedona-geos/src/st_pointonsurface.rs b/c/sedona-geos/src/st_pointonsurface.rs new file mode 100644 index 000000000..cbbf6f5b6 --- /dev/null +++ b/c/sedona-geos/src/st_pointonsurface.rs @@ -0,0 +1,127 @@ +// 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. + +use std::sync::Arc; + +use arrow_array::builder::BinaryBuilder; +use datafusion_common::{error::Result, DataFusionError}; +use datafusion_expr::ColumnarValue; +use geos::{Geom, Geometry}; +use sedona_expr::{ + item_crs::ItemCrsKernel, + scalar_udf::{ScalarKernelRef, SedonaScalarKernel}, +}; +use sedona_geometry::wkb_factory::WKB_MIN_PROBABLE_BYTES; +use sedona_schema::{ + datatypes::{SedonaType, WKB_GEOMETRY}, + matchers::ArgMatcher, +}; + +use crate::executor::GeosExecutor; +use crate::geos_to_wkb::write_geos_geometry; + +/// ST_PointOnSurface() implementation using the geos crate +pub fn st_point_on_surface_impl() -> Vec { + ItemCrsKernel::wrap_impl(STPointOnSurface { + matcher: ArgMatcher::new(vec![ArgMatcher::is_geometry()], WKB_GEOMETRY), + }) +} + +#[derive(Debug)] +struct STPointOnSurface { + matcher: ArgMatcher, +} + +impl SedonaScalarKernel for STPointOnSurface { + fn return_type(&self, args: &[SedonaType]) -> Result> { + self.matcher.match_args(args) + } + + fn invoke_batch( + &self, + arg_types: &[SedonaType], + args: &[ColumnarValue], + ) -> Result { + let executor = GeosExecutor::new(arg_types, args); + let mut builder = BinaryBuilder::with_capacity( + executor.num_iterations(), + WKB_MIN_PROBABLE_BYTES * executor.num_iterations(), + ); + executor.execute_wkb_void(|maybe_geom| { + match maybe_geom { + Some(geom) => { + invoke_scalar(&geom, &mut builder)?; + builder.append_value([]); + } + _ => builder.append_null(), + } + Ok(()) + })?; + + executor.finish(Arc::new(builder.finish())) + } +} + +fn invoke_scalar(geom: &Geometry, writer: &mut impl std::io::Write) -> Result<()> { + let result = geom + .point_on_surface() + .map_err(|e| DataFusionError::Execution(format!("ST_PointOnSurface failed: {e}")))?; + write_geos_geometry(&result, writer)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use datafusion_common::ScalarValue; + use rstest::rstest; + use sedona_expr::scalar_udf::SedonaScalarUDF; + use sedona_schema::datatypes::{WKB_GEOMETRY, WKB_GEOMETRY_ITEM_CRS}; + use sedona_testing::testers::ScalarUdfTester; + + use super::*; + + #[rstest] + fn udf(#[values(WKB_GEOMETRY)] sedona_type: SedonaType) { + let udf = SedonaScalarUDF::from_impl("st_pointonsurface", st_point_on_surface_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]); + + tester.assert_return_type(sedona_type.clone()); + + let result = tester + .invoke_scalar("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))") + .unwrap(); + tester.assert_scalar_result_equals(result, "POINT (2 2)"); + + let result = tester.invoke_scalar("POINT (1 2)").unwrap(); + tester.assert_scalar_result_equals(result, "POINT (1 2)"); + + let result = tester.invoke_scalar(ScalarValue::Null).unwrap(); + assert!(result.is_null()); + } + + #[rstest] + fn udf_invoke_item_crs(#[values(WKB_GEOMETRY_ITEM_CRS.clone())] sedona_type: SedonaType) { + let udf = SedonaScalarUDF::from_impl("st_pointonsurface", st_point_on_surface_impl()); + let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type.clone()]); + tester.assert_return_type(sedona_type); + + let result = tester + .invoke_scalar("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 4, 0 0))") + .unwrap(); + tester.assert_scalar_result_equals(result, "POINT (2 2)"); + } +} diff --git a/docs/reference/sql/st_buildarea.qmd b/docs/reference/sql/st_buildarea.qmd new file mode 100644 index 000000000..ccedce96f --- /dev/null +++ b/docs/reference/sql/st_buildarea.qmd @@ -0,0 +1,41 @@ +--- +# 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_BuildArea +description: Returns a geometry that encloses the area formed by the given linework. +kernels: + - returns: geometry + args: [geometry] + - returns: geography + args: [geography] +--- + +## Description + +Treats the input geometry's edges as linework and assembles them into a polygon or multipolygon. +Open rings are closed automatically before assembly. If no closed ring can be formed, returns NULL. + +## Examples + +```sql +SELECT ST_BuildArea(ST_GeomFromWKT('LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)')); +``` + +```sql +SELECT ST_BuildArea(ST_GeogFromWKT('LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)')); +``` diff --git a/docs/reference/sql/st_delaunaytriangles.qmd b/docs/reference/sql/st_delaunaytriangles.qmd new file mode 100644 index 000000000..0028696d6 --- /dev/null +++ b/docs/reference/sql/st_delaunaytriangles.qmd @@ -0,0 +1,60 @@ +--- +# 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_DelaunayTriangles +description: Returns a geometry collection of Delaunay triangles covering the vertices of the input geometry. +kernels: + - returns: geometry + args: [geometry] + - returns: geometry + args: + - geometry + - name: tolerance + type: float64 + description: Snap-rounding tolerance for vertices. Use 0.0 for exact computation. + - returns: geometry + args: + - geometry + - name: tolerance + type: float64 + description: Snap-rounding tolerance for vertices. Use 0.0 for exact computation. + - name: flags + type: integer + description: Use 0 for polygon triangles or 1 for edge-only MULTILINESTRING output. +--- + +## Description + +Computes a Delaunay triangulation of the vertices of the input geometry. The result is a +`GEOMETRYCOLLECTION` of `POLYGON` triangles unless the `flags` parameter is set to request +`MULTILINESTRING` output. An optional tolerance value can be specified to snap nearby vertices +together before triangulation. + +## Examples + +```sql +SELECT ST_DelaunayTriangles(ST_GeomFromWKT('MULTIPOINT ((0 0), (1 0), (0 1))')); +``` + +```sql +SELECT ST_DelaunayTriangles(ST_GeomFromWKT('MULTIPOINT ((0 0), (1 0), (0 1))'), 0.0); +``` + +```sql +SELECT ST_DelaunayTriangles(ST_GeomFromWKT('MULTIPOINT ((0 0), (1 0), (0 1))'), 0.0, 1); +``` diff --git a/docs/reference/sql/st_exteriorring.qmd b/docs/reference/sql/st_exteriorring.qmd new file mode 100644 index 000000000..5f4e8bec6 --- /dev/null +++ b/docs/reference/sql/st_exteriorring.qmd @@ -0,0 +1,41 @@ +--- +# 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_ExteriorRing +description: Returns the exterior ring of a polygon as a linestring geometry. +kernels: + - returns: geometry + args: [geometry] + - returns: geography + args: [geography] +--- + +## Description + +Returns the exterior ring (outer boundary) of a polygon as a `LINESTRING`. Returns NULL for +non-polygon geometry types. + +## Examples + +```sql +SELECT ST_ExteriorRing(ST_GeomFromWKT('POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 3, 3 3, 3 1, 1 1))')); +``` + +```sql +SELECT ST_ExteriorRing(ST_GeogFromWKT('POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 3, 3 3, 3 1, 1 1))')); +``` diff --git a/docs/reference/sql/st_numinteriorrings.qmd b/docs/reference/sql/st_numinteriorrings.qmd index 5afb978d5..8f470f971 100644 --- a/docs/reference/sql/st_numinteriorrings.qmd +++ b/docs/reference/sql/st_numinteriorrings.qmd @@ -29,7 +29,7 @@ kernels: Returns the number of interior rings (holes) in a polygon. For a polygon without holes, returns 0. For multipolygons, returns the number of interior rings in the first polygon. Returns NULL for non-polygon geometry types. -Alias: `ST_NumInteriorRing`. +Aliases: `ST_NumInteriorRing`. ## Examples diff --git a/docs/reference/sql/st_pointonsurface.qmd b/docs/reference/sql/st_pointonsurface.qmd index 8b386552f..0c8a5fea0 100644 --- a/docs/reference/sql/st_pointonsurface.qmd +++ b/docs/reference/sql/st_pointonsurface.qmd @@ -19,6 +19,8 @@ title: ST_PointOnSurface description: Returns a point guaranteed to lie on the surface of a geometry or geography. kernels: + - returns: geometry + args: [geometry] - returns: geography args: [geography] --- diff --git a/python/sedonadb/tests/functions/test_functions.py b/python/sedonadb/tests/functions/test_functions.py index 95c36c0ae..2d0955383 100644 --- a/python/sedonadb/tests/functions/test_functions.py +++ b/python/sedonadb/tests/functions/test_functions.py @@ -750,6 +750,59 @@ def test_st_buffer_style_parameters( ) +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "expected"), + [ + (None, None), + ("LINESTRING (0 0, 1 0, 1 1, 0 0)", "POLYGON ((0 0, 1 1, 1 0, 0 0))"), + ( + "MULTILINESTRING ((0 0, 1 0, 1 1, 0 0), (2 2, 3 2, 3 3, 2 2))", + "MULTIPOLYGON (((1 1, 1 0, 0 0, 1 1)), ((3 3, 3 2, 2 2, 3 3)))", + ), + ], +) +def test_st_buildarea(eng, geom, expected): + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT ST_BuildArea({geom_or_null(geom)})", expected) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "sedona_expected", "postgis_expected"), + [ + # Both engines return an empty geometry for empty linework, not NULL. + # SedonaDB returns GEOMETRYCOLLECTION EMPTY; PostGIS returns POLYGON EMPTY. + ("LINESTRING EMPTY", "GEOMETRYCOLLECTION EMPTY", "POLYGON EMPTY"), + ("MULTILINESTRING EMPTY", "GEOMETRYCOLLECTION EMPTY", "POLYGON EMPTY"), + ], +) +def test_st_buildarea_empty_linework(eng, geom, sedona_expected, postgis_expected): + is_postgis = eng is PostGIS + eng = eng.create_or_skip() + expected = postgis_expected if is_postgis else sedona_expected + eng.assert_query_result(f"SELECT ST_BuildArea({geom_or_null(geom)})", expected) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "sedona_expected", "postgis_expected"), + [ + ("POINT (0 0)", None, None), + ( + "POLYGON ((0 0, 1 0, 1 1, 0 0))", + None, + "POLYGON ((0 0, 1 1, 1 0, 0 0))", + ), + ], +) +def test_st_buildarea_non_linework(eng, geom, sedona_expected, postgis_expected): + is_postgis = eng is PostGIS + eng = eng.create_or_skip() + expected = postgis_expected if is_postgis else sedona_expected + eng.assert_query_result(f"SELECT ST_BuildArea({geom_or_null(geom)})", expected) + + @pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) @pytest.mark.parametrize( ("geom", "expected"), @@ -1421,6 +1474,88 @@ def test_st_dump(eng): assert actual["geom"] == shapely.from_wkt(expected["geom"]).wkb +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "expected"), + [ + (None, None), + ("LINESTRING EMPTY", "GEOMETRYCOLLECTION EMPTY"), + ("POLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY"), + ( + "LINESTRING (0 0, 1 0, 0.5 1)", + "GEOMETRYCOLLECTION (POLYGON ((0.5 1, 0 0, 1 0, 0.5 1)))", + ), + ( + "POLYGON ((0 0, 1 0, 0.5 1, 0 0))", + "GEOMETRYCOLLECTION (POLYGON ((0.5 1, 0 0, 1 0, 0.5 1)))", + ), + ( + "MULTIPOINT ((0 0), (1 0), (0.5 1))", + "GEOMETRYCOLLECTION (POLYGON ((0.5 1, 0 0, 1 0, 0.5 1)))", + ), + ], +) +def test_st_delaunaytriangles(eng, geom, expected): + eng = eng.create_or_skip() + eng.assert_query_result( + f"SELECT ST_DelaunayTriangles({geom_or_null(geom)})", expected + ) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "tolerance", "expected"), + [ + (None, None, None), + ( + "MULTIPOINT ((0 0), (1 0), (0.5 1))", + 0.0, + "GEOMETRYCOLLECTION (POLYGON ((0.5 1, 0 0, 1 0, 0.5 1)))", + ), + ( + "MULTIPOINT ((0 0), (0.001 0), (1 0), (0.5 1))", + 1.0, + "GEOMETRYCOLLECTION (POLYGON ((0.5 1, 0 0, 1 0, 0.5 1)))", + ), + ], +) +def test_st_delaunaytriangles_tolerance(eng, geom, tolerance, expected): + eng = eng.create_or_skip() + if tolerance is None: + eng.assert_query_result( + f"SELECT ST_DelaunayTriangles({geom_or_null(geom)}, NULL)", expected + ) + else: + eng.assert_query_result( + f"SELECT ST_DelaunayTriangles({geom_or_null(geom)}, {tolerance})", expected + ) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "only_edges", "expected"), + [ + (None, False, None), + ( + "MULTIPOINT ((0 0), (1 0), (0.5 1))", + False, + "GEOMETRYCOLLECTION (POLYGON ((0.5 1, 0 0, 1 0, 0.5 1)))", + ), + ( + "MULTIPOINT ((0 0), (1 0), (0.5 1))", + True, + "MULTILINESTRING ((0.5 1, 1 0), (0 0, 0.5 1), (0 0, 1 0))", + ), + ], +) +def test_st_delaunaytriangles_flags(eng, geom, only_edges, expected): + eng = eng.create_or_skip() + flag = 1 if only_edges else 0 + eng.assert_query_result( + f"SELECT ST_DelaunayTriangles({geom_or_null(geom)}, 0.0, {flag})", expected + ) + + @pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) @pytest.mark.parametrize( ("geom", "expected"), @@ -1451,6 +1586,32 @@ def test_st_envelope(eng, geom, expected): eng.assert_query_result(f"SELECT ST_Envelope({geom_or_null(geom)})", expected) +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "expected"), + [ + (None, None), + # Both engines return LINESTRING EMPTY for POLYGON EMPTY, not NULL + ("POLYGON EMPTY", "LINESTRING EMPTY"), + ("LINESTRING EMPTY", None), + ( + "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", + "LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)", + ), + ( + "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", + "LINESTRING (0 0, 4 0, 4 4, 0 4, 0 0)", + ), + ("POINT (0 0)", None), + ("LINESTRING (0 0, 1 1)", None), + ("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))", None), + ], +) +def test_st_exteriorring(eng, geom, expected): + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT ST_ExteriorRing({geom_or_null(geom)})", expected) + + @pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) @pytest.mark.parametrize( ("geom", "expected"), @@ -2659,6 +2820,38 @@ def test_st_pointn(eng, geometry, n, expected): ) +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "expected"), + [ + (None, None), + ("POINT EMPTY", "POINT (nan nan)"), + ("LINESTRING EMPTY", "POINT (nan nan)"), + ("POLYGON EMPTY", "POINT (nan nan)"), + ("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))", "POINT (2 2)"), + ( + "POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", + "POINT (2 3)", + ), + ("LINESTRING (0 0, 1 1, 2 0)", "POINT (1 1)"), + ("POINT (1 2)", "POINT (1 2)"), + ("MULTIPOLYGON (((0 0, 4 0, 4 4, 0 4, 0 0)))", "POINT (2 2)"), + ("MULTIPOINT ((2 3))", "POINT (2 3)"), + ( + "MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", + "POINT (1 1)", + ), + ( + "GEOMETRYCOLLECTION (POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0)), POINT (10 10))", + "POINT (2 2)", + ), + ], +) +def test_st_pointonsurface(eng, geom, expected): + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT ST_PointOnSurface({geom_or_null(geom)})", expected) + + @pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) @pytest.mark.parametrize( ("geometry", "expected"), @@ -3765,6 +3958,10 @@ def test_st_numinteriorrings_basic(eng, geom, expected): f"SELECT ST_NumInteriorRings({geom_or_null(geom)})", expected, ) + eng.assert_query_result( + f"SELECT ST_NumInteriorRing({geom_or_null(geom)})", + expected, + ) @pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) diff --git a/r/sedonafns/bootstrap.R b/r/sedonafns/bootstrap.R index 9e96cc7a5..fd362c3a5 100644 --- a/r/sedonafns/bootstrap.R +++ b/r/sedonafns/bootstrap.R @@ -15,27 +15,31 @@ # specific language governing permissions and limitations # under the License. -# On r-universe, bootstrap.R runs before dependencies are installed. -# Install the packages we need for source generation. -required_packages <- c("yaml", "here", "glue", "rlang", "roxygen2") -missing_packages <- required_packages[ - !vapply(required_packages, requireNamespace, logical(1), quietly = TRUE) -] -if (length(missing_packages) > 0 && nzchar(Sys.getenv("MY_UNIVERSE"))) { - message( - "Running on r-universe, installing bootstrap dependencies: ", - paste(missing_packages, collapse = ", ") - ) - install.packages(missing_packages) -} +if (nzchar(Sys.getenv("R_SEDONADB_FNS_SKIP_BOOTSTRAP"))) { + message("Skipping sedonafns bootstrap because R_SEDONADB_FNS_SKIP_BOOTSTRAP is set") +} else { + # On r-universe, bootstrap.R runs before dependencies are installed. + # Install the packages we need for source generation. + required_packages <- c("yaml", "here", "glue", "rlang", "roxygen2") + missing_packages <- required_packages[ + !vapply(required_packages, requireNamespace, logical(1), quietly = TRUE) + ] + if (length(missing_packages) > 0 && nzchar(Sys.getenv("MY_UNIVERSE"))) { + message( + "Running on r-universe, installing bootstrap dependencies: ", + paste(missing_packages, collapse = ", ") + ) + install.packages(missing_packages) + } -source("tools/update-sd-funcs.R") -update_sd_funcs() + source("tools/update-sd-funcs.R") + update_sd_funcs() -local({ - # Let the configure script know that we don't want to bootstrap or we will - # go in circles - on.exit(Sys.setenv(R_SEDONADB_FNS_SKIP_BOOTSTRAP = "")) - Sys.setenv(R_SEDONADB_FNS_SKIP_BOOTSTRAP = "1") - roxygen2::roxygenise(".") -}) + local({ + # Let the configure script know that we don't want to bootstrap or we will + # go in circles + on.exit(Sys.setenv(R_SEDONADB_FNS_SKIP_BOOTSTRAP = "")) + Sys.setenv(R_SEDONADB_FNS_SKIP_BOOTSTRAP = "1") + roxygen2::roxygenise(".") + }) +}