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

paleolimbot 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 b38ad859 feat(rust/sedona-functions): make ST_Translate accept deltaZ 
arg (#524)
b38ad859 is described below

commit b38ad85968a0ffc029f92dc766140a41ecae58a8
Author: Hiroaki Yutani <[email protected]>
AuthorDate: Tue Jan 20 00:14:49 2026 +0900

    feat(rust/sedona-functions): make ST_Translate accept deltaZ arg (#524)
---
 python/sedonadb/tests/functions/test_transforms.py |  68 ++++++
 rust/sedona-functions/src/st_translate.rs          | 237 +++++++++++++++++++--
 2 files changed, 283 insertions(+), 22 deletions(-)

diff --git a/python/sedonadb/tests/functions/test_transforms.py 
b/python/sedonadb/tests/functions/test_transforms.py
index 5ae0d20d..4f82c5dd 100644
--- a/python/sedonadb/tests/functions/test_transforms.py
+++ b/python/sedonadb/tests/functions/test_transforms.py
@@ -192,3 +192,71 @@ def test_st_translate(eng, geom, dx, dy, expected):
         f"SELECT ST_Translate({geom_or_null(geom)}, {val_or_null(dx)}, 
{val_or_null(dy)})",
         expected,
     )
+
+
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+    ("geom", "dx", "dy", "dz", "expected"),
+    [
+        # Nulls
+        (None, None, None, None, None),
+        (None, 1.0, 2.0, 3.0, None),
+        ("POINT Z (0 1 2)", None, 2.0, 3.0, None),
+        ("POINT Z (0 1 2)", 1.0, None, 3.0, None),
+        ("POINT Z (0 1 2)", 1.0, 2.0, None, None),
+        ("POINT Z (0 1 2)", 1.0, 2.0, 3.0, "POINT Z (1 3 5)"),  # Positives
+        ("POINT Z (0 1 2)", -1.0, -2.0, -3.0, "POINT Z (-1 -1 -1)"),  # 
Negatives
+        ("POINT Z (0 1 2)", 0.0, 0.0, 0.0, "POINT Z (0 1 2)"),  # Zeroes
+        ("POINT Z (0 1 2)", 1, 2, 3, "POINT Z (1 3 5)"),  # Integers
+        ("POINT (0 1)", 1.0, 2.0, 3.0, "POINT (1 3)"),  # 2D
+        ("POINT M (0 1 2)", 1.0, 2.0, 3.0, "POINT M (1 3 2)"),  # M
+        ("POINT ZM (0 1 2 3)", 1.0, 2.0, 3.0, "POINT ZM (1 3 5 3)"),  # ZM
+        # Not points
+        ("LINESTRING Z (0 1 2, 2 3 4)", 1.0, 2.0, 3.0, "LINESTRING Z (1 3 5, 3 
5 7)"),
+        (
+            "POLYGON Z ((0 0 0, 1 0 2, 0 1 2, 0 0 0))",
+            1.0,
+            2.0,
+            3.0,
+            "POLYGON Z ((1 2 3, 2 2 5, 1 3 5, 1 2 3))",
+        ),
+        ("MULTIPOINT Z (0 1 2, 2 3 4)", 1.0, 2.0, 3.0, "MULTIPOINT Z (1 3 5, 3 
5 7)"),
+        (
+            "MULTILINESTRING Z ((0 1 2, 2 3 4))",
+            1.0,
+            2.0,
+            3.0,
+            "MULTILINESTRING Z ((1 3 5, 3 5 7))",
+        ),
+        (
+            "MULTIPOLYGON Z (((0 0 0, 1 0 2, 0 1 2, 0 0 0)))",
+            1.0,
+            2.0,
+            3.0,
+            "MULTIPOLYGON Z (((1 2 3, 2 2 5, 1 3 5, 1 2 3)))",
+        ),
+        (
+            "GEOMETRYCOLLECTION Z (POINT Z (0 1 2))",
+            1.0,
+            2.0,
+            3.0,
+            "GEOMETRYCOLLECTION Z (POINT Z (1 3 5))",
+        ),
+        # WKT output of geoarrow-c is causing this (both correctly output
+        # empties)
+        ("POINT EMPTY", 1.0, 2.0, 3.0, "POINT (nan nan)"),
+        ("POINT Z EMPTY", 1.0, 2.0, 3.0, "POINT Z (nan nan nan)"),
+        ("LINESTRING EMPTY", 1.0, 2.0, 3.0, "LINESTRING EMPTY"),
+        ("POLYGON EMPTY", 1.0, 2.0, 3.0, "POLYGON EMPTY"),
+        ("MULTIPOINT EMPTY", 1.0, 2.0, 3.0, "MULTIPOINT EMPTY"),
+        ("MULTILINESTRING EMPTY", 1.0, 2.0, 3.0, "MULTILINESTRING EMPTY"),
+        ("MULTIPOLYGON EMPTY", 1.0, 2.0, 3.0, "MULTIPOLYGON EMPTY"),
+        ("GEOMETRYCOLLECTION EMPTY", 1.0, 2.0, 3.0, "GEOMETRYCOLLECTION 
EMPTY"),
+    ],
+)
+def test_st_translate_3d(eng, geom, dx, dy, dz, expected):
+    eng = eng.create_or_skip()
+    eng.assert_query_result(
+        f"SELECT ST_Translate({geom_or_null(geom)}, {val_or_null(dx)}, 
{val_or_null(dy)}, {val_or_null(dz)})",
+        expected,
+    )
diff --git a/rust/sedona-functions/src/st_translate.rs 
b/rust/sedona-functions/src/st_translate.rs
index b18c129f..647e29cf 100644
--- a/rust/sedona-functions/src/st_translate.rs
+++ b/rust/sedona-functions/src/st_translate.rs
@@ -14,13 +14,14 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-use arrow_array::builder::BinaryBuilder;
+use arrow_array::{builder::BinaryBuilder, types::Float64Type, Array, 
PrimitiveArray};
 use arrow_schema::DataType;
 use datafusion_common::{cast::as_float64_array, error::Result, 
DataFusionError};
 use datafusion_expr::{
     scalar_doc_sections::DOC_SECTION_OTHER, ColumnarValue, Documentation, 
Volatility,
 };
 
