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

HyukjinKwon 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 579f74b8a5ea [SPARK-60573][CONNECT] Preserve the full day-time 
interval range in Spark Connect Arrow decoding
579f74b8a5ea is described below

commit 579f74b8a5eab79c2f6a046b1f63ed711290f58a
Author: Linhong Liu <[email protected]>
AuthorDate: Wed Oct 28 10:26:22 2026 -0700

    [SPARK-60573][CONNECT] Preserve the full day-time interval range in Spark 
Connect Arrow decoding
    
    ### What changes were proposed in this pull request?
    
    Read day-time interval values directly as microseconds and convert them 
with SparkIntervalUtils.microsToDuration.
    
    ### Why are the changes needed?
    
    Arrow's DurationVector.getObject converts microseconds through a signed 
64-bit nanosecond count. That intermediate value saturates for valid large 
Spark intervals. For example, 10000000000000000 microseconds decodes as 
approximately 9223372036854775 microseconds.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, a bug fix. The Spark Connect JVM client retains the full day-time 
interval range when decoding Arrow results. The wire representation is 
unchanged.
    
    ### How was this patch tested?
    
    Added six regression cases covering Long.MinValue, Long.MaxValue, -1, 0, 1 
and a large positive microsecond count, each with NULL and interval-to-string 
decoding. Three large-range cases fail on unchanged Apache code. On JDK 17, all 
125 ArrowEncoderSuite tests passed; the six regression cases passed again after 
formatting. Connect common/client Scalastyle and Scalafmt checks, plus git diff 
--check, passed.
    
    ```bash
    build/sbt 'connect-client-jvm/testOnly 
org.apache.spark.sql.connect.client.arrow.ArrowEncoderSuite'
    ```
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: OpenAI Codex (GPT-6)
    
    Closes #59817 from linhongliu-db/fix-connect-interval-range.
    
    Authored-by: Linhong Liu <[email protected]>
    Signed-off-by: Hyukjin Kwon <[email protected]>
    (cherry picked from commit 57742c90eee4626c5e869e960f0f27c22d7ee201)
    Signed-off-by: Hyukjin Kwon <[email protected]>
---
 .../connect/client/arrow/ArrowEncoderSuite.scala   | 31 ++++++++++++++++++++++
 .../connect/client/arrow/ArrowVectorReader.scala   |  6 ++++-
 2 files changed, 36 insertions(+), 1 deletion(-)

diff --git 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/arrow/ArrowEncoderSuite.scala
 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/arrow/ArrowEncoderSuite.scala
index d4319f2b8e55..6cd9731851f2 100644
--- 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/arrow/ArrowEncoderSuite.scala
+++ 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/arrow/ArrowEncoderSuite.scala
@@ -1222,6 +1222,37 @@ class ArrowEncoderSuite extends ConnectFunSuite {
           DayTimeIntervalType.DEFAULT.startField,
           DayTimeIntervalType.DEFAULT.endField)
       })
+
+  for (micros <- Seq(Long.MinValue, Long.MaxValue, -1L, 0L, 1L, 
10000000000000000L)) {
+    test(s"day-time intervals retain full microsecond range: $micros") {
+      val duration = Duration.of(micros, ChronoUnit.MICROS)
+      val values = Seq(duration, null, Duration.ZERO)
+      val result = roundTrip(DayTimeIntervalEncoder, values.iterator)
+      try {
+        assert(result.toSeq == values)
+      } finally {
+        result.close()
+      }
+      val strings =
+        roundTripWithDifferentIOEncoders(DayTimeIntervalEncoder, 
StringEncoder, values.iterator)
+      try {
+        assert(strings.toSeq == values.map { value =>
+          if (value == null) {
+            null
+          } else {
+            toDayTimeIntervalString(
+              durationToMicros(value),
+              ANSI_STYLE,
+              DayTimeIntervalType.DEFAULT.startField,
+              DayTimeIntervalType.DEFAULT.endField)
+          }
+        })
+      } finally {
+        strings.close()
+      }
+    }
+  }
+
   UpCastTestCase(YearMonthIntervalEncoder, i => Period.ofMonths(i))
     .test(
       StringEncoder,
diff --git 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/arrow/ArrowVectorReader.scala
 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/arrow/ArrowVectorReader.scala
index 1f71ed8f8432..bcaddbc697b8 100644
--- 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/arrow/ArrowVectorReader.scala
+++ 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/arrow/ArrowVectorReader.scala
@@ -242,7 +242,11 @@ private[arrow] class ViewVarBinaryVectorReader(v: 
ViewVarBinaryVector)
 
 private[arrow] class DurationVectorReader(v: DurationVector)
     extends TypedArrowVectorReader[DurationVector](v) {
-  override def getDuration(i: Int): Duration = vector.getObject(i)
+  // Spark stores day-time intervals as microseconds. Arrow getObject converts 
via a Long
+  // nanosecond count, which saturates for valid large intervals. Decode the 
raw microseconds
+  // directly to retain the full Spark interval range.
+  override def getDuration(i: Int): Duration =
+    
SparkIntervalUtils.microsToDuration(DurationVector.get(vector.getDataBuffer, i))
   override def getString(i: Int): String = {
     SparkIntervalUtils.toDayTimeIntervalString(
       SparkIntervalUtils.durationToMicros(getDuration(i)),


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

Reply via email to