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/02/07 21:28:26 UTC

(sedona) branch fix/1979-envelope-empty updated (ca1ade779f -> cf0ccf93a2)

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

jiayu pushed a change to branch fix/1979-envelope-empty
in repository https://gitbox.apache.org/repos/asf/sedona.git


 discard ca1ade779f [GH-1979] Fix ST_Envelope and ST_Envelope_Aggr empty geometry handling
     new cf0ccf93a2 [GH-1979] Fix ST_Envelope and ST_Envelope_Aggr empty geometry handling

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (ca1ade779f)
            \
             N -- N -- N   refs/heads/fix/1979-envelope-empty (cf0ccf93a2)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

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.


Summary of changes:
 .../java/org/apache/sedona/flink/AggregatorTest.java   | 14 ++++++++++++++
 .../apache/sedona/sql/aggregateFunctionTestScala.scala | 18 ++++++++++++++++++
 2 files changed, 32 insertions(+)


(sedona) 01/01: [GH-1979] Fix ST_Envelope and ST_Envelope_Aggr empty geometry handling

Posted by ji...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jiayu pushed a commit to branch fix/1979-envelope-empty
in repository https://gitbox.apache.org/repos/asf/sedona.git

commit cf0ccf93a20b176a3b0ed8ec167e0bc2bff69ead
Author: Jia Yu <ji...@apache.org>
AuthorDate: Sat Feb 7 13:14:42 2026 -0800

    [GH-1979] Fix ST_Envelope and ST_Envelope_Aggr empty geometry handling
    
    ST_Envelope (scalar): Already correctly returns same-type EMPTY geometry for
    empty inputs (matching PostGIS). Added test to verify this behavior.
    
    ST_Envelope_Aggr (aggregate): Skip empty geometries during accumulation and
    return null when all inputs are empty, matching PostGIS ST_Extent behavior.
    Fixed in Spark (AggregateFunctions.scala), Flink (Aggregators.java), and
    Snowflake (ST_Envelope_Aggr.java, ST_Envelope_Agg.java).
    
    Added tests in Spark (aggregateFunctionTestScala: all-empty returns null,
    mixed empty/non-empty preserves valid envelope), Flink (AggregatorTest:
    all-empty returns null), and common (FunctionsTest: scalar envelope with
    empty geometries).
    
    Fixes #1979
---
 .../java/org/apache/sedona/common/FunctionsTest.java  | 19 +++++++++++++++++++
 .../apache/sedona/flink/expressions/Aggregators.java  |  5 ++++-
 .../java/org/apache/sedona/flink/AggregatorTest.java  | 14 ++++++++++++++
 .../snowflake/snowsql/udtfs/ST_Envelope_Agg.java      |  6 ++++++
 .../snowflake/snowsql/udtfs/ST_Envelope_Aggr.java     |  6 ++++++
 .../sedona_sql/expressions/AggregateFunctions.scala   |  2 +-
 .../sedona/sql/aggregateFunctionTestScala.scala       | 18 ++++++++++++++++++
 7 files changed, 68 insertions(+), 2 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 e379e67d6d..5c94dca7c7 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/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..244d5f4b6c 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,9 @@ public class Aggregators {
                 rawSerializer = GeometryTypeSerializer.class,
                 bridgedTo = Geometry.class)
             Object o) {
-      Envelope envelope = ((Geometry) o).getEnvelopeInternal();
+      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..efed1e0303 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,19 @@ public class AggregatorTest extends TestBase {
         last.getField(0).toString());
   }
 
+  @Test
+  public void testEnvelop_Aggr_EmptyGeometries() {
+    tableEnv.executeSql(
+        "CREATE 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));
+    tableEnv.executeSql("DROP VIEW empty_geom_view");
+  }
+
   @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..4a5e6bbea9 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
@@ -50,6 +50,9 @@ public class ST_Envelope_Agg {
 
   public Stream<OutputRow> process(byte[] geom) throws ParseException {
     Geometry geometry = GeometrySerde.deserialize(geom);
+    if (geometry.isEmpty()) {
+      return Stream.empty();
+    }
     if (buffer == null) {
       buffer = geometry.getEnvelopeInternal();
     } else {
@@ -59,6 +62,9 @@ public class ST_Envelope_Agg {
   }
 
   public Stream<OutputRow> endPartition() {
+    if (buffer == null || buffer.isNull()) {
+      return Stream.empty();
+    }
     // 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..b1d05e437c 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
@@ -50,6 +50,9 @@ public class ST_Envelope_Aggr {
 
   public Stream<OutputRow> process(byte[] geom) throws ParseException {
     Geometry geometry = GeometrySerde.deserialize(geom);
+    if (geometry.isEmpty()) {
+      return Stream.empty();
+    }
     if (buffer == null) {
       buffer = geometry.getEnvelopeInternal();
     } else {
@@ -59,6 +62,9 @@ public class ST_Envelope_Aggr {
   }
 
   public Stream<OutputRow> endPartition() {
+    if (buffer == null || buffer.isNull()) {
+      return Stream.empty();
+    }
     // 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..f38f87f07f 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,24 @@ 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)
+    }
+
     it("Passed ST_Union_aggr") {
 
       var polygonCsvDf = sparkSession.read