This is an automated email from the ASF dual-hosted git repository.

zhztheplayer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new 1cf2c0d956 [VL] Eager aggregation: Refactor option partialMerge.mode 
(#13328)
1cf2c0d956 is described below

commit 1cf2c0d956b555422785d673dfb76c9ca9046269
Author: Hongze Zhang <[email protected]>
AuthorDate: Fri Oct 16 16:38:54 2026 +0100

    [VL] Eager aggregation: Refactor option partialMerge.mode (#13328)
---
 .../execution/StarSchemaJoinAggregateSuite.scala   |  51 ++++++--
 docs/Configuration.md                              |   3 +-
 .../org/apache/gluten/config/GlutenConfig.scala    |  32 +++--
 .../extension/joinagg/ImplementJoinAggregate.scala | 138 ++++++++++++++++-----
 .../joinagg/PushAggregateThroughJoin.scala         |   9 +-
 .../execution/PushAggregateThroughJoinSuite.scala  |  77 ++++++------
 6 files changed, 215 insertions(+), 95 deletions(-)

diff --git 
a/backends-velox/src/test/scala/org/apache/gluten/execution/StarSchemaJoinAggregateSuite.scala
 
b/backends-velox/src/test/scala/org/apache/gluten/execution/StarSchemaJoinAggregateSuite.scala
index 373ceedef3..0b8bb11e41 100644
--- 
a/backends-velox/src/test/scala/org/apache/gluten/execution/StarSchemaJoinAggregateSuite.scala
+++ 
b/backends-velox/src/test/scala/org/apache/gluten/execution/StarSchemaJoinAggregateSuite.scala
@@ -32,9 +32,12 @@ class VanillaJoinAggregateLogicalOnlyExtensions extends 
(SparkSessionExtensions
   }
 }
 
-class VanillaStarSchemaJoinAggregateLogicalOnlySuite extends 
StarSchemaJoinAggregateSuite {
+class StarSchemaJoinAggregateVanillaLogicalOnlySuite extends 
StarSchemaJoinAggregateSuite {
   override protected def sparkConf: SparkConf = {
     super.sparkConf
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH.key, 
Int.MaxValue.toString)
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED.key, 
"false")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_TWO_STAGE_MODE.key, "none")
       .set(GlutenConfig.GLUTEN_ENABLED.key, "false")
       .set("spark.shuffle.manager", "sort")
       .set(
@@ -52,9 +55,12 @@ class VanillaJoinAggregateExtensions extends 
(SparkSessionExtensions => Unit) {
   }
 }
 
-class VanillaStarSchemaJoinAggregateSuite extends StarSchemaJoinAggregateSuite 
{
+class StarSchemaJoinAggregateVanillaSuite extends StarSchemaJoinAggregateSuite 
{
   override protected def sparkConf: SparkConf = {
     super.sparkConf
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH.key, 
Int.MaxValue.toString)
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED.key, 
"false")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_TWO_STAGE_MODE.key, "none")
       .set(GlutenConfig.GLUTEN_ENABLED.key, "false")
       .set("spark.shuffle.manager", "sort")
       .set("spark.sql.extensions", 
classOf[VanillaJoinAggregateExtensions].getCanonicalName)
@@ -63,28 +69,45 @@ class VanillaStarSchemaJoinAggregateSuite extends 
StarSchemaJoinAggregateSuite {
   override protected def checkPlan(df: DataFrame): Unit = {}
 }
 
-class StarSchemaJoinAggregateSingleDepthSuite extends 
StarSchemaJoinAggregateSuite {
+class StarSchemaJoinAggregateMaxDepthOneSuite extends 
StarSchemaJoinAggregateSuite {
   override protected def sparkConf: SparkConf = {
     super.sparkConf
       .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH.key, "1")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED.key, 
"false")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_TWO_STAGE_MODE.key, "none")
   }
 }
 
