You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ge...@apache.org on 2026/07/10 18:43:06 UTC

(spark) branch branch-4.x updated: [SPARK-58040][SQL] Extract a MathUtils.doubleToLong helper to simplify Ceil/Floor overflow codegen

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

gengliangwang pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 36c8ac0187c5 [SPARK-58040][SQL] Extract a MathUtils.doubleToLong helper to simplify Ceil/Floor overflow codegen
36c8ac0187c5 is described below

commit 36c8ac0187c5a0f727707700a07b9f8b9b5da956
Author: Gengliang Wang <ge...@apache.org>
AuthorDate: Fri Jul 10 11:42:28 2026 -0700

    [SPARK-58040][SQL] Extract a MathUtils.doubleToLong helper to simplify Ceil/Floor overflow codegen
    
    ### What changes were proposed in this pull request?
    
    `Ceil` and `Floor` (ANSI mode, `DOUBLE -> LONG`) check for overflow before casting the rounded
    double to long. That check is currently implemented twice, in a private `CeilFloor` helper object in
    `mathExpressions.scala`:
    
    - `CeilFloor.doubleToLong` for the interpreted path, and
    - `CeilFloor.doubleToLongCode`, which re-emits the bound check as ~9 lines of Java into every
      `Ceil`/`Floor` codegen stage.
    
    This PR replaces both with a single shared `MathUtils.doubleToLong(value, context)` utility that the
    interpreted and generated code both call, and deletes the `CeilFloor` object. The generated code
    collapses from the inline block:
    
    ```java
    double roundedValue = java.lang.Math.ceil(input);
    if (!java.lang.Double.isNaN(roundedValue) &&
        (roundedValue < (double) java.lang.Long.MIN_VALUE ||
         roundedValue >= (double) java.lang.Long.MAX_VALUE)) {
      throw QueryExecutionErrors.arithmeticOverflowError("long overflow", "", errCtx);
    }
    result = (long) roundedValue;
    ```
    
    to a single call:
    
    ```java
    result = org.apache.spark.sql.catalyst.util.MathUtils.doubleToLong(
      java.lang.Math.ceil(input), errCtx);
    ```
    
    `MathUtils` is the natural home: it already hosts the other arithmetic-overflow helpers
    (`addExact`, `negateExact`, `withOverflow`) and follows the same "shared by the eval and codegen
    paths so the two never diverge" convention already used for `pmod`. Placing `doubleToLong` there also
    means one fewer copy of the bound check emitted per stage (part of the broader effort to shrink
    whole-stage-codegen output, SPARK-56908).
    
    ### Why are the changes needed?
    
    The overflow check is type-independent bookkeeping that does not need to be re-emitted into each
    generated stage. Consolidating it into `MathUtils`:
    - removes the duplicated logic (the `CeilFloor.doubleToLong` eval copy and the
      `doubleToLongCode` string builder collapse into one method), so the two paths cannot drift, and
    - shrinks the generated code for every ANSI `CEIL`/`FLOOR` over a `DOUBLE` from the multi-line
      bound check to a single call.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. The check, the error (`ARITHMETIC_OVERFLOW`), and the query context are identical; `defineCodeGen`
    applies the same null-safety wrapper the previous `nullSafeCodeGen` did. This is a pure
    code-organization change.
    
    ### How was this patch tested?
    
    Existing tests, run with whole-stage codegen both on and off:
    - `MathExpressionsSuite` (`ceil` and `floor`, including the ANSI overflow cases and
      `checkConsistencyBetweenInterpretedAndCodegenAllowingException` for `DoubleType`), and
    - regenerated `postgreSQL/float8.sql.out` (unchanged, confirming the behavior is preserved).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #57143 from gengliangwang/SPARK-58040-mathutils-doubletolong.
    
    Authored-by: Gengliang Wang <ge...@apache.org>
    Signed-off-by: Gengliang Wang <ge...@apache.org>
    (cherry picked from commit bbe1b19aa17bb723975f9b20006007aa7b78f03e)
    Signed-off-by: Gengliang Wang <ge...@apache.org>
---
 .../apache/spark/sql/catalyst/util/MathUtils.scala | 12 ++++++
 .../sql/catalyst/expressions/mathExpressions.scala | 48 ++++------------------
 2 files changed, 20 insertions(+), 40 deletions(-)

diff --git a/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/MathUtils.scala b/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/MathUtils.scala
index a4c6c75358f4..e318a60ce99a 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/MathUtils.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/MathUtils.scala
@@ -123,6 +123,18 @@ object MathUtils {
     if (r < 0) (r + n) % n else r
   }
 
