Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
834b5fb
Add native collect_list aggregate support
peterxcli Jun 25, 2026
ed83aa5
add test
peterxcli Jun 26, 2026
2ee3a90
Constructs inner collect_list/collect_set accumulators with the original
peterxcli Jun 26, 2026
5cf3767
Merge branch 'main' into multi-stage-distinct-combined-collect-list-c…
peterxcli Jun 26, 2026
4f493c4
Merge branch 'main' of https://github.com/apache/datafusion-comet int…
peterxcli Jun 26, 2026
dbf7ba5
Fixes Spark test plan-shape assertions for native `collect_list`.
peterxcli Jun 27, 2026
fb04505
Merge branch 'main' into multi-stage-distinct-combined-collect-list-c…
peterxcli Jun 27, 2026
1a5bebd
Merge branch 'main' into multi-stage-distinct-combined-collect-list-c…
peterxcli Jun 27, 2026
c431f30
Merge branch 'multi-stage-distinct-combined-collect-list-collect-set'…
peterxcli Jun 27, 2026
9af9156
fix preitter?
peterxcli Jun 27, 2026
0d10df1
fallback collect partial merge when child is spark buffer
peterxcli Jun 28, 2026
41a2a80
Decode Spark collect partial merge state
peterxcli Jun 28, 2026
165325b
fix lint
peterxcli Jun 28, 2026
d15be96
fix build
peterxcli Jun 29, 2026
36e928d
simplify
peterxcli Jul 1, 2026
917f7a7
Merge branch 'main' into multi-stage-distinct-combined-collect-list-c…
peterxcli Jul 1, 2026
4e4d29d
Merge branch 'main' into multi-stage-distinct-combined-collect-list-c…
peterxcli Jul 8, 2026
c3b3587
Merge branch 'main' into multi-stage-distinct-combined-collect-list-c…
peterxcli Jul 12, 2026
ed2d365
Merge branch 'main' into multi-stage-distinct-combined-collect-list-c…
peterxcli Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions dev/diffs/3.4.3.diff
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,14 @@ index cf40e944c09..bdd5be4f462 100644

test("A cached table preserves the partitioning and ordering of its cached SparkPlan") {
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
index 1cc09c3d7fc..a84939c045b 100644
index 1cc09c3d7fc..908693e9c12 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
@@ -26,8 +26,9 @@ import org.scalatest.matchers.must.Matchers.the
@@ -24,10 +24,12 @@ import scala.util.Random
import org.scalatest.matchers.must.Matchers.the

import org.apache.spark.SparkException
+import org.apache.spark.sql.comet.CometHashAggregateExec
import org.apache.spark.sql.execution.WholeStageCodegenExec
import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
+import org.apache.spark.sql.comet.CometHashAggregateExec
Expand All @@ -274,7 +277,7 @@ index 1cc09c3d7fc..a84939c045b 100644
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._
import org.apache.spark.sql.internal.SQLConf
@@ -691,7 +692,9 @@ class DataFrameAggregateSuite extends QueryTest
@@ -691,7 +693,9 @@ class DataFrameAggregateSuite extends QueryTest
case _ => false
}.isDefined)
} else {
Expand All @@ -285,7 +288,23 @@ index 1cc09c3d7fc..a84939c045b 100644
}

// test case for ObjectHashAggregate and SortAggregate
@@ -755,7 +758,7 @@ class DataFrameAggregateSuite extends QueryTest
@@ -700,7 +704,8 @@ class DataFrameAggregateSuite extends QueryTest
val objHashAggOrSortAggPlan =
stripAQEPlan(objHashAggOrSortAggDF.queryExecution.executedPlan)
if (useObjectHashAgg) {
- assert(objHashAggOrSortAggPlan.isInstanceOf[ObjectHashAggregateExec])
+ assert(objHashAggOrSortAggPlan.isInstanceOf[ObjectHashAggregateExec] ||
+ objHashAggOrSortAggPlan.isInstanceOf[CometHashAggregateExec])
} else {
assert(objHashAggOrSortAggPlan.isInstanceOf[SortAggregateExec])
}
@@ -750,12 +755,12 @@ class DataFrameAggregateSuite extends QueryTest
assert(sortAggPlans.isEmpty)