-class StarSchemaJoinAggregatePartialMergeEnabledSuite extends 
StarSchemaJoinAggregateSuite {
+class StarSchemaJoinAggregatePartialMergeTwoStageSuite extends 
StarSchemaJoinAggregateSuite {
   override protected def sparkConf: SparkConf = {
     super.sparkConf
-      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_MODE.key, 
"all")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH.key, 
Int.MaxValue.toString)
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED.key, 
"true")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_TWO_STAGE_MODE.key, 
"final")
   }
 }
 
-class StarSchemaJoinAggregateTopPartialMergeSuite extends 
StarSchemaJoinAggregateSuite {
+class StarSchemaJoinAggregateTwoStageFinalSuite extends 
StarSchemaJoinAggregateSuite {
   override protected def sparkConf: SparkConf = {
     super.sparkConf
-      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_MODE.key, 
"top")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH.key, 
Int.MaxValue.toString)
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED.key, 
"false")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_TWO_STAGE_MODE.key, 
"final")
   }
 }
 
-class StarSchemaJoinAggregateSuite extends VeloxTPCHTableSupport with 
AdaptiveSparkPlanHelper {
+class StarSchemaJoinAggregateDefaultSuite extends StarSchemaJoinAggregateSuite 
{
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH.key, 
Int.MaxValue.toString)
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED.key, 
"false")
+      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_TWO_STAGE_MODE.key, "none")
+  }
+}
+
+abstract class StarSchemaJoinAggregateSuite
+  extends VeloxTPCHTableSupport
+  with AdaptiveSparkPlanHelper {
   private val factMeasureColumnNames = Set(
     "sales_price",
     "return_amt",
@@ -108,8 +131,6 @@ class StarSchemaJoinAggregateSuite extends 
VeloxTPCHTableSupport with AdaptiveSp
     super.sparkConf
       .set(GlutenConfig.COLUMNAR_FORCE_SHUFFLED_HASH_JOIN_ENABLED.key, "true")
       .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_ENABLED.key, "true")
-      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH.key, 
s"${Int.MaxValue}")
-      .set(GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_MODE.key, 
"none")
       .set("spark.sql.adaptive.enabled", "false")
   }
 
@@ -1074,4 +1095,14 @@ class StarSchemaJoinAggregateSuite extends 
VeloxTPCHTableSupport with AdaptiveSp
 
     runQueryAndCompare(query)(df => checkDf(df))
   }
+
+  test("Support filtered aggregates alongside pushed aggregates") {
+    val query = """
+                  |SELECT SUM(ss.ss_sales_price),
+                  |       MAX(d.d_moy) FILTER (WHERE d.d_year = 1999)
+                  |FROM store_sales ss
+                  |JOIN date_dim d ON ss.ss_sold_date_sk = d.d_date_sk
+                  |""".stripMargin
+    runQueryAndCompare(query)(df => checkDf(df))
+  }
 }
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 117d5782b0..fa5dd41b7c 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -138,7 +138,8 @@ nav_order: 15
 | spark.gluten.sql.orc.charType.scan.fallback.enabled                 | 🔄 
Dynamic    | true              | Force fallback for orc char type scan.         
                                                                                
                                                                                
                                                                                
                                                                                
                   [...]
 | spark.gluten.sql.pushAggregateThroughJoin.enabled                   | 🔄 
Dynamic    | false             | Enables the push-aggregate-through-join 
optimization in Gluten. When enabled, aggregate operators may be pushed below 
joins during logical optimization and corresponding physical plans may be 
rewritten to execute the aggregation earlier.                                   
                                                                                
                                  [...]
 | spark.gluten.sql.pushAggregateThroughJoin.maxDepth                  | 🔄 
Dynamic    | 2147483647        | Maximum join traversal depth when applying the 
push-aggregate-through-join optimization. A value of 1 allows pushing an 
aggregate through a single join; larger values allow the rule to traverse and 
push through multiple consecutive joins.                                        
                                                                                
                            [...]
-| spark.gluten.sql.pushAggregateThroughJoin.partialMerge.mode         | 🔄 
Dynamic    | NONE              | Controls where PartialMerge aggregates are 
retained during aggregate pushdown: none retains none, top retains one at the 
original partial aggregate position, and all retains one at every successful 
join push.                                                                      
                                                                                
                            [...]