+  // Casts a rounded double (the result of Math.ceil/Math.floor) to long, throwing an arithmetic
+  // overflow error when the value cannot be represented as a long. NaN is passed through to the
+  // JVM cast (which yields 0), matching the previous behavior. Shared by the eval and codegen
+  // paths of `Ceil`/`Floor` so the two never diverge.
+  def doubleToLong(value: Double, context: QueryContext): Long = {
+    if (!value.isNaN &&
+      (value < Long.MinValue.toDouble || value >= Long.MaxValue.toDouble)) {
+      throw ExecutionErrors.arithmeticOverflowError("long overflow", context = context)
+    }
+    value.toLong
+  }
+
   def withOverflow[A](f: => A, hint: String = "", context: QueryContext = null): A = {
     try {
       f
diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala
index 7a76968018c0..81b576146abe 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala
@@ -252,34 +252,6 @@ case class Cbrt(child: Expression) extends UnaryMathExpression(math.cbrt, "CBRT"
   override protected def withNewChildInternal(newChild: Expression): Cbrt = copy(child = newChild)
 }
 
-private object CeilFloor {
-  def doubleToLong(value: Double, context: QueryContext): Long = {
-    if (!value.isNaN &&
-        (value < Long.MinValue.toDouble || value >= Long.MaxValue.toDouble)) {
-      throw QueryExecutionErrors.arithmeticOverflowError("long overflow", context = context)
-    }
-    value.toLong
-  }
-
-  def doubleToLongCode(
-      input: String,
-      funcName: String,
-      result: String,
-      roundedValue: String,
-      errorContext: String): String = {
-    s"""
-       |double $roundedValue = java.lang.Math.$funcName($input);
-       |if (!java.lang.Double.isNaN($roundedValue) &&
-       |    ($roundedValue < (double) java.lang.Long.MIN_VALUE ||
-       |     $roundedValue >= (double) java.lang.Long.MAX_VALUE)) {
-       |  throw QueryExecutionErrors.arithmeticOverflowError(
-       |    "long overflow", "", $errorContext);
-       |}
-       |$result = (long) $roundedValue;
-       |""".stripMargin
-  }
-}
-
 case class Ceil(child: Expression, failOnError: Boolean = SQLConf.get.ansiEnabled)
   extends UnaryMathExpression(math.ceil, "CEIL") with SupportQueryContext {
   override def dataType: DataType = child.dataType match {
@@ -299,7 +271,7 @@ case class Ceil(child: Expression, failOnError: Boolean = SQLConf.get.ansiEnable
   protected override def nullSafeEval(input: Any): Any = child.dataType match {
     case LongType => input.asInstanceOf[Long]
     case DoubleType if failOnError =>
-      CeilFloor.doubleToLong(f(input.asInstanceOf[Double]), getContextOrNull())
+      MathUtils.doubleToLong(f(input.asInstanceOf[Double]), getContextOrNull())
     case DoubleType => f(input.asInstanceOf[Double]).toLong
     case DecimalType.Fixed(_, _) => input.asInstanceOf[Decimal].ceil
   }
@@ -311,11 +283,9 @@ case class Ceil(child: Expression, failOnError: Boolean = SQLConf.get.ansiEnable
         defineCodeGen(ctx, ev, c => s"$c.ceil()")
       case LongType => defineCodeGen(ctx, ev, c => s"$c")
       case DoubleType if failOnError =>
-        nullSafeCodeGen(ctx, ev, c => {
-          val roundedValue = ctx.freshName("roundedValue")
-          val errorContext = getContextOrNullCode(ctx)
-          CeilFloor.doubleToLongCode(c, funcName, ev.value, roundedValue, errorContext)
-        })
+        val mathUtils = MathUtils.getClass.getCanonicalName.stripSuffix("$")
+        defineCodeGen(ctx, ev, c =>
+          s"$mathUtils.doubleToLong(java.lang.Math.$funcName($c), ${getContextOrNullCode(ctx)})")
       case _ => defineCodeGen(ctx, ev, c => s"(long)(java.lang.Math.${funcName}($c))")
     }
   }
@@ -591,7 +561,7 @@ case class Floor(child: Expression, failOnError: Boolean = SQLConf.get.ansiEnabl
   protected override def nullSafeEval(input: Any): Any = child.dataType match {
     case LongType => input.asInstanceOf[Long]
     case DoubleType if failOnError =>
-      CeilFloor.doubleToLong(f(input.asInstanceOf[Double]), getContextOrNull())
+      MathUtils.doubleToLong(f(input.asInstanceOf[Double]), getContextOrNull())
     case DoubleType => f(input.asInstanceOf[Double]).toLong
     case DecimalType.Fixed(_, _) => input.asInstanceOf[Decimal].floor
   }
@@ -603,11 +573,9 @@ case class Floor(child: Expression, failOnError: Boolean = SQLConf.get.ansiEnabl
         defineCodeGen(ctx, ev, c => s"$c.floor()")
       case LongType => defineCodeGen(ctx, ev, c => s"$c")
       case DoubleType if failOnError =>
-        nullSafeCodeGen(ctx, ev, c => {
-          val roundedValue = ctx.freshName("roundedValue")
-          val errorContext = getContextOrNullCode(ctx)
-          CeilFloor.doubleToLongCode(c, funcName, ev.value, roundedValue, errorContext)
-        })
+        val mathUtils = MathUtils.getClass.getCanonicalName.stripSuffix("$")
+        defineCodeGen(ctx, ev, c =>
+          s"$mathUtils.doubleToLong(java.lang.Math.$funcName($c), ${getContextOrNullCode(ctx)})")
       case _ => defineCodeGen(ctx, ev, c => s"(long)(java.lang.Math.${funcName}($c))")
     }
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org