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 540549b3a1 [GH-1979] Fix ST_Envelope and ST_Envelope_Aggr empty 
geometry handling (#2622)
540549b3a1 is described below

commit 540549b3a1f6369cf970cd30cb077eba0a5f4ab8
Author: Jia Yu <[email protected]>
AuthorDate: Sun Feb 8 00:26:15 2026 -0700

    [GH-1979] Fix ST_Envelope and ST_Envelope_Aggr empty geometry handling 
(#2622)
---
 .../org/apache/sedona/common/FunctionsTest.java    | 19 ++++++++++++++
 docs/api/flink/Aggregator.md                       |  2 +-
 .../api/snowflake/vector-data/AggregateFunction.md |  2 +-
 docs/api/sql/AggregateFunction.md                  |  2 +-
 .../sedona/flink/expressions/Aggregators.java      |  6 ++++-
 .../org/apache/sedona/flink/AggregatorTest.java    | 13 ++++++++++
 .../snowflake/snowsql/udtfs/ST_Envelope_Agg.java   |  9 +++++++
 .../snowflake/snowsql/udtfs/ST_Envelope_Aggr.java  |  9 +++++++
 .../expressions/AggregateFunctions.scala           |  2 +-
 .../sedona/sql/aggregateFunctionTestScala.scala    | 29 +++++++++++++++++++---
 10 files changed, 84 insertions(+), 9 deletions(-)

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 94bef0499e..895d24ead6 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -808,6 +808,25 @@ public class FunctionsTest extends TestBase {
     assertEquals(4326, concave.getSRID());
   }
 
+  @Test
+  public void envelopeEmptyGeometry() throws ParseException {
+    // ST_Envelope of EMPTY should return same-type EMPTY (matching PostGIS 
behavior)
+    Geometry emptyLineString = Constructors.geomFromWKT("LINESTRING EMPTY", 0);
+    Geometry result = Functions.envelope(emptyLineString);
+    assertTrue(result.isEmpty());
+    assertEquals("LineString", result.getGeometryType());
+
+    Geometry emptyPolygon = Constructors.geomFromWKT("POLYGON EMPTY", 0);
+    result = Functions.envelope(emptyPolygon);
+    assertTrue(result.isEmpty());
+    assertEquals("Polygon", result.getGeometryType());
+
+    Geometry emptyPoint = Constructors.geomFromWKT("POINT EMPTY", 0);
+    result = Functions.envelope(emptyPoint);
+    assertTrue(result.isEmpty());
+    assertEquals("Point", result.getGeometryType());
+  }
+
   @Test
   public void envelopeAndCentroidSRID() throws ParseException {
     Geometry geom = Constructors.geomFromWKT("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 
0))", 3857);
diff --git a/docs/api/flink/Aggregator.md b/docs/api/flink/Aggregator.md
index 94f252f617..368ba088b2 100644
--- a/docs/api/flink/Aggregator.md
+++ b/docs/api/flink/Aggregator.md
@@ -19,7 +19,7 @@
 
 ## ST_Envelope_Agg
 
-Introduction: Return the entire envelope boundary of all geometries in A
+Introduction: Return the entire envelope boundary of all geometries in A. 
Empty geometries and null values are skipped. If all inputs are empty or null, 
the result is null. This behavior is consistent with PostGIS's `ST_Extent`.
 
 Format: `ST_Envelope_Agg (A: geometryColumn)`
 
diff --git a/docs/api/snowflake/vector-data/AggregateFunction.md 
b/docs/api/snowflake/vector-data/AggregateFunction.md
index b8ba8ee4e2..91cff58578 100644
--- a/docs/api/snowflake/vector-data/AggregateFunction.md
+++ b/docs/api/snowflake/vector-data/AggregateFunction.md
@@ -22,7 +22,7 @@
 
 ## ST_Envelope_Agg
 
-Introduction: Return the entire envelope boundary of all geometries in A
+Introduction: Return the entire envelope boundary of all geometries in A. 
Empty geometries and null values are skipped. If all inputs are empty or null, 
the result is null. This behavior is consistent with PostGIS's `ST_Extent`.
 
 Format: `ST_Envelope_Agg (A:geometryColumn)`
 
diff --git a/docs/api/sql/AggregateFunction.md 
b/docs/api/sql/AggregateFunction.md
index 4d0165f2b3..5a2c222c68 100644
--- a/docs/api/sql/AggregateFunction.md
+++ b/docs/api/sql/AggregateFunction.md
@@ -51,7 +51,7 @@ SELECT category, ST_Collect_Agg(geom) FROM geometries GROUP 
BY category
 
 ## ST_Envelope_Agg
 
-Introduction: Return the entire envelope boundary of all geometries in A
+Introduction: Return the entire envelope boundary of all geometries in A. 
Empty geometries and null values are skipped. If all inputs are empty or null, 
the result is null. This behavior is consistent with PostGIS's `ST_Extent`.
 
 Format: `ST_Envelope_Agg (A: geometryColumn)`
 
diff --git 
a/flink/src/main/java/org/apache/sedona/flink/expressions/Aggregators.java 
b/flink/src/main/java/org/apache/sedona/flink/expressions/Aggregators.java
index 84cebd6adc..947507d16a 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Aggregators.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Aggregators.java
@@ -56,6 +56,7 @@ public class Aggregators {
         rawSerializer = GeometryTypeSerializer.class,
         bridgedTo = Geometry.class)
     public Geometry getValue(Accumulators.Envelope acc) {
+      if (acc.minX > acc.maxX) return null;
       return createPolygon(acc.minX, acc.minY, acc.maxX, acc.maxY);
     }
 
@@ -66,7 +67,10 @@ public class Aggregators {
                 rawSerializer = GeometryTypeSerializer.class,
                 bridgedTo = Geometry.class)
             Object o) {
-      Envelope envelope = ((Geometry) o).getEnvelopeInternal();
+      if (o == null) return;
+      Geometry geometry = (Geometry) o;
+      if (geometry.isEmpty()) return;
+      Envelope envelope = geometry.getEnvelopeInternal();
       acc.minX = Math.min(acc.minX, envelope.getMinX());
       acc.minY = Math.min(acc.minY, envelope.getMinY());
       acc.maxX = Math.max(acc.maxX, envelope.getMaxX());
diff --git a/flink/src/test/java/org/apache/sedona/flink/AggregatorTest.java 
b/flink/src/test/java/org/apache/sedona/flink/AggregatorTest.java
index 0220ff434d..4511788dc3 100644
--- a/flink/src/test/java/org/apache/sedona/flink/AggregatorTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/AggregatorTest.java
@@ -20,6 +20,7 @@ package org.apache.sedona.flink;
 
 import static org.apache.flink.table.api.Expressions.*;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import org.apache.flink.table.api.*;
 import org.apache.flink.types.Row;
@@ -46,6 +47,18 @@ public class AggregatorTest extends TestBase {
         last.getField(0).toString());
   }
 