+| spark.gluten.sql.pushAggregateThroughJoin.partialMerge.enabled      | 🔄 
Dynamic    | false             | Retains local partial merges between joins 
crossed by aggregate pushdown.                                                  
                                                                                
                                                                                
                                                                                
                       [...]
+| spark.gluten.sql.pushAggregateThroughJoin.twoStage.mode             | 🔄 
Dynamic    | NONE              | Controls physical staging of the final join 
aggregate: NONE uses a single final stage; FINAL adds a local partial merge 
before the final aggregation exchange.                                          
                                                                                
                                                                                
                          [...]
 | spark.gluten.sql.removeNativeWriteFilesSortAndProject               | 🔄 
Dynamic    | true              | When true, Gluten will remove the vanilla 
Spark V1Writes added sort and project for velox backend.                        
                                                                                
                                                                                
                                                                                
                        [...]
 | spark.gluten.sql.rewrite.dateTimestampComparison                    | 🔄 
Dynamic    | true              | Rewrite the comparision between date and 
timestamp to timestamp comparison.For example `from_unixtime(ts) > date` will 
be rewritten to `ts > to_unixtime(date)`                                        
                                                                                
                                                                                
                           [...]
 | spark.gluten.sql.scan.detailedMetrics.enabled                       | 🔄 
Dynamic    | true              | When true (default), Velox backend scan 
operators register all detailed SQL metrics. When false, only essential scan 
metrics are registered to reduce driver memory usage. Also enabled 
automatically when spark.gluten.sql.debug is true. Does not affect the 
ClickHouse backend.                                                             
                                                   [...]
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala 
b/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
index 6699c6eb22..1459e7fed0 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
@@ -30,8 +30,10 @@ import java.util.Locale
 import scala.collection.JavaConverters._
 import scala.collection.mutable
 
