diff --git a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java index ebe40162095..2b1f0a2e5b1 100644 --- a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java +++ b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java @@ -19,21 +19,22 @@ import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcDateVectorGetter.Getter; import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcDateVectorGetter.Holder; import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcDateVectorGetter.createGetter; -import static org.apache.arrow.driver.jdbc.utils.DateTimeUtils.getTimestampValue; import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_DAY; import static org.apache.calcite.avatica.util.DateTimeUtils.unixDateToString; import java.sql.Date; import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Calendar; import java.util.concurrent.TimeUnit; import java.util.function.IntSupplier; import org.apache.arrow.driver.jdbc.accessor.ArrowFlightJdbcAccessor; import org.apache.arrow.driver.jdbc.accessor.ArrowFlightJdbcAccessorFactory; -import org.apache.arrow.driver.jdbc.utils.DateTimeUtils; import org.apache.arrow.vector.DateDayVector; import org.apache.arrow.vector.DateMilliVector; import org.apache.arrow.vector.ValueVector; +import org.apache.arrow.vector.util.DateUtility; /** Accessor for the Arrow types: {@link DateDayVector} and {@link DateMilliVector}. */ public class ArrowFlightJdbcDateVectorAccessor extends ArrowFlightJdbcAccessor { @@ -87,17 +88,12 @@ public Object getObject() { @Override public Date getDate(Calendar calendar) { - fillHolder(); - if (this.wasNull) { + final LocalDateTime localDateTime = getLocalDateTime(calendar); + if (localDateTime == null) { return null; } - long value = holder.value; - long milliseconds = this.timeUnit.toMillis(value); - - long millisWithCalendar = DateTimeUtils.applyCalendarOffset(milliseconds, calendar); - - return new Date(getTimestampValue(millisWithCalendar).getTime()); + return new Date(Timestamp.valueOf(localDateTime).getTime()); } private void fillHolder() { @@ -108,11 +104,36 @@ private void fillHolder() { @Override public Timestamp getTimestamp(Calendar calendar) { - Date date = getDate(calendar); - if (date == null) { + final LocalDateTime localDateTime = getLocalDateTime(calendar); + if (localDateTime == null) { + return null; + } + + return Timestamp.valueOf(localDateTime); + } + + private LocalDateTime getLocalDateTime(Calendar calendar) { + getter.get(getCurrentRow(), holder); + this.wasNull = holder.isSet == 0; + this.wasNullConsumer.setWasNull(this.wasNull); + if (this.wasNull) { return null; } - return new Timestamp(date.getTime()); + + final LocalDateTime localDateTime = + DateUtility.getLocalDateTimeFromEpochMilli(this.timeUnit.toMillis(holder.value)); + final ZoneId defaultTimeZone = Calendar.getInstance().getTimeZone().toZoneId(); + final ZoneId sourceTimeZone; + if (calendar != null) { + sourceTimeZone = calendar.getTimeZone().toZoneId(); + } else { + sourceTimeZone = defaultTimeZone; + } + + return localDateTime + .atZone(sourceTimeZone) + .withZoneSameInstant(defaultTimeZone) + .toLocalDateTime(); } @Override diff --git a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java index debdd0fcb4b..20a4d610c45 100644 --- a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java +++ b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java @@ -24,7 +24,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; +import java.time.ZoneId; import java.util.Calendar; import java.util.TimeZone; import java.util.concurrent.TimeUnit; @@ -40,12 +40,11 @@ public class ArrowFlightJdbcTimeStampVectorAccessor extends ArrowFlightJdbcAcces private final TimeZone timeZone; private final Getter getter; - private final TimeUnit timeUnit; - private final LongToLocalDateTime longToLocalDateTime; + private final LongToUTCDateTime longToUTCDateTime; private final Holder holder; /** Functional interface used to convert a number (in any time resolution) to LocalDateTime. */ - interface LongToLocalDateTime { + interface LongToUTCDateTime { LocalDateTime fromLong(long value); } @@ -59,8 +58,7 @@ public ArrowFlightJdbcTimeStampVectorAccessor( this.getter = createGetter(vector); this.timeZone = getTimeZoneForVector(vector); - this.timeUnit = getTimeUnitForVector(vector); - this.longToLocalDateTime = getLongToLocalDateTimeForVector(vector, this.timeZone); + this.longToUTCDateTime = getLongToUTCDateTimeForVector(vector); } @Override @@ -83,16 +81,22 @@ private LocalDateTime getLocalDateTime(Calendar calendar) { long value = holder.value; - LocalDateTime localDateTime = this.longToLocalDateTime.fromLong(value); + LocalDateTime localDateTime = this.longToUTCDateTime.fromLong(value); + ZoneId defaultTimeZone = Calendar.getInstance().getTimeZone().toZoneId(); + ZoneId sourceTimeZone; - if (calendar != null) { - TimeZone timeZone = calendar.getTimeZone(); - long millis = this.timeUnit.toMillis(value); - localDateTime = - localDateTime.minus( - timeZone.getOffset(millis) - this.timeZone.getOffset(millis), ChronoUnit.MILLIS); + if (this.timeZone != null) { + sourceTimeZone = this.timeZone.toZoneId(); + } else if (calendar != null) { + sourceTimeZone = calendar.getTimeZone().toZoneId(); + } else { + sourceTimeZone = defaultTimeZone; } - return localDateTime; + + return localDateTime + .atZone(sourceTimeZone) + .withZoneSameInstant(defaultTimeZone) + .toLocalDateTime(); } @Override @@ -143,24 +147,20 @@ protected static TimeUnit getTimeUnitForVector(TimeStampVector vector) { } } - protected static LongToLocalDateTime getLongToLocalDateTimeForVector( - TimeStampVector vector, TimeZone timeZone) { - String timeZoneID = timeZone.getID(); - + protected static LongToUTCDateTime getLongToUTCDateTimeForVector(TimeStampVector vector) { ArrowType.Timestamp arrowType = (ArrowType.Timestamp) vector.getField().getFieldType().getType(); switch (arrowType.getUnit()) { case NANOSECOND: - return nanoseconds -> DateUtility.getLocalDateTimeFromEpochNano(nanoseconds, timeZoneID); + return DateUtility::getLocalDateTimeFromEpochNano; case MICROSECOND: - return microseconds -> DateUtility.getLocalDateTimeFromEpochMicro(microseconds, timeZoneID); + return DateUtility::getLocalDateTimeFromEpochMicro; case MILLISECOND: - return milliseconds -> DateUtility.getLocalDateTimeFromEpochMilli(milliseconds, timeZoneID); + return DateUtility::getLocalDateTimeFromEpochMilli; case SECOND: return seconds -> - DateUtility.getLocalDateTimeFromEpochMilli( - TimeUnit.SECONDS.toMillis(seconds), timeZoneID); + DateUtility.getLocalDateTimeFromEpochMilli(TimeUnit.SECONDS.toMillis(seconds)); default: throw new UnsupportedOperationException("Invalid Arrow time unit"); } @@ -172,7 +172,7 @@ protected static TimeZone getTimeZoneForVector(TimeStampVector vector) { String timezoneName = arrowType.getTimezone(); if (timezoneName == null) { - return TimeZone.getTimeZone("UTC"); + return null; } return TimeZone.getTimeZone(timezoneName); diff --git a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java index 9363e3486c5..cf1cc8b2a76 100644 --- a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java +++ b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java @@ -19,9 +19,11 @@ import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_DAY; import java.sql.Timestamp; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.TimeZone; import java.util.concurrent.TimeUnit; @@ -32,20 +34,16 @@ private DateTimeUtils() { // Prevent instantiation. } - /** Subtracts given Calendar's TimeZone offset from epoch milliseconds. */ + /** Apply calendar timezone to epoch milliseconds. */ public static long applyCalendarOffset(long milliseconds, Calendar calendar) { if (calendar == null) { calendar = Calendar.getInstance(); } - - final TimeZone tz = calendar.getTimeZone(); - final TimeZone defaultTz = TimeZone.getDefault(); - - if (tz != defaultTz) { - milliseconds -= tz.getOffset(milliseconds) - defaultTz.getOffset(milliseconds); - } - - return milliseconds; + Instant currInstant = Instant.ofEpochMilli(milliseconds); + LocalDateTime getTimestampWithoutTZ = + LocalDateTime.ofInstant(currInstant, TimeZone.getTimeZone("UTC").toZoneId()); + ZonedDateTime parsedTime = getTimestampWithoutTZ.atZone(calendar.getTimeZone().toZoneId()); + return parsedTime.toInstant().toEpochMilli(); } /** diff --git a/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java index 2e329f148e6..3be42df69fd 100644 --- a/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java +++ b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java @@ -16,6 +16,7 @@ */ package org.apache.arrow.driver.jdbc.accessor.impl.calendar; +import static java.util.Optional.ofNullable; import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeStampVectorAccessor.getTimeUnitForVector; import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeStampVectorAccessor.getTimeZoneForVector; import static org.hamcrest.CoreMatchers.equalTo; @@ -25,7 +26,9 @@ import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.TimeZone; import java.util.concurrent.TimeUnit; @@ -179,10 +182,13 @@ public void testShouldGetTimestampReturnValidTimestampWithoutCalendar( public void testShouldGetTimestampReturnValidTimestampWithCalendar( Supplier vectorSupplier) throws Exception { setup(vectorSupplier); + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Hong_Kong")); + TimeZone timeZone = TimeZone.getTimeZone(AMERICA_SAO_PAULO); Calendar calendar = Calendar.getInstance(timeZone); - TimeZone timeZoneForVector = getTimeZoneForVector(vector); + TimeZone finalTimeZoneForResultWithoutCalendar = + ofNullable(getTimeZoneForVector(vector)).orElse(TimeZone.getTimeZone("Asia/Hong_Kong")); accessorIterator.iterate( vector, @@ -190,13 +196,14 @@ public void testShouldGetTimestampReturnValidTimestampWithCalendar( final Timestamp resultWithoutCalendar = accessor.getTimestamp(null); final Timestamp result = accessor.getTimestamp(calendar); - long offset = - (long) timeZone.getOffset(resultWithoutCalendar.getTime()) - - timeZoneForVector.getOffset(resultWithoutCalendar.getTime()); - - assertThat(resultWithoutCalendar.getTime() - result.getTime(), is(offset)); - assertThat(accessor.wasNull(), is(false)); + assertOffsetIsConsistentWithAccessorGetters( + timeZone, + finalTimeZoneForResultWithoutCalendar, + result.getTime(), + resultWithoutCalendar.getTime(), + accessor); }); + TimeZone.setDefault(null); } @ParameterizedTest @@ -230,7 +237,8 @@ public void testShouldGetDateReturnValidDateWithCalendar(Supplier