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

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


The following commit(s) were added to refs/heads/main by this push:
     new 26630c38 GH-463: Improve TZ support for JDBC driver (#464)
26630c38 is described below

commit 26630c38f563821aa40d1c5a6df30101dde8c171
Author: Diego Fernández Giraldo <[email protected]>
AuthorDate: Sun Apr 27 23:28:37 2025 -0600

    GH-463: Improve TZ support for JDBC driver (#464)
    
    This PR adds support for natively fetching `java.time.*` objects through
    the JDBC driver.
    
    DateVector
    - getObject(LocalDate.class)
    
    DateTimeVector
    - getObject(OffsetDateTime.class)
    - getObject(LocalDateTime.class)
    - getObject(ZonedDateTime.class)
    - getObject(Instant.class)
    
    TimeVector
    - getObject(LocalTime.class)
    
    This PR also changes the behavior for vectors that include TZ info.
    These will now return as `TIMESTAMP_WITH_TIMEZONE`.
    
    The behavior for different ways to access a TimeStampVector are as
    follows:
    
    | | Vector with TZ | Vector W/O TZ |
    
    
|---------------------------------|------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
    | getTimestamp() | Get timestamp in the vector TZ | Get timestamp in UTC
    |
    | getTimestamp(calendar) | Get timestamp by adjusting from the vector TZ
    to the desired calendar TZ | Get timestamp by adjusting from UTC to the
    desired calendar TZ (a bug, IMO) |
    | getObject(LocalDateTime.class) | Get LocalDateTime by taking the
    timestamp at the vector TZ and taking the "wall-clock" time at that
    moment | Treat the epoch value in the vector as the "wall-clock" time |
    | getObject(Instant.class) | Get Instant represented by the value in the
    vector TZ | Get Instant represented by the value in UTC |
    | getObject(OffsetDateTime.class) | Get OffsetDateTime represented by
    the value in the vector TZ (will account for daylight adjustment) | Get
    OffsetDateTime represented by the value in UTC (will account for
    daylight adjustment) |
    | getObject(ZonedDateTime.class) | Get ZonedDateTime represented by the
    value in the vector at in its TZ | Get ZonedDateTime represented by the
    value in the vector at in UTC |
    
    Closes #463
---
 .../ArrowFlightJdbcDateVectorAccessor.java         |  19 ++++
 .../ArrowFlightJdbcTimeStampVectorAccessor.java    | 101 ++++++++++++++++--
 .../ArrowFlightJdbcTimeVectorAccessor.java         |  19 ++++
 .../apache/arrow/driver/jdbc/utils/SqlTypes.java   |   8 +-
 .../driver/jdbc/ArrowDatabaseMetadataTest.java     |   5 +-
 ...ArrowFlightJdbcTimeStampVectorAccessorTest.java | 118 ++++++++++++++++++++-
 .../arrow/driver/jdbc/utils/SqlTypesTest.java      |  12 +++
 7 files changed, 269 insertions(+), 13 deletions(-)

diff --git 
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java
 
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java
index ebe40162..cdafeffc 100644
--- 
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java
+++ 
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java
@@ -24,7 +24,9 @@ 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.SQLException;
 import java.sql.Timestamp;
+import java.time.LocalDate;
 import java.util.Calendar;
 import java.util.concurrent.TimeUnit;
 import java.util.function.IntSupplier;
@@ -85,6 +87,19 @@ public class ArrowFlightJdbcDateVectorAccessor extends 
ArrowFlightJdbcAccessor {
     return this.getDate(null);
   }
 
+  @Override
+  public <T> T getObject(final Class<T> type) throws SQLException {
+    final Object value;
+    if (type == LocalDate.class) {
+      value = getLocalDate();
+    } else if (type == Date.class) {
+      value = getObject();
+    } else {
+      throw new SQLException("Object type not supported for Date Vector");
+    }
+    return !type.isPrimitive() && wasNull ? null : type.cast(value);
+  }
+
   @Override
   public Date getDate(Calendar calendar) {
     fillHolder();
@@ -134,4 +149,8 @@ public class ArrowFlightJdbcDateVectorAccessor extends 
ArrowFlightJdbcAccessor {
 
     throw new IllegalArgumentException("Invalid Arrow vector");
   }
+
+  private LocalDate getLocalDate() {
+    return getDate(null).toLocalDate();
+  }
 }
diff --git 
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java
 
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java
index debdd0fc..813fbc7c 100644
--- 
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java
+++ 
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java
@@ -21,11 +21,18 @@ import static 
org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdb
 import static 
org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeStampVectorGetter.createGetter;
 
 import java.sql.Date;
+import java.sql.SQLException;
 import java.sql.Time;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.Calendar;
+import java.util.Objects;
+import java.util.Set;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 import java.util.function.IntSupplier;
@@ -43,6 +50,7 @@ public class ArrowFlightJdbcTimeStampVectorAccessor extends 
ArrowFlightJdbcAcces
   private final TimeUnit timeUnit;
   private final LongToLocalDateTime longToLocalDateTime;
   private final Holder holder;
+  private final boolean isZoned;
 
   /** Functional interface used to convert a number (in any time resolution) 
to LocalDateTime. */
   interface LongToLocalDateTime {
@@ -58,6 +66,9 @@ public class ArrowFlightJdbcTimeStampVectorAccessor extends 
ArrowFlightJdbcAcces
     this.holder = new Holder();
     this.getter = createGetter(vector);
 
+    // whether the vector included TZ info
+    this.isZoned = getVectorIsZoned(vector);
+    // non-null, either the vector TZ or default to UTC
     this.timeZone = getTimeZoneForVector(vector);
     this.timeUnit = getTimeUnitForVector(vector);
     this.longToLocalDateTime = getLongToLocalDateTimeForVector(vector, 
this.timeZone);
@@ -68,11 +79,62 @@ public class ArrowFlightJdbcTimeStampVectorAccessor extends 
ArrowFlightJdbcAcces
     return Timestamp.class;
   }
 
+  @Override
+  public <T> T getObject(final Class<T> type) throws SQLException {
+    final Object value;
+    if (!this.isZoned
+        & Set.of(OffsetDateTime.class, ZonedDateTime.class, 
Instant.class).contains(type)) {
+      throw new SQLException(
+          "Vectors without timezones can't be converted to objects with 
offset/tz info.");
+    } else if (type == OffsetDateTime.class) {
+      value = getOffsetDateTime();
+    } else if (type == LocalDateTime.class) {
+      value = getLocalDateTime(null);
+    } else if (type == ZonedDateTime.class) {
+      value = getZonedDateTime();
+    } else if (type == Instant.class) {
+      value = getInstant();
+    } else if (type == Timestamp.class) {
+      value = getObject();
+    } else {
+      throw new SQLException("Object type not supported for TimeStamp Vector");
+    }
+
+    return !type.isPrimitive() && wasNull ? null : type.cast(value);
+  }
+
   @Override
   public Object getObject() {
     return this.getTimestamp(null);
   }
 
+  private ZonedDateTime getZonedDateTime() {
+    LocalDateTime localDateTime = getLocalDateTime(null);
+    if (localDateTime == null) {
+      return null;
+    }
+
+    return localDateTime.atZone(this.timeZone.toZoneId());
+  }
+
+  private OffsetDateTime getOffsetDateTime() {
+    LocalDateTime localDateTime = getLocalDateTime(null);
+    if (localDateTime == null) {
+      return null;
+    }
+    ZoneOffset offset = 
this.timeZone.toZoneId().getRules().getOffset(localDateTime);
+    return localDateTime.atOffset(offset);
+  }
+
+  private Instant getInstant() {
+    LocalDateTime localDateTime = getLocalDateTime(null);
+    if (localDateTime == null) {
+      return null;
+    }
+    ZoneOffset offset = 
this.timeZone.toZoneId().getRules().getOffset(localDateTime);
+    return localDateTime.toInstant(offset);
+  }
+
   private LocalDateTime getLocalDateTime(Calendar calendar) {
     getter.get(getCurrentRow(), holder);
     this.wasNull = holder.isSet == 0;
@@ -85,7 +147,9 @@ public class ArrowFlightJdbcTimeStampVectorAccessor extends 
ArrowFlightJdbcAcces
 
     LocalDateTime localDateTime = this.longToLocalDateTime.fromLong(value);
 
-    if (calendar != null) {
+    // Adjust timestamp to desired calendar (if provided) only if the column 
includes TZ info,
+    // otherwise treat as wall-clock time
+    if (calendar != null && this.isZoned) {
       TimeZone timeZone = calendar.getTimeZone();
       long millis = this.timeUnit.toMillis(value);
       localDateTime =
@@ -102,7 +166,7 @@ public class ArrowFlightJdbcTimeStampVectorAccessor extends 
ArrowFlightJdbcAcces
       return null;
     }
 
-    return new Date(Timestamp.valueOf(localDateTime).getTime());
+    return new Date(getTimestampWithOffset(calendar, localDateTime).getTime());
   }
 
   @Override
@@ -112,7 +176,7 @@ public class ArrowFlightJdbcTimeStampVectorAccessor extends 
ArrowFlightJdbcAcces
       return null;
     }
 
-    return new Time(Timestamp.valueOf(localDateTime).getTime());
+    return new Time(getTimestampWithOffset(calendar, localDateTime).getTime());
   }
 
   @Override
@@ -122,6 +186,24 @@ public class ArrowFlightJdbcTimeStampVectorAccessor 
extends ArrowFlightJdbcAcces
       return null;
     }
 
+    return getTimestampWithOffset(calendar, localDateTime);
+  }
+
+  /**
+   * Apply offset to LocalDateTime to get a Timestamp with legacy behavior. 
Previously we applied
+   * the offset to the LocalDateTime even if the underlying Vector did not 
have a TZ. In order to
+   * support java.time.* accessors, we fixed this so we only apply the offset 
if the underlying
+   * vector includes TZ info. In order to maintain backward compatibility, we 
apply the offset if
+   * needed for getDate, getTime, and getTimestamp.
+   */
+  private Timestamp getTimestampWithOffset(Calendar calendar, LocalDateTime 
localDateTime) {
+    if (calendar != null && !isZoned) {
+      TimeZone timeZone = calendar.getTimeZone();
+      long millis = Timestamp.valueOf(localDateTime).getTime();
+      localDateTime =
+          localDateTime.minus(
+              timeZone.getOffset(millis) - this.timeZone.getOffset(millis), 
ChronoUnit.MILLIS);
+    }
     return Timestamp.valueOf(localDateTime);
   }
 