+use sedona_common::sedona_internal_err;
 use sedona_expr::{
     item_crs::ItemCrsKernel,
     scalar_udf::{SedonaScalarKernel, SedonaScalarUDF},
@@ -34,7 +35,7 @@ use sedona_schema::{
     datatypes::{SedonaType, WKB_GEOMETRY},
     matchers::ArgMatcher,
 };
-use std::{iter::zip, sync::Arc};
+use std::sync::Arc;
 
 use crate::executor::WkbExecutor;
 
@@ -42,7 +43,10 @@ use crate::executor::WkbExecutor;
 pub fn st_translate_udf() -> SedonaScalarUDF {
     SedonaScalarUDF::new(
         "st_translate",
-        ItemCrsKernel::wrap_impl(vec![Arc::new(STTranslate)]),
+        ItemCrsKernel::wrap_impl(vec![
+            Arc::new(STTranslate { is_3d: true }),
+            Arc::new(STTranslate { is_3d: false }),
+        ]),
         Volatility::Immutable,
         Some(st_translate_doc()),
     )
@@ -62,18 +66,27 @@ fn st_translate_doc() -> Documentation {
 }
 
 #[derive(Debug)]
-struct STTranslate;
+struct STTranslate {
+    is_3d: bool,
+}
 
 impl SedonaScalarKernel for STTranslate {
     fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
-        let matcher = ArgMatcher::new(
+        let matchers = if self.is_3d {
             vec![
                 ArgMatcher::is_geometry(),
                 ArgMatcher::is_numeric(),
                 ArgMatcher::is_numeric(),
-            ],
-            WKB_GEOMETRY,
-        );
+                ArgMatcher::is_numeric(),
+            ]
+        } else {
+            vec![
+                ArgMatcher::is_geometry(),
+                ArgMatcher::is_numeric(),
+                ArgMatcher::is_numeric(),
+            ]
+        };
+        let matcher = ArgMatcher::new(matchers, WKB_GEOMETRY);
 
         matcher.match_args(args)
     }
@@ -89,21 +102,40 @@ impl SedonaScalarKernel for STTranslate {
             WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
         );
 
-        let deltax = args[1]
-            .cast_to(&DataType::Float64, None)?
-            .to_array(executor.num_iterations())?;
-        let deltay = args[2]
-            .cast_to(&DataType::Float64, None)?
-            .to_array(executor.num_iterations())?;
-        let deltax_array = as_float64_array(&deltax)?;
-        let deltay_array = as_float64_array(&deltay)?;
-        let mut delta_iter = zip(deltax_array, deltay_array);
+        let array_args = args[1..]
+            .iter()
+            .map(|arg| {
+                arg.cast_to(&DataType::Float64, None)?
+                    .to_array(executor.num_iterations())
+            })
+            .collect::<Result<Vec<Arc<dyn arrow_array::Array>>>>()?;
+
+        let deltax_array = as_float64_array(&array_args[0])?;
+        let deltay_array = as_float64_array(&array_args[1])?;
+
+        let mut deltas = if self.is_3d {
+            if args.len() != 4 {
+                return sedona_internal_err!("Invalid number of arguments are 
passed");
+            }
+
+            let deltaz_array = as_float64_array(&array_args[2])?;
+            Deltas::new(deltax_array, deltay_array, Some(deltaz_array))
+        } else {
+            if args.len() != 3 {
+                return sedona_internal_err!("Invalid number of arguments are 
passed");
+            }
+
+            Deltas::new(deltax_array, deltay_array, None)
+        };
 
         executor.execute_wkb_void(|maybe_wkb| {
-            let (deltax, deltay) = delta_iter.next().unwrap();
-            match (maybe_wkb, deltax, deltay) {
-                (Some(wkb), Some(deltax), Some(deltay)) => {
-                    let trans = Translate { deltax, deltay };
+            match (maybe_wkb, deltas.next().unwrap()) {
+                (Some(wkb), Some((deltax, deltay, deltaz))) => {
+                    let trans = Translate {
+                        deltax,
+                        deltay,
+                        deltaz,
+                    };
                     transform(wkb, &trans, &mut builder)
                         .map_err(|e| DataFusionError::External(Box::new(e)))?;
                     builder.append_value([]);
@@ -120,10 +152,76 @@ impl SedonaScalarKernel for STTranslate {
     }
 }
 
+#[derive(Debug)]
+struct Deltas<'a> {
+    index: usize,
+    x: &'a PrimitiveArray<Float64Type>,
+    y: &'a PrimitiveArray<Float64Type>,
+    z: Option<&'a PrimitiveArray<Float64Type>>,
+    no_null: bool,
+}
+
+impl<'a> Deltas<'a> {
+    fn new(
+        x: &'a PrimitiveArray<Float64Type>,
+        y: &'a PrimitiveArray<Float64Type>,
+        z: Option<&'a PrimitiveArray<Float64Type>>,
+    ) -> Self {
+        let no_null = x.null_count() == 0
+            && y.null_count() == 0
+            && match z {
+                Some(z) => z.null_count() == 0,
+                None => true,
+            };
+
+        Self {
+            index: 0,
+            x,
+            y,
+            z,
+            no_null,
+        }
+    }
+    fn is_null(&self, i: usize) -> bool {
+        if self.no_null {
+            return false;
+        }
+
+        self.x.is_null(i)
+            || self.y.is_null(i)
+            || match self.z {
+                Some(z) => z.is_null(i),
+                None => false,
+            }
+    }
+}
+
+impl<'a> Iterator for Deltas<'a> {
+    type Item = Option<(f64, f64, f64)>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let i = self.index;
+        self.index += 1;
+
+        if self.is_null(i) {
+            return Some(None);
+        }
+
+        let x = self.x.value(i);
+        let y = self.y.value(i);
+        let z = match self.z {
+            Some(z) => z.value(i),
+            None => 0.0,
+        };
+        Some(Some((x, y, z)))
+    }
+}
+
 #[derive(Debug)]
 struct Translate {
     deltax: f64,
     deltay: f64,
+    deltaz: f64,
 }
 
 impl CrsTransform for Translate {
@@ -132,6 +230,16 @@ impl CrsTransform for Translate {
         coord.1 += self.deltay;
         Ok(())
     }
+
+    fn transform_coord_3d(
+        &self,
+        coord: &mut (f64, f64, f64),
+    ) -> std::result::Result<(), SedonaGeometryError> {
+        coord.0 += self.deltax;
+        coord.1 += self.deltay;
+        coord.2 += self.deltaz;
+        Ok(())
+    }
 }
 
 #[cfg(test)]
@@ -155,7 +263,7 @@ mod tests {
     }
 
     #[rstest]
-    fn udf(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: SedonaType) 
{
+    fn udf_2d(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: 
SedonaType) {
         let tester = ScalarUdfTester::new(
             st_translate_udf().into(),
             vec![
@@ -225,6 +333,91 @@ mod tests {
         assert_array_equal(&result, &expected);
     }
 
+    #[rstest]
+    fn udf_3d(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: 
SedonaType) {
+        let tester = ScalarUdfTester::new(
+            st_translate_udf().into(),
+            vec![
+                sedona_type.clone(),
+                SedonaType::Arrow(DataType::Float64),
+                SedonaType::Arrow(DataType::Float64),
+                SedonaType::Arrow(DataType::Float64),
+            ],
+        );
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let points = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT EMPTY"),
+                Some("POINT EMPTY"),
+                Some("POINT EMPTY"),
+                Some("POINT Z EMPTY"),
+                Some("POINT (0 1)"),
+                Some("POINT Z (4 5 6)"),
+            ],
+            &sedona_type,
+        );
+
+        let dx = create_array!(
+            Float64,
+            [
+                Some(1.0),
+                None,
+                Some(1.0),
+                Some(1.0),
+                Some(1.0),
+                Some(1.0),
+                Some(1.0),
+                Some(1.0)
+            ]
+        );
+        let dy = create_array!(
+            Float64,
+            [
+                Some(2.0),
+                Some(2.0),
+                None,
+                Some(2.0),
+                Some(2.0),
+                Some(2.0),
+                Some(2.0),
+                Some(2.0)
+            ]
+        );
+        let dz = create_array!(
+            Float64,
+            [
+                Some(3.0),
+                Some(3.0),
+                Some(3.0),
+                None,
+                Some(3.0),
+                Some(3.0),
+                Some(3.0),
+                Some(3.0)
+            ]
+        );
+
+        let expected = create_array(
+            &[
+                None,
+                None,
+                None,
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT Z EMPTY"),
+                Some("POINT (1 3)"),
+                Some("POINT Z (5 7 9)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        let result = tester.invoke_arrays(vec![points, dx, dy, dz]).unwrap();
+        assert_array_equal(&result, &expected);
+    }
+
     #[rstest]
     fn udf_invoke_item_crs(#[values(WKB_GEOMETRY_ITEM_CRS.clone())] 
sedona_type: SedonaType) {
         let tester = ScalarUdfTester::new(

Reply via email to