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 ca1ade779f2937c1ac3d1e0f6a795c8bbf2a4c67 Author: Jia Yu <[email protected]> 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). Fixes #1979 --- .../java/org/apache/sedona/common/FunctionsTest.java | 19 +++++++++++++++++++ .../apache/sedona/flink/expressions/Aggregators.java | 5 ++++- .../snowflake/snowsql/udtfs/ST_Envelope_Agg.java | 6 ++++++ .../snowflake/snowsql/udtfs/ST_Envelope_Aggr.java | 6 ++++++ .../sedona_sql/expressions/AggregateFunctions.scala | 2 +- 5 files changed, 36 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/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 {
