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

kou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-swift.git


The following commit(s) were added to refs/heads/main by this push:
     new 19d0dbe  fix: Support pre-epoch dates in `Date32` and `Date64` arrays 
(#194)
19d0dbe is described below

commit 19d0dbe55abe8c5267ef613dab94f8e161a32618
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Wed Sep 16 18:14:47 2026 -0700

    fix: Support pre-epoch dates in `Date32` and `Date64` arrays (#194)
    
    ## What's Changed
    
    Arrow `Date32` and `Date64` values are signed integers (days and
    milliseconds since the UNIX epoch), but `Date32Array` and `Date64Array`
    read them as `UInt32` and `UInt64`.
    
    - `Date32Array`: a negative day such as `-1` (`1969-12-31`) is read as
    `4294967295`, and `UInt32 * 86400` traps with `Swift runtime failure:
    arithmetic overflow`. Only days in `0...49710` (`1970-01-01` to
    `2106-02-07`) can be read. Any date before `1970-01-01` or after
    `2106-02-07` crashes the process, in both debug and release builds.
    - `Date64Array`: a negative millisecond value wraps around to a huge
    positive value, so `1969-12-31` is read as about `1.8e16` seconds after
    the epoch.
    - `Date32BufferBuilder`: `Int32(seconds / 86400)` truncates toward zero,
    so a pre-epoch `Date` that is not at UTC midnight is stored one day
    later. For example, `1969-12-31T12:00:00Z` becomes `1970-01-01`.
    
    This PR:
    
    - loads `Int32` in `Date32Array` and multiplies in `TimeInterval`.
    Loading `Int32` alone is not enough because `Int32 * 86400` overflows
    for dates after `2038-01-19`.
    - loads `Int64` in `Date64Array`.
    - uses `.rounded(.down)` in `Date32BufferBuilder`, so pre-epoch values
    are floored to their day just like post-epoch values.
    
    This was found in the Apache Spark Connect Swift client, which vendors
    these sources: collecting `DATE'1969-12-31'` crashed the client.
    
    ## Testing
    
    Added three regression tests to `ArrayTests`:
    
    - `testDate32ArrayOutsideUnsignedRange` checks both the stored `Int32`
    day and the decoded `Date` for `0001-01-01`, `1969-12-31`, `1970-01-01`,
    `2038-01-20`, `2106-02-07`, `2106-02-08`, and `9999-12-31`.
    - `testDate32BuilderPreEpochTimeOfDay` checks that pre-epoch times of
    day are floored to their day.
    - `testDate64ArrayPreEpoch` checks `0001-01-01` and `1969-12-31`.
    
    Without this fix, `testDate32ArrayOutsideUnsignedRange` crashes with
    signal 5, and the other two tests fail. With this fix, the full test
    suite passes (52 tests, 0 failures).
    
    Generated-by: Claude Opus 5
    
    ---------
    
    Co-authored-by: Sutou Kouhei <[email protected]>
---
 Sources/Arrow/ArrowArray.swift         |  8 ++++----
 Sources/Arrow/ArrowBufferBuilder.swift |  2 +-
 Tests/ArrowTests/ArrayTests.swift      | 36 ++++++++++++++++++++++++++++++++++
 3 files changed, 41 insertions(+), 5 deletions(-)

diff --git a/Sources/Arrow/ArrowArray.swift b/Sources/Arrow/ArrowArray.swift
index 759e1de..a27181c 100644
--- a/Sources/Arrow/ArrowArray.swift
+++ b/Sources/Arrow/ArrowArray.swift
@@ -217,8 +217,8 @@ public class Date32Array: ArrowArray<Date> {
         }
 
         let byteOffset = self.arrowData.stride * Int(index)
-        let milliseconds = self.arrowData.buffers[1].rawPointer.advanced(by: 
byteOffset).load(as: UInt32.self)
-        return Date(timeIntervalSince1970: TimeInterval(milliseconds * 86400))
+        let days = self.arrowData.buffers[1].rawPointer.advanced(by: 
byteOffset).load(as: Int32.self)
+        return Date(timeIntervalSince1970: TimeInterval(days) * 86400)
     }
 }
 
@@ -229,8 +229,8 @@ public class Date64Array: ArrowArray<Date> {
         }
 
         let byteOffset = self.arrowData.stride * Int(index)
