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.

@@ -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.

@@ -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.

@@ -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")