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 static int nPoints(Geometry geometry) {
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 static Geometry delaunayTriangle(Geometry geometry, double tolerance, int
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 void simplifyPolygonHull() throws ParseException {
});
}
+ @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 void testZmFlag() throws ParseException {
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
+
+* [GH-3359] - Return 2D results from `ST_NDims`, `ST_CoordDim` and `ST_Zmflag` for empty geometries instead of throwing
+
#### GeoPandas API
* [GH-3269] - 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")