-        let milliseconds = self.arrowData.buffers[1].rawPointer.advanced(by: 
byteOffset).load(as: UInt64.self)
-        return Date(timeIntervalSince1970: TimeInterval(milliseconds / 1000))
+        let milliseconds = self.arrowData.buffers[1].rawPointer.advanced(by: 
byteOffset).load(as: Int64.self)
+        return Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
     }
 }
 
diff --git a/Sources/Arrow/ArrowBufferBuilder.swift 
b/Sources/Arrow/ArrowBufferBuilder.swift
index 4e518c6..3364606 100644
--- a/Sources/Arrow/ArrowBufferBuilder.swift
+++ b/Sources/Arrow/ArrowBufferBuilder.swift
@@ -317,7 +317,7 @@ public class AbstractWrapperBufferBuilder<T, U>: 
ArrowBufferBuilder {
 public class Date32BufferBuilder: AbstractWrapperBufferBuilder<Date, Int32> {
     public override func append(_ newValue: ItemType?) {
         if let val = newValue {
-            let daysSinceEpoch = Int32(val.timeIntervalSince1970 / 86400)
+            let daysSinceEpoch = Int32((val.timeIntervalSince1970 / 
86400).rounded(.down))
             self.bufferBuilder.append(daysSinceEpoch)
         } else {
             self.bufferBuilder.append(nil)
diff --git a/Tests/ArrowTests/ArrayTests.swift 
b/Tests/ArrowTests/ArrayTests.swift
index 0f1ee8f..889452d 100644
--- a/Tests/ArrowTests/ArrayTests.swift
+++ b/Tests/ArrowTests/ArrayTests.swift
@@ -127,6 +127,42 @@ final class ArrayTests: XCTestCase { // 
swiftlint:disable:this type_body_length
         XCTAssertEqual(date64Array[0]!, date1)
     }
 
+    func testDate32ArrayOutsideUnsignedRange() throws {
+        // 0001-01-01, 1969-12-31, 1970-01-01, 2038-01-20, 2106-02-07, 
2106-02-08, 9999-12-31
+        let days: [Int32] = [-719162, -1, 0, 24856, 49710, 49711, 2932896]
+        let date32Builder: Date32ArrayBuilder = try 
ArrowArrayBuilders.loadDate32ArrayBuilder()
+        for day in days {
+            date32Builder.append(Date(timeIntervalSince1970: TimeInterval(day) 
* 86400))
+        }
+        let date32Array = try date32Builder.finish()
+        for (index, day) in days.enumerated() {
+            let rawDay = date32Array.arrowData.buffers[1].rawPointer
+                .advanced(by: index * MemoryLayout<Int32>.stride).load(as: 
Int32.self)
+            XCTAssertEqual(rawDay, day)
+            XCTAssertEqual(date32Array[UInt(index)]!.timeIntervalSince1970, 
TimeInterval(day) * 86400)
+        }
+    }
+
+    func testDate32BuilderPreEpochTimeOfDay() throws {
+        let date32Builder: Date32ArrayBuilder = try 
ArrowArrayBuilders.loadDate32ArrayBuilder()
+        date32Builder.append(Date(timeIntervalSince1970: -43200)) // 
1969-12-31T12:00:00Z
+        date32Builder.append(Date(timeIntervalSince1970: -1)) // 
1969-12-31T23:59:59Z
+        date32Builder.append(Date(timeIntervalSince1970: 43200)) // 
1970-01-01T12:00:00Z
+        let date32Array = try date32Builder.finish()
+        XCTAssertEqual(date32Array[0]!.timeIntervalSince1970, -86400)
+        XCTAssertEqual(date32Array[1]!.timeIntervalSince1970, -86400)
+        XCTAssertEqual(date32Array[2]!.timeIntervalSince1970, 0)
+    }
+
+    func testDate64ArrayPreEpoch() throws {
+        let date64Builder: Date64ArrayBuilder = try 
ArrowArrayBuilders.loadDate64ArrayBuilder()
+        date64Builder.append(Date(timeIntervalSince1970: -86400 * 719162)) // 
0001-01-01
+        date64Builder.append(Date(timeIntervalSince1970: -86400)) // 1969-12-31
+        let date64Array = try date64Builder.finish()
+        XCTAssertEqual(date64Array[0]!.timeIntervalSince1970, -86400 * 719162)
+        XCTAssertEqual(date64Array[1]!.timeIntervalSince1970, -86400)
+    }
+
     func testBinaryArray() throws {
         let binaryBuilder = try ArrowArrayBuilders.loadBinaryArrayBuilder()
         for index in 0..<100 {

Reply via email to