From 06ab323cc82598339651d8df26d65050ef705639 Mon Sep 17 00:00:00 2001 From: Abhishek Pathania Date: Fri, 11 Sep 2026 14:09:27 +0530 Subject: [PATCH] GH-1293: Use floor division when splitting epoch millis into day and time DateTimeUtils.getTimestampValue split epoch millis into an epoch day and a time-of-day remainder using `/` and `%`, which truncate toward zero. Only the remainder half compensated for negative input, so for any negative value that was not an exact multiple of a day the two halves described different days and the returned timestamp was one day later than the instant given. This is reachable from ArrowFlightJdbcDateVectorAccessor.getDate(Calendar), where the calendar offset shifts a day-aligned date value off that alignment, making every pre-1970 date come back a day late. Use Math.floorDiv and Math.floorMod for both halves, which keeps the day and the remainder on the same day and makes the manual negative adjustment redundant. Positive input is unaffected, since both functions agree with `/` and `%` there. Closes #1293. --- .../arrow/driver/jdbc/utils/DateTimeUtils.java | 13 +++++-------- .../arrow/driver/jdbc/utils/DateTimeUtilsTest.java | 11 +++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java index 9363e3486c..c4e7fda59b 100644 --- a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java @@ -55,15 +55,12 @@ public static long applyCalendarOffset(long milliseconds, Calendar calendar) { * @return a {@link Timestamp} object representing the given Epoch millis */ public static Timestamp getTimestampValue(long millisWithCalendar) { - long milliseconds = millisWithCalendar; - if (milliseconds < 0) { - // LocalTime#ofNanoDay only accepts positive values - milliseconds -= ((milliseconds / MILLIS_PER_DAY) - 1) * MILLIS_PER_DAY; - } - + // Millis are negative before 1970, where only floor semantics keep the epoch day + // and the time-of-day remainder on the same day (and the remainder non-negative). return Timestamp.valueOf( LocalDateTime.of( - LocalDate.ofEpochDay(millisWithCalendar / MILLIS_PER_DAY), - LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(milliseconds % MILLIS_PER_DAY)))); + LocalDate.ofEpochDay(Math.floorDiv(millisWithCalendar, MILLIS_PER_DAY)), + LocalTime.ofNanoOfDay( + TimeUnit.MILLISECONDS.toNanos(Math.floorMod(millisWithCalendar, MILLIS_PER_DAY))))); } } diff --git a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtilsTest.java b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtilsTest.java index 9c66352023..70bcb9b4c2 100644 --- a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtilsTest.java +++ b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtilsTest.java @@ -95,4 +95,15 @@ public void testShouldGetTimestampNegative() { assertThat(expected, is(actual)); } + + @Test + public void testShouldGetTimestampNegativeNotAlignedToDay() { + final long epochMilli = negativeEpochMilli + 3600000L; // 1950-06-01 01:00:00 UTC + final Instant instant = Instant.ofEpochMilli(epochMilli); + + final Timestamp expected = Timestamp.from(instant); + final Timestamp actual = DateTimeUtils.getTimestampValue(epochMilli); + + assertThat(expected, is(actual)); + } }