ErikBPF commented on code in PR #5903:
URL: https://github.com/apache/datafusion-comet/pull/5903#discussion_r4365068125


##########
spark/src/test/scala/org/apache/comet/exec/CometAggregateSuite.scala:
##########
@@ -366,6 +366,54 @@ class CometAggregateSuite extends CometTestBase with 
AdaptiveSparkPlanHelper {
       classOf[LocalTableScanExec])
   }
 
+  test("collect_list and collect_set over non-nullable nested fields") {
+    withTempPath { path =>
+      // Keep every group in one input and final partition so aggregation 
outgrows its memory pool.
+      spark
+        .createDataFrame((0 until 32768).map { i =>
+          (i % 4096, i % 8192, s"${i % 8192}-" + "x" * 256)
+        })
+        .coalesce(1)
+        .write
+        .parquet(path.getAbsolutePath)
+      withParquetTable(path.getAbsolutePath, "tbl") {
+        // The literal and coalesce create required fields after the Parquet 
scan, which
+        // otherwise widens field nullability. Sorting removes collect order 
differences.
+        val struct = "named_struct('flag', true, 'id', coalesce(_2, 0), 
'value', _3)"
+        // Ungrouped aggregation uses Accumulator rather than 
GroupsAccumulator and fails
+        // on mismatched nested nullability even without spilling.
+        checkSparkAnswerAndOperator(
+          sql(s"""
+          SELECT sort_array(collect_list(s)), sort_array(collect_set(s))
+          FROM (SELECT $struct AS s FROM tbl WHERE _1 < 8)"""),
+          Seq(classOf[CometHashAggregateExec]))
+
+        withSQLConf(
+          SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+          SQLConf.SHUFFLE_PARTITIONS.key -> "1",
+          CometConf.COMET_BATCH_SIZE.key -> "1024",
+          CometConf.COMET_OFFHEAP_MEMORY_POOL_FRACTION.key -> "0.002",
+          CometConf.COMET_RESPECT_DATAFUSION_CONFIGS.key -> "true",
+          "spark.comet.datafusion.execution.sort_spill_reservation_bytes" -> 
"65536") {
+          Seq(struct, s"array($struct)").foreach { value =>
+            val (_, cometPlan) = checkSparkAnswerAndOperator(
+              sql(s"""
+              SELECT _1, sort_array(collect_list(s)), 
sort_array(collect_set(s))
+              FROM (
+                SELECT _1, $value AS s
+                FROM tbl
+              ) GROUP BY _1"""),
+              Seq(classOf[CometHashAggregateExec]))
+            val aggregates = collect(cometPlan) { case agg: 
CometHashAggregateExec => agg }
+            assert(
+              aggregates.map(_.metrics("spill_count").value).sum > 0L,
+              s"Expected the native collection aggregate to 
spill:\n$cometPlan")

Review Comment:
   You are right that summing `spill_count` over every `CometHashAggregateExec` 
does not show which stage spilled, and I measured it before changing anything. 
On current `main` this fixture spills in neither stage: printing `spill_count` 
for every aggregate with the helper's settings alone, and again with 
`COMET_RESPECT_DATAFUSION_CONFIGS` plus `sort_spill_reservation_bytes=65536`, 
gives `Final = 0` and `Partial = 0` in both runs. Shrinking the pool to 400 KiB 
(fraction `0.0002`) does not spill either; the Final aggregate fails the 
allocation instead: `Failed to acquire 1122504 bytes ... fair limit is 429496 
bytes`.
   
   So neither the old assertion nor `spilledAggregates(df, Final)` can pass on 
this fixture, and the two extra configs buy nothing. In `8c5700c5a` I removed 
the spill assertion and the hand-written config block. The grouped test keeps 
both nested shapes plus the native-execution and Spark-answer assertions, which 
is what actually reproduces #5239: the mismatch fires when the Final aggregate 
materializes the collected state, and it needs no spilling. The RED evidence is 
in the test comment: removing `asNullable` fails with `column types must match 
schema types`.
   



##########
spark/src/test/scala/org/apache/comet/exec/CometAggregateSuite.scala:
##########
@@ -366,6 +366,54 @@ class CometAggregateSuite extends CometTestBase with 
AdaptiveSparkPlanHelper {
       classOf[LocalTableScanExec])
   }
 
+  test("collect_list and collect_set over non-nullable nested fields") {
+    withTempPath { path =>
+      // Keep every group in one input and final partition so aggregation 
outgrows its memory pool.
+      spark
+        .createDataFrame((0 until 32768).map { i =>
+          (i % 4096, i % 8192, s"${i % 8192}-" + "x" * 256)
+        })
+        .coalesce(1)
+        .write
+        .parquet(path.getAbsolutePath)
+      withParquetTable(path.getAbsolutePath, "tbl") {
+        // The literal and coalesce create required fields after the Parquet 
scan, which
+        // otherwise widens field nullability. Sorting removes collect order 
differences.
+        val struct = "named_struct('flag', true, 'id', coalesce(_2, 0), 
'value', _3)"
+        // Ungrouped aggregation uses Accumulator rather than 
GroupsAccumulator and fails
+        // on mismatched nested nullability even without spilling.
+        checkSparkAnswerAndOperator(
+          sql(s"""
+          SELECT sort_array(collect_list(s)), sort_array(collect_set(s))
+          FROM (SELECT $struct AS s FROM tbl WHERE _1 < 8)"""),
+          Seq(classOf[CometHashAggregateExec]))

Review Comment:
   Done in `8c5700c5a`. The ungrouped case moved out of the Scala test into 
`collect_list.sql` and `collect_set.sql`: `cl_src_struct_required` / 
`cs_src_struct_required` build the required-fields struct and 
`array(named_struct(...))` after the Parquet scan, and both functions run 
through `sort_array`. The harness's default `query` directive asserts the Spark 
answer and native execution, so it covers the same `Accumulator` path without 
the memory settings that the Scala copy needed and did not use.
   
   Results: `collect_list` 2/2, `collect_set` 8/8. With `asNullable` removed, 
both `collect_list` cases fail with `column types must match schema types`; the 
ungrouped `collect_set` cases still pass, so that half is coverage rather than 
a regression gate. The grouped Scala case remains the gate for both functions.
   



##########
spark/src/test/scala/org/apache/comet/exec/CometAggregateSuite.scala:
##########
@@ -188,6 +188,46 @@ class CometAggregateSuite extends CometTestBase with 
AdaptiveSparkPlanHelper {
       classOf[LocalTableScanExec])
   }
 
+  test("collect_list and collect_set over non-nullable nested fields survive 
spilling") {
+    withTempPath { path =>
+      // Keep every group in one input and final partition so aggregation 
outgrows its memory pool.
+      spark
+        .createDataFrame((0 until 32768).map { i =>
+          (i % 4096, i % 8192, s"${i % 8192}-" + "x" * 256)
+        })
+        .coalesce(1)
+        .write
+        .parquet(path.getAbsolutePath)
+      withParquetTable(path.getAbsolutePath, "tbl") {
+        withSQLConf(
+          SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+          SQLConf.SHUFFLE_PARTITIONS.key -> "1",
+          CometConf.COMET_BATCH_SIZE.key -> "1024",
+          CometConf.COMET_OFFHEAP_MEMORY_POOL_FRACTION.key -> "0.002",
+          CometConf.COMET_RESPECT_DATAFUSION_CONFIGS.key -> "true",
+          "spark.comet.datafusion.execution.sort_spill_reservation_bytes" -> 
"65536") {
+          // The literal and coalesce create required fields after the Parquet 
scan, which
+          // otherwise widens field nullability. Sorting removes collect order 
differences.
+          val struct = "named_struct('flag', true, 'id', coalesce(_2, 0), 
'value', _3)"
+          Seq(struct, s"array($struct)").foreach { value =>

Review Comment:
   Updated in `8c5700c5a`: your ungrouped query now lives in `collect_list.sql` 
/ `collect_set.sql` rather than in the Scala test, following comphead's 
placement suggestion. It covers the required-fields struct and `array<struct>` 
for both functions, and the SQL harness still asserts the Spark answer and 
native execution.
   
   RED check on that head: removing `asNullable` makes both `collect_list` 
cases fail with the nullability mismatch. The ungrouped `collect_set` cases 
pass either way, so they are coverage rather than a gate; the grouped Scala 
case exercises both functions and does fail without the fix.
   



-- 
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