nahtonaj commented on code in PR #59048:
URL: https://github.com/apache/spark/pull/59048#discussion_r4168466602


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggUtils.scala:
##########
@@ -462,6 +463,13 @@ object AggUtils {
       val aggregateAttributes = aggregateExpressions.map(_.resultAttribute)
 
       ProjectAggregationBufferExec(
+        // A global aggregation must initialize its buffer exactly once per 
empty batch. Planning

Review Comment:
   Agreed, thanks. I moved the empty-batch initialization into 
`StatefulStreamlineAggregateExec`. For a global aggregation that operator 
already requires `AllTuples`, so it runs on a single partition; when that 
partition has no input it merges the empty grouping key against state 
(`processEmptyGroupingKey`), producing the stored value or the initialized 
buffer. `ProjectAggregationBufferExec` and `AggUtils` go back to initializing 
buffers on every partition, so there is no extra single-partition step, which 
matches how `HashAggregateExec` handles it.



##########
sql/core/src/test/scala/org/apache/spark/sql/streaming/StreamlineStreamingAggregationRealTimeSuite.scala:
##########
@@ -131,4 +134,46 @@ class StreamlineStreamingAggregationRealTimeSuite extends 
StreamRealTimeModeSuit
       }
     )
   }
+
+  testWithAllStateVersions("global aggregation over empty input emits the 
initialized row") {
+    val inputData = LowLatencyMemoryStream[Int]
+    val sink = new ContinuousMemorySink()
+
+    val agg = inputData.toDF()
+      .filter($"value" < 0)
+      .agg(count("*").as("count"), sum("value").as("sum"))
+
+    // Update mode may re-emit the current row for no-data batches in 
real-time mode, so the
+    // assertions pin the value of every emitted row instead of counting rows.
+    testStream(agg, OutputMode.Update, sink = sink)(
+      StartStream(),
+      AddData(inputData, 1, 2),
+      Execute { _ =>
+        // Every row is filtered out, so the aggregation input is empty for 
this batch: only the
+        // initialized value may be emitted, as the ordinary plan does.
+        eventually(timeout(60.seconds)) {
+          assert(sink.allData.nonEmpty, "expected the initialized row for the 
empty batch")
+        }
+        assert(sink.allData.forall(_ == Row(0L, null)),
+          s"unexpected rows for the empty batch: ${sink.allData}")
+        sink.clear()

Review Comment:
   Good catch. I removed the `clear()`. The sink does not order rows across 
batches, so the test no longer relies on arrival order: it waits until the 
accumulated row `(1, -5)` has been emitted at least twice. Only the batch with 
`-5` and later batches can produce it, so the second one proves a later empty 
batch kept the accumulated value; a reset would emit the initialized row 
instead. It also checks that every emitted row is either the initialized or the 
accumulated value.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to