-object PartialMergeMode extends Enumeration {
-  val NONE, TOP, ALL = Value
+object JoinAggregateTwoStageMode extends Enumeration {
+  // TODO: Support ALL at existing shuffle boundaries; splitting below joins 
can otherwise
+  //  introduce unnecessary shuffles, especially for broadcast hash joins.
+  val NONE, FINAL = Value
 }
 
 trait ShuffleWriterType {
@@ -162,8 +164,11 @@ class GlutenConfig(conf: SQLConf) extends 
GlutenCoreConfig(conf) {
   def pushAggregateThroughJoinMaxDepth: Int =
     getConf(PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH)
 
-  def pushAggregateThroughJoinPartialMergeMode: PartialMergeMode.Value =
-    getConf(PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_MODE)
+  def pushAggregateThroughJoinPartialMergeEnabled: Boolean =
+    getConf(PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED)
+
+  def pushAggregateThroughJoinTwoStageMode: JoinAggregateTwoStageMode.Value =
+    getConf(PUSH_AGGREGATE_THROUGH_JOIN_TWO_STAGE_MODE)
 
   def forceOrcCharTypeScanFallbackEnabled: Boolean =
     getConf(VELOX_FORCE_ORC_CHAR_TYPE_SCAN_FALLBACK)
@@ -791,14 +796,19 @@ object GlutenConfig extends ConfigRegistry {
       .checkValue(_ >= 1, "must be greater than or equal to 1.")
       .createWithDefault(Int.MaxValue)
 
-  val PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_MODE =
-    buildConf("spark.gluten.sql.pushAggregateThroughJoin.partialMerge.mode")
+  val PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED =
+    buildConf("spark.gluten.sql.pushAggregateThroughJoin.partialMerge.enabled")
+      .doc("Retains local partial merges between joins crossed by aggregate 
pushdown.")
+      .booleanConf
+      .createWithDefault(false)
+
+  val PUSH_AGGREGATE_THROUGH_JOIN_TWO_STAGE_MODE =
+    buildConf("spark.gluten.sql.pushAggregateThroughJoin.twoStage.mode")
       .doc(
-        "Controls where PartialMerge aggregates are retained during aggregate 
pushdown: " +
-          "none retains none, top retains one at the original partial 
aggregate position, " +
-          "and all retains one at every successful join push.")
-      .enumConf(PartialMergeMode)
-      .createWithDefault(PartialMergeMode.NONE)
+        "Controls physical staging of the final join aggregate: NONE uses a 
single final " +
+          "stage; FINAL adds a local partial merge before the final 
aggregation exchange.")
+      .enumConf(JoinAggregateTwoStageMode)
+      .createWithDefault(JoinAggregateTwoStageMode.NONE)
 
   val GLUTEN_SOFT_AFFINITY_ENABLED =
     buildConf("spark.gluten.soft-affinity.enabled")
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/joinagg/ImplementJoinAggregate.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/joinagg/ImplementJoinAggregate.scala
index 5a9434e473..6a48a33ccb 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/joinagg/ImplementJoinAggregate.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/joinagg/ImplementJoinAggregate.scala
@@ -16,11 +16,11 @@
  */
 package org.apache.gluten.extension.joinagg
 
-import org.apache.gluten.config.GlutenConfig
+import org.apache.gluten.config.{GlutenConfig, JoinAggregateTwoStageMode}
 
 import org.apache.spark.sql.SparkSession
-import org.apache.spark.sql.catalyst.expressions.{Alias, GetStructField, 
NamedExpression}
-import org.apache.spark.sql.catalyst.expressions.aggregate.AggregateExpression
+import org.apache.spark.sql.catalyst.expressions.{Alias, Expression, 
GetStructField, NamedExpression}
+import 
org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, 
Complete, Final, Partial, PartialMerge}
 import org.apache.spark.sql.catalyst.planning.PhysicalAggregation
 import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, LogicalPlan}
 import org.apache.spark.sql.execution.{ProjectExec, SparkPlan, SparkStrategy}
@@ -29,6 +29,11 @@ import 
org.apache.spark.sql.execution.aggregate.HashAggregateExec
 import scala.collection.mutable.ArrayBuffer
 
 case class ImplementJoinAggregate(spark: SparkSession) extends SparkStrategy {
+  private object AggregationStage extends Enumeration {
+    // DISTRIBUTED requires grouped input but does not add a mapper aggregate.
+    val LOCAL, DISTRIBUTED, TWO_STAGE = Value
+  }
+
   /*
    * This strategy lowers the logical join-aggregate wrapper shape produced by
    * `PushAggregateThroughJoin` into ordinary Spark physical hash aggregates.
@@ -117,7 +122,8 @@ case class ImplementJoinAggregate(spark: SparkSession) 
extends SparkStrategy {
       resultExpressions,
       childPlan,
       unpackInputBuffers = false,
-      packOutputBuffers = true)
+      packOutputBuffers = true,
+      stage = AggregationStage.LOCAL)
   }
 
   private def planPartialMergePhase(
@@ -131,7 +137,31 @@ case class ImplementJoinAggregate(spark: SparkSession) 
extends SparkStrategy {
       resultExpressions,
       childPlan,
       unpackInputBuffers = true,
-      packOutputBuffers = true)
+      packOutputBuffers = true,
+      stage = AggregationStage.LOCAL)
+  }
+
+  private def planFinalPhase(
+      grouping: Seq[NamedExpression],
+      aggregateExpressions: Seq[AggregateExpression],
+      resultExpressions: Seq[NamedExpression],
+      childPlan: SparkPlan): Option[SparkPlan] = {
+    val stage =
+      if (
+        GlutenConfig.get.pushAggregateThroughJoinTwoStageMode == 
JoinAggregateTwoStageMode.FINAL
+      ) {
+        AggregationStage.TWO_STAGE
+      } else {
+        AggregationStage.DISTRIBUTED
+      }
+    planPhase(
+      grouping,
+      aggregateExpressions,
+      resultExpressions,
+      childPlan,
+      unpackInputBuffers = true,
+      packOutputBuffers = false,
+      stage = stage)
   }
 
   private def planPhase(
@@ -140,7 +170,8 @@ case class ImplementJoinAggregate(spark: SparkSession) 
extends SparkStrategy {
       resultExpressions: Seq[NamedExpression],
       childPlan: SparkPlan,
       unpackInputBuffers: Boolean,
-      packOutputBuffers: Boolean): Option[SparkPlan] = {
+      packOutputBuffers: Boolean,
+      stage: AggregationStage.Value): Option[SparkPlan] = {
     val rewrittenAggExprs = rewriteWrapperAggregates(aggregateExpressions)
     if (rewrittenAggExprs.isEmpty) {
       return None
@@ -150,16 +181,75 @@ case class ImplementJoinAggregate(spark: SparkSession) 
extends SparkStrategy {
     } else {
       childPlan
     }
+    val stageChild = if (stage == AggregationStage.TWO_STAGE) {
+      buildMapperAggregate(grouping, preparedChild, rewrittenAggExprs)
+    } else {
+      preparedChild
+    }
+    val stageExpressions = if (stage == AggregationStage.TWO_STAGE) {
+      rewrittenAggExprs.map {
+        ae =>
+          val mode = ae.mode match {
+            case Partial => PartialMerge
+            case Complete => Final
+            case other => other
+          }
+          // Like Spark's AggUtils.mayRemoveAggFilters, remove filters from 
buffer-merge stages.
+          ae.copy(mode = mode, filter = None)
+      }
+    } else {
+      rewrittenAggExprs
+    }
+    val distribution = stage match {
+      case AggregationStage.LOCAL => None
+      case AggregationStage.DISTRIBUTED | AggregationStage.TWO_STAGE =>
+        Some(grouping.map(_.toAttribute))
+    }
     if (packOutputBuffers) {
-      planBufferPhase(
+      buildBufferAggregate(
         grouping,
         aggregateExpressions,
         resultExpressions,
-        preparedChild,
-        rewrittenAggExprs)
+        stageChild,
+        stageExpressions,
+        distribution)
     } else {
-      planFinalOutput(grouping, resultExpressions, preparedChild, 
rewrittenAggExprs)
+      buildFinalAggregate(grouping, resultExpressions, stageChild, 
stageExpressions, distribution)
+    }
+  }
+
+  private def buildMapperAggregate(
+      grouping: Seq[NamedExpression],
+      childPlan: SparkPlan,
+      aggregateExpressions: Seq[AggregateExpression]): SparkPlan = {
+    val mergeExpressions = aggregateExpressions.map {
+      ae =>
+        val mode = ae.mode match {
+          case Partial | Complete => Partial
+          case _ => PartialMerge
+        }
+        ae.copy(mode = mode)
     }
+    // Preserve the reducer stage's input-buffer IDs when emitting mapper 
buffers.
+    val bufferOutputs = mergeExpressions.flatMap {
+      ae =>
+        val function = ae.aggregateFunction
+        
function.aggBufferAttributes.zip(function.inputAggBufferAttributes).map {
+          case (output, input) =>
+            Alias(output, input.name)(exprId = input.exprId, qualifier = 
input.qualifier)
+        }
+    }
+    HashAggregateExec(
+      requiredChildDistributionExpressions = None,
+      isStreaming = false,
+      numShufflePartitions = None,
+      groupingExpressions = grouping,
+      aggregateExpressions = mergeExpressions,
+      aggregateAttributes = 
mergeExpressions.flatMap(_.aggregateFunction.aggBufferAttributes),
+      initialInputBufferOffset = 0,
+      resultExpressions = grouping.map(_.toAttribute) ++ bufferOutputs,
+      child = childPlan
+    )
   }
 
   private def unpackInputBufferFields(
@@ -199,14 +289,15 @@ case class ImplementJoinAggregate(spark: SparkSession) 
extends SparkStrategy {
     }
   }
 
-  private def planBufferPhase(
+  private def buildBufferAggregate(
       grouping: Seq[NamedExpression],
       aggregateExpressions: Seq[AggregateExpression],
       resultExpressions: Seq[NamedExpression],
       childPlan: SparkPlan,
-      rewrittenAggExprs: Seq[AggregateExpression]): Option[SparkPlan] = {
+      rewrittenAggExprs: Seq[AggregateExpression],
+      requiredChildDistributionExpressions: Option[Seq[Expression]]): 
Option[SparkPlan] = {
     val hashAgg = HashAggregateExec(
-      requiredChildDistributionExpressions = None,
+      requiredChildDistributionExpressions = 
requiredChildDistributionExpressions,
       isStreaming = false,
       numShufflePartitions = None,
       groupingExpressions = grouping,
@@ -276,32 +367,19 @@ case class ImplementJoinAggregate(spark: SparkSession) 
extends SparkStrategy {
     Some(ProjectExec(rewrittenResultExpressions, hashAgg))
   }
 
-  private def planFinalPhase(
-      grouping: Seq[NamedExpression],
-      aggregateExpressions: Seq[AggregateExpression],
-      resultExpressions: Seq[NamedExpression],
-      childPlan: SparkPlan): Option[SparkPlan] = {
-    planPhase(
-      grouping,
-      aggregateExpressions,
-      resultExpressions,
-      childPlan,
-      unpackInputBuffers = true,
-      packOutputBuffers = false)
-  }
-
-  private def planFinalOutput(
+  private def buildFinalAggregate(
       grouping: Seq[NamedExpression],
       resultExpressions: Seq[NamedExpression],
       childPlan: SparkPlan,
-      rewrittenAggExprs: Seq[AggregateExpression]): Option[SparkPlan] = {
+      rewrittenAggExprs: Seq[AggregateExpression],
+      requiredChildDistributionExpressions: Option[Seq[Expression]]): 
Option[SparkPlan] = {
     val aggregateAttrs = rewrittenAggExprs.map(_.resultAttribute)
     val rewrittenResultExpressions =
       rewriteResultAsAggregateAttributes(resultExpressions, rewrittenAggExprs)
 
     Some(
       HashAggregateExec(
-        requiredChildDistributionExpressions = 
Some(grouping.map(_.toAttribute)),
+        requiredChildDistributionExpressions = 
requiredChildDistributionExpressions,
         isStreaming = false,
         numShufflePartitions = None,
         groupingExpressions = grouping,
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/joinagg/PushAggregateThroughJoin.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/joinagg/PushAggregateThroughJoin.scala
index 4f318f10c8..a9a39290f7 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/joinagg/PushAggregateThroughJoin.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/joinagg/PushAggregateThroughJoin.scala
@@ -16,7 +16,7 @@
  */
 package org.apache.gluten.extension.joinagg
 
-import org.apache.gluten.config.{GlutenConfig, PartialMergeMode}
+import org.apache.gluten.config.GlutenConfig
 
 import org.apache.spark.sql.SparkSession
 import org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, 
AttributeReference, AttributeSet, EqualTo, Expression, NamedExpression, 
PredicateHelper}
@@ -174,7 +174,7 @@ case class PushAggregateThroughJoin(spark: SparkSession)
   private def pushPartialWrapperAggregate(lowerPartialAgg: Aggregate): 
LogicalPlan = {
     // Push one join edge per iteration. This keeps the rewrite local and lets 
`maxDepth` bound
     // how far a pushed aggregate is allowed to travel through a multi-join 
subtree.
-    val partialMergeMode = 
GlutenConfig.get.pushAggregateThroughJoinPartialMergeMode
+    val partialMergeEnabled = 
GlutenConfig.get.pushAggregateThroughJoinPartialMergeEnabled
     var current: LogicalPlan = lowerPartialAgg
     var changed = true
     var pushCount = 0
@@ -182,9 +182,8 @@ case class PushAggregateThroughJoin(spark: SparkSession)
       changed = false
       current = current.transformUp {
         case partialAgg: Aggregate if 
isPurePartialWrapperAggregate(partialAgg) =>
-          // The first successful push leaves a merge at the original partial 
aggregate position.
-          val retainPartialMerge = partialMergeMode == PartialMergeMode.ALL ||
-            (partialMergeMode == PartialMergeMode.TOP && pushCount == 0)
+          // The physical strategy owns the top merge; retain logical merges 
only between joins.
+          val retainPartialMerge = partialMergeEnabled && pushCount > 0
           val pushed = if (retainPartialMerge) {
             val (partialMergeAgg, freshLowerPartialAgg) = 
splitPartialAggregateForMerge(partialAgg)
             pushOnce(freshLowerPartialAgg).map {
diff --git 
a/gluten-substrait/src/test/scala/org/apache/gluten/execution/PushAggregateThroughJoinSuite.scala
 
b/gluten-substrait/src/test/scala/org/apache/gluten/execution/PushAggregateThroughJoinSuite.scala
index 9f204ba688..bdbaf07caf 100644
--- 
a/gluten-substrait/src/test/scala/org/apache/gluten/execution/PushAggregateThroughJoinSuite.scala
+++ 
b/gluten-substrait/src/test/scala/org/apache/gluten/execution/PushAggregateThroughJoinSuite.scala
@@ -38,7 +38,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
 
   private case class PushdownCase(inputSql: String)
 
-  private case class ExpectedAggCounts(none: Int, top: Int, all: Int)
+  private case class ExpectedAggCounts(partialMergeDisabled: Int, 
partialMergeEnabled: Int)
 
   override protected def sparkConf: SparkConf = {
     // Avoid Janino projection codegen here because Spark 4's 
QueryExecutionErrors
@@ -95,18 +95,19 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       maxDepth: Int,
       expectedPushCount: Int,
       expectedAggCounts: ExpectedAggCounts): Unit = {
-    val modes = Seq(
-      "none" -> expectedAggCounts.none,
-      "top" -> expectedAggCounts.top,
-      "all" -> expectedAggCounts.all)
-    modes.foreach {
-      case (partialMergeMode, expectedAggCount) =>
-        withClue(s"partialMergeMode=$partialMergeMode, maxDepth=$maxDepth: ") {
+    Seq(false, true).foreach {
+      partialMergeEnabled =>
+        val expectedAggCount = if (partialMergeEnabled) {
+          expectedAggCounts.partialMergeEnabled
+        } else {
+          expectedAggCounts.partialMergeDisabled
+        }
+        withClue(s"partialMergeEnabled=$partialMergeEnabled, 
maxDepth=$maxDepth: ") {
           withSQLConf(
             GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_ENABLED.key -> "true",
             GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_MAX_DEPTH.key -> 
maxDepth.toString,
-            GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_MODE.key ->
-              partialMergeMode
+            GlutenConfig.PUSH_AGGREGATE_THROUGH_JOIN_PARTIAL_MERGE_ENABLED.key 
->
+              partialMergeEnabled.toString
           ) {
             val (withoutRuleRows, withoutRuleLogicalPlan, 
withoutRulePhysicalPlan) =
               withExtraPlanning(Nil, Nil) {
@@ -160,25 +161,25 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
 
             if (debugMode) {
               // scalastyle:off println
-              println("=== Optimized Plan Before (without 
PushJoinAggregatePreAggregation) ===")
+              println("=== Optimized Plan Before (without 
PushAggregateThroughJoin) ===")
               println(withoutRuleLogicalPlan.treeString)
-              println("=== Optimized Plan After (with 
PushJoinAggregatePreAggregation) ===")
+              println("=== Optimized Plan After (with 
PushAggregateThroughJoin) ===")
               println(withRuleLogicalPlan.treeString)
               println(
-                "=== Optimized Plan After (with 
PushJoinAggregatePreAggregation and strategy) ===")
+                "=== Optimized Plan After (with PushAggregateThroughJoin and 
strategy) ===")
               println(withRuleAndStrategyLogicalPlan.treeString)
-              println("=== Physical Plan Before (without 
PushJoinAggregatePreAggregation) ===")
+              println("=== Physical Plan Before (without 
PushAggregateThroughJoin) ===")
               println(withoutRulePhysicalPlan.treeString)
-              println("=== Physical Plan After (with 
PushJoinAggregatePreAggregation only) ===")
+              println("=== Physical Plan After (with PushAggregateThroughJoin 
only) ===")
               println(withRulePhysicalPlan.treeString)
               println(
-                "=== Physical Plan After (with PushJoinAggregatePreAggregation 
and strategy) ===")
+                "=== Physical Plan After (with PushAggregateThroughJoin and 
strategy) ===")
               println(withRuleAndStrategyPhysicalPlan.treeString)
-              println("=== Result Before (without 
PushJoinAggregatePreAggregation) ===")
+              println("=== Result Before (without PushAggregateThroughJoin) 
===")
               println(withoutRuleRows.mkString("\n"))
-              println("=== Result After (with PushJoinAggregatePreAggregation 
only) ===")
+              println("=== Result After (with PushAggregateThroughJoin only) 
===")
               println(withRuleRows.mkString("\n"))
-              println("=== Result After (with PushJoinAggregatePreAggregation 
and strategy) ===")
+              println("=== Result After (with PushAggregateThroughJoin and 
strategy) ===")
               println(withRuleAndStrategyRows.mkString("\n"))
               // scalastyle:on println
             }
@@ -235,7 +236,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 2,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 4))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 3))
   }
 
   test("pre-aggregate store_sales for sum") {
@@ -253,7 +254,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 1,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 3))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
   }
 
   test("pre-aggregate store_sales for avg") {
@@ -271,7 +272,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 1,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 3))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
   }
 
   test("pre-aggregate store_sales for sum on fact table") {
@@ -289,7 +290,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 1,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 3))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
   }
 
   test("pre-aggregate store_sales for avg on fact table") {
@@ -307,7 +308,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 1,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 3))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
   }
 
   test("pre-aggregate store_sales for sum on three-way join") {
@@ -327,7 +328,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 2,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 4))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 3))
   }
 
   test("pre-aggregate store_sales for sum and avg on different fact columns on 
three-way join") {
@@ -348,7 +349,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 2,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 4))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 3))
   }
 
   test("pre-aggregate store_sales for sum and avg on same fact column on 
three-way join") {
@@ -369,7 +370,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 2,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 4))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 3))
   }
 
   test("pre-aggregate store_sales by i_item_desc") {
@@ -387,7 +388,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 1,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 3))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
   }
 
   test("pre-aggregate store_sales by substr(i_item_desc, 3), 3 ways") {
@@ -407,7 +408,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 2,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 4))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 3))
   }
 
   test("pre-aggregate store_sales for sum with item filter") {
@@ -424,7 +425,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 1,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 3))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
   }
 
   test("pre-aggregate three-way joins independently under union all") {
@@ -467,17 +468,17 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = 1,
       expectedPushCount = 3,
-      expectedAggCounts = ExpectedAggCounts(none = 6, top = 9, all = 9))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 6, 
partialMergeEnabled = 6))
     runCaseWithMaxDepth(
       pushdownCase,
       maxDepth = 2,
       expectedPushCount = 6,
-      expectedAggCounts = ExpectedAggCounts(none = 6, top = 9, all = 12))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 6, 
partialMergeEnabled = 9))
     runCaseWithMaxDepth(
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 6,
-      expectedAggCounts = ExpectedAggCounts(none = 6, top = 9, all = 12))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 6, 
partialMergeEnabled = 9))
   }
 
   test("pre-aggregate store_sales for sum on three-way join with maxDepth=1 / 
maxDepth=2") {
@@ -497,17 +498,17 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = 1,
       expectedPushCount = 1,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 3))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
     runCaseWithMaxDepth(
       pushdownCase,
       maxDepth = 2,
       expectedPushCount = 2,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 4))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 3))
     runCaseWithMaxDepth(
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 2,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 4))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 3))
   }
 
   test("partial merge is not retained when no side can be pushed") {
@@ -522,7 +523,7 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 0,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 2, all = 2))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
   }
 
   test("pre-aggregate with filter inside inner equi-join") {
@@ -540,6 +541,6 @@ class PushAggregateThroughJoinSuite extends PlanTest with 
SharedSparkSession {
       pushdownCase,
       maxDepth = Int.MaxValue,
       expectedPushCount = 1,
-      expectedAggCounts = ExpectedAggCounts(none = 2, top = 3, all = 3))
+      expectedAggCounts = ExpectedAggCounts(partialMergeDisabled = 2, 
partialMergeEnabled = 2))
   }
 }


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

Reply via email to