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 a4f3fee5 feat(rust/sedona-functions): Implement ST_Force3DM and 
ST_Force4D (#620)
a4f3fee5 is described below

commit a4f3fee5786568ee4c59c8048774ad5f23677c27
Author: Hiroaki Yutani <[email protected]>
AuthorDate: Sun Feb 15 10:58:44 2026 +0900

    feat(rust/sedona-functions): Implement ST_Force3DM and ST_Force4D (#620)
---
 python/sedonadb/tests/functions/test_functions.py |   59 +
 rust/sedona-functions/src/register.rs             |    2 +
 rust/sedona-functions/src/st_force_dim.rs         | 1221 ++++++++++++++-------
 rust/sedona-geometry/src/transform.rs             |    5 +-
 4 files changed, 880 insertions(+), 407 deletions(-)

diff --git a/python/sedonadb/tests/functions/test_functions.py 
b/python/sedonadb/tests/functions/test_functions.py
index 779dc762..904a3875 100644
--- a/python/sedonadb/tests/functions/test_functions.py
+++ b/python/sedonadb/tests/functions/test_functions.py
@@ -1519,6 +1519,65 @@ def test_st_force_dim(eng, geom, expected_2d, 
expected_3d):
     eng.assert_query_result(f"SELECT ST_Force3D({geom_or_null(geom)}, 5)", 
expected_3d)
 
 
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+    ("geom", "m", "expected_without_m", "expected_with_m"),
+    [
+        (None, 5, None, None),
+        (
+            "POINT EMPTY",
+            5,
+            "POINT M (nan nan nan)",
+            "POINT M (nan nan nan)",
+        ),
+        ("POINT (0 1)", 5, "POINT M (0 1 0)", "POINT M (0 1 5)"),
+        ("POINT Z (0 1 2)", 5, "POINT M (0 1 0)", "POINT M (0 1 5)"),
+        ("POINT M (0 1 3)", 5, "POINT M (0 1 3)", "POINT M (0 1 3)"),
+        ("POINT ZM (0 1 2 3)", 5, "POINT M (0 1 3)", "POINT M (0 1 3)"),
+        ("POINT (0 1)", None, "POINT M (0 1 0)", None),
+    ],
+)
+def test_st_force3dm(eng, geom, m, expected_without_m, expected_with_m):
+    eng = eng.create_or_skip()
+    eng.assert_query_result(
+        f"SELECT ST_Force3DM({geom_or_null(geom)})", expected_without_m
+    )
+    eng.assert_query_result(
+        f"SELECT ST_Force3DM({geom_or_null(geom)}, {val_or_null(m)})", 
expected_with_m
+    )
+
+
[email protected]("eng", [SedonaDB, PostGIS])
[email protected](
+    ("geom", "z", "m", "expected_without_defaults", "expected_with_defaults"),
+    [
+        (None, 5, 7, None, None),
+        (
+            "POINT EMPTY",
+            5,
+            7,
+            "POINT ZM (nan nan nan nan)",
+            "POINT ZM (nan nan nan nan)",
+        ),
+        ("POINT (0 1)", 5, 7, "POINT ZM (0 1 0 0)", "POINT ZM (0 1 5 7)"),
+        ("POINT Z (0 1 2)", 5, 7, "POINT ZM (0 1 2 0)", "POINT ZM (0 1 2 7)"),
+        ("POINT M (0 1 3)", 5, 7, "POINT ZM (0 1 0 3)", "POINT ZM (0 1 5 3)"),
+        ("POINT ZM (0 1 2 3)", 5, 7, "POINT ZM (0 1 2 3)", "POINT ZM (0 1 2 
3)"),
+        ("POINT (0 1)", None, 7, "POINT ZM (0 1 0 0)", None),
+        ("POINT (0 1)", 5, None, "POINT ZM (0 1 0 0)", None),
+    ],
+)
+def test_st_force4d(eng, geom, z, m, expected_without_defaults, 
expected_with_defaults):
+    eng = eng.create_or_skip()
+    eng.assert_query_result(
+        f"SELECT ST_Force4D({geom_or_null(geom)})", expected_without_defaults
+    )
+    eng.assert_query_result(
+        f"SELECT ST_Force4D({geom_or_null(geom)}, {val_or_null(z)}, 
{val_or_null(m)})",
+        expected_with_defaults,
+    )
+
+
 @pytest.mark.parametrize("eng", [SedonaDB, PostGIS])
 @pytest.mark.parametrize(
     ("geom", "expected"),
diff --git a/rust/sedona-functions/src/register.rs 
b/rust/sedona-functions/src/register.rs
index a44c9dd2..82645401 100644
--- a/rust/sedona-functions/src/register.rs
+++ b/rust/sedona-functions/src/register.rs
@@ -120,6 +120,8 @@ pub fn default_function_set() -> FunctionSet {
         crate::st_translate::st_translate_udf,
         crate::st_force_dim::st_force2d_udf,
         crate::st_force_dim::st_force3d_udf,
+        crate::st_force_dim::st_force3dm_udf,
+        crate::st_force_dim::st_force4d_udf,
         crate::st_xyzm_minmax::st_mmax_udf,
         crate::st_xyzm_minmax::st_mmin_udf,
         crate::st_xyzm_minmax::st_xmax_udf,
diff --git a/rust/sedona-functions/src/st_force_dim.rs 
b/rust/sedona-functions/src/st_force_dim.rs
index acdfefaa..5946c05c 100644
--- a/rust/sedona-functions/src/st_force_dim.rs
+++ b/rust/sedona-functions/src/st_force_dim.rs
@@ -1,405 +1,816 @@
-// 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, Float64Array};
-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 geo_traits::Dimensions;
-use sedona_expr::{
-    item_crs::ItemCrsKernel,
-    scalar_udf::{SedonaScalarKernel, SedonaScalarUDF},
-};
-use sedona_geometry::{
-    error::SedonaGeometryError,
-    transform::{transform, CrsTransform},
-    wkb_factory::WKB_MIN_PROBABLE_BYTES,
-};
-use sedona_schema::{
-    datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY},
-    matchers::ArgMatcher,
-};
-
-use crate::executor::WkbExecutor;
-
-// *** 2D *************************
-
-/// ST_Force2D() scalar UDF
-pub fn st_force2d_udf() -> SedonaScalarUDF {
-    SedonaScalarUDF::new(
-        "st_force2d",
-        ItemCrsKernel::wrap_impl(vec![
-            Arc::new(STForce2D {
-                is_geography: false,
-            }),
-            Arc::new(STForce2D { is_geography: true }),
-        ]),
-        Volatility::Immutable,
-        Some(st_force2d_doc()),
-    )
-}
-
-fn st_force2d_doc() -> Documentation {
-    Documentation::builder(
-        DOC_SECTION_OTHER,
-        "Forces the geometry into a 2-dimensional model",
-        "ST_Force2D (geom: Geometry)",
-    )
-    .with_argument("geom", "geometry: Input geometry")
-    .with_sql_example("SELECT ST_Force2D(ST_GeomFromWKT('POINT Z (1 2 3)'))")
-    .build()
-}
-
-#[derive(Debug)]
-struct STForce2D {
-    is_geography: bool,
-}
-
-impl SedonaScalarKernel for STForce2D {
-    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
-        let matcher = if self.is_geography {
-            ArgMatcher::new(vec![ArgMatcher::is_geography()], WKB_GEOGRAPHY)
-        } else {
-            ArgMatcher::new(vec![ArgMatcher::is_geometry()], WKB_GEOMETRY)
-        };
-
-        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 = BinaryBuilder::with_capacity(
-            executor.num_iterations(),
-            WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
-        );
-
-        let trans = Force2DTransform {};
-        executor.execute_wkb_void(|maybe_wkb| {
-            match maybe_wkb {
-                Some(wkb) => {
-                    transform(wkb, &trans, &mut builder)
-                        .map_err(|e| DataFusionError::External(Box::new(e)))?;
-                    builder.append_value([]);
-                }
-                _ => {
-                    builder.append_null();
-                }
-            }
-
-            Ok(())
-        })?;
-
-        executor.finish(Arc::new(builder.finish()))
-    }
-}
-
-#[derive(Debug)]
-struct Force2DTransform {}
-
-impl CrsTransform for Force2DTransform {
-    fn output_dim(&self) -> Option<geo_traits::Dimensions> {
-        Some(geo_traits::Dimensions::Xy)
-    }
-
-    fn transform_coord(
-        &self,
-        _coord: &mut (f64, f64),
-    ) -> std::result::Result<(), SedonaGeometryError> {
-        Ok(())
-    }
-}
-
-// *** 3D *************************
-
-/// ST_Force3D() scalar UDF
-pub fn st_force3d_udf() -> SedonaScalarUDF {
-    SedonaScalarUDF::new(
-        "st_force3d",
-        ItemCrsKernel::wrap_impl(vec![
-            Arc::new(STForce3D {
-                is_geography: false,
-            }),
-            Arc::new(STForce3D { is_geography: true }),
-        ]),
-        Volatility::Immutable,
-        Some(st_force3d_doc()),
-    )
-}
-
-fn st_force3d_doc() -> Documentation {
-    Documentation::builder(
-        DOC_SECTION_OTHER,
-        "Forces the geometry into a 3-dimensional model.",
-        "ST_Force3D (geom: Geometry)",
-    )
-    .with_argument("geom", "geometry: Input geometry")
-    .with_argument("z", "numeric: default Z value")
-    .with_sql_example("SELECT ST_Force3D(ST_GeomFromWKT('POINT (1 2)'))")
-    .build()
-}
-
-#[derive(Debug)]
-struct STForce3D {
-    is_geography: bool,
-}
-
-impl SedonaScalarKernel for STForce3D {
-    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
-        let matcher = if self.is_geography {
-            ArgMatcher::new(
-                vec![
-                    ArgMatcher::is_geography(),
-                    ArgMatcher::optional(ArgMatcher::is_numeric()),
-                ],
-                WKB_GEOGRAPHY,
-            )
-        } else {
-            ArgMatcher::new(
-                vec![
-                    ArgMatcher::is_geometry(),
-                    ArgMatcher::optional(ArgMatcher::is_numeric()),
-                ],
-                WKB_GEOMETRY,
-            )
-        };
-
-        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 = BinaryBuilder::with_capacity(
-            executor.num_iterations(),
-            WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
-        );
-
-        let z_array = match args.get(1) {
-            Some(arg) => arg
-                .cast_to(&DataType::Float64, None)?
-                .to_array(executor.num_iterations())?,
-            None => Arc::new(Float64Array::from(vec![0.0; 
executor.num_iterations()])),
-        };
-        let z_array = as_float64_array(&z_array)?;
-
-        let mut z_iter = z_array.iter();
-        executor.execute_wkb_void(|maybe_wkb| {
-            match (maybe_wkb, z_iter.next().unwrap()) {
-                (Some(wkb), Some(z)) => {
-                    let trans = Force3DTransform { z };
-                    transform(wkb, &trans, &mut builder)
-                        .map_err(|e| DataFusionError::External(Box::new(e)))?;
-                    builder.append_value([]);
-                }
-                _ => {
-                    builder.append_null();
-                }
-            }
-
-            Ok(())
-        })?;
-
-        executor.finish(Arc::new(builder.finish()))
-    }
-}
-
-#[derive(Debug)]
-struct Force3DTransform {
-    z: f64,
-}
-
-impl CrsTransform for Force3DTransform {
-    fn output_dim(&self) -> Option<geo_traits::Dimensions> {
-        Some(geo_traits::Dimensions::Xyz)
-    }
-
-    fn transform_coord(
-        &self,
-        _coord: &mut (f64, f64),
-    ) -> std::result::Result<(), SedonaGeometryError> {
-        Err(SedonaGeometryError::Invalid(
-            "Unexpected call to transform_coord()".to_string(),
-        ))
-    }
-    fn transform_coord_xyz(
-        &self,
-        coord: &mut (f64, f64, f64),
-        input_dims: Dimensions,
-    ) -> Result<(), SedonaGeometryError> {
-        // If the input doesn't have Z coordinate, fill with the default value
-        if matches!(
-            input_dims,
-            Dimensions::Xy | Dimensions::Xym | Dimensions::Unknown(_)
-        ) {
-            coord.2 = self.z
-        }
-        Ok(())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use arrow_array::create_array;
-    use datafusion_expr::ScalarUDF;
-    use rstest::rstest;
-    use sedona_schema::datatypes::WKB_VIEW_GEOMETRY;
-    use sedona_testing::{
-        compare::assert_array_equal, create::create_array, 
testers::ScalarUdfTester,
-    };
-
-    use super::*;
-
-    #[test]
-    fn udf_metadata() {
-        let st_force2d: ScalarUDF = st_force2d_udf().into();
-        assert_eq!(st_force2d.name(), "st_force2d");
-        assert!(st_force2d.documentation().is_some());
-
-        let st_force3d: ScalarUDF = st_force3d_udf().into();
-        assert_eq!(st_force3d.name(), "st_force3d");
-        assert!(st_force3d.documentation().is_some());
-    }
-
-    #[rstest]
-    fn udf_2d(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: 
SedonaType) {
-        let tester = ScalarUdfTester::new(st_force2d_udf().into(), 
vec![sedona_type.clone()]);
-        tester.assert_return_type(WKB_GEOMETRY);
-
-        let points = create_array(
-            &[
-                None,
-                Some("POINT EMPTY"),
-                Some("POINT Z EMPTY"),
-                Some("POINT (1 2)"),
-                Some("POINT Z (3 4 5)"),
-                Some("POINT ZM (8 9 10 11)"),
-            ],
-            &sedona_type,
-        );
-
-        let expected = create_array(
-            &[
-                None,
-                Some("POINT EMPTY"),
-                Some("POINT EMPTY"),
-                Some("POINT (1 2)"),
-                Some("POINT (3 4)"),
-                Some("POINT (8 9)"),
-            ],
-            &WKB_GEOMETRY,
-        );
-
-        let result = tester.invoke_arrays(vec![points]).unwrap();
-        assert_array_equal(&result, &expected);
-    }
-
-    #[rstest]
-    fn udf_3d_without_z(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] 
sedona_type: SedonaType) {
-        let tester = ScalarUdfTester::new(st_force3d_udf().into(), 
vec![sedona_type.clone()]);
-        tester.assert_return_type(WKB_GEOMETRY);
-
-        let points = create_array(
-            &[
-                None,
-                Some("POINT EMPTY"),
-                Some("POINT Z EMPTY"),
-                Some("POINT (1 2)"),
-                Some("POINT Z (3 4 5)"),
-                Some("POINT M (6 7 8)"),
-                Some("POINT ZM (9 10 11 12)"),
-            ],
-            &sedona_type,
-        );
-
-        let expected = create_array(
-            &[
-                None,
-                Some("POINT Z EMPTY"),
-                Some("POINT Z EMPTY"),
-                Some("POINT Z (1 2 0)"),
-                Some("POINT Z (3 4 5)"),
-                Some("POINT Z (6 7 0)"),
-                Some("POINT Z (9 10 11)"),
-            ],
-            &WKB_GEOMETRY,
-        );
-
-        let result = tester.invoke_arrays(vec![points]).unwrap();
-        assert_array_equal(&result, &expected);
-    }
-
-    #[rstest]
-    fn udf_3d_with_z(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: 
SedonaType) {
-        let tester = ScalarUdfTester::new(
-            st_force3d_udf().into(),
-            vec![sedona_type.clone(), SedonaType::Arrow(DataType::Float64)],
-        );
-        tester.assert_return_type(WKB_GEOMETRY);
-
-        let points = create_array(
-            &[
-                None,
-                Some("POINT EMPTY"),
-                Some("POINT (1 2)"),
-                Some("POINT (1 2)"),
-                Some("POINT Z (3 4 5)"),
-                Some("POINT M (6 7 8)"),
-                Some("POINT ZM (9 10 11 12)"),
-            ],
-            &sedona_type,
-        );
-        let z = create_array!(
-            Float64,
-            [
-                Some(9.0),
-                Some(9.0),
-                Some(9.0),
-                None,
-                Some(9.0),
-                Some(9.0),
-                Some(9.0)
-            ]
-        );
-
-        let expected = create_array(
-            &[
-                None,
-                Some("POINT Z EMPTY"),
-                Some("POINT Z (1 2 9)"),
-                None,
-                Some("POINT Z (3 4 5)"),
-                Some("POINT Z (6 7 9)"),
-                Some("POINT Z (9 10 11)"),
-            ],
-            &WKB_GEOMETRY,
-        );
-
-        let result = tester.invoke_arrays(vec![points, z]).unwrap();
-        assert_array_equal(&result, &expected);
-    }
-}
+// 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, Float64Array};
+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 geo_traits::Dimensions;
+use sedona_expr::{
+    item_crs::ItemCrsKernel,
+    scalar_udf::{SedonaScalarKernel, SedonaScalarUDF},
+};
+use sedona_geometry::{
+    error::SedonaGeometryError,
+    transform::{transform, CrsTransform},
+    wkb_factory::WKB_MIN_PROBABLE_BYTES,
+};
+use sedona_schema::{
+    datatypes::{SedonaType, WKB_GEOGRAPHY, WKB_GEOMETRY},
+    matchers::ArgMatcher,
+};
+
+use crate::executor::WkbExecutor;
+
+fn optional_numeric_arg_as_f64(
+    args: &[ColumnarValue],
+    index: usize,
+    len: usize,
+) -> Result<Float64Array> {
+    let array = match args.get(index) {
+        Some(arg) => arg.cast_to(&DataType::Float64, None)?.to_array(len)?,
+        None => Arc::new(Float64Array::from(vec![0.0; len])),
+    };
+
+    Ok(as_float64_array(&array)?.clone())
+}
+
+fn build_return_matcher(is_geography: bool, num_optional_numeric_args: usize) 
-> ArgMatcher {
+    let mut matchers = vec![if is_geography {
+        ArgMatcher::is_geography()
+    } else {
+        ArgMatcher::is_geometry()
+    }];
+    for _ in 0..num_optional_numeric_args {
+        matchers.push(ArgMatcher::optional(ArgMatcher::is_numeric()));
+    }
+    let output_type = if is_geography {
+        WKB_GEOGRAPHY
+    } else {
+        WKB_GEOMETRY
+    };
+    ArgMatcher::new(matchers, output_type)
+}
+
+// *** 2D *************************
+
+/// ST_Force2D() scalar UDF
+pub fn st_force2d_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "st_force2d",
+        ItemCrsKernel::wrap_impl(vec![
+            Arc::new(STForce2D {
+                is_geography: false,
+            }),
+            Arc::new(STForce2D { is_geography: true }),
+        ]),
+        Volatility::Immutable,
+        Some(st_force2d_doc()),
+    )
+}
+
+fn st_force2d_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Forces the geometry into a 2-dimensional model",
+        "ST_Force2D (geom: Geometry)",
+    )
+    .with_argument("geom", "geometry: Input geometry")
+    .with_sql_example("SELECT ST_Force2D(ST_GeomFromWKT('POINT Z (1 2 3)'))")
+    .build()
+}
+
+#[derive(Debug)]
+struct STForce2D {
+    is_geography: bool,
+}
+
+impl SedonaScalarKernel for STForce2D {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        build_return_matcher(self.is_geography, 0).match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = WkbExecutor::new(arg_types, args);
+        let mut builder = BinaryBuilder::with_capacity(
+            executor.num_iterations(),
+            WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
+        );
+
+        let trans = Force2DTransform {};
+        executor.execute_wkb_void(|maybe_wkb| {
+            match maybe_wkb {
+                Some(wkb) => {
+                    transform(wkb, &trans, &mut builder)
+                        .map_err(|e| DataFusionError::External(Box::new(e)))?;
+                    builder.append_value([]);
+                }
+                _ => {
+                    builder.append_null();
+                }
+            }
+
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+#[derive(Debug)]
+struct Force2DTransform {}
+
+impl CrsTransform for Force2DTransform {
+    fn output_dim(&self) -> Option<geo_traits::Dimensions> {
+        Some(geo_traits::Dimensions::Xy)
+    }
+
+    fn transform_coord(
+        &self,
+        _coord: &mut (f64, f64),
+    ) -> std::result::Result<(), SedonaGeometryError> {
+        Ok(())
+    }
+}
+
+// *** 3D *************************
+
+/// ST_Force3D() scalar UDF
+pub fn st_force3d_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "st_force3d",
+        ItemCrsKernel::wrap_impl(vec![
+            Arc::new(STForce3D {
+                is_geography: false,
+            }),
+            Arc::new(STForce3D { is_geography: true }),
+        ]),
+        Volatility::Immutable,
+        Some(st_force3d_doc()),
+    )
+}
+
+fn st_force3d_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Forces the geometry into a 3-dimensional model.",
+        "ST_Force3D (geom: Geometry)",
+    )
+    .with_argument("geom", "geometry: Input geometry")
+    .with_argument("z", "numeric: default Z value")
+    .with_sql_example("SELECT ST_Force3D(ST_GeomFromWKT('POINT (1 2)'))")
+    .build()
+}
+
+#[derive(Debug)]
+struct STForce3D {
+    is_geography: bool,
+}
+
+impl SedonaScalarKernel for STForce3D {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        build_return_matcher(self.is_geography, 1).match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = WkbExecutor::new(arg_types, args);
+        let num_rows = executor.num_iterations();
+        let mut builder = BinaryBuilder::with_capacity(num_rows, 
WKB_MIN_PROBABLE_BYTES * num_rows);
+
+        let z_array = optional_numeric_arg_as_f64(args, 1, num_rows)?;
+        let mut z_iter = z_array.iter();
+        executor.execute_wkb_void(|maybe_wkb| {
+            match (maybe_wkb, z_iter.next().unwrap()) {
+                (Some(wkb), Some(z)) => {
+                    let trans = Force3DTransform { z };
+                    transform(wkb, &trans, &mut builder)
+                        .map_err(|e| DataFusionError::External(Box::new(e)))?;
+                    builder.append_value([]);
+                }
+                _ => {
+                    builder.append_null();
+                }
+            }
+
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+#[derive(Debug)]
+struct Force3DTransform {
+    z: f64,
+}
+
+impl CrsTransform for Force3DTransform {
+    fn output_dim(&self) -> Option<geo_traits::Dimensions> {
+        Some(geo_traits::Dimensions::Xyz)
+    }
+
+    fn transform_coord(
+        &self,
+        _coord: &mut (f64, f64),
+    ) -> std::result::Result<(), SedonaGeometryError> {
+        Err(SedonaGeometryError::Invalid(
+            "Unexpected call to transform_coord()".to_string(),
+        ))
+    }
+    fn transform_coord_xyz(
+        &self,
+        coord: &mut (f64, f64, f64),
+        input_dims: Dimensions,
+    ) -> Result<(), SedonaGeometryError> {
+        if matches!(
+            input_dims,
+            Dimensions::Xy | Dimensions::Xym | Dimensions::Unknown(_)
+        ) {
+            coord.2 = self.z
+        }
+        Ok(())
+    }
+}
+
+// *** 3DM *************************
+
+/// ST_Force3DM() scalar UDF
+pub fn st_force3dm_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "st_force3dm",
+        ItemCrsKernel::wrap_impl(vec![
+            Arc::new(STForce3DM {
+                is_geography: false,
+            }),
+            Arc::new(STForce3DM { is_geography: true }),
+        ]),
+        Volatility::Immutable,
+        Some(st_force3dm_doc()),
+    )
+}
+
+fn st_force3dm_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Forces the geometry into a 3DM-dimensional model.",
+        "ST_Force3DM (geom: Geometry)",
+    )
+    .with_argument("geom", "geometry: Input geometry")
+    .with_argument("m", "numeric: default M value")
+    .with_sql_example("SELECT ST_Force3DM(ST_GeomFromWKT('POINT (1 2)'))")
+    .build()
+}
+
+#[derive(Debug)]
+struct STForce3DM {
+    is_geography: bool,
+}
+
+impl SedonaScalarKernel for STForce3DM {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        build_return_matcher(self.is_geography, 1).match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = WkbExecutor::new(arg_types, args);
+        let num_rows = executor.num_iterations();
+        let mut builder = BinaryBuilder::with_capacity(num_rows, 
WKB_MIN_PROBABLE_BYTES * num_rows);
+
+        let m_array = optional_numeric_arg_as_f64(args, 1, num_rows)?;
+        let mut m_iter = m_array.iter();
+        executor.execute_wkb_void(|maybe_wkb| {
+            match (maybe_wkb, m_iter.next().unwrap()) {
+                (Some(wkb), Some(m)) => {
+                    let trans = Force3DMTransform { m };
+                    transform(wkb, &trans, &mut builder)
+                        .map_err(|e| DataFusionError::External(Box::new(e)))?;
+                    builder.append_value([]);
+                }
+                _ => {
+                    builder.append_null();
+                }
+            }
+
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+#[derive(Debug)]
+struct Force3DMTransform {
+    m: f64,
+}
+
+impl CrsTransform for Force3DMTransform {
+    fn output_dim(&self) -> Option<geo_traits::Dimensions> {
+        Some(geo_traits::Dimensions::Xym)
+    }
+
+    fn transform_coord(
+        &self,
+        _coord: &mut (f64, f64),
+    ) -> std::result::Result<(), SedonaGeometryError> {
+        Err(SedonaGeometryError::Invalid(
+            "Unexpected call to transform_coord()".to_string(),
+        ))
+    }
+
+    fn transform_coord_xym(
+        &self,
+        coord: &mut (f64, f64, f64),
+        input_dims: Dimensions,
+    ) -> Result<(), SedonaGeometryError> {
+        if matches!(
+            input_dims,
+            Dimensions::Xy | Dimensions::Xyz | Dimensions::Unknown(_)
+        ) {
+            coord.2 = self.m;
+        }
+        Ok(())
+    }
+}
+
+// *** 4D *************************
+
+/// ST_Force4D() scalar UDF
+pub fn st_force4d_udf() -> SedonaScalarUDF {
+    SedonaScalarUDF::new(
+        "st_force4d",
+        ItemCrsKernel::wrap_impl(vec![
+            Arc::new(STForce4D {
+                is_geography: false,
+            }),
+            Arc::new(STForce4D { is_geography: true }),
+        ]),
+        Volatility::Immutable,
+        Some(st_force4d_doc()),
+    )
+}
+
+fn st_force4d_doc() -> Documentation {
+    Documentation::builder(
+        DOC_SECTION_OTHER,
+        "Forces the geometry into a 4-dimensional model.",
+        "ST_Force4D (geom: Geometry)",
+    )
+    .with_argument("geom", "geometry: Input geometry")
+    .with_argument("z", "numeric: default Z value")
+    .with_argument("m", "numeric: default M value")
+    .with_sql_example("SELECT ST_Force4D(ST_GeomFromWKT('POINT (1 2)'))")
+    .build()
+}
+
+#[derive(Debug)]
+struct STForce4D {
+    is_geography: bool,
+}
+
+impl SedonaScalarKernel for STForce4D {
+    fn return_type(&self, args: &[SedonaType]) -> Result<Option<SedonaType>> {
+        build_return_matcher(self.is_geography, 2).match_args(args)
+    }
+
+    fn invoke_batch(
+        &self,
+        arg_types: &[SedonaType],
+        args: &[ColumnarValue],
+    ) -> Result<ColumnarValue> {
+        let executor = WkbExecutor::new(arg_types, args);
+        let num_rows = executor.num_iterations();
+        let mut builder = BinaryBuilder::with_capacity(num_rows, 
WKB_MIN_PROBABLE_BYTES * num_rows);
+
+        let z_array = optional_numeric_arg_as_f64(args, 1, num_rows)?;
+        let m_array = optional_numeric_arg_as_f64(args, 2, num_rows)?;
+        let mut z_iter = z_array.iter();
+        let mut m_iter = m_array.iter();
+        executor.execute_wkb_void(|maybe_wkb| {
+            match (maybe_wkb, z_iter.next().unwrap(), m_iter.next().unwrap()) {
+                (Some(wkb), Some(z), Some(m)) => {
+                    let trans = Force4DTransform { z, m };
+                    transform(wkb, &trans, &mut builder)
+                        .map_err(|e| DataFusionError::External(Box::new(e)))?;
+                    builder.append_value([]);
+                }
+                _ => {
+                    builder.append_null();
+                }
+            }
+
+            Ok(())
+        })?;
+
+        executor.finish(Arc::new(builder.finish()))
+    }
+}
+
+#[derive(Debug)]
+struct Force4DTransform {
+    z: f64,
+    m: f64,
+}
+
+impl CrsTransform for Force4DTransform {
+    fn output_dim(&self) -> Option<geo_traits::Dimensions> {
+        Some(geo_traits::Dimensions::Xyzm)
+    }
+
+    fn transform_coord(
+        &self,
+        _coord: &mut (f64, f64),
+    ) -> std::result::Result<(), SedonaGeometryError> {
+        Err(SedonaGeometryError::Invalid(
+            "Unexpected call to transform_coord()".to_string(),
+        ))
+    }
+
+    fn transform_coord_xyzm(
+        &self,
+        coord: &mut (f64, f64, f64, f64),
+        input_dims: Dimensions,
+    ) -> Result<(), SedonaGeometryError> {
+        match input_dims {
+            Dimensions::Xy | Dimensions::Unknown(_) => {
+                coord.2 = self.z;
+                coord.3 = self.m;
+            }
+            Dimensions::Xyz => {
+                coord.3 = self.m;
+            }
+            Dimensions::Xym => {
+                coord.2 = self.z;
+            }
+            Dimensions::Xyzm => {}
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use arrow_array::create_array;
+    use datafusion_expr::ScalarUDF;
+    use rstest::rstest;
+    use sedona_schema::datatypes::WKB_VIEW_GEOMETRY;
+    use sedona_testing::{
+        compare::assert_array_equal, create::create_array, 
testers::ScalarUdfTester,
+    };
+
+    use super::*;
+
+    #[test]
+    fn udf_metadata() {
+        let st_force2d: ScalarUDF = st_force2d_udf().into();
+        assert_eq!(st_force2d.name(), "st_force2d");
+        assert!(st_force2d.documentation().is_some());
+
+        let st_force3d: ScalarUDF = st_force3d_udf().into();
+        assert_eq!(st_force3d.name(), "st_force3d");
+        assert!(st_force3d.documentation().is_some());
+
+        let st_force3dm: ScalarUDF = st_force3dm_udf().into();
+        assert_eq!(st_force3dm.name(), "st_force3dm");
+        assert!(st_force3dm.documentation().is_some());
+
+        let st_force4d: ScalarUDF = st_force4d_udf().into();
+        assert_eq!(st_force4d.name(), "st_force4d");
+        assert!(st_force4d.documentation().is_some());
+    }
+
+    #[rstest]
+    fn udf_2d(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: 
SedonaType) {
+        let tester = ScalarUdfTester::new(st_force2d_udf().into(), 
vec![sedona_type.clone()]);
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let points = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT Z EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT Z (3 4 5)"),
+                Some("POINT ZM (8 9 10 11)"),
+            ],
+            &sedona_type,
+        );
+
+        let expected = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT (3 4)"),
+                Some("POINT (8 9)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        let result = tester.invoke_arrays(vec![points]).unwrap();
+        assert_array_equal(&result, &expected);
+    }
+
+    #[rstest]
+    fn udf_3d_without_z(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] 
sedona_type: SedonaType) {
+        let tester = ScalarUdfTester::new(st_force3d_udf().into(), 
vec![sedona_type.clone()]);
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let points = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT Z EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT Z (3 4 5)"),
+                Some("POINT M (6 7 8)"),
+                Some("POINT ZM (9 10 11 12)"),
+            ],
+            &sedona_type,
+        );
+
+        let expected = create_array(
+            &[
+                None,
+                Some("POINT Z EMPTY"),
+                Some("POINT Z EMPTY"),
+                Some("POINT Z (1 2 0)"),
+                Some("POINT Z (3 4 5)"),
+                Some("POINT Z (6 7 0)"),
+                Some("POINT Z (9 10 11)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        let result = tester.invoke_arrays(vec![points]).unwrap();
+        assert_array_equal(&result, &expected);
+    }
+
+    #[rstest]
+    fn udf_3d_with_z(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: 
SedonaType) {
+        let tester = ScalarUdfTester::new(
+            st_force3d_udf().into(),
+            vec![sedona_type.clone(), SedonaType::Arrow(DataType::Float64)],
+        );
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let points = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT (1 2)"),
+                Some("POINT Z (3 4 5)"),
+                Some("POINT M (6 7 8)"),
+                Some("POINT ZM (9 10 11 12)"),
+            ],
+            &sedona_type,
+        );
+        let z = create_array!(
+            Float64,
+            [
+                Some(9.0),
+                Some(9.0),
+                Some(9.0),
+                None,
+                Some(9.0),
+                Some(9.0),
+                Some(9.0)
+            ]
+        );
+
+        let expected = create_array(
+            &[
+                None,
+                Some("POINT Z EMPTY"),
+                Some("POINT Z (1 2 9)"),
+                None,
+                Some("POINT Z (3 4 5)"),
+                Some("POINT Z (6 7 9)"),
+                Some("POINT Z (9 10 11)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        let result = tester.invoke_arrays(vec![points, z]).unwrap();
+        assert_array_equal(&result, &expected);
+    }
+
+    #[rstest]
+    fn udf_3dm_without_m(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] 
sedona_type: SedonaType) {
+        let tester = ScalarUdfTester::new(st_force3dm_udf().into(), 
vec![sedona_type.clone()]);
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let points = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT M EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT Z (3 4 5)"),
+                Some("POINT M (6 7 8)"),
+                Some("POINT ZM (9 10 11 12)"),
+            ],
+            &sedona_type,
+        );
+
+        let expected = create_array(
+            &[
+                None,
+                Some("POINT M EMPTY"),
+                Some("POINT M EMPTY"),
+                Some("POINT M (1 2 0)"),
+                Some("POINT M (3 4 0)"),
+                Some("POINT M (6 7 8)"),
+                Some("POINT M (9 10 12)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        let result = tester.invoke_arrays(vec![points]).unwrap();
+        assert_array_equal(&result, &expected);
+    }
+
+    #[rstest]
+    fn udf_3dm_with_m(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] sedona_type: 
SedonaType) {
+        let tester = ScalarUdfTester::new(
+            st_force3dm_udf().into(),
+            vec![sedona_type.clone(), SedonaType::Arrow(DataType::Float64)],
+        );
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let points = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT (1 2)"),
+                Some("POINT Z (3 4 5)"),
+                Some("POINT M (6 7 8)"),
+                Some("POINT ZM (9 10 11 12)"),
+            ],
+            &sedona_type,
+        );
+        let m = create_array!(
+            Float64,
+            [
+                Some(9.0),
+                Some(9.0),
+                Some(9.0),
+                None,
+                Some(9.0),
+                Some(9.0),
+                Some(9.0)
+            ]
+        );
+
+        let expected = create_array(
+            &[
+                None,
+                Some("POINT M EMPTY"),
+                Some("POINT M (1 2 9)"),
+                None,
+                Some("POINT M (3 4 9)"),
+                Some("POINT M (6 7 8)"),
+                Some("POINT M (9 10 12)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        let result = tester.invoke_arrays(vec![points, m]).unwrap();
+        assert_array_equal(&result, &expected);
+    }
+
+    #[rstest]
+    fn udf_4d_without_defaults(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] 
sedona_type: SedonaType) {
+        let tester = ScalarUdfTester::new(st_force4d_udf().into(), 
vec![sedona_type.clone()]);
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let points = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT Z EMPTY"),
+                Some("POINT M EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT Z (3 4 5)"),
+                Some("POINT M (6 7 8)"),
+                Some("POINT ZM (9 10 11 12)"),
+            ],
+            &sedona_type,
+        );
+
+        let expected = create_array(
+            &[
+                None,
+                Some("POINT ZM EMPTY"),
+                Some("POINT ZM EMPTY"),
+                Some("POINT ZM EMPTY"),
+                Some("POINT ZM (1 2 0 0)"),
+                Some("POINT ZM (3 4 5 0)"),
+                Some("POINT ZM (6 7 0 8)"),
+                Some("POINT ZM (9 10 11 12)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        let result = tester.invoke_arrays(vec![points]).unwrap();
+        assert_array_equal(&result, &expected);
+    }
+
+    #[rstest]
+    fn udf_4d_with_defaults(#[values(WKB_GEOMETRY, WKB_VIEW_GEOMETRY)] 
sedona_type: SedonaType) {
+        let tester = ScalarUdfTester::new(
+            st_force4d_udf().into(),
+            vec![
+                sedona_type.clone(),
+                SedonaType::Arrow(DataType::Float64),
+                SedonaType::Arrow(DataType::Float64),
+            ],
+        );
+        tester.assert_return_type(WKB_GEOMETRY);
+
+        let points = create_array(
+            &[
+                None,
+                Some("POINT EMPTY"),
+                Some("POINT (1 2)"),
+                Some("POINT (1 2)"),
+                Some("POINT Z (3 4 5)"),
+                Some("POINT M (6 7 8)"),
+                Some("POINT ZM (9 10 11 12)"),
+            ],
+            &sedona_type,
+        );
+        let z = create_array!(
+            Float64,
+            [
+                Some(8.0),
+                Some(8.0),
+                Some(8.0),
+                None,
+                Some(8.0),
+                Some(8.0),
+                Some(8.0)
+            ]
+        );
+        let m = create_array!(
+            Float64,
+            [
+                Some(9.0),
+                Some(9.0),
+                Some(9.0),
+                Some(9.0),
+                Some(9.0),
+                Some(9.0),
+                Some(9.0)
+            ]
+        );
+
+        let expected = create_array(
+            &[
+                None,
+                Some("POINT ZM EMPTY"),
+                Some("POINT ZM (1 2 8 9)"),
+                None,
+                Some("POINT ZM (3 4 5 9)"),
+                Some("POINT ZM (6 7 8 8)"),
+                Some("POINT ZM (9 10 11 12)"),
+            ],
+            &WKB_GEOMETRY,
+        );
+
+        let result = tester.invoke_arrays(vec![points, z, m]).unwrap();
+        assert_array_equal(&result, &expected);
+    }
+}
diff --git a/rust/sedona-geometry/src/transform.rs 
b/rust/sedona-geometry/src/transform.rs
index 688ce239..2540535e 100644
--- a/rust/sedona-geometry/src/transform.rs
+++ b/rust/sedona-geometry/src/transform.rs
@@ -458,7 +458,8 @@ where
     match input_dims {
         // If the input doesn't have M coordinate, fill with 0.
         Dimensions::Xy | Dimensions::Xyz | Dimensions::Unknown(_) => 
(coord.x(), coord.y(), 0.0),
-        Dimensions::Xym | Dimensions::Xyzm => (coord.x(), coord.y(), 
coord.nth_or_panic(2)),
+        Dimensions::Xym => (coord.x(), coord.y(), coord.nth_or_panic(2)),
+        Dimensions::Xyzm => (coord.x(), coord.y(), coord.nth_or_panic(3)),
     }
 }
 
@@ -914,7 +915,7 @@ mod test {
         };
         assert_eq!(
             fill_or_extract_xym(&coord_xyzm, Dimensions::Xyzm),
-            (1.0, 2.0, 3.0)
+            (1.0, 2.0, 4.0)
         );
     }
 


Reply via email to