+  @Test
+  public void testEnvelope_Aggr_EmptyGeometries() {
+    tableEnv.executeSql(
+        "CREATE OR REPLACE TEMPORARY VIEW empty_geom_view AS "
+            + "SELECT ST_GeomFromWKT(wkt) as geom FROM ("
+            + "VALUES ('POINT EMPTY'), ('LINESTRING EMPTY'), ('POLYGON EMPTY')"
+            + ") AS t(wkt)");
+    Table result = tableEnv.sqlQuery("SELECT ST_Envelope_Aggr(geom) FROM 
empty_geom_view");
+    Row last = last(result);
+    assertNull(last.getField(0));
+  }
+
   @Test
   public void testKNN() {
     Table pointTable = createPointTable(testDataSize);
diff --git 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/udtfs/ST_Envelope_Agg.java
 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/udtfs/ST_Envelope_Agg.java
index f02cbe1068..cbfea5d5cd 100644
--- 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/udtfs/ST_Envelope_Agg.java
+++ 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/udtfs/ST_Envelope_Agg.java
@@ -49,7 +49,13 @@ public class ST_Envelope_Agg {
   public ST_Envelope_Agg() {}
 
   public Stream<OutputRow> process(byte[] geom) throws ParseException {
+    if (geom == null) {
+      return Stream.empty();
+    }
     Geometry geometry = GeometrySerde.deserialize(geom);
+    if (geometry == null || geometry.isEmpty()) {
+      return Stream.empty();
+    }
     if (buffer == null) {
       buffer = geometry.getEnvelopeInternal();
     } else {
@@ -59,6 +65,9 @@ public class ST_Envelope_Agg {
   }
 
   public Stream<OutputRow> endPartition() {
+    if (buffer == null || buffer.isNull()) {
+      return Stream.of(new OutputRow(null));
+    }
     // Returns the value we initialized in the constructor.
     Polygon poly =
         geometryFactory.createPolygon(
diff --git 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/udtfs/ST_Envelope_Aggr.java
 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/udtfs/ST_Envelope_Aggr.java
index 10aa2d5afa..8dd00cc6f8 100644
--- 
a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/udtfs/ST_Envelope_Aggr.java
+++ 
b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/udtfs/ST_Envelope_Aggr.java
@@ -49,7 +49,13 @@ public class ST_Envelope_Aggr {
   public ST_Envelope_Aggr() {}
 
   public Stream<OutputRow> process(byte[] geom) throws ParseException {
+    if (geom == null) {
+      return Stream.empty();
+    }
     Geometry geometry = GeometrySerde.deserialize(geom);
+    if (geometry == null || geometry.isEmpty()) {
+      return Stream.empty();
+    }
     if (buffer == null) {
       buffer = geometry.getEnvelopeInternal();
     } else {
@@ -59,6 +65,9 @@ public class ST_Envelope_Aggr {
   }
 
   public Stream<OutputRow> endPartition() {
+    if (buffer == null || buffer.isNull()) {
+      return Stream.of(new OutputRow(null));
+    }
     // Returns the value we initialized in the constructor.
     Polygon poly =
         geometryFactory.createPolygon(
diff --git 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/AggregateFunctions.scala
 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/AggregateFunctions.scala
index ca169a2598..608ad5c141 100644
--- 
a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/AggregateFunctions.scala
+++ 
b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/AggregateFunctions.scala
@@ -130,7 +130,7 @@ private[apache] class ST_Envelope_Aggr
   val serde = ExpressionEncoder[Geometry]()
 
   def reduce(buffer: Option[EnvelopeBuffer], input: Geometry): 
Option[EnvelopeBuffer] = {
-    if (input == null) return buffer
+    if (input == null || input.isEmpty) return buffer
     val env = input.getEnvelopeInternal
     val envBuffer = EnvelopeBuffer(env.getMinX, env.getMaxX, env.getMinY, 
env.getMaxY)
     buffer match {
diff --git 
a/spark/common/src/test/scala/org/apache/sedona/sql/aggregateFunctionTestScala.scala
 
b/spark/common/src/test/scala/org/apache/sedona/sql/aggregateFunctionTestScala.scala
index cd9991b657..bba263d2d6 100644
--- 
a/spark/common/src/test/scala/org/apache/sedona/sql/aggregateFunctionTestScala.scala
+++ 
b/spark/common/src/test/scala/org/apache/sedona/sql/aggregateFunctionTestScala.scala
@@ -50,6 +50,29 @@ class aggregateFunctionTestScala extends TestBaseScala {
       assert(boundary.take(1)(0).get(0) == 
geometryFactory.createPolygon(coordinates))
     }
 
+    it("Passed ST_Envelope_aggr with empty geometries returns null") {
+      val emptyDf = sparkSession.sql(
+        "SELECT ST_GeomFromWKT(wkt) as geom FROM VALUES ('POINT EMPTY'), 
('LINESTRING EMPTY'), ('POLYGON EMPTY') AS t(wkt)")
+      emptyDf.createOrReplaceTempView("emptydf")
+      val result = sparkSession.sql("SELECT ST_Envelope_Aggr(emptydf.geom) 
FROM emptydf")
+      assert(result.take(1)(0).get(0) == null)
+    }
+
+    it("Passed ST_Envelope_aggr with mixed empty and non-empty geometries") {
+      val mixedDf = sparkSession.sql(
+        "SELECT ST_GeomFromWKT(wkt) as geom FROM VALUES ('POINT EMPTY'), 
('POINT (1 2)'), ('POINT (3 4)') AS t(wkt)")
+      mixedDf.createOrReplaceTempView("mixeddf")
+      val result = sparkSession.sql("SELECT ST_Envelope_Aggr(mixeddf.geom) 
FROM mixeddf")
+      val envelope = result.take(1)(0).get(0).asInstanceOf[Geometry]
+      assert(envelope != null)
+      assert(!envelope.isEmpty)
+      val env = envelope.getEnvelopeInternal
+      assert(env.getMinX == 1.0)
+      assert(env.getMinY == 2.0)
+      assert(env.getMaxX == 3.0)
+      assert(env.getMaxY == 4.0)
+    }
+
     it("Passed ST_Union_aggr") {
 
       var polygonCsvDf = sparkSession.read
@@ -423,8 +446,7 @@ class aggregateFunctionTestScala extends TestBaseScala {
       assert(result == null)
     }
 
-    it(
-      "ST_Envelope_Aggr should return empty geometry if inputs are mixed with 
null and empty geometries") {
+    it("ST_Envelope_Aggr should return null if inputs are mixed with null and 
empty geometries") {
       sparkSession
         .sql("""
           |SELECT explode(array(
@@ -441,8 +463,7 @@ class aggregateFunctionTestScala extends TestBaseScala {
         sparkSession.sql("SELECT ST_Envelope_Aggr(geom) FROM 
mixed_null_empty_envelope")
       val result = envelopeDF.take(1)(0).get(0)
 
-      assert(result != null)
-      assert(result.asInstanceOf[Geometry].isEmpty)
+      assert(result == null)
     }
   }
 

Reply via email to