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

andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new c706360490 fix: avoid duplicate CheckOverflow evaluation for decimal 
division (#5225)
c706360490 is described below

commit c70636049031fb03a000e166a9b720e817a1361e
Author: Peter Lee <[email protected]>
AuthorDate: Sat Aug 8 13:13:40 2026 +0800

    fix: avoid duplicate CheckOverflow evaluation for decimal division (#5225)
---
 .github/workflows/pr_build_linux.yml               |   1 +
 .github/workflows/pr_build_macos.yml               |   1 +
 .../scala/org/apache/comet/serde/arithmetic.scala  |  29 +++---
 .../apache/spark/sql/comet/DecimalPrecision.scala  |  13 ++-
 .../sql/comet/CometDecimalPromotionSuite.scala     | 107 +++++++++++++++++++++
 .../comet/CometDecimalArithmeticViewSuite.scala    |  17 +---
 6 files changed, 131 insertions(+), 37 deletions(-)

diff --git a/.github/workflows/pr_build_linux.yml 
b/.github/workflows/pr_build_linux.yml
index 3edaa3e569..b42d80d348 100644
--- a/.github/workflows/pr_build_linux.yml
+++ b/.github/workflows/pr_build_linux.yml
@@ -365,6 +365,7 @@ jobs:
               org.apache.spark.sql.comet.CometShuffleFallbackStickinessSuite
               org.apache.spark.sql.comet.PlanDataInjectorSuite
               org.apache.spark.sql.comet.CometDecimalArithmeticViewSuite
+              org.apache.spark.sql.comet.CometDecimalPromotionSuite
               org.apache.spark.sql.comet.CometScanWithPlanDataSuite
               org.apache.spark.sql.comet.util.UtilsSuite
               org.apache.comet.vector.NativeUtilSuite
diff --git a/.github/workflows/pr_build_macos.yml 
b/.github/workflows/pr_build_macos.yml
index 42714de24c..71a7e94c5b 100644
--- a/.github/workflows/pr_build_macos.yml
+++ b/.github/workflows/pr_build_macos.yml
@@ -181,6 +181,7 @@ jobs:
               org.apache.spark.sql.comet.CometShuffleFallbackStickinessSuite
               org.apache.spark.sql.comet.PlanDataInjectorSuite
               org.apache.spark.sql.comet.CometDecimalArithmeticViewSuite
+              org.apache.spark.sql.comet.CometDecimalPromotionSuite
               org.apache.spark.sql.comet.CometScanWithPlanDataSuite
               org.apache.spark.sql.comet.util.UtilsSuite
               org.apache.comet.vector.NativeUtilSuite
diff --git a/spark/src/main/scala/org/apache/comet/serde/arithmetic.scala 
b/spark/src/main/scala/org/apache/comet/serde/arithmetic.scala
index 058ff2ea5f..c5a9e57092 100644
--- a/spark/src/main/scala/org/apache/comet/serde/arithmetic.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/arithmetic.scala
@@ -248,8 +248,16 @@ object CometMultiply extends 
CometExpressionSerde[Multiply] with MathBase {
 
 object CometDivide extends CometExpressionSerde[Divide] with MathBase {
 
-  override def getSupportLevel(expr: Divide): SupportLevel =
-    mathDataTypeSupportLevel(expr.left.dataType)
+  override def getSupportLevel(expr: Divide): SupportLevel = {
+    if (expr.dataType.isInstanceOf[DecimalType] &&
+      (!expr.left.dataType.isInstanceOf[DecimalType] ||
+        !expr.right.dataType.isInstanceOf[DecimalType])) {
+      // This is only a sanity check; Spark's type coercion should prevent 
this case.
+      Unsupported(Some("Decimal division with a decimal result requires 
decimal operands"))
+    } else {
+      mathDataTypeSupportLevel(expr.left.dataType)
+    }
+  }
 
   override def convert(
       expr: Divide,
@@ -260,7 +268,7 @@ object CometDivide extends CometExpressionSerde[Divide] 
with MathBase {
     // For now, use NullIf to swap zeros with nulls.
     val rightExpr =
       if (expr.evalMode != EvalMode.ANSI) nullIfWhenPrimitive(expr.right) else 
expr.right
-    val divideExpr = createMathExpression(
+    createMathExpression(
       expr,
       expr.left,
       rightExpr,
@@ -269,21 +277,6 @@ object CometDivide extends CometExpressionSerde[Divide] 
with MathBase {
       expr.dataType,
       expr.evalMode,
       (builder, mathExpr) => builder.setDivide(mathExpr))
-
-    // For decimal division Spark applies CheckOverflow after dividing: in 
ANSI mode overflow
-    // throws NUMERIC_VALUE_OUT_OF_RANGE; in legacy/try mode it returns null.  
The Rust
-    // spark_decimal_div_internal uses i128::MAX as a sentinel for overflow, 
so without this
-    // wrapper an ANSI overflow would silently return a garbage value instead 
of throwing.
-    if (divideExpr.isDefined && expr.dataType.isInstanceOf[DecimalType] &&
-      serializeDataType(expr.dataType).isDefined) {
-      val builder = ExprOuterClass.CheckOverflow.newBuilder()
-      builder.setChild(divideExpr.get)
-      builder.setFailOnError(expr.evalMode == EvalMode.ANSI)
-      builder.setDatatype(serializeDataType(expr.dataType).get)
-      Some(ExprOuterClass.Expr.newBuilder().setCheckOverflow(builder).build())
-    } else {
-      divideExpr
-    }
   }
 }
 
diff --git 
a/spark/src/main/scala/org/apache/spark/sql/comet/DecimalPrecision.scala 
b/spark/src/main/scala/org/apache/spark/sql/comet/DecimalPrecision.scala
index 2d5247df7e..08efcfc9c9 100644
--- a/spark/src/main/scala/org/apache/spark/sql/comet/DecimalPrecision.scala
+++ b/spark/src/main/scala/org/apache/spark/sql/comet/DecimalPrecision.scala
@@ -41,9 +41,14 @@ import org.apache.spark.sql.types.DecimalType
 object DecimalPrecision {
   def promote(expr: Expression): Expression = {
     expr.transformUp {
-      // This means the binary expression is already optimized with the rule 
in Spark. This can
-      // happen if the Spark version is < 3.4
-      case e: BinaryArithmetic if e.left.prettyName == "promote_precision" => e
+      // Recursive exprToProto calls can re-promote every decimal binary 
operator below. Collapse
+      // only equivalent wrappers so the shared promotion rule remains 
idempotent.
+      case outer @ CheckOverflow(
+            inner @ CheckOverflow(child: BinaryArithmetic, _, _),
+            dataType,
+            nullOnOverflow)
+          if inner.dataType == dataType && inner.nullOnOverflow == 
nullOnOverflow =>
+        outer.copy(child = child)
 
       case add @ Add(DecimalExpression(_, _), DecimalExpression(_, _), _)
           if add.dataType.isInstanceOf[DecimalType] =>
@@ -57,6 +62,8 @@ object DecimalPrecision {
           if mul.dataType.isInstanceOf[DecimalType] =>
         CheckOverflow(mul, mul.dataType.asInstanceOf[DecimalType], 
mul.evalMode != EvalMode.ANSI)
 
+      // Native decimal division uses i128::MAX as an overflow sentinel, so 
this wrapper must
+      // convert it to null or an ANSI error according to the expression's 
eval mode.
       case div @ Divide(DecimalExpression(_, _), DecimalExpression(_, _), _)
           if div.dataType.isInstanceOf[DecimalType] =>
         CheckOverflow(div, div.dataType.asInstanceOf[DecimalType], 
div.evalMode != EvalMode.ANSI)
diff --git 
a/spark/src/test/scala/org/apache/spark/sql/comet/CometDecimalPromotionSuite.scala
 
b/spark/src/test/scala/org/apache/spark/sql/comet/CometDecimalPromotionSuite.scala
new file mode 100644
index 0000000000..346e7bef27
--- /dev/null
+++ 
b/spark/src/test/scala/org/apache/spark/sql/comet/CometDecimalPromotionSuite.scala
@@ -0,0 +1,107 @@
+/*
+ * 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.spark.sql.comet
+
+import org.apache.spark.sql.CometTestBase
+import org.apache.spark.sql.catalyst.expressions.{ArrayContains, 
AttributeReference, Divide, EvalMode}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{DecimalType, IntegerType}
+
+import org.apache.comet.serde.{CometDivide, ExprOuterClass, QueryPlanSerde, 
Unsupported}
+
+class CometDecimalPromotionSuite extends CometTestBase {
+
+  private case class Operation(
+      name: String,
+      symbol: String,
+      hasProto: ExprOuterClass.Expr => Boolean)
+
+  private case class TestMode(name: String, ansiEnabled: Boolean, failOnError: 
Boolean)
+
+  test("issue #5190: decimal promotion is idempotent during recursive 
serialization") {
+    val left = "CAST(id AS DECIMAL(10, 0))"
+    val right = "CAST(id + 1 AS DECIMAL(10, 0))"
+    val divide = Operation(name = "divide", symbol = "/", hasProto = 
_.hasDivide)
+    val operations = Seq(
+      Operation(name = "add", symbol = "+", hasProto = _.hasAdd),
+      Operation(name = "subtract", symbol = "-", hasProto = _.hasSubtract),
+      Operation(name = "multiply", symbol = "*", hasProto = _.hasMultiply),
+      divide,
+      Operation(name = "remainder", symbol = "%", hasProto = _.hasRemainder))
+    val legacy = TestMode(name = "LEGACY", ansiEnabled = false, failOnError = 
false)
+    val ansi = TestMode(name = "ANSI", ansiEnabled = true, failOnError = true)
+
+    def check(operation: Operation, mode: TestMode, arithmetic: String): Unit 
= {
+      val name = s"${operation.name} ${mode.name}"
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> mode.ansiEnabled.toString) {
+        val plan = spark
+          .sql(s"SELECT array_contains(array($arithmetic), $arithmetic) FROM 
range(1, 4)")
+          .queryExecution
+          .optimizedPlan
+        val expression = plan.expressions.head
+        val arrayContains = expression.collectFirst { case e: ArrayContains => 
e }.get
+        val promoted = DecimalPrecision.promote(expression)
+        assert(
+          DecimalPrecision.promote(promoted) == promoted,
+          s"$name promotion is not idempotent: $promoted")
+
+        // This proto-shape check relies on CometArrayContains re-entering 
exprToProto for its
+        // children. If https://github.com/apache/datafusion-comet/issues/5248 
changes that,
+        // re-point it to another recursively serializing serde.
+        val arithmeticProto = QueryPlanSerde
+          .exprToProto(expression, plan.children.head.output)
+          .get
+          .getScalarFunc
+          .getArgs(1)
+        assert(arithmeticProto.hasCheckOverflow, s"$name: $arithmeticProto")
+        val overflow = arithmeticProto.getCheckOverflow
+        assert(
+          overflow.getDatatype === QueryPlanSerde
+            .serializeDataType(arrayContains.right.dataType)
+            .get,
+          s"$name has the wrong CheckOverflow datatype: $arithmeticProto")
+        assert(overflow.getFailOnError === mode.failOnError, s"$name: 
$arithmeticProto")
+        assert(
+          operation.hasProto(overflow.getChild),
+          s"$name has duplicate CheckOverflow: $arithmeticProto")
+      }
+    }
+
+    operations.foreach { operation =>
+      Seq(legacy, ansi).foreach { mode =>
+        check(operation, mode, s"$left ${operation.symbol} $right")
+      }
+    }
+    check(
+      divide,
+      TestMode(name = "TRY", ansiEnabled = true, failOnError = false),
+      s"try_divide($left, $right)")
+  }
+
+  test("decimal Divide with a non-decimal operand is unsupported") {
+    // This is only a sanity check; Spark's type coercion should prevent this 
case.
+    val decimal = AttributeReference("decimal", DecimalType(10, 0))()
+    val integer = AttributeReference("integer", IntegerType)()
+    val divide = Divide(decimal, integer, EvalMode.LEGACY)
+
+    assert(divide.dataType === DecimalType(10, 0))
+    assert(CometDivide.getSupportLevel(divide).isInstanceOf[Unsupported])
+  }
+}
diff --git 
a/spark/src/test/spark-4.1+/org/apache/spark/sql/comet/CometDecimalArithmeticViewSuite.scala
 
b/spark/src/test/spark-4.1+/org/apache/spark/sql/comet/CometDecimalArithmeticViewSuite.scala
index 37189714ad..0a214cf91f 100644
--- 
a/spark/src/test/spark-4.1+/org/apache/spark/sql/comet/CometDecimalArithmeticViewSuite.scala
+++ 
b/spark/src/test/spark-4.1+/org/apache/spark/sql/comet/CometDecimalArithmeticViewSuite.scala
@@ -94,22 +94,7 @@ class CometDecimalArithmeticViewSuite extends CometTestBase {
           val ansiOverflow = proto.getCheckOverflow
           assert(ansiOverflow.getFailOnError, s"$name under session 
ANSI=$sessionAnsiEnabled")
 
-          // Decimal division already adds its own CheckOverflow inside the 
one added by
-          // DecimalPrecision, so peel that wrapper before inspecting the 
Divide proto.
-          val mathExprProto =
-            if (name == "divide") {
-              assert(
-                ansiOverflow.getChild.hasCheckOverflow,
-                s"$name under session ANSI=$sessionAnsiEnabled")
-              val divideOverflow = ansiOverflow.getChild.getCheckOverflow
-              assert(
-                divideOverflow.getFailOnError,
-                s"$name under session ANSI=$sessionAnsiEnabled")
-              divideOverflow.getChild
-            } else {
-              ansiOverflow.getChild
-            }
-
+          val mathExprProto = ansiOverflow.getChild
           val tryExprProto = getMathExpr(mathExprProto).getLeft
           assert(tryExprProto.hasCheckOverflow, s"$name under session 
ANSI=$sessionAnsiEnabled")
           assert(


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

Reply via email to