This is an automated email from the ASF dual-hosted git repository.

jiayu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new 434be5c  feat: Add ST_Perimeter implementation based on georust/geo 
and benchmarks (#76)
434be5c is described below

commit 434be5ce2753b5fb7ceb25f45fce974666d6073b
Author: Feng Zhang <[email protected]>
AuthorDate: Sat Sep 13 14:35:27 2025 -0700

    feat: Add ST_Perimeter implementation based on georust/geo and benchmarks 
(#76)
---
 Cargo.lock                               |  20 ++---
 benchmarks/test_functions.py             |  16 ++++
 rust/sedona-geo/benches/geo-functions.rs |   3 +
 rust/sedona-geo/src/lib.rs               |   1 +
 rust/sedona-geo/src/register.rs          |   2 +
 rust/sedona-geo/src/st_perimeter.rs      | 142 +++++++++++++++++++++++++++++++
 6 files changed, 174 insertions(+), 10 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 2337df6..fa69a36 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -622,9 +622,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sso"
-version = "1.83.0"
+version = "1.84.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "643cd43af212d2a1c4dedff6f044d7e1961e5d9e7cfe773d70f31d9842413886"
+checksum = "357a841807f6b52cb26123878b3326921e2a25faca412fabdd32bd35b7edd5d3"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -644,9 +644,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-ssooidc"
-version = "1.84.0"
+version = "1.85.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "20ec4a95bd48e0db7a424356a161f8d87bd6a4f0af37204775f0da03d9e39fc3"
+checksum = "67e05f33b6c9026fecfe9b3b6740f34d41bc6ff641a6a32dabaab60209245b75"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -666,9 +666,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sts"
-version = "1.85.0"
+version = "1.86.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "410309ad0df4606bc721aff0d89c3407682845453247213a0ccc5ff8801ee107"
+checksum = "e7d835f123f307cafffca7b9027c14979f1d403b417d8541d67cf252e8a21e35"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -2617,7 +2617,7 @@ dependencies = [
 [[package]]
 name = "geo-generic-alg"
 version = "0.1.0"
-source = 
"git+https://github.com/wherobots/geo.git?branch=generic-alg#567d1da8e094e74d0fdc5ce90f9bdd8f90dae40b";
+source = 
"git+https://github.com/wherobots/geo.git?branch=generic-alg#d5b277ef31e54ae30ebbeea2c30c917feeb00222";
 dependencies = [
  "earcutr",
  "float_next_after",
@@ -2652,7 +2652,7 @@ dependencies = [
 [[package]]
 name = "geo-traits"
 version = "0.2.0"
-source = 
"git+https://github.com/wherobots/geo.git?branch=generic-alg#567d1da8e094e74d0fdc5ce90f9bdd8f90dae40b";
+source = 
"git+https://github.com/wherobots/geo.git?branch=generic-alg#d5b277ef31e54ae30ebbeea2c30c917feeb00222";
 dependencies = [
  "geo-types",
 ]
@@ -2669,7 +2669,7 @@ dependencies = [
 [[package]]
 name = "geo-traits-ext"
 version = "0.1.0"
-source = 
"git+https://github.com/wherobots/geo.git?branch=generic-alg#567d1da8e094e74d0fdc5ce90f9bdd8f90dae40b";
+source = 
"git+https://github.com/wherobots/geo.git?branch=generic-alg#d5b277ef31e54ae30ebbeea2c30c917feeb00222";
 dependencies = [
  "approx",
  "geo-traits 0.2.0",
@@ -2681,7 +2681,7 @@ dependencies = [
 [[package]]
 name = "geo-types"
 version = "0.7.16"
-source = 
"git+https://github.com/wherobots/geo.git?branch=generic-alg#567d1da8e094e74d0fdc5ce90f9bdd8f90dae40b";
+source = 
"git+https://github.com/wherobots/geo.git?branch=generic-alg#d5b277ef31e54ae30ebbeea2c30c917feeb00222";
 dependencies = [
  "approx",
  "num-traits",
diff --git a/benchmarks/test_functions.py b/benchmarks/test_functions.py
index 1c89c50..6d5c33b 100644
--- a/benchmarks/test_functions.py
+++ b/benchmarks/test_functions.py
@@ -148,3 +148,19 @@ class TestBenchFunctions(TestBenchBase):
             eng.execute_and_collect(f"SELECT ST_Length(geom1) from {table}")
 
         benchmark(queries)
+
+    @pytest.mark.parametrize("eng", [SedonaDB, PostGIS, DuckDB])
+    @pytest.mark.parametrize(
+        "table",
+        [
+            "polygons_simple",
+            "polygons_complex",
+        ],
+    )
+    def test_st_perimeter(self, benchmark, eng, table):
+        eng = self._get_eng(eng)
+
+        def queries():
+            eng.execute_and_collect(f"SELECT ST_Perimeter(geom1) from {table}")
+
+        benchmark(queries)
diff --git a/rust/sedona-geo/benches/geo-functions.rs 
b/rust/sedona-geo/benches/geo-functions.rs
index 18d288c..2630e6c 100644
--- a/rust/sedona-geo/benches/geo-functions.rs
+++ b/rust/sedona-geo/benches/geo-functions.rs
@@ -27,6 +27,9 @@ fn criterion_benchmark(c: &mut Criterion) {
     benchmark::scalar(c, &f, "geo", "st_area", Polygon(10));
     benchmark::scalar(c, &f, "geo", "st_area", Polygon(500));
 
+    benchmark::scalar(c, &f, "geo", "st_perimeter", Polygon(10));
+    benchmark::scalar(c, &f, "geo", "st_perimeter", Polygon(500));
+
     benchmark::scalar(
         c,
         &f,
diff --git a/rust/sedona-geo/src/lib.rs b/rust/sedona-geo/src/lib.rs
index 6af5dd2..0a5224a 100644
--- a/rust/sedona-geo/src/lib.rs
+++ b/rust/sedona-geo/src/lib.rs
@@ -24,5 +24,6 @@ mod st_intersection_aggr;
 mod st_intersects;
 mod st_length;
 mod st_line_interpolate_point;
+mod st_perimeter;
 mod st_union_aggr;
 pub mod to_geo;
diff --git a/rust/sedona-geo/src/register.rs b/rust/sedona-geo/src/register.rs
index 5dfb25d..9041c2d 100644
--- a/rust/sedona-geo/src/register.rs
+++ b/rust/sedona-geo/src/register.rs
@@ -23,6 +23,7 @@ use crate::st_union_aggr::st_union_aggr_impl;
 use crate::{
     st_area::st_area_impl, st_centroid::st_centroid_impl, 
st_distance::st_distance_impl,
     st_dwithin::st_dwithin_impl, st_intersects::st_intersects_impl, 
st_length::st_length_impl,
+    st_perimeter::st_perimeter_impl,
 };
 
 pub fn scalar_kernels() -> Vec<(&'static str, ScalarKernelRef)> {
@@ -33,6 +34,7 @@ pub fn scalar_kernels() -> Vec<(&'static str, 
ScalarKernelRef)> {
         ("st_distance", st_distance_impl()),
         ("st_dwithin", st_dwithin_impl()),
         ("st_length", st_length_impl()),
+        ("st_perimeter", st_perimeter_impl()),
         ("st_lineinterpolatepoint", st_line_interpolate_point_impl()),
     ]
 }
diff --git a/rust/sedona-geo/src/st_perimeter.rs 
b/rust/sedona-geo/src/st_perimeter.rs
new file mode 100644
index 0000000..ad5ffbc
--- /dev/null
+++ b/rust/sedona-geo/src/st_perimeter.rs
@@ -0,0 +1,142 @@
+// 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::Float64Builder;
+use arrow_schema::DataType;
+use datafusion_common::error::Result;
+use datafusion_expr::ColumnarValue;
+use geo_generic_alg::algorithm::{line_measures::Euclidean, 
LengthMeasurableExt};
+use sedona_expr::scalar_udf::{ScalarKernelRef, SedonaScalarKernel};
+use sedona_functions::executor::WkbExecutor;
+use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
+use wkb::reader::Wkb;
+
+/// ST_Perimeter() implementation using [LengthMeasurableExt::perimeter_ext] 
with Euclidean metric
+pub fn st_perimeter_impl() -> ScalarKernelRef {
+    Arc::new(STPerimeter {})
+}
+
+#[derive(Debug)]
+struct STPerimeter {}
+
+impl SedonaScalarKernel for STPerimeter {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        let matcher = ArgMatcher::new(
+            vec![ArgMatcher::is_geometry()],
+            SedonaType::Arrow(DataType::Float64),
+        );
+
+        matcher.match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = WkbExecutor::new(arg_types, args);
+        let mut builder = 
Float64Builder::with_capacity(executor.num_iterations());
+        executor.execute_wkb_void(|maybe_wkb| {
+            match maybe_wkb {
+                Some(wkb) => {
+                    builder.append_value(invoke_scalar(&wkb)?);
+                }
+                _ => builder.append_null(),
+            }
+
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+fn invoke_scalar(wkb: &Wkb) -> Result<f64> {
+    Ok(wkb.perimeter_ext(&Euclidean))
+}
+
+#[cfg(test)]
+mod tests {
+    use arrow_array::{create_array, ArrayRef};
+    use datafusion_common::scalar::ScalarValue;
+    use rstest::rstest;
+    use sedona_functions::register::stubs::st_perimeter_udf;
+    use sedona_schema::datatypes::{WKB_GEOMETRY, WKB_VIEW_GEOMETRY};
+    use sedona_testing::testers::ScalarUdfTester;
+
+    use super::*;
+
+    #[rstest]
+    fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) 
{
+        let mut udf = st_perimeter_udf();
+        udf.add_kernel(st_perimeter_impl());
+        let tester = ScalarUdfTester::new(udf.into(), vec![sedona_type]);
+
+        assert_eq!(
+            tester.return_type().unwrap(),
+            SedonaType::Arrow(DataType::Float64)
+        );
+
+        // Test with a square polygon
+        assert_eq!(
+            tester
+                .invoke_wkb_scalar(Some("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"))
+                .unwrap(),
+            ScalarValue::Float64(Some(4.0))
+        );
+
+        let input_wkt = vec![
+            Some("POINT(1 2)"), // Point should have 0 perimeter
+            None,
+            Some("LINESTRING (0 0, 3 4)"), // LineString perimeter equals 
length (0.0)
+            Some("POLYGON ((0 0, 4 0, 4 3, 0 3, 0 0))"), // Rectangle 
perimeter: 2*(4+3) = 14
+            Some("POLYGON ((0 0, 1 0, 0.5 1, 0 0))"), // Triangle with sides 
approx 1, 1.118, 1.118
+            Some("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 
2 3, 2 2)))"), // Two unit squares
+        ];
+        let expected: ArrayRef = create_array!(
+            Float64,
+            [
+                Some(0.0),
+                None,
+                Some(0.0),
+                Some(14.0),
+                Some(3.236_067_977_499_79), // 1 + sqrt(1.25) + sqrt(1.25)
+                Some(8.0)                   // 4 + 4
+            ]
+        );
+        assert_eq!(&tester.invoke_wkb_array(input_wkt).unwrap(), &expected);
+    }
+
+    #[test]
+    fn test_polygon_with_hole() {
+        let mut udf = st_perimeter_udf();
+        udf.add_kernel(st_perimeter_impl());
+        let tester = ScalarUdfTester::new(udf.into(), vec![WKB_GEOMETRY]);
+
+        // Polygon with a hole: outer ring 40, inner ring 24
+        assert_eq!(
+            tester
+                .invoke_wkb_scalar(Some(
+                    "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 8 2, 8 8, 2 
8, 2 2))"
+                ))
+                .unwrap(),
+            ScalarValue::Float64(Some(64.0))
+        );
+    }
+}

Reply via email to