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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6c81c8b3f [SEDONA-585] [SEDONA-586] Add ST_ForceCollection, 
ST_Force3DZ (#1460)
6c81c8b3f is described below

commit 6c81c8b3f57f91c06af8c74f06d38053a39eda44
Author: Jia Yu <[email protected]>
AuthorDate: Fri Jun 7 00:47:52 2024 -0700

    [SEDONA-585] [SEDONA-586] Add ST_ForceCollection, ST_Force3DZ (#1460)
    
    * [TASK-78] Add ST_ForceCollection (#164)
    
    * feat: Add ST_ForceCollection
    
    * fix: snowflake tests
    
    * fix: snowflake tests
    
    * chore: remove print statement
    
    * chore: add GeometryCollection test
    
    * docs: add detailed explanation
    
    * Update version
    
    * [TASK-78] Add ST_Force3DZ (#165)
    
    * feat: add ST_Force3DZ
    
    * fix: add snowflake function registration for all arities
    
    * fix: add 3D support for snowflake
    
    * fix: typo
    
    * Update version
    
    ---------
    
    Co-authored-by: Furqaan Khan <[email protected]>
---
 .../java/org/apache/sedona/common/Functions.java   | 12 +++++
 .../org/apache/sedona/common/FunctionsTest.java    | 28 ++++++++++
 docs/api/flink/Function.md                         | 62 ++++++++++++++++++++++
 docs/api/snowflake/vector-data/Function.md         | 58 ++++++++++++++++++++
 docs/api/sql/Function.md                           | 62 ++++++++++++++++++++++
 .../main/java/org/apache/sedona/flink/Catalog.java |  2 +
 .../apache/sedona/flink/expressions/Functions.java | 24 +++++++++
 .../java/org/apache/sedona/flink/FunctionTest.java | 45 ++++++++++++++++
 python/sedona/sql/st_functions.py                  | 21 ++++++++
 python/tests/sql/test_dataframe_api.py             |  4 ++
 python/tests/sql/test_function.py                  | 14 +++++
 .../sedona/snowflake/snowsql/TestFunctions.java    | 24 +++++++++
 .../sedona/snowflake/snowsql/TestFunctionsV2.java  | 24 +++++++++
 .../org/apache/sedona/snowflake/snowsql/UDFs.java  | 34 ++++++++++++
 .../apache/sedona/snowflake/snowsql/UDFsV2.java    | 28 ++++++++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  2 +
 .../sql/sedona_sql/expressions/Functions.scala     | 17 ++++++
 .../sql/sedona_sql/expressions/st_functions.scala  |  9 ++++
 .../apache/sedona/sql/dataFrameAPITestScala.scala  | 21 ++++++++
 .../org/apache/sedona/sql/functionTestScala.scala  | 26 +++++++++
 20 files changed, 517 insertions(+)

diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java 
b/common/src/main/java/org/apache/sedona/common/Functions.java
index 6ee9d49ef..ac88ce04f 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1553,6 +1553,18 @@ public class Functions {
         return GeomUtils.get3DGeom(geometry, 0.0);
     }
 
+    public static Geometry forceCollection(Geometry geom) {
+        return new 
GeometryFactory().createGeometryCollection(convertGeometryToArray(geom));
+    }
+
+    private static Geometry[] convertGeometryToArray(Geometry geom) {
+        Geometry[] array = new Geometry[geom.getNumGeometries()];
+        for (int i = 0; i < array.length; i++) {
+            array[i] = geom.getGeometryN(i);
+        }
+        return array;
+    }
+
     public static Integer nRings(Geometry geometry) throws Exception {
         String geometryType = geometry.getGeometryType();
         if (!(geometry instanceof Polygon || geometry instanceof 
MultiPolygon)) {
diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java 
b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
index beeeb9919..017f65e64 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -1305,6 +1305,34 @@ public class FunctionsTest extends TestBase {
         assertEquals(expectedDims, Functions.nDims(forcedLine3D));
     }
 
+    @Test
+    public void forceCollection() throws ParseException {
+        Geometry geom = Constructors.geomFromWKT("MULTIPOINT (30 10, 40 40, 20 
20, 10 30, 10 10, 20 50)", 0);
+        int actual = Functions.numGeometries(Functions.forceCollection(geom));
+        int expected = 6;
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("MULTIPOLYGON(((0 0 0,0 1 0,1 1 0,1 0 
0,0 0 0)),((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((1 1 0,1 1 1,1 0 1,1 0 0,1 1 
0)),((0 1 0,0 1 1,1 1 1,1 1 0,0 1 0)),((0 0 1,1 0 1,1 1 1,0 1 1,0 0 1)))", 0);
+        actual = Functions.numGeometries(Functions.forceCollection(geom));
+        expected = 5;
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("MULTILINESTRING ((10 10, 20 20, 30 
30), (15 15, 25 25, 35 35))", 0);
+        actual = Functions.numGeometries(Functions.forceCollection(geom));
+        expected = 2;
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("POLYGON ((30 10, 40 40, 20 40, 10 20, 
30 10))", 0);
+        actual = Functions.numGeometries(Functions.forceCollection(geom));
+        expected = 1;
+        assertEquals(expected, actual);
+
+        geom = Constructors.geomFromWKT("GEOMETRYCOLLECTION(POLYGON((0 0 1,0 5 
1,5 0 1,0 0 1),(1 1 1,3 1 1,1 3 1,1 1 1)))",0);
+        String actualWKT = Functions.asWKT(Functions.forceCollection(geom));
+        String expectedWKT = "GEOMETRYCOLLECTION Z(POLYGON Z((0 0 1, 0 5 1, 5 
0 1, 0 0 1), (1 1 1, 3 1 1, 1 3 1, 1 1 1)))";
+        assertEquals(expectedWKT, actualWKT);
+    }
+
     @Test
     public void force3DObject3DDefaultValue() {
         int expectedDims = 3;
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index ce5ca3fe9..9a51cae82 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -1266,6 +1266,68 @@ Output:
 LINESTRING EMPTY
 ```
 
+## ST_Force3DZ
+
+Introduction: Forces the geometry into a 3-dimensional model so that all 
output representations will have X, Y and Z coordinates.
+An optionally given zValue is tacked onto the geometry if the geometry is 
2-dimensional. Default value of zValue is 0.0
+If the given geometry is 3-dimensional, no change is performed on it.
+If the given geometry is empty, no change is performed on it. This function is 
an alias for [ST_Force3D](#st_force3d).
+
+!!!Note
+    Example output is after calling ST_AsText() on returned geometry, which 
adds Z for in the WKT for 3D geometries
+
+Format: `ST_Force3DZ(geometry: Geometry, zValue: Double)`
+
+Since: `v1.6.1`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DZ(ST_GeomFromText('POLYGON((0 0 2,0 5 2,5 0 2,0 0 
2),(1 1 2,3 1 2,1 3 2,1 1 2))'), 2.3))
+```
+
+Output:
+
+```
+POLYGON Z((0 0 2, 0 5 2, 5 0 2, 0 0 2), (1 1 2, 3 1 2, 1 3 2, 1 1 2))
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DZ(ST_GeomFromText('LINESTRING(0 1,1 0,2 0)'), 2.3))
+```
+
+Output:
+
+```
+LINESTRING Z(0 1 2.3, 1 0 2.3, 2 0 2.3)
+```
+
+## ST_ForceCollection
+
+Introduction: This function converts the input geometry into a 
GeometryCollection, regardless of the original geometry type. If the input is a 
multipart geometry, such as a MultiPolygon or MultiLineString, it will be 
decomposed into a GeometryCollection containing each individual Polygon or 
LineString element from the original multipart geometry.
+
+Format: `ST_ForceCollection(geom: Geometry)`
+
+Since: `v1.6.1`
+
+SQL Example
+
+```sql
+SELECT ST_ForceCollection(
+            ST_GeomFromWKT(
+                "MULTIPOINT (30 10, 40 40, 20 20, 10 30, 10 10, 20 50)"
+    )
+)
+```
+
+Output:
+
+```
+GEOMETRYCOLLECTION (POINT (30 10), POINT (40 40), POINT (20 20), POINT (10 
30), POINT (10 10), POINT (20 50))
+```
+
 ## ST_ForcePolygonCCW
 
 Introduction: For (Multi)Polygon geometries, this function sets the exterior 
ring orientation to counter-clockwise and interior rings to clockwise 
orientation. Non-polygonal geometries are returned unchanged.
diff --git a/docs/api/snowflake/vector-data/Function.md 
b/docs/api/snowflake/vector-data/Function.md
index 65037e0fa..0da9461f0 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -999,6 +999,64 @@ Input: `LINESTRING EMPTY`
 
 Output: `LINESTRING EMPTY`
 
+## ST_Force3DZ
+
+Introduction: Forces the geometry into a 3-dimensional model so that all 
output representations will have X, Y and Z coordinates.
+An optionally given zValue is tacked onto the geometry if the geometry is 
2-dimensional. Default value of zValue is 0.0
+If the given geometry is 3-dimensional, no change is performed on it.
+If the given geometry is empty, no change is performed on it. This function is 
an alias for [ST_Force3D](#st_force3d).
+
+!!!Note
+    Example output is after calling ST_AsText() on returned geometry, which 
adds Z for in the WKT for 3D geometries
+
+Format: `ST_Force3DZ(geometry: Geometry, zValue: Double)`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DZ(ST_GeomFromText('POLYGON((0 0 2,0 5 2,5 0 2,0 0 
2),(1 1 2,3 1 2,1 3 2,1 1 2))'), 2.3))
+```
+
+Output:
+
+```
+POLYGON Z((0 0 2, 0 5 2, 5 0 2, 0 0 2), (1 1 2, 3 1 2, 1 3 2, 1 1 2))
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DZ(ST_GeomFromText('LINESTRING(0 1,1 0,2 0)'), 2.3))
+```
+
+Output:
+
+```
+LINESTRING Z(0 1 2.3, 1 0 2.3, 2 0 2.3)
+```
+
+## ST_ForceCollection
+
+Introduction: This function converts the input geometry into a 
GeometryCollection, regardless of the original geometry type. If the input is a 
multipart geometry, such as a MultiPolygon or MultiLineString, it will be 
decomposed into a GeometryCollection containing each individual Polygon or 
LineString element from the original multipart geometry.
+
+Format: `ST_ForceCollection(geom: Geometry)`
+
+SQL Example
+
+```sql
+SELECT ST_ForceCollection(
+            ST_GeomFromWKT(
+                "MULTIPOINT (30 10, 40 40, 20 20, 10 30, 10 10, 20 50)"
+    )
+)
+```
+
+Output:
+
+```
+GEOMETRYCOLLECTION (POINT (30 10), POINT (40 40), POINT (20 20), POINT (10 
30), POINT (10 10), POINT (20 50))
+```
+
 ## ST_ForcePolygonCCW
 
 Introduction: For (Multi)Polygon geometries, this function sets the exterior 
ring orientation to counter-clockwise and interior rings to clockwise 
orientation. Non-polygonal geometries are returned unchanged.
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index f26d2d5c1..9a7a3eafe 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -1271,6 +1271,68 @@ Output:
 LINESTRING EMPTY
 ```
 
+## ST_Force3DZ
+
+Introduction: Forces the geometry into a 3-dimensional model so that all 
output representations will have X, Y and Z coordinates.
+An optionally given zValue is tacked onto the geometry if the geometry is 
2-dimensional. Default value of zValue is 0.0
+If the given geometry is 3-dimensional, no change is performed on it.
+If the given geometry is empty, no change is performed on it. This function is 
an alias for [ST_Force3D](#st_force3d).
+
+!!!Note
+    Example output is after calling ST_AsText() on returned geometry, which 
adds Z for in the WKT for 3D geometries
+
+Format: `ST_Force3DZ(geometry: Geometry, zValue: Double)`
+
+Since: `v1.6.1`
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DZ(ST_GeomFromText('POLYGON((0 0 2,0 5 2,5 0 2,0 0 
2),(1 1 2,3 1 2,1 3 2,1 1 2))'), 2.3))
+```
+
+Output:
+
+```
+POLYGON Z((0 0 2, 0 5 2, 5 0 2, 0 0 2), (1 1 2, 3 1 2, 1 3 2, 1 1 2))
+```
+
+SQL Example
+
+```sql
+SELECT ST_AsText(ST_Force3DZ(ST_GeomFromText('LINESTRING(0 1,1 0,2 0)'), 2.3))
+```
+
+Output:
+
+```
+LINESTRING Z(0 1 2.3, 1 0 2.3, 2 0 2.3)
+```
+
+## ST_ForceCollection
+
+Introduction: This function converts the input geometry into a 
GeometryCollection, regardless of the original geometry type. If the input is a 
multipart geometry, such as a MultiPolygon or MultiLineString, it will be 
decomposed into a GeometryCollection containing each individual Polygon or 
LineString element from the original multipart geometry.
+
+Format: `ST_ForceCollection(geom: Geometry)`
+
+Since: `v1.6.1`
+
+SQL Example
+
+```sql
+SELECT ST_ForceCollection(
+            ST_GeomFromWKT(
+                "MULTIPOINT (30 10, 40 40, 20 20, 10 30, 10 10, 20 50)"
+    )
+)
+```
+
+Output:
+
+```
+GEOMETRYCOLLECTION (POINT (30 10), POINT (40 40), POINT (20 20), POINT (10 
30), POINT (10 10), POINT (20 50))
+```
+
 ## ST_ForcePolygonCCW
 
 Introduction: For (Multi)Polygon geometries, this function sets the exterior 
ring orientation to counter-clockwise and interior rings to clockwise 
orientation. Non-polygonal geometries are returned unchanged.
diff --git a/flink/src/main/java/org/apache/sedona/flink/Catalog.java 
b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
index 9356e7af8..df8c86186 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -158,6 +158,8 @@ public class Catalog {
                 new Functions.ST_GeometricMedian(),
                 new Functions.ST_NumPoints(),
                 new Functions.ST_Force3D(),
+                new Functions.ST_Force3DZ(),
+                new Functions.ST_ForceCollection(),
                 new Functions.ST_ForcePolygonCW(),
                 new Functions.ST_ForceRHR(),
                 new Functions.ST_NRings(),
diff --git 
a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java 
b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
index 195d823d0..c1e031b83 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
@@ -1101,6 +1101,30 @@ public class Functions {
         }
     }
 
+    public static class ST_Force3DZ extends ScalarFunction {
+
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o,
+                             @DataTypeHint("Double") Double zValue) {
+            Geometry geometry = (Geometry) o;
+            return org.apache.sedona.common.Functions.force3D(geometry, 
zValue);
+        }
+
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o) {
+            Geometry geometry = (Geometry) o;
+            return org.apache.sedona.common.Functions.force3D(geometry);
+        }
+    }
+
+    public static class ST_ForceCollection extends ScalarFunction {
+        @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
+        public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o) {
+            Geometry geometry = (Geometry) o;
+            return 
org.apache.sedona.common.Functions.forceCollection(geometry);
+        }
+    }
+
     public static class ST_ForcePolygonCW extends ScalarFunction {
         @DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class)
         public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = 
org.locationtech.jts.geom.Geometry.class) Object o) {
diff --git a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java 
b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
index 7fa10990a..6db3f9493 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -1312,6 +1312,51 @@ public class FunctionTest extends TestBase{
         assertEquals(expectedDims, actual);
     }
 
+    @Test
+    public void testForce3DZ() {
+        Integer expectedDims = 3;
+        Table polyTable = tableEnv.sqlQuery("SELECT 
ST_Force3DZ(ST_GeomFromWKT('LINESTRING(0 1, 1 0, 2 0)'), 1.2) " +
+                "AS " + polygonColNames[0]);
+        Integer actual = (Integer) 
first(polyTable.select(call(Functions.ST_NDims.class.getSimpleName(), 
$(polygonColNames[0])))).getField(0);
+        assertEquals(expectedDims, actual);
+
+        polyTable = tableEnv.sqlQuery("SELECT 
ST_Force3DZ(ST_GeomFromWKT('LINESTRING(0 1, 1 0, 2 0)')) " +
+                "AS " + polygonColNames[0]);
+        actual = (Integer) 
first(polyTable.select(call(Functions.ST_NDims.class.getSimpleName(), 
$(polygonColNames[0])))).getField(0);
+        assertEquals(expectedDims, actual);
+    }
+
+    @Test
+    public void testForceCollection() {
+        int actual = (int) first(
+                tableEnv.sqlQuery("SELECT ST_GeomFromWKT('MULTIPOINT (30 10, 
40 40, 20 20, 10 30, 10 10, 20 50)') AS 
geom").select(call(Functions.ST_ForceCollection.class.getSimpleName(), 
$("geom"))).as("geom")
+                        
.select(call(Functions.ST_NumGeometries.class.getSimpleName(), $("geom")))
+        ).getField(0);
+        int expected = 6;
+        assertEquals(expected, actual);
+
+        actual = (int) first(
+                tableEnv.sqlQuery("SELECT ST_GeomFromWKT('MULTIPOLYGON(((0 0 
0,0 1 0,1 1 0,1 0 0,0 0 0)),((0 0 0,1 0 0,1 0 1,0 0 1,0 0 0)),((1 1 0,1 1 1,1 0 
1,1 0 0,1 1 0)),((0 1 0,0 1 1,1 1 1,1 1 0,0 1 0)),((0 0 1,1 0 1,1 1 1,0 1 1,0 0 
1)))') AS 
geom").select(call(Functions.ST_ForceCollection.class.getSimpleName(), 
$("geom"))).as("geom")
+                        
.select(call(Functions.ST_NumGeometries.class.getSimpleName(), $("geom")))
+        ).getField(0);
+        expected = 5;
+        assertEquals(expected, actual);
+
+        actual = (int) first(
+                tableEnv.sqlQuery("SELECT ST_GeomFromWKT('MULTILINESTRING ((10 
10, 20 20, 30 30), (15 15, 25 25, 35 35))') AS 
geom").select(call(Functions.ST_ForceCollection.class.getSimpleName(), 
$("geom"))).as("geom")
+                        
.select(call(Functions.ST_NumGeometries.class.getSimpleName(), $("geom")))
+        ).getField(0);
+        expected = 2;
+        assertEquals(expected, actual);
+
+        actual = (int) first(
+                tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POLYGON ((30 10, 40 
40, 20 40, 10 20, 30 10))') AS 
geom").select(call(Functions.ST_ForceCollection.class.getSimpleName(), 
$("geom"))).as("geom")
+                        
.select(call(Functions.ST_NumGeometries.class.getSimpleName(), $("geom")))
+        ).getField(0);
+        expected = 1;
+        assertEquals(expected, actual);
+    }
+
     @Test
     public void testTriangulatePolygon() {
         Table polyTable = tableEnv.sqlQuery("SELECT 
ST_TriangulatePolygon(ST_GeomFromWKT('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), 
(5 5, 5 8, 8 8, 8 5, 5 5))')) as poly");
diff --git a/python/sedona/sql/st_functions.py 
b/python/sedona/sql/st_functions.py
index f03858ba9..82ca9c2fe 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -1574,6 +1574,27 @@ def ST_Force3D(geometry: ColumnOrName, zValue: 
Optional[Union[ColumnOrName, floa
     args = (geometry, zValue)
     return _call_st_function("ST_Force3D", args)
 
+@validate_argument_types
+def ST_Force3DZ(geometry: ColumnOrName, zValue: Optional[Union[ColumnOrName, 
float]] = 0.0) -> Column:
+    """
+    Return a geometry with a 3D coordinate of value 'zValue' forced upon it. 
No change happens if the geometry is already 3D
+    :param zValue: Optional value of z coordinate to be potentially added, 
default value is 0.0
+    :param geometry: Geometry column to make 3D
+    :return: 3D geometry with either already present z coordinate if any, or 
zcoordinate with given zValue
+    """
+    args = (geometry, zValue)
+    return _call_st_function("ST_Force3DZ", args)
+
+@validate_argument_types
+def ST_ForceCollection(geometry: ColumnOrName) -> Column:
+    """
+    Converts a geometry to a geometry collection
+
+    :param geometry: Geometry column to change orientation
+    :return: a Geometry Collection
+    """
+    return _call_st_function("ST_ForceCollection", geometry)
+
 @validate_argument_types
 def ST_ForcePolygonCW(geometry: ColumnOrName) -> Column:
     """
diff --git a/python/tests/sql/test_dataframe_api.py 
b/python/tests/sql/test_dataframe_api.py
index 185c17d89..42818f07f 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -124,6 +124,8 @@ test_configurations = [
     (stf.ST_FlipCoordinates, ("point",), "point_geom", "", "POINT (1 0)"),
     (stf.ST_Force_2D, ("point",), "point_geom", "", "POINT (0 1)"),
     (stf.ST_Force3D, ("point", 1.0), "point_geom", "", "POINT Z (0 1 1)"),
+    (stf.ST_Force3DZ, ("point", 1.0), "point_geom", "", "POINT Z (0 1 1)"),
+    (stf.ST_ForceCollection, ("multipoint",), "multipoint_geom", 
"ST_NumGeometries(geom)", 4),
     (stf.ST_ForcePolygonCW, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 
3, 3 0, 0 0), (1 1, 2 1, 2 2, 1 1))"),
     (stf.ST_ForcePolygonCCW, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 
3 0, 3 3, 0 0), (1 1, 2 2, 2 1, 1 1))"),
     (stf.ST_ForceRHR, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 3, 3 
0, 0 0), (1 1, 2 1, 2 2, 1 1))"),
@@ -302,6 +304,8 @@ wrong_type_configurations = [
     (stf.ST_ExteriorRing, (None,)),
     (stf.ST_FlipCoordinates, (None,)),
     (stf.ST_Force_2D, (None,)),
+    (stf.ST_Force3DZ, (None,)),
+    (stf.ST_ForceCollection, (None,)),
     (stf.ST_ForcePolygonCW, (None,)),
     (stf.ST_ForcePolygonCCW, (None,)),
     (stf.ST_ForceRHR, (None,)),
diff --git a/python/tests/sql/test_function.py 
b/python/tests/sql/test_function.py
index 160d9894c..7686eb81a 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1468,6 +1468,20 @@ class TestPredicateJoin(TestBase):
         actual = actualDf.selectExpr("ST_NDims(geom)").take(1)[0][0]
         assert expected == actual
 
+    def test_force3DZ(self):
+        expected = 3
+        actualDf = self.spark.sql("SELECT 
ST_Force3DZ(ST_GeomFromText('LINESTRING(0 1, 1 0, 2 0)'), 1.1) AS geom")
+        actual = actualDf.selectExpr("ST_NDims(geom)").take(1)[0][0]
+        assert expected == actual
+
+    def test_st_force_collection(self):
+        basedf = self.spark.sql("SELECT ST_GeomFromWKT('MULTIPOINT (30 10, 40 
40, 20 20, 10 30, 10 10, 20 50)') AS mpoint, ST_GeomFromWKT('POLYGON ((30 10, 
40 40, 20 40, 10 20, 30 10))') AS poly")
+        actual = 
basedf.selectExpr("ST_NumGeometries(ST_ForceCollection(mpoint))").take(1)[0][0]
+        assert actual == 6
+
+        actual = 
basedf.selectExpr("ST_NumGeometries(ST_ForceCollection(poly))").take(1)[0][0]
+        assert actual == 1
+
     def test_forcePolygonCW(self):
         actualDf = self.spark.sql("SELECT 
ST_ForcePolygonCW(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 
20 35),(30 20, 20 15, 20 25, 30 20))')) AS polyCW")
         actual = actualDf.selectExpr("ST_AsText(polyCW)").take(1)[0][0]
diff --git 
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
 
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
index 1c1ad3e2d..c0842d36b 100644
--- 
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
+++ 
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
@@ -1072,6 +1072,30 @@ public class TestFunctions extends TestBase {
         );
     }
 
+    @Test
+    public void test_ST_Force3DZ() {
+        registerUDF("ST_Force3DZ", byte[].class);
+        verifySqlSingleRes(
+                "SELECT 
sedona.ST_AsText(sedona.ST_Force3DZ(sedona.ST_GeomFromText('LINESTRING(0 1, 1 
2, 2 1)')))",
+                "LINESTRING Z(0 1 0, 1 2 0, 2 1 0)"
+        );
+        registerUDF("ST_Force3DZ", byte[].class, double.class);
+        verifySqlSingleRes(
+                "SELECT 
sedona.ST_AsText(sedona.ST_Force3DZ(sedona.ST_GeomFromText('LINESTRING(0 1, 1 
2, 2 1)'), 1))",
+                "LINESTRING Z(0 1 1, 1 2 1, 2 1 1)"
+        );
+    }
+
+    @Test
+    public void test_ST_ForceCollection() {
+        registerUDF("ST_ForceCollection", byte[].class);
+        registerUDF("ST_NumGeometries", byte[].class);
+        verifySqlSingleRes(
+                "SELECT 
sedona.ST_NumGeometries(sedona.ST_ForceCollection(sedona.ST_GeomFromWKT('MULTIPOINT
 (30 10, 40 40, 20 20, 10 30, 10 10, 20 50)')))",
+                6
+        );
+    }
+
     @Test
     public void test_ST_ForcePolygonCW() {
         registerUDF("ST_ForcePolygonCW", byte[].class);
diff --git 
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
 
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
index c018b1d3e..350ccbace 100644
--- 
a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
+++ 
b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
@@ -1031,6 +1031,30 @@ public class TestFunctionsV2
         );
     }
 
+    @Test
+    public void test_ST_Force3DZ() {
+        registerUDFV2("ST_Force3DZ", String.class);
+        verifySqlSingleRes(
+                "SELECT 
ST_AsText(sedona.ST_Force3DZ(ST_GeometryFromWKT('LINESTRING(0 1, 1 2, 2 1)')))",
+                "LINESTRINGZ(0 1 0,1 2 0,2 1 0)"
+        );
+        registerUDFV2("ST_Force3DZ", String.class, double.class);
+        verifySqlSingleRes(
+                "SELECT 
ST_AsText(sedona.ST_Force3DZ(ST_GeometryFromWKT('LINESTRING(0 1, 1 2, 2 1)'), 
1))",
+                "LINESTRINGZ(0 1 1,1 2 1,2 1 1)"
+        );
+    }
+
+    @Test
+    public void test_ST_ForceCollection() {
+        registerUDFV2("ST_ForceCollection", String.class);
+        registerUDFV2("ST_NumGeometries", String.class);
+        verifySqlSingleRes(
+                "SELECT 
ST_NumGeometries(sedona.ST_ForceCollection(ST_GeomFromWKT('MULTIPOINT (30 10, 
40 40, 20 20, 10 30, 10 10, 20 50)')))",
+                6
+        );
+    }
+
     @Test
     public void test_ST_ForcePolygonCW() {
         registerUDFV2("ST_ForcePolygonCW", String.class);
diff --git 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
index d88a21091..b478f1ee0 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
@@ -1435,6 +1435,40 @@ public class UDFs {
         );
     }
 
+    @UDFAnnotations.ParamMeta(argNames = {"geom", "zValue"})
+    public static byte[] ST_Force3DZ(byte[] geom, double zValue) {
+        WKBWriter writer = new WKBWriter(3);
+        return GeometrySerde.serialize(
+                Functions.force3D(
+                        GeometrySerde.deserialize(
+                                writer.write(GeometrySerde.deserialize(geom))
+                        ),
+                        zValue
+                )
+        );
+    }
+
+    @UDFAnnotations.ParamMeta(argNames = {"geom"})
+    public static byte[] ST_Force3DZ(byte[] geom) {
+        WKBWriter writer = new WKBWriter(3);
+        return GeometrySerde.serialize(
+                Functions.force3D(
+                        GeometrySerde.deserialize(
+                                writer.write(GeometrySerde.deserialize(geom))
+                        )
+                )
+        );
+    }
+
+    @UDFAnnotations.ParamMeta(argNames = {"geom"})
+    public static byte[] ST_ForceCollection(byte[] geom) {
+        return GeometrySerde.serialize(
+                Functions.forceCollection(
+                        GeometrySerde.deserialize(geom)
+                )
+        );
+    }
+
     @UDFAnnotations.ParamMeta(argNames = {"geom"})
     public static byte[] ST_ForcePolygonCW(byte[] geom) {
         return GeometrySerde.serialize(
diff --git 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
index bbcf8669d..4c419b71d 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
@@ -1217,6 +1217,34 @@ public class UDFsV2
         );
     }
 
+    @UDFAnnotations.ParamMeta(argNames = {"geom", "zValue"}, argTypes = 
{"Geometry", "double"}, returnTypes = "Geometry")
+    public static String ST_Force3DZ(String geom, double zValue) {
+        return GeometrySerde.serGeoJson(
+                Functions.force3D(
+                        GeometrySerde.deserGeoJson(geom),
+                        zValue
+                )
+        );
+    }
+
+    @UDFAnnotations.ParamMeta(argNames = {"geom"}, argTypes = {"Geometry"}, 
returnTypes = "Geometry")
+    public static String ST_Force3DZ(String geom) {
+        return GeometrySerde.serGeoJson(
+                Functions.force3D(
+                        GeometrySerde.deserGeoJson(geom)
+                )
+        );
+    }
+
+    @UDFAnnotations.ParamMeta(argNames = {"geom"}, argTypes = {"Geometry"}, 
returnTypes = "Geometry")
+    public static String ST_ForceCollection(String geom) {
+        return GeometrySerde.serGeoJson(
+                Functions.forceCollection(
+                        GeometrySerde.deserGeoJson(geom)
+                )
+        );
+    }
+
     @UDFAnnotations.ParamMeta(argNames = {"geom"}, argTypes = {"Geometry"}, 
returnTypes = "Geometry")
     public static String ST_ForcePolygonCW(String geom) {
         return GeometrySerde.serGeoJson(
diff --git 
a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala 
b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index 8f79dc85c..509687a6d 100644
--- a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -191,6 +191,8 @@ object Catalog {
     function[ST_LengthSpheroid](),
     function[ST_NumPoints](),
     function[ST_Force3D](0.0),
+    function[ST_Force3DZ](0.0),
+    function[ST_ForceCollection](),
     function[ST_NRings](),
     function[ST_Translate](0.0),
     function[ST_TriangulatePolygon](),
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index cc6ec66d4..d414d30d0 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -1200,6 +1200,23 @@ case class ST_Force3D(inputExpressions: Seq[Expression])
   }
 }
 
+case class ST_Force3DZ(inputExpressions: Seq[Expression])
+  extends InferredExpression(inferrableFunction2(Functions.force3D)) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+case class ST_ForceCollection(inputExpressions: Seq[Expression])
+  extends InferredExpression(Functions.forceCollection _) {
+
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
+
 case class ST_ForcePolygonCW(inputExpressions: Seq[Expression])
   extends InferredExpression(Functions.forcePolygonCW _) {
   protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
index 956efa7e8..a54e59044 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
@@ -427,6 +427,15 @@ object st_functions extends DataFrameAPI {
 
   def ST_Force3D(geometry: String, zValue: Double): Column = 
wrapExpression[ST_Force3D](geometry, zValue)
 
+  def ST_Force3DZ(geometry: Column): Column = 
wrapExpression[ST_Force3DZ](geometry, 0.0)
+  def ST_Force3DZ(geometry: String): Column = 
wrapExpression[ST_Force3DZ](geometry, 0.0)
+  def ST_Force3DZ(geometry: Column, zValue: Column): Column = 
wrapExpression[ST_Force3DZ](geometry, zValue)
+  def ST_Force3DZ(geometry: String, zValue: Double): Column = 
wrapExpression[ST_Force3DZ](geometry, zValue)
+
+  def ST_ForceCollection(geometry: Column): Column = 
wrapExpression[ST_ForceCollection](geometry)
+
+  def ST_ForceCollection(geometry: String): Column = 
wrapExpression[ST_ForceCollection](geometry)
+
   def ST_ForcePolygonCW(geometry: Column): Column = 
wrapExpression[ST_ForcePolygonCW](geometry)
   def ST_ForcePolygonCW(geometry: String): Column = 
wrapExpression[ST_ForcePolygonCW](geometry)
 
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala 
b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
index 2c9eeaeed..72a55323d 100644
--- 
a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
+++ 
b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
@@ -1348,6 +1348,27 @@ class dataFrameAPITestScala extends TestBaseScala {
       assertEquals(expectedGeomDefaultValue, 
wktWriter.write(actualGeomDefaultValue))
     }
 
+    it("Passed ST_Force3DZ") {
+      val lineDf = sparkSession.sql("SELECT ST_GeomFromWKT('LINESTRING (0 1, 1 
0, 2 0)') AS geom")
+      val expectedGeom = "LINESTRING Z(0 1 2.3, 1 0 2.3, 2 0 2.3)"
+      val expectedGeomDefaultValue = "LINESTRING Z(0 1 0, 1 0 0, 2 0 0)"
+      val wktWriter = new WKTWriter(3)
+      val forcedGeom = lineDf.select(ST_Force3DZ("geom", 
2.3)).take(1)(0).get(0).asInstanceOf[Geometry]
+      assertEquals(expectedGeom, wktWriter.write(forcedGeom))
+      val lineDfDefaultValue = sparkSession.sql("SELECT 
ST_GeomFromWKT('LINESTRING (0 1, 1 0, 2 0)') AS geom")
+      val actualGeomDefaultValue = 
lineDfDefaultValue.select(ST_Force3DZ("geom")).take(1)(0).get(0).asInstanceOf[Geometry]
+      assertEquals(expectedGeomDefaultValue, 
wktWriter.write(actualGeomDefaultValue))
+    }
+
+    it("Passed ST_ForceCollection") {
+      val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('MULTIPOINT (30 10, 
40 40, 20 20, 10 30, 10 10, 20 50)') AS mpoint, ST_GeomFromWKT('POLYGON ((30 
10, 40 40, 20 40, 10 20, 30 10))') AS poly")
+      var actual = 
baseDf.select(ST_NumGeometries(ST_ForceCollection("mpoint"))).first().get(0)
+      assert(actual == 6)
+
+      actual = 
baseDf.select(ST_NumGeometries(ST_ForceCollection("poly"))).first().get(0)
+      assert(actual == 1)
+    }
+
     it("Passed ST_TriangulatePolygon") {
       val baseDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((0 0, 10 
0, 10 10, 0 10, 0 0), (5 5, 5 8, 8 8, 8 5, 5 5))') as poly")
       val actual = 
baseDf.select(ST_AsText(ST_TriangulatePolygon("poly"))).first().getString(0)
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala 
b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index f65f95d21..cf7e40460 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -2274,6 +2274,32 @@ class functionTestScala extends TestBaseScala with 
Matchers with GeometrySample
     }
   }
 
+  it("Should pass ST_'Force3DZ'") {
+    val geomTestCases = Map(
+      ("'LINESTRING (0 1, 1 0, 2 0)'") -> ("'LINESTRING Z(0 1 1, 1 0 1, 2 0 
1)'", "'LINESTRING Z(0 1 0, 1 0 0, 2 0 0)'"),
+      ("'LINESTRING Z(0 1 3, 1 0 3, 2 0 3)'") -> ("'LINESTRING Z(0 1 3, 1 0 3, 
2 0 3)'", "'LINESTRING Z(0 1 3, 1 0 3, 2 0 3)'"),
+      ("'LINESTRING EMPTY'") -> ("'LINESTRING EMPTY'", "'LINESTRING EMPTY'")
+    )
+    for (((geom), expectedResult) <- geomTestCases) {
+      val df = sparkSession.sql(s"SELECT 
ST_AsText(ST_Force3DZ(ST_GeomFromWKT($geom), 1)) AS geom, " + 
s"$expectedResult")
+      val dfDefaultValue = sparkSession.sql(s"SELECT 
ST_AsText(ST_Force3DZ(ST_GeomFromWKT($geom))) AS geom, " + s"$expectedResult")
+      val actual = df.take(1)(0).get(0).asInstanceOf[String]
+      val expected = 
df.take(1)(0).get(1).asInstanceOf[GenericRowWithSchema].get(0).asInstanceOf[String]
+      val actualDefaultValue = 
dfDefaultValue.take(1)(0).get(0).asInstanceOf[String]
+      val expectedDefaultValue = 
dfDefaultValue.take(1)(0).get(1).asInstanceOf[GenericRowWithSchema].get(1).asInstanceOf[String]
+      assertEquals(expected, actual)
+      assertEquals(expectedDefaultValue, actualDefaultValue);
+    }
+  }
+
+  it("Passed ST_ForceCollection") {
+    var actual = sparkSession.sql("SELECT 
ST_NumGeometries(ST_ForceCollection(ST_GeomFromWKT('MULTIPOINT (30 10, 40 40, 
20 20, 10 30, 10 10, 20 50)')))").first().get(0)
+    assert(actual == 6)
+
+    actual = sparkSession.sql("SELECT 
ST_NumGeometries(ST_ForceCollection(ST_GeomFromWKT('POLYGON ((30 10, 40 40, 20 
40, 10 20, 30 10))')))").first().get(0)
+    assert(actual == 1)
+  }
+
   it("should pass ST_NRings") {
     val geomTestCases = Map(
       ("'POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))'") -> 1,


Reply via email to