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 {