@@ -170,11 +252,14 @@ public class ArrowFlightJdbcTimeStampVectorAccessor 
extends ArrowFlightJdbcAcces
     ArrowType.Timestamp arrowType =
         (ArrowType.Timestamp) vector.getField().getFieldType().getType();
 
-    String timezoneName = arrowType.getTimezone();
-    if (timezoneName == null) {
-      return TimeZone.getTimeZone("UTC");
-    }
-
+    String timezoneName = Objects.requireNonNullElse(arrowType.getTimezone(), 
"UTC");
     return TimeZone.getTimeZone(timezoneName);
   }
+
+  protected static boolean getVectorIsZoned(TimeStampVector vector) {
+    ArrowType.Timestamp arrowType =
+        (ArrowType.Timestamp) vector.getField().getFieldType().getType();
+
+    return arrowType.getTimezone() != null;
+  }
 }
diff --git 
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeVectorAccessor.java
 
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeVectorAccessor.java
index 2c03ee63..d525c2fd 100644
--- 
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeVectorAccessor.java
+++ 
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeVectorAccessor.java
@@ -20,8 +20,10 @@ import static 
org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdb
 import static 
org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeVectorGetter.Holder;
 import static 
org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeVectorGetter.createGetter;
 
+import java.sql.SQLException;
 import java.sql.Time;
 import java.sql.Timestamp;
