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

cloud-fan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 94d23c3220f5 [SPARK-56663][SQL][FOLLOWUP] Fix silent overflow in 
date_trunc fast path.
94d23c3220f5 is described below

commit 94d23c3220f5b9eb0503d756e430b0371c79b58a
Author: chenhao-db <[email protected]>
AuthorDate: Thu Jun 4 22:32:17 2026 +0800

    [SPARK-56663][SQL][FOLLOWUP] Fix silent overflow in date_trunc fast path.
    
    ### What changes were proposed in this pull request?
    
    Fixes the bug introduced by https://github.com/apache/spark/pull/55610. 
When `local` is a very large negative value, `Math.floorMod(local, unitMicros)` 
is positive because `unitMicros` is positive, and the subtraction can overflow 
to positive values. This produces a inconsistent result from the slow path.
    
    In most common cases, this optimization still takes effect. The performance 
loss is ignorable.
    
    ### Why are the changes needed?
    
    Fix incorrect result.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    New unit tests. It passes before 
https://github.com/apache/spark/pull/55610, or after this change. But it fails 
on the current master.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    No.
    
    Closes #56313 from chenhao-db/fix_trunc_overflow.
    
    Authored-by: chenhao-db <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../apache/spark/sql/catalyst/util/DateTimeUtils.scala    |  6 +++---
 .../sql/catalyst/expressions/DateExpressionsSuite.scala   | 15 +++++++++++++++
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
index f4a47db0fe43..c5e6b5cbce93 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
@@ -510,7 +510,7 @@ object DateTimeUtils extends SparkDateTimeUtils {
     val offsetMicros = originalOffsetSec * MICROS_PER_SECOND
     try {
       val local = Math.addExact(micros, offsetMicros)
-      val truncatedLocal = local - Math.floorMod(local, unitMicros)
+      val truncatedLocal = Math.subtractExact(local, Math.floorMod(local, 
unitMicros))
       val candidate = Math.subtractExact(truncatedLocal, offsetMicros)
       if (!rules.isFixedOffset) {
         val candidateSec = Math.floorDiv(candidate, MICROS_PER_SECOND)
@@ -537,9 +537,9 @@ object DateTimeUtils extends SparkDateTimeUtils {
     level match {
       case TRUNC_TO_MICROSECOND => micros
       case TRUNC_TO_MILLISECOND =>
-        micros - Math.floorMod(micros, MICROS_PER_MILLIS)
+        Math.subtractExact(micros, Math.floorMod(micros, MICROS_PER_MILLIS))
       case TRUNC_TO_SECOND =>
-        micros - Math.floorMod(micros, MICROS_PER_SECOND)
+        Math.subtractExact(micros, Math.floorMod(micros, MICROS_PER_SECOND))
       case TRUNC_TO_MINUTE =>
         truncToUnitFast(micros, zoneId, MICROS_PER_MINUTE, ChronoUnit.MINUTES)
       case TRUNC_TO_HOUR =>
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala
index 824d08d67c50..4a2b23fe059b 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala
@@ -853,6 +853,21 @@ class DateExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
     }
   }
 
+  test("TruncTimestamp of Long.MinValue overflows with ArithmeticException") {
+    withDefaultTimeZone(UTC) {
+      // Long.MinValue is the smallest representable timestamp value (in 
micros). Truncating it
+      // rounds the value down to an earlier instant, which falls below the 
representable micros
+      // range. The overflow must surface as an ArithmeticException instead of 
silently wrapping
+      // around to a bogus (positive) timestamp.
+      val minTimestamp = Literal.create(Long.MinValue, TimestampType)
+      Seq("YEAR", "QUARTER", "MONTH", "WEEK", "DAY", "HOUR", "MINUTE",
+          "SECOND", "MILLISECOND").foreach { fmt =>
+        checkExceptionInExpression[ArithmeticException](
+          TruncTimestamp(Literal.create(fmt, StringType), minTimestamp), "")
+      }
+    }
+  }
+
   test("unsupported fmt fields for trunc/date_trunc results null") {
     Seq("INVALID", "decade", "century", "millennium", "whatever", 
null).foreach { field =>
       testTruncDate(Date.valueOf("2000-03-08"), field, null)


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

Reply via email to