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 45fa7d606c [GLUTEN-10134][VL] Throw on decimal Add/Subtract/Multiply 
overflow in ANSI mode (#13170)
45fa7d606c is described below

commit 45fa7d606cb1c1dc34fe3eb5111802768f1173dc
Author: rdtr <[email protected]>
AuthorDate: Sun Oct 11 00:00:18 2026 -0700

    [GLUTEN-10134][VL] Throw on decimal Add/Subtract/Multiply overflow in ANSI 
mode (#13170)
---
 .../backendsapi/bolt/BoltSparkPlanExecApi.scala    |   1 +
 .../backendsapi/velox/VeloxSparkPlanExecApi.scala  |  24 +++-
 .../functions/ArithmeticAnsiValidateSuite.scala    | 151 ++++++++++++++++++---
 .../gluten/backendsapi/SparkPlanExecApi.scala      |  12 +-
 .../gluten/expression/ExpressionConverter.scala    |   1 +
 5 files changed, 161 insertions(+), 28 deletions(-)

diff --git 
a/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltSparkPlanExecApi.scala
 
b/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltSparkPlanExecApi.scala
index 006435066d..331e984455 100644
--- 
a/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltSparkPlanExecApi.scala
+++ 
b/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltSparkPlanExecApi.scala
@@ -164,6 +164,7 @@ class BoltSparkPlanExecApi extends SparkPlanExecApi {
 
   override def getDecimalArithmeticExprName(
       exprName: String,
+      original: BinaryArithmetic,
       allowPrecisionLoss: Boolean): String =
     if (!allowPrecisionLoss) { exprName + "_deny_precision_loss" }
     else { exprName }
diff --git 
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala
 
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala
index a7fd67ea81..640e42690b 100644
--- 
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala
+++ 
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala
@@ -165,9 +165,27 @@ class VeloxSparkPlanExecApi extends SparkPlanExecApi with 
Logging {
     }
   }
 
-  override def getDecimalArithmeticExprName(exprName: String, 
allowPrecisionLoss: Boolean): String =
-    if (!allowPrecisionLoss) { exprName + "_deny_precision_loss" }
-    else { exprName }
+  /**
+   * In ANSI mode, decimal Add, Subtract and Multiply map to Velox's checked_ 
functions, which throw
+   * on overflow instead of returning null.
+   */
+  override def getDecimalArithmeticExprName(
+      exprName: String,
+      original: BinaryArithmetic,
+      allowPrecisionLoss: Boolean): String = {
+    val name = if (ExpressionUtils.withAnsiEvalMode(original)) {
+      original match {
+        case _: Add => ExpressionNames.CHECKED_ADD
+        case _: Subtract => ExpressionNames.CHECKED_SUBTRACT
+        case _: Multiply => ExpressionNames.CHECKED_MULTIPLY
+        case _ => exprName
+      }
+    } else {
+      exprName
+    }
+    if (!allowPrecisionLoss) { name + "_deny_precision_loss" }
+    else { name }
+  }
 
   /** Transform map_entries to Substrait. */
   override def genMapEntriesTransformer(
diff --git 
a/backends-velox/src/test/scala/org/apache/gluten/functions/ArithmeticAnsiValidateSuite.scala
 
b/backends-velox/src/test/scala/org/apache/gluten/functions/ArithmeticAnsiValidateSuite.scala
index 9b7a14a51f..795e6a16c4 100644
--- 
a/backends-velox/src/test/scala/org/apache/gluten/functions/ArithmeticAnsiValidateSuite.scala
+++ 
b/backends-velox/src/test/scala/org/apache/gluten/functions/ArithmeticAnsiValidateSuite.scala
@@ -33,22 +33,38 @@ class ArithmeticAnsiValidateSuite extends 
FunctionsValidateSuite {
       .set(SQLConf.ANSI_ENABLED.key, "true")
   }
 
+  private val maxDecimal = "CAST(%s AS DECIMAL(38,0))".format("9" * 38)
+  private val minDecimal = "CAST(-%s AS DECIMAL(38,0))".format("9" * 38)
+
+  // Reads `columns` from a table and evaluates `expr` on them, so the 
arithmetic runs in Velox.
+  // With ANSI mode on, the overflow throws Velox's `error`. With ANSI mode 
off, the result
+  // matches Spark: NULL for decimals and a wrapped value for integers.
+  private def checkOverflow(columns: String, expr: String, error: String): 
Unit = {
+    withTempPath {
+      path =>
+        sql(s"SELECT $columns").write.parquet(path.getCanonicalPath)
+        withTempView("overflow_tab") {
+          
spark.read.parquet(path.getCanonicalPath).createOrReplaceTempView("overflow_tab")
+          val query = s"SELECT $expr FROM overflow_tab"
+          withSQLConf(SQLConf.ANSI_ENABLED.key -> "true") {
+            val e = intercept[SparkException](sql(query).collect())
+            assert(e.getMessage.contains(error), e.getMessage)
+          }
+          withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+            runQueryAndCompare(query) {
+              checkGlutenPlan[ProjectExecTransformer]
+            }
+          }
+        }
+    }
+  }
+
   test("add") {
     runQueryAndCompare("SELECT int_field1 + 100 FROM datatab WHERE int_field1 
IS NOT NULL") {
       checkGlutenPlan[ProjectExecTransformer]
     }
 
-    val df = sql("SELECT 2147483647 + 1")
-
-    if (isSparkVersionGE("4.0")) {
-      intercept[SparkException] {
-        df.collect()
-      }
-    } else {
-      intercept[ArithmeticException] {
-        df.collect()
-      }
-    }
+    checkOverflow("2147483647 AS a, 1 AS b", "a + b", "Arithmetic overflow")
   }
 
   test("subtract") {
@@ -62,16 +78,7 @@ class ArithmeticAnsiValidateSuite extends 
FunctionsValidateSuite {
       checkGlutenPlan[ProjectExecTransformer]
     }
 
-    val df = sql("SELECT 2147483647 + 1")
-    if (isSparkVersionGE("4.0")) {
-      intercept[SparkException] {
-        df.collect()
-      }
-    } else {
-      intercept[ArithmeticException] {
-        df.collect()
-      }
-    }
+    checkOverflow("2147483647 AS a, 2 AS b", "a * b", "Arithmetic overflow")
   }
 
   test("divide") {
@@ -93,4 +100,106 @@ class ArithmeticAnsiValidateSuite extends 
FunctionsValidateSuite {
     }
   }
 
+  test("decimal add overflow") {
+    // Normal decimal add should succeed and match Spark results
+    runQueryAndCompare(
+      "SELECT CAST(1.0 AS DECIMAL(10,2)) + CAST(2.0 AS DECIMAL(10,2))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+
+    // Overflow: max DECIMAL(38,0) + 1
+    checkOverflow(
+      s"$maxDecimal AS a, CAST(1 AS DECIMAL(38,0)) AS b",
+      "a + b",
+      "Decimal overflow in add")
+  }
+
+  test("decimal subtract overflow") {
+    // Normal decimal subtract should succeed and match Spark results
+    runQueryAndCompare(
+      "SELECT CAST(5.0 AS DECIMAL(10,2)) - CAST(2.0 AS DECIMAL(10,2))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+
+    // Overflow: -max DECIMAL(38,0) - 1
+    checkOverflow(
+      s"$minDecimal AS a, CAST(1 AS DECIMAL(38,0)) AS b",
+      "a - b",
+      "Decimal overflow in subtract")
+  }
+
+  test("decimal try_add") {
+    // Normal case should match Spark results
+    runQueryAndCompare(
+      "SELECT try_add(CAST(1.0 AS DECIMAL(10,2)), CAST(2.0 AS 
DECIMAL(10,2)))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+    // Overflow should return null
+    runQueryAndCompare(
+      "SELECT try_add(CAST(99999999999999999999999999999999999999 AS 
DECIMAL(38,0)), " +
+        "CAST(1 AS DECIMAL(38,0)))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+  }
+
+  test("decimal try_subtract") {
+    // Normal case should match Spark results
+    runQueryAndCompare(
+      "SELECT try_subtract(CAST(5.0 AS DECIMAL(10,2)), CAST(2.0 AS 
DECIMAL(10,2)))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+    // Overflow should return null
+    runQueryAndCompare(
+      "SELECT try_subtract(CAST(-99999999999999999999999999999999999999 AS 
DECIMAL(38,0)), " +
+        "CAST(1 AS DECIMAL(38,0)))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+  }
+
+  test("decimal multiply overflow") {
+    // Normal decimal multiply should succeed and match Spark results
+    runQueryAndCompare(
+      "SELECT CAST(2.0 AS DECIMAL(10,2)) * CAST(3.0 AS DECIMAL(10,2))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+
+    // Overflow: max DECIMAL(38,0) * 2
+    checkOverflow(
+      s"$maxDecimal AS a, CAST(2 AS DECIMAL(38,0)) AS b",
+      "a * b",
+      "Decimal overflow in multiply")
+  }
+
+  test("decimal try_multiply") {
+    // Normal case should match Spark results
+    runQueryAndCompare(
+      "SELECT try_multiply(CAST(2.0 AS DECIMAL(10,2)), CAST(3.0 AS 
DECIMAL(10,2)))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+    // Overflow should return null
+    runQueryAndCompare(
+      "SELECT try_multiply(CAST(99999999999999999999999999999999999999 AS 
DECIMAL(38,0)), " +
+        "CAST(2 AS DECIMAL(38,0)))") {
+      checkGlutenPlan[ProjectExecTransformer]
+    }
+  }
+
+  test("decimal overflow with allowPrecisionLoss disabled") {
+    // Uses checked_*_deny_precision_loss. A missing function would fail 
validation and fall back
+    // to Spark, which also throws on overflow, so check that the query runs 
on Velox first.
+    withSQLConf(SQLConf.DECIMAL_OPERATIONS_ALLOW_PREC_LOSS.key -> "false") {
+      runQueryAndCompare(
+        "SELECT CAST(1.5 AS DECIMAL(10,2)) + CAST(2.25 AS DECIMAL(10,2)), " +
+          "CAST(1.5 AS DECIMAL(10,2)) - CAST(2.25 AS DECIMAL(10,2)), " +
+          "CAST(1.5 AS DECIMAL(10,2)) * CAST(2.25 AS DECIMAL(10,2))") {
+        checkGlutenPlan[ProjectExecTransformer]
+      }
+
+      val columns = s"$maxDecimal AS max, $minDecimal AS min, " +
+        "CAST(1 AS DECIMAL(38,0)) AS one, CAST(2 AS DECIMAL(38,0)) AS two"
+      checkOverflow(columns, "max + one", "Decimal overflow in add")
+      checkOverflow(columns, "min - one", "Decimal overflow in subtract")
+      checkOverflow(columns, "max * two", "Decimal overflow in multiply")
+    }
+  }
 }
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala
index 0141320030..1bbd92f3c5 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala
@@ -280,10 +280,14 @@ trait SparkPlanExecApi {
     GenericExpressionTransformer(substraitExprName, Seq(left, right), original)
   }
 
-  // Default: ignore allowPrecisionLoss and return exprName unchanged. 
Non-Velox backends
-  // (e.g. ClickHouse) do not use the _deny_precision_loss naming convention; 
they handle
-  // decimal precision through their own mechanisms. VeloxSparkPlanExecApi 
overrides this.
-  def getDecimalArithmeticExprName(exprName: String, allowPrecisionLoss: 
Boolean): String = exprName
+  // Default: ignore 'original' and allowPrecisionLoss, and return exprName 
unchanged. Non-Velox
+  // backends (e.g. ClickHouse) do not use the checked_ and 
_deny_precision_loss naming
+  // conventions; they handle decimal overflow and precision through their own 
mechanisms.
+  // VeloxSparkPlanExecApi overrides this.
+  def getDecimalArithmeticExprName(
+      exprName: String,
+      original: BinaryArithmetic,
+      allowPrecisionLoss: Boolean): String = exprName
 
   /** Transform map_entries to Substrait. */
   def genMapEntriesTransformer(
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/expression/ExpressionConverter.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/expression/ExpressionConverter.scala
index 97e21a375b..9bfd90cdd7 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/expression/ExpressionConverter.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/expression/ExpressionConverter.scala
@@ -671,6 +671,7 @@ object ExpressionConverter extends SQLConfHelper with 
Logging {
       case b: BinaryArithmetic if DecimalArithmeticUtil.isDecimalArithmetic(b) 
=>
         val exprName = 
BackendsApiManager.getSparkPlanExecApiInstance.getDecimalArithmeticExprName(
           substraitExprName,
+          b,
           SparkShimLoader.getSparkShims.decimalAllowPrecisionLoss(b))
         if (!BackendsApiManager.getSettings.transformCheckOverflow) {
           GenericExpressionTransformer(


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

Reply via email to