val objHashAggPlans = collect(aggPlan) {
- case objHashAgg: ObjectHashAggregateExec => objHashAgg
+ case agg @ (_: ObjectHashAggregateExec | _: CometHashAggregateExec) => agg
}
assert(objHashAggPlans.nonEmpty)

val exchangePlans = collect(aggPlan) {
Expand Down
27 changes: 23 additions & 4 deletions dev/diffs/3.5.8.diff
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,14 @@ index e5494726695..00937f025c2 100644

test("A cached table preserves the partitioning and ordering of its cached SparkPlan") {
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
index 6f3090d8908..a0e9309888d 100644
index 6f3090d8908..5db376a3754 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
@@ -27,8 +27,9 @@ import org.apache.spark.{SparkException, SparkThrowable}
@@ -25,10 +25,12 @@ import org.scalatest.matchers.must.Matchers.the

import org.apache.spark.{SparkException, SparkThrowable}
import org.apache.spark.sql.catalyst.plans.logical.Expand
+import org.apache.spark.sql.comet.CometHashAggregateExec
import org.apache.spark.sql.execution.WholeStageCodegenExec
import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
+import org.apache.spark.sql.comet.CometHashAggregateExec
Expand All @@ -255,7 +258,7 @@ index 6f3090d8908..a0e9309888d 100644
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._
import org.apache.spark.sql.internal.SQLConf
@@ -729,7 +730,9 @@ class DataFrameAggregateSuite extends QueryTest
@@ -729,7 +731,9 @@ class DataFrameAggregateSuite extends QueryTest
case _ => false
}.isDefined)
} else {
Expand All @@ -266,7 +269,23 @@ index 6f3090d8908..a0e9309888d 100644
}

// test case for ObjectHashAggregate and SortAggregate
@@ -793,7 +796,7 @@ class DataFrameAggregateSuite extends QueryTest
@@ -738,7 +742,8 @@ class DataFrameAggregateSuite extends QueryTest
val objHashAggOrSortAggPlan =
stripAQEPlan(objHashAggOrSortAggDF.queryExecution.executedPlan)
if (useObjectHashAgg) {
- assert(objHashAggOrSortAggPlan.isInstanceOf[ObjectHashAggregateExec])
+ assert(objHashAggOrSortAggPlan.isInstanceOf[ObjectHashAggregateExec] ||
+ objHashAggOrSortAggPlan.isInstanceOf[CometHashAggregateExec])
} else {
assert(objHashAggOrSortAggPlan.isInstanceOf[SortAggregateExec])
}
@@ -788,12 +793,12 @@ class DataFrameAggregateSuite extends QueryTest
assert(sortAggPlans.isEmpty)

val objHashAggPlans = collect(aggPlan) {
- case objHashAgg: ObjectHashAggregateExec => objHashAgg
+ case agg @ (_: ObjectHashAggregateExec | _: CometHashAggregateExec) => agg
}
assert(objHashAggPlans.nonEmpty)

val exchangePlans = collect(aggPlan) {
Expand Down
27 changes: 23 additions & 4 deletions dev/diffs/4.0.2.diff
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,14 @@ index 0f42502f1d9..e9ff802141f 100644

withTempView("t0", "t1", "t2") {
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
index 9db406ff12f..19b4bb8e39d 100644
index 9db406ff12f..d08b5302103 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
@@ -29,8 +29,9 @@ import org.apache.spark.sql.catalyst.util.AUTO_GENERATED_ALIAS
@@ -27,10 +27,12 @@ import org.apache.spark.{SparkArithmeticException, SparkRuntimeException}
import org.apache.spark.sql.catalyst.plans.logical.Expand
import org.apache.spark.sql.catalyst.util.AUTO_GENERATED_ALIAS
import org.apache.spark.sql.errors.DataTypeErrors.toSQLId
+import org.apache.spark.sql.comet.CometHashAggregateExec
import org.apache.spark.sql.execution.WholeStageCodegenExec
import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
+import org.apache.spark.sql.comet.CometHashAggregateExec
Expand All @@ -392,7 +395,7 @@ index 9db406ff12f..19b4bb8e39d 100644
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._
import org.apache.spark.sql.internal.SQLConf
@@ -791,7 +792,9 @@ class DataFrameAggregateSuite extends QueryTest
@@ -791,7 +793,9 @@ class DataFrameAggregateSuite extends QueryTest
case _ => false
}.isDefined)
} else {
Expand All @@ -403,7 +406,23 @@ index 9db406ff12f..19b4bb8e39d 100644
}

// test case for ObjectHashAggregate and SortAggregate
@@ -855,7 +858,7 @@ class DataFrameAggregateSuite extends QueryTest
@@ -800,7 +804,8 @@ class DataFrameAggregateSuite extends QueryTest
val objHashAggOrSortAggPlan =
stripAQEPlan(objHashAggOrSortAggDF.queryExecution.executedPlan)
if (useObjectHashAgg) {
- assert(objHashAggOrSortAggPlan.isInstanceOf[ObjectHashAggregateExec])
+ assert(objHashAggOrSortAggPlan.isInstanceOf[ObjectHashAggregateExec] ||
+ objHashAggOrSortAggPlan.isInstanceOf[CometHashAggregateExec])
} else {
assert(objHashAggOrSortAggPlan.isInstanceOf[SortAggregateExec])
}
@@ -850,12 +855,12 @@ class DataFrameAggregateSuite extends QueryTest
assert(sortAggPlans.isEmpty)

val objHashAggPlans = collect(aggPlan) {
- case objHashAgg: ObjectHashAggregateExec => objHashAgg
+ case agg @ (_: ObjectHashAggregateExec | _: CometHashAggregateExec) => agg
}
assert(objHashAggPlans.nonEmpty)

val exchangePlans = collect(aggPlan) {
Expand Down
27 changes: 23 additions & 4 deletions dev/diffs/4.1.2.diff
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,14 @@ index 0d807aeae4d..6d7744e771b 100644

withTempView("t0", "t1", "t2") {
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
index bfe15b33768..31f8ba4b876 100644
index bfe15b33768..550549f1c7c 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameAggregateSuite.scala
@@ -30,8 +30,9 @@ import org.apache.spark.sql.catalyst.util.AUTO_GENERATED_ALIAS
@@ -28,10 +28,12 @@ import org.apache.spark.{SparkArithmeticException, SparkRuntimeException}
import org.apache.spark.sql.catalyst.plans.logical.Expand
import org.apache.spark.sql.catalyst.util.AUTO_GENERATED_ALIAS
import org.apache.spark.sql.errors.DataTypeErrors.toSQLId
+import org.apache.spark.sql.comet.CometHashAggregateExec
import org.apache.spark.sql.execution.WholeStageCodegenExec
import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
+import org.apache.spark.sql.comet.CometHashAggregateExec
Expand All @@ -406,7 +409,7 @@ index bfe15b33768..31f8ba4b876 100644
import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions._
import org.apache.spark.sql.internal.SQLConf
@@ -792,7 +793,9 @@ class DataFrameAggregateSuite extends QueryTest
@@ -792,7 +794,9 @@ class DataFrameAggregateSuite extends QueryTest
case _ => false
}.isDefined)
} else {
Expand All @@ -417,7 +420,23 @@ index bfe15b33768..31f8ba4b876 100644
}

// test case for ObjectHashAggregate and SortAggregate
@@ -856,7 +859,7 @@ class DataFrameAggregateSuite extends QueryTest
@@ -801,7 +805,8 @@ class DataFrameAggregateSuite extends QueryTest
val objHashAggOrSortAggPlan =
stripAQEPlan(objHashAggOrSortAggDF.queryExecution.executedPlan)
if (useObjectHashAgg) {
- assert(objHashAggOrSortAggPlan.isInstanceOf[ObjectHashAggregateExec])
+ assert(objHashAggOrSortAggPlan.isInstanceOf[ObjectHashAggregateExec] ||
+ objHashAggOrSortAggPlan.isInstanceOf[CometHashAggregateExec])
} else {
assert(objHashAggOrSortAggPlan.isInstanceOf[SortAggregateExec])
}
@@ -851,12 +856,12 @@ class DataFrameAggregateSuite extends QueryTest
assert(sortAggPlans.isEmpty)

val objHashAggPlans = collect(aggPlan) {
- case objHashAgg: ObjectHashAggregateExec => objHashAgg
+ case agg @ (_: ObjectHashAggregateExec | _: CometHashAggregateExec) => agg
}
assert(objHashAggPlans.nonEmpty)

val exchangePlans = collect(aggPlan) {
Expand Down
8 changes: 8 additions & 0 deletions docs/source/contributor-guide/expression-audits/agg_funcs.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
- Spark 3.5.8 (2026-05-26)
- Spark 4.0.1 (2026-05-26)

## collect_list

- Spark 3.4.3 (audited 2026-06-24): `CollectList` extends `Collect[ArrayBuffer[Any]]`, returns `ArrayType(child.dataType, containsNull = false)`, ignores NULL inputs in `update()` (Hive-compatible semantics), and yields an empty array as `defaultResult`. `nullable = false`. No `checkInputDataTypes` override, so any input type accepted by Spark's analyzer is accepted. Registered as both `collect_list` and `array_agg` aliases in `FunctionRegistry`.
- Spark 3.5.8 (audited 2026-06-24): identical to 3.4.3.
- Spark 4.0.1 (audited 2026-06-24): only structural change is adding `with UnaryLike[Expression]` to the case class; no behavior change.
- Spark 4.1.1 (audited 2026-06-24): identical to 4.0.1.
- Comet implementation: native side delegates to `datafusion_spark::function::aggregate::collect::SparkCollectList`, which wraps `ArrayAggAccumulator` with `ignore_nulls = true` and converts a final NULL accumulator state to an empty array. Native intermediate state is `ArrayType(child.dataType, containsNull = true)`, so Comet rewrites intermediate `Partial`/`PartialMerge` object-aggregate buffer attributes from Spark's serialized `BinaryType` to that native state type.

## median

- Spark 3.4.3 (audited 2026-06-24): `Median(child)` is a `RuntimeReplaceableAggregate` with `replacement = Percentile(child, Literal(0.5))`. Catalyst rewrites `median(x)` to `percentile(x, 0.5)` before Comet sees the plan, so it is served by `CometPercentile`.
Expand Down
4 changes: 2 additions & 2 deletions docs/source/user-guide/latest/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ The tables below list every Spark built-in expression with its current status.
| `any_value` | ✅ | |
| `approx_count_distinct` | 🔜 | tracking [#4098](https://github.com/apache/datafusion-comet/issues/4098) |
| `approx_percentile` | ✅ | Byte, short, int, long, float, and double input; other input types fall back to Spark |
| `array_agg` | 🔜 | Array aggregate (related to `collect_list`, [#2524](https://github.com/apache/datafusion-comet/issues/2524)) |
| `array_agg` | | Alias for `collect_list` |
| `avg` | ✅ | Interval types fall back |
| `bit_and` | ✅ | |
| `bit_or` | ✅ | |
| `bit_xor` | ✅ | |
| `bool_and` | ✅ | |
| `bool_or` | ✅ | |
| `collect_list` | 🔜 | [#2524](https://github.com/apache/datafusion-comet/issues/2524) |
| `collect_list` | | |
| `collect_set` | ✅ | |
| `corr` | ✅ | |
| `count` | ✅ | |
Expand Down
Loading
Loading