sunchao commented on code in PR #4565:
URL: https://github.com/apache/datafusion-comet/pull/4565#discussion_r4104683220


##########
spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala:
##########
@@ -82,6 +82,7 @@ object CometExecRule {
       classOf[GenerateExec] -> CometExplodeExec,
       classOf[HashAggregateExec] -> CometHashAggregateExec,
       classOf[ObjectHashAggregateExec] -> CometObjectHashAggregateExec,
+      classOf[SortAggregateExec] -> CometSortAggregateExec,

Review Comment:
   [P1] Traverse `SortExec` when tagging incompatible aggregate producers. With 
Comet shuffle enabled, AQE disabled and 
`spark.sql.execution.useObjectHashAggregateExec=false`, `SELECT g, first(v), 
collect_list(v), count(DISTINCT v) FROM t GROUP BY g` creates sort-aggregate 
merge stages that must fall back because `FIRST` does not support 
`PartialMerge`. However, `findPartialAggInPlan` stops at their intervening 
`SortExec`, so the native partial remains untagged. Spark then consumes Comet's 
array-valued `collect_list` buffer where it requires serialized binary, causing 
query failure instead of safe fallback. Before this registration, these sort 
aggregates remained on Spark. Extend the producer search through row-preserving 
sorts and test this mixed aggregate query.
   
   Evidence: On Spark 4.1.3, used an eight-row Parquet table with `g=id%2` and 
integer `v=id`. A harness containing the exact-head `findPartialAggInPlan` 
returned `None` for all three buffer-consuming sort aggregates, while the 
object-hash equivalent found the producer. The exact-head buffer adjustment 
changed `collect_list` state from `BinaryType` to 
`ArrayType(IntegerType,true)`. A bounded Spark merge-contract check rejected 
that array state with `ClassCastException: GenericArrayData cannot be cast to 
[B`. `unsupportedPartialMergeFallbackReason` explicitly rejects the query's 
`FIRST` merge. Reproduction source and output: 
`/tmp/pr4565-current-validation/PR4565CurrentValidation.scala` and 
`validation-final.log`.



##########
spark/src/main/scala/org/apache/spark/sql/comet/operators.scala:
##########
@@ -2051,29 +2043,72 @@ object CometObjectHashAggregateExec
   }
 }
 
-case class CometHashAggregateExec(
-    override val nativeOp: Operator,
-    override val originalPlan: SparkPlan,
-    override val output: Seq[Attribute],
-    groupingExpressions: Seq[NamedExpression],
-    aggregateExpressions: Seq[AggregateExpression],
-    resultExpressions: Seq[NamedExpression],
-    input: Seq[Attribute],
-    child: SparkPlan,
-    override val serializedPlanOpt: SerializedPlan)
+object CometSortAggregateExec
+    extends CometOperatorSerde[SortAggregateExec]
+    with CometBaseAggregate {
+
+  override def enabledConfig: Option[ConfigEntry[Boolean]] = Some(
+    CometConf.COMET_EXEC_AGGREGATE_ENABLED)
+
+  override def getSupportLevel(op: SortAggregateExec): SupportLevel =
+    baseAggregateSupportLevel(op)
+
+  override def convert(
+      aggregate: SortAggregateExec,
+      builder: Operator.Builder,
+      childOp: OperatorOuterClass.Operator*): 
Option[OperatorOuterClass.Operator] = {
+
+    // SortAggregate is planned for TypedImperativeAggregate functions whose 
intermediate
+    // buffer formats differ between Spark and Comet (same risk as 
ObjectHashAggregate).
+    // Require Comet shuffle so a Partial->Final pair never spans the 
JVM/native boundary.
+    if (!isCometShuffleEnabled(aggregate.conf)) {
+      return None
+    }
+
+    doConvert(aggregate, builder, childOp: _*)
+  }
+
+  override def createExec(nativeOp: Operator, op: SortAggregateExec): 
CometNativeExec = {
+    // The native AggregateExec auto-detects Sorted input mode from the 
child's output ordering
+    // and produces output sorted by the grouping keys; 
CometExec.outputOrdering defaults to
+    // originalPlan.outputOrdering, which is SortAggregateExec's grouping-key 
ordering, so
+    // downstream operators that elided a sort against it still see a 
satisfying ordering.
+    CometSortAggregateExec(

Review Comment:
   [P1] Preserve the advertised ordering for array grouping keys. With native 
caching enabled at startup, cache a single-partition relation sorted by array 
key `k`, containing `(NULL,'n'), (array(),'e'), (array(1),'o')`, then run 
`SELECT k, first(v) FROM cached_sorted GROUP BY k ORDER BY k`. Spark eliminates 
the sorts because the cache and sort aggregates advertise ascending order. 
Comet's native scan does not carry that ordering, so DataFusion selects 
`Linear`. Its vectorized group interning can reorder these keys, producing 
`NULL, [1], []` instead of `NULL, [], [1]`. This violates the explicit `ORDER 
BY`. Carry the guaranteed ordering into native planning, enforce it with a 
native sort, or fall back. The earlier retraction tested primitive/string keys 
and does not cover this reproduced array-key case.
   
   Evidence: Spark 4.1.3 produced `SortAggregate[Final] -> 
SortAggregate[Partial] -> InMemoryTableScan` with zero executable `SortExec` 
nodes and returned `NULL, [], [1]`. Compiled 
`/tmp/pr4565-validation/aggregate_order.rs` against this checkout's native 
dependencies: both DataFusion 55 aggregate stages reported `Linear` and 
returned `NULL, [1], []`. `new_group_values` selects `GroupValuesColumn<false>` 
for this array schema, whose vectorized interning does not preserve first-seen 
order. The reachable Comet cache path requires 
`spark.comet.exec.inMemoryCache.enabled=true` with the Comet plugin and shuffle 
configured. Its serializer supports arrays, while `ScanExec` constructs empty 
ordering metadata.



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