You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2026/09/13 06:52:13 UTC

(sedona) 01/01: [GH-3359] Return 2D results from ST_NDims, ST_CoordDim and ST_Zmflag for empty geometries

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

jiayuasu pushed a commit to branch fix/ndims-empty-geometry
in repository https://gitbox.apache.org/repos/asf/sedona.git

commit 36ca7286ee30121442eb53339307b6cef25992cd
Author: Jia Yu <ji...@apache.org>
AuthorDate: Sat Sep 12 23:52:01 2026 -0700

    [GH-3359] Return 2D results from ST_NDims, ST_CoordDim and ST_Zmflag for empty geometries
    
    Functions.nDims and Functions.zmFlag dereferenced Geometry.getCoordinate(),
    which JTS returns as null for empty geometries, so any empty input failed
    the whole query with a NullPointerException. Guard the null coordinate and
    report empties as 2D (nDims = 2, zmFlag = 0), matching PostGIS and the
    existing ST_HasZ / ST_HasM behavior. Spark, Flink and Snowflake all
    delegate to the common implementation.
---
 .../java/org/apache/sedona/common/Functions.java   |  8 ++++
 .../org/apache/sedona/common/FunctionsTest.java    | 46 ++++++++++++++++++++++
 docs/api/sql/Geometry-Accessors/ST_CoordDim.md     | 14 ++++++-
 docs/api/sql/Geometry-Accessors/ST_NDims.md        | 14 ++++++-
 docs/api/sql/Geometry-Accessors/ST_Zmflag.md       | 16 +++++++-
 docs/setup/release-notes.md                        |  4 ++
 .../org/apache/sedona/sql/functionTestScala.scala  | 19 +++++++++
 7 files changed, 118 insertions(+), 3 deletions(-)

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 011c0bf4e09..e3057e5ae12 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1048,6 +1048,10 @@ public class Functions {
   public static int nDims(Geometry geometry) {
     int count_dimension = 0;
     Coordinate geom = geometry.getCoordinate();
+    if (geom == null) {
+      // Empty geometries have no coordinate to inspect; report them as 2D like PostGIS does.
+      return 2;
+    }
     Double x_cord = geom.getX();
     Double y_cord = geom.getY();
     Double z_cord = geom.getZ();
@@ -1300,6 +1304,10 @@ public class Functions {
 
   public static int zmFlag(Geometry geom) {
     Coordinate coords = geom.getCoordinate();
+    if (coords == null) {
+      // Empty geometries have no coordinate to inspect; report them as 2D like PostGIS does.
+      return 0;
+    }
     boolean hasZ = !Double.isNaN(coords.getZ());
     boolean hasM = !Double.isNaN(coords.getM());
     if (hasM && hasZ) {
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 9e3a7d6b005..0bd0c833090 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -2819,6 +2819,34 @@ public class FunctionsTest extends TestBase {
         });
   }
 
+  @Test
+  public void nDims() throws ParseException {
+    assertEquals(2, Functions.nDims(Constructors.geomFromWKT("POINT (1 2)", 0)));
+    assertEquals(3, Functions.nDims(Constructors.geomFromWKT("LINESTRING (1 2 3, 4 5 6)", 0)));
+    assertEquals(
+        3,
+        Functions.nDims(Constructors.geomFromWKT("POLYGON M ((1 2 3, 3 4 3, 5 6 3, 1 2 3))", 0)));
+    assertEquals(4, Functions.nDims(Constructors.geomFromWKT("POINT ZM (1 2 3 4)", 0)));
+  }
+
+  @Test
+  public void nDimsEmptyGeometries() throws ParseException {
+    String[] emptyWkts = {
+      "POINT EMPTY",
+      "LINESTRING EMPTY",
+      "POLYGON EMPTY",
+      "MULTIPOINT EMPTY",
+      "MULTILINESTRING EMPTY",
+      "MULTIPOLYGON EMPTY",
+      "GEOMETRYCOLLECTION EMPTY"
+    };
+    for (String wkt : emptyWkts) {
+      assertEquals(wkt, 2, Functions.nDims(Constructors.geomFromWKT(wkt, 0)));
+    }
+    assertEquals(2, Functions.nDims(GEOMETRY_FACTORY.createPoint()));
+    assertEquals(2, Functions.nDims(GEOMETRY_FACTORY.createGeometryCollection()));
+  }
+
   @Test
   public void force3DObject2D() {
     int expectedDims = 3;
@@ -4852,6 +4880,24 @@ public class FunctionsTest extends TestBase {
     assertEquals(_4D, Functions.zmFlag(geom));
   }
 
+  @Test
+  public void testZmFlagEmptyGeometries() throws ParseException {
+    String[] emptyWkts = {
+      "POINT EMPTY",
+      "LINESTRING EMPTY",
+      "POLYGON EMPTY",
+      "MULTIPOINT EMPTY",
+      "MULTILINESTRING EMPTY",
+      "MULTIPOLYGON EMPTY",
+      "GEOMETRYCOLLECTION EMPTY"
+    };
+    for (String wkt : emptyWkts) {
+      assertEquals(wkt, 0, Functions.zmFlag(Constructors.geomFromWKT(wkt, 0)));
+    }
+    assertEquals(0, Functions.zmFlag(GEOMETRY_FACTORY.createPoint()));
+    assertEquals(0, Functions.zmFlag(GEOMETRY_FACTORY.createGeometryCollection()));
+  }
+
   @Test
   public void hausdorffDistanceDefaultGeom2D() throws Exception {
     Polygon polygon1 =
diff --git a/docs/api/sql/Geometry-Accessors/ST_CoordDim.md b/docs/api/sql/Geometry-Accessors/ST_CoordDim.md
index 64e6bc61af4..48ca773fa69 100644
--- a/docs/api/sql/Geometry-Accessors/ST_CoordDim.md
+++ b/docs/api/sql/Geometry-Accessors/ST_CoordDim.md
@@ -19,7 +19,7 @@
 
 # ST_CoordDim
 
-Introduction: Returns the coordinate dimensions of the geometry. It is an alias of `ST_NDims`.
+Introduction: Returns the coordinate dimensions of the geometry. It is an alias of `ST_NDims`. Empty geometries return 2.
 
 ![ST_CoordDim](../../../image/ST_CoordDim/ST_CoordDim.svg "ST_CoordDim")
 
@@ -52,3 +52,15 @@ Output:
 ```
 2
 ```
+
+Spark SQL Example with an empty geometry:
+
+```sql
+SELECT ST_CoordDim(ST_GeomFromWKT('LINESTRING EMPTY'))
+```
+
+Output:
+
+```
+2
+```
diff --git a/docs/api/sql/Geometry-Accessors/ST_NDims.md b/docs/api/sql/Geometry-Accessors/ST_NDims.md
index 8a678c79fa3..a58d459c8f0 100644
--- a/docs/api/sql/Geometry-Accessors/ST_NDims.md
+++ b/docs/api/sql/Geometry-Accessors/ST_NDims.md
@@ -19,7 +19,7 @@
 
 # ST_NDims
 
-Introduction: Returns the coordinate dimension of the geometry.
+Introduction: Returns the coordinate dimension of the geometry. Empty geometries return 2.
 
 ![ST_NDims](../../../image/ST_NDims/ST_NDims.svg "ST_NDims")
 
@@ -52,3 +52,15 @@ Output:
 ```
 2
 ```
+
+Spark SQL example with an empty geometry:
+
+```sql
+SELECT ST_NDims(ST_GeomFromText('POINT EMPTY'))
+```
+
+Output:
+
+```
+2
+```
diff --git a/docs/api/sql/Geometry-Accessors/ST_Zmflag.md b/docs/api/sql/Geometry-Accessors/ST_Zmflag.md
index 91474c1b944..05096c8a1d9 100644
--- a/docs/api/sql/Geometry-Accessors/ST_Zmflag.md
+++ b/docs/api/sql/Geometry-Accessors/ST_Zmflag.md
@@ -21,7 +21,7 @@
 
 Introduction: Returns a code indicating the Z and M coordinate dimensions present in the input geometry.
 
-Values are: 0 = 2D, 1 = 3D-M, 2 = 3D-Z, 3 = 4D.
+Values are: 0 = 2D, 1 = 3D-M, 2 = 3D-Z, 3 = 4D. Empty geometries return 0.
 
 ![ST_Zmflag](../../../image/ST_Zmflag/ST_Zmflag.svg "ST_Zmflag")
 
@@ -58,3 +58,17 @@ Output:
 ```
 3
 ```
+
+SQL Example
+
+```sql
+SELECT ST_Zmflag(
+        ST_GeomFromWKT('POLYGON EMPTY')
+)
+```
+
+Output:
+
+```
+0
+```
diff --git a/docs/setup/release-notes.md b/docs/setup/release-notes.md
index c1d7641ab26..d87259d9f29 100644
--- a/docs/setup/release-notes.md
+++ b/docs/setup/release-notes.md
@@ -69,6 +69,10 @@
 
 ### Bug Fixes
 
+#### Sedona SQL
+
+* [<a href='https://github.com/apache/sedona/issues/3359'>GH-3359</a>] - Return 2D results from `ST_NDims`, `ST_CoordDim` and `ST_Zmflag` for empty geometries instead of throwing
+
 #### GeoPandas API
 
 * [<a href='https://github.com/apache/sedona/issues/3269'>GH-3269</a>] - Warn when `geom_equals` or `geom_equals_exact` compares geometry operands with mismatched CRSs
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 6e0bbd5c69b..1bf7154b4a9 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
@@ -1124,6 +1124,16 @@ class functionTestScala
       assert(test.take(1)(0).get(0).asInstanceOf[Int] == 4)
     }
 
+    it("Passed ST_NDims and ST_CoordDim with empty geometries") {
+      Seq("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY")
+        .foreach { wkt =>
+          val test = sparkSession.sql(
+            s"SELECT ST_NDims(ST_GeomFromWKT('$wkt')), ST_CoordDim(ST_GeomFromWKT('$wkt'))")
+          assert(test.first().getInt(0) == 2)
+          assert(test.first().getInt(1) == 2)
+        }
+    }
+
     it("Passed ST_GeometryType") {
       var test = sparkSession.sql(
         "SELECT ST_GeometryType(ST_GeomFromText('LINESTRING(77.29 29.07,77.42 29.26,77.27 29.31,77.29 29.07)'))")
@@ -1506,6 +1516,15 @@ class functionTestScala
       assert(actual == 3)
     }
 
+    it("Passed ST_Zmflag with empty geometries") {
+      Seq("POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY")
+        .foreach { wkt =>
+          val actual =
+            sparkSession.sql(s"SELECT ST_Zmflag(ST_GeomFromWKT('$wkt'))").first().get(0)
+          assert(actual == 0)
+        }
+    }
+
     it("Should pass ST_StartPoint function") {
       Given("Polygon Data Frame, Point DataFrame, LineString Data Frame")