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

philo-he 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 1f875df927 [GLUTEN-10134][VL] Fix cast modes for aggregate 
intermediate states (#13190)
1f875df927 is described below

commit 1f875df927404dd8f2061825346cf57a4fc5f935
Author: Reema <[email protected]>
AuthorDate: Thu Oct 8 09:19:02 2026 +0300

    [GLUTEN-10134][VL] Fix cast modes for aggregate intermediate states (#13190)
---
 .../execution/HashAggregateExecTransformer.scala   |   6 +-
 .../execution/GlutenAggregateCastModeSuite.scala   | 119 +++++++++++++++++++++
 2 files changed, 123 insertions(+), 2 deletions(-)

diff --git 
a/backends-velox/src/main/scala/org/apache/gluten/execution/HashAggregateExecTransformer.scala
 
b/backends-velox/src/main/scala/org/apache/gluten/execution/HashAggregateExecTransformer.scala
index 3a162aa5c9..8e32c7c9e7 100644
--- 
a/backends-velox/src/main/scala/org/apache/gluten/execution/HashAggregateExecTransformer.scala
+++ 
b/backends-velox/src/main/scala/org/apache/gluten/execution/HashAggregateExecTransformer.scala
@@ -23,6 +23,7 @@ import 
org.apache.gluten.expression.ConverterUtils.FunctionConfig
 import org.apache.gluten.substrait.`type`.{TypeBuilder, TypeNode}
 import org.apache.gluten.substrait.{AggregationParams, SubstraitContext}
 import org.apache.gluten.substrait.expression.{AggregateFunctionNode, 
ExpressionBuilder, ExpressionNode, ScalarFunctionNode}
+import org.apache.gluten.substrait.expression.CastNode.CastMode
 import org.apache.gluten.substrait.extensions.{AdvancedExtensionNode, 
ExtensionBuilder}
 import org.apache.gluten.substrait.rel.{RelBuilder, RelNode}
 import org.apache.gluten.utils.VeloxIntermediateData
@@ -144,7 +145,8 @@ abstract class HashAggregateExecTransformer(
                     .makeCast(
                       ConverterUtils.getTypeNode(sparkType, nullable = false),
                       ExpressionBuilder.makeSelection(colIdx, 
adjustedOrders(idx)),
-                      SQLConf.get.ansiEnabled))
+                      if (SQLConf.get.ansiEnabled) CastMode.ANSI else 
CastMode.LEGACY
+                    ))
               } else {
                 // Velox and Spark have the same type
                 expressionNodes.add(ExpressionBuilder.makeSelection(colIdx, 
adjustedOrders(idx)))
@@ -345,7 +347,7 @@ abstract class HashAggregateExecTransformer(
                       ExpressionBuilder.makeCast(
                         ConverterUtils.getTypeNode(veloxType, attr.nullable),
                         aggFuncInputAttrNode,
-                        SQLConf.get.ansiEnabled)
+                        if (SQLConf.get.ansiEnabled) CastMode.ANSI else 
CastMode.LEGACY)
                     } else {
                       newInputAttributes += attr
                       aggFuncInputAttrNode
diff --git 
a/gluten-ut/test/src/test/scala/org/apache/gluten/execution/GlutenAggregateCastModeSuite.scala
 
b/gluten-ut/test/src/test/scala/org/apache/gluten/execution/GlutenAggregateCastModeSuite.scala
new file mode 100644
index 0000000000..57a34a5eaa
--- /dev/null
+++ 
b/gluten-ut/test/src/test/scala/org/apache/gluten/execution/GlutenAggregateCastModeSuite.scala
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.execution
+
+import org.apache.gluten.config.GlutenConfig
+import org.apache.gluten.substrait.SubstraitContext
+import org.apache.gluten.utils.BackendTestUtils
+
+import org.apache.spark.SparkConf
+import org.apache.spark.sql.catalyst.expressions.aggregate.{Final, Partial, 
PartialMerge, StddevSamp}
+import org.apache.spark.sql.execution.ColumnarCollapseTransformStages
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.test.SharedSparkSession
+
+import com.google.protobuf.Message
+import io.substrait.proto.{Expression => SubstraitExpression}
+import io.substrait.proto.Expression.Cast.FailureBehavior
+
+import scala.collection.JavaConverters._
+
+class GlutenAggregateCastModeSuite extends GlutenQueryComparisonTest with 
SharedSparkSession {
+
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf
+      .set("spark.plugins", "org.apache.gluten.GlutenPlugin")
+      .set("spark.default.parallelism", "2")
+      .set("spark.memory.offHeap.enabled", "true")
+      .set("spark.memory.offHeap.size", "1024MB")
+      .set("spark.ui.enabled", "false")
+      .set(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key, "false")
+      .set(SQLConf.SHUFFLE_PARTITIONS.key, "2")
+      .set(GlutenConfig.GLUTEN_ANSI_FALLBACK_ENABLED.key, "false")
+      .set(GlutenConfig.MERGE_TWO_PHASES_ENABLED.key, "false")
+      .set(GlutenConfig.COLUMNAR_CUDF_ENABLED.key, "false")
+  }
+
+  private def collectCasts(message: Message): Seq[SubstraitExpression.Cast] = {
+    val current = message match {
+      case cast: SubstraitExpression.Cast => Seq(cast)
+      case _ => Seq.empty
+    }
+    current ++ message.getAllFields.values().asScala.toSeq.flatMap {
+      case child: Message => collectCasts(child)
+      case children: java.util.List[_] =>
+        children.asScala.toSeq.collect { case child: Message => child 
}.flatMap(collectCasts)
+      case _ => Seq.empty
+    }
+  }
+
+  for {
+    ansiEnabled <- Seq(false, true)
+    mode <- Seq(Partial, PartialMerge, Final)
+  } {
+    test(s"$mode statistical aggregate buffer casts respect ANSI mode 
$ansiEnabled") {
+      assume(BackendTestUtils.isVeloxBackendLoaded())
+
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> ansiEnabled.toString) {
+        // DISTINCT introduces a partial-merge phase for the non-distinct 
aggregate.
+        val query = """
+                      |SELECT stddev_samp(id), count(DISTINCT id % 7)
+                      |FROM range(0, 32, 1, 2)
+                      |""".stripMargin
+        val df = runAndCompare(query)
+        val aggregates = collect(df.queryExecution.executedPlan) {
+          case aggregate: HashAggregateExecBaseTransformer
+              if aggregate.aggregateExpressions.exists(
+                expression =>
+                  expression.aggregateFunction.isInstanceOf[StddevSamp] &&
+                    expression.mode == mode) => aggregate
+        }
+        assert(aggregates.nonEmpty, df.queryExecution.executedPlan.toString)
+
+        val expectedBehavior = if (ansiEnabled) {
+          FailureBehavior.FAILURE_BEHAVIOR_THROW_EXCEPTION
+        } else {
+          FailureBehavior.FAILURE_BEHAVIOR_UNSPECIFIED
+        }
+        aggregates.foreach {
+          aggregate =>
+            // Isolate this aggregate so casts from its children cannot 
satisfy the assertions.
+            val input =
+              
ColumnarCollapseTransformStages.wrapInputIteratorTransformer(aggregate.child)
+            val isolated = aggregate
+              .withNewChildren(Seq(input))
+              .asInstanceOf[HashAggregateExecBaseTransformer]
+            val relation = isolated.transform(new 
SubstraitContext).root.toProtobuf
+            val casts = collectCasts(relation)
+            if (mode == Partial || mode == PartialMerge) {
+              // Velox stores the count as BIGINT; Spark's statistical buffer 
uses DOUBLE.
+              val extractedCounts = casts.filter(_.getType.hasFp64)
+              assert(extractedCounts.nonEmpty, relation.toString)
+              extractedCounts.foreach(cast => assert(cast.getFailureBehavior 
== expectedBehavior))
+            }
+            if (mode == PartialMerge || mode == Final) {
+              // Reconstructing the Velox buffer requires the reverse 
DOUBLE-to-BIGINT cast.
+              val reconstructedCounts = casts.filter(_.getType.hasI64)
+              assert(reconstructedCounts.nonEmpty, relation.toString)
+              reconstructedCounts.foreach(
+                cast => assert(cast.getFailureBehavior == expectedBehavior))
+            }
+        }
+      }
+    }
+  }
+}


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

Reply via email to