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:12 UTC

(sedona) branch fix/ndims-empty-geometry created (now 36ca7286ee3)

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

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


      at 36ca7286ee3 [GH-3359] Return 2D results from ST_NDims, ST_CoordDim and ST_Zmflag for empty geometries

This branch includes the following new commits:

     new 36ca7286ee3 [GH-3359] Return 2D results from ST_NDims, ST_CoordDim and ST_Zmflag for empty geometries

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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

Posted by ji...@apache.org.
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")