+import java.time.LocalTime;
 import java.util.Calendar;
 import java.util.concurrent.TimeUnit;
 import java.util.function.IntSupplier;
@@ -121,6 +123,19 @@ public class ArrowFlightJdbcTimeVectorAccessor extends 
ArrowFlightJdbcAccessor {
     return this.getTime(null);
   }
 
+  @Override
+  public <T> T getObject(final Class<T> type) throws SQLException {
+    final Object value;
+    if (type == LocalTime.class) {
+      value = getLocalTime();
+    } else if (type == Time.class) {
+      value = getObject();
+    } else {
+      throw new SQLException("Object type not supported for Time Vector");
+    }
+    return !type.isPrimitive() && wasNull ? null : type.cast(value);
+  }
+
   @Override
   public Time getTime(Calendar calendar) {
     fillHolder();
@@ -134,6 +149,10 @@ public class ArrowFlightJdbcTimeVectorAccessor extends 
ArrowFlightJdbcAccessor {
     return new 
ArrowFlightJdbcTime(DateTimeUtils.applyCalendarOffset(milliseconds, calendar));
   }
 
+  private LocalTime getLocalTime() {
+    return getTime(null).toLocalTime();
+  }
+
   private void fillHolder() {
     getter.get(getCurrentRow(), holder);
     this.wasNull = holder.isSet == 0;
diff --git 
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/SqlTypes.java
 
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/SqlTypes.java
index 96cb056d..1b76ca0c 100644
--- 
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/SqlTypes.java
+++ 
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/SqlTypes.java
@@ -16,6 +16,7 @@
  */
 package org.apache.arrow.driver.jdbc.utils;
 
+import com.google.common.base.Strings;
 import java.sql.Types;
 import java.util.HashMap;
 import java.util.Map;
@@ -120,7 +121,12 @@ public class SqlTypes {
       case Time:
         return Types.TIME;
       case Timestamp:
-        return Types.TIMESTAMP;
+        String tz = ((ArrowType.Timestamp) arrowType).getTimezone();
+        if (Strings.isNullOrEmpty(tz)) {
+          return Types.TIMESTAMP;
+        } else {
+          return Types.TIMESTAMP_WITH_TIMEZONE;
+        }
       case Bool:
         return Types.BOOLEAN;
       case Decimal:
diff --git 
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ArrowDatabaseMetadataTest.java
 
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ArrowDatabaseMetadataTest.java
index 88a172e4..70d3bcbd 100644
--- 
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ArrowDatabaseMetadataTest.java
+++ 
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ArrowDatabaseMetadataTest.java
@@ -299,8 +299,9 @@ public class ArrowDatabaseMetadataTest {
   private static Connection connection;
 
   static {
-    List<Integer> expectedGetColumnsDataTypes = Arrays.asList(3, 93, 4);
-    List<String> expectedGetColumnsTypeName = Arrays.asList("DECIMAL", 
"TIMESTAMP", "INTEGER");
+    List<Integer> expectedGetColumnsDataTypes = Arrays.asList(3, 2014, 4);
+    List<String> expectedGetColumnsTypeName =
+        Arrays.asList("DECIMAL", "TIMESTAMP_WITH_TIMEZONE", "INTEGER");
     List<Integer> expectedGetColumnsRadix = Arrays.asList(10, null, 10);
     List<Integer> expectedGetColumnsColumnSize = Arrays.asList(5, 29, 10);
     List<Integer> expectedGetColumnsDecimalDigits = Arrays.asList(2, 9, 0);
diff --git 
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java
 
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java
index 2e329f14..e4863bd8 100644
--- 
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java
+++ 
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java
@@ -16,17 +16,23 @@
  */
 package org.apache.arrow.driver.jdbc.accessor.impl.calendar;
 
-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.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeStampVectorAccessor.*;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.sql.Date;
+import java.sql.SQLException;
 import java.sql.Time;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.Calendar;
+import java.util.Objects;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
@@ -199,6 +205,99 @@ public class ArrowFlightJdbcTimeStampVectorAccessorTest {
         });
   }
 
+  @ParameterizedTest
+  @MethodSource("data")
+  public void testShouldGetObjectReturnValidLocalDateTime(
+      Supplier<TimeStampVector> vectorSupplier, String vectorType, String 
timeZone)
+      throws Exception {
+    setup(vectorSupplier);
+    final String expectedTimeZone = Objects.requireNonNullElse(timeZone, 
"UTC");
+
+    accessorIterator.iterate(
+        vector,
+        (accessor, currentRow) -> {
+          final LocalDateTime value = accessor.getObject(LocalDateTime.class);
+          final LocalDateTime expectedValue =
+              getZonedDateTime(currentRow, expectedTimeZone).toLocalDateTime();
+
+          assertThat(value, equalTo(expectedValue));
+          assertThat(accessor.wasNull(), is(false));
+        });
+  }
+
+  @ParameterizedTest
+  @MethodSource("data")
+  public void testShouldGetObjectReturnValidInstant(
+      Supplier<TimeStampVector> vectorSupplier, String vectorType, String 
timeZone)
+      throws Exception {
+    setup(vectorSupplier);
+    final String expectedTimeZone = Objects.requireNonNullElse(timeZone, 
"UTC");
+    final boolean vectorHasTz = timeZone != null;
+    accessorIterator.iterate(
+        vector,
+        (accessor, currentRow) -> {
+          if (vectorHasTz) {
+            final Instant value = accessor.getObject(Instant.class);
+            final Instant expectedValue =
+                getZonedDateTime(currentRow, expectedTimeZone).toInstant();
+
+            assertThat(value, equalTo(expectedValue));
+            assertThat(accessor.wasNull(), is(false));
+          } else {
+            assertThrows(SQLException.class, () -> 
accessor.getObject(Instant.class));
+          }
+        });
+  }
+
+  @ParameterizedTest
+  @MethodSource("data")
+  public void testShouldGetObjectReturnValidOffsetDateTime(
+      Supplier<TimeStampVector> vectorSupplier, String vectorType, String 
timeZone)
+      throws Exception {
+    setup(vectorSupplier);
+    final String expectedTimeZone = Objects.requireNonNullElse(timeZone, 
"UTC");
+    final boolean vectorHasTz = timeZone != null;
+    accessorIterator.iterate(
+        vector,
+        (accessor, currentRow) -> {
+          if (vectorHasTz) {
+            final OffsetDateTime value = 
accessor.getObject(OffsetDateTime.class);
+            final OffsetDateTime expectedValue =
+                getZonedDateTime(currentRow, 
expectedTimeZone).toOffsetDateTime();
+
+            assertThat(value, equalTo(expectedValue));
+            assertThat(value.getOffset(), equalTo(expectedValue.getOffset()));
+            assertThat(accessor.wasNull(), is(false));
+          } else {
+            assertThrows(SQLException.class, () -> 
accessor.getObject(OffsetDateTime.class));
+          }
+        });
+  }
+
+  @ParameterizedTest
+  @MethodSource("data")
+  public void testShouldGetObjectReturnValidZonedDateTime(
+      Supplier<TimeStampVector> vectorSupplier, String vectorType, String 
timeZone)
+      throws Exception {
+    setup(vectorSupplier);
+    final String expectedTimeZone = Objects.requireNonNullElse(timeZone, 
"UTC");
+    final boolean vectorHasTz = timeZone != null;
+    accessorIterator.iterate(
+        vector,
+        (accessor, currentRow) -> {
+          if (vectorHasTz) {
+            final ZonedDateTime value = 
accessor.getObject(ZonedDateTime.class);
+            final ZonedDateTime expectedValue = getZonedDateTime(currentRow, 
expectedTimeZone);
+
+            assertThat(value, equalTo(expectedValue));
+            assertThat(value.getZone(), equalTo(ZoneId.of(expectedTimeZone)));
+            assertThat(accessor.wasNull(), is(false));
+          } else {
+            assertThrows(SQLException.class, () -> 
accessor.getObject(ZonedDateTime.class));
+          }
+        });
+  }
+
   @ParameterizedTest
   @MethodSource("data")
   public void testShouldGetTimestampReturnNull(Supplier<TimeStampVector> 
vectorSupplier) {
@@ -320,6 +419,21 @@ public class ArrowFlightJdbcTimeStampVectorAccessorTest {
     return expectedTimestamp;
   }
 
+  /** ZonedDateTime contains all necessary information to generate any 
java.time object. */
+  private ZonedDateTime getZonedDateTime(int currentRow, String timeZone) {
+    Object object = vector.getObject(currentRow);
+    TimeZone tz = TimeZone.getTimeZone(timeZone);
+    ZonedDateTime expectedTimestamp = null;
+    if (object instanceof LocalDateTime) {
+      expectedTimestamp = ((LocalDateTime) object).atZone(tz.toZoneId());
+    } else if (object instanceof Long) {
+      TimeUnit timeUnit = getTimeUnitForVector(vector);
+      Instant instant = Instant.ofEpochMilli(timeUnit.toMillis((Long) object));
+      expectedTimestamp = ZonedDateTime.ofInstant(instant, tz.toZoneId());
+    }
+    return expectedTimestamp;
+  }
+
   @ParameterizedTest
   @MethodSource("data")
   public void testShouldGetObjectClass(Supplier<TimeStampVector> 
vectorSupplier) throws Exception {
diff --git 
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/SqlTypesTest.java
 
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/SqlTypesTest.java
index 00af3c96..a6dd6b32 100644
--- 
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/SqlTypesTest.java
+++ 
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/SqlTypesTest.java
@@ -48,9 +48,15 @@ public class SqlTypesTest {
     assertEquals(Types.DATE, getSqlTypeIdFromArrowType(new 
ArrowType.Date(DateUnit.MILLISECOND)));
     assertEquals(
         Types.TIME, getSqlTypeIdFromArrowType(new 
ArrowType.Time(TimeUnit.MILLISECOND, 32)));
+    assertEquals(
+        Types.TIMESTAMP,
+        getSqlTypeIdFromArrowType(new 
ArrowType.Timestamp(TimeUnit.MILLISECOND, null)));
     assertEquals(
         Types.TIMESTAMP,
         getSqlTypeIdFromArrowType(new 
ArrowType.Timestamp(TimeUnit.MILLISECOND, "")));
+    assertEquals(
+        Types.TIMESTAMP_WITH_TIMEZONE,
+        getSqlTypeIdFromArrowType(new 
ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")));
 
     assertEquals(Types.BOOLEAN, getSqlTypeIdFromArrowType(new 
ArrowType.Bool()));
 
@@ -95,9 +101,15 @@ public class SqlTypesTest {
 
     assertEquals("DATE", getSqlTypeNameFromArrowType(new 
ArrowType.Date(DateUnit.MILLISECOND)));
     assertEquals("TIME", getSqlTypeNameFromArrowType(new 
ArrowType.Time(TimeUnit.MILLISECOND, 32)));
+    assertEquals(
+        "TIMESTAMP",
+        getSqlTypeNameFromArrowType(new 
ArrowType.Timestamp(TimeUnit.MILLISECOND, null)));
     assertEquals(
         "TIMESTAMP",
         getSqlTypeNameFromArrowType(new 
ArrowType.Timestamp(TimeUnit.MILLISECOND, "")));
+    assertEquals(
+        "TIMESTAMP_WITH_TIMEZONE",
+        getSqlTypeNameFromArrowType(new 
ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")));
 
     assertEquals("BOOLEAN", getSqlTypeNameFromArrowType(new ArrowType.Bool()));
 

Reply via email to