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 d31d9b0c GH-839: Fix support for ResultSet.getObject for
TIMESTAMP_WITH_TIMEZONE (#840)
d31d9b0c is described below
commit d31d9b0cac6de22ba6649c38486281ffe6346294
Author: Diego Fernández Giraldo <[email protected]>
AuthorDate: Mon Oct 6 19:56:35 2025 -0600
GH-839: Fix support for ResultSet.getObject for TIMESTAMP_WITH_TIMEZONE
(#840)
## What's Changed
Turns out AvaticaSite.get does not account for TIMESTAMP_WITH_TIMEZONE
types so we add support on an override function.
Closes #818.
Closes #839.
---
.../ArrowFlightJdbcVectorSchemaRootResultSet.java | 31 ++++
.../driver/jdbc/FlightServerTestExtension.java | 6 +
.../arrow/driver/jdbc/TimestampResultSetTest.java | 164 +++++++++++++++++++++
3 files changed, 201 insertions(+)
diff --git
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightJdbcVectorSchemaRootResultSet.java
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightJdbcVectorSchemaRootResultSet.java
index 0dc2b07c..622e5fe7 100644
---
a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightJdbcVectorSchemaRootResultSet.java
+++
b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightJdbcVectorSchemaRootResultSet.java
@@ -19,6 +19,7 @@ package org.apache.arrow.driver.jdbc;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
+import java.sql.Types;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@@ -28,14 +29,17 @@ import org.apache.arrow.driver.jdbc.utils.ConvertUtils;
import org.apache.arrow.util.AutoCloseables;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.calcite.avatica.AvaticaConnection;
import org.apache.calcite.avatica.AvaticaResultSet;
import org.apache.calcite.avatica.AvaticaResultSetMetaData;
+import org.apache.calcite.avatica.AvaticaSite;
import org.apache.calcite.avatica.AvaticaStatement;
import org.apache.calcite.avatica.ColumnMetaData;
import org.apache.calcite.avatica.Meta;
import org.apache.calcite.avatica.Meta.Frame;
import org.apache.calcite.avatica.Meta.Signature;
import org.apache.calcite.avatica.QueryState;
+import org.apache.calcite.avatica.util.Cursor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -102,6 +106,33 @@ public class ArrowFlightJdbcVectorSchemaRootResultSet
extends AvaticaResultSet {
execute2(new ArrowFlightJdbcCursor(vectorSchemaRoot),
this.signature.columns);
}
+ /**
+ * The default method in AvaticaResultSet does not properly handle
TIMESTASMP_WITH_TIMEZONE, so we
+ * override here to add support.
+ *
+ * @param columnIndex the first column is 1, the second is 2, ...
+ * @return Object
+ * @throws SQLException if there is an underlying exception
+ */
+ @Override
+ public Object getObject(int columnIndex) throws SQLException {
+ this.checkOpen();
+
+ Cursor.Accessor accessor;
+ try {
+ accessor = accessorList.get(columnIndex - 1);
+ } catch (IndexOutOfBoundsException e) {
+ throw AvaticaConnection.HELPER.createException("invalid column ordinal:
" + columnIndex);
+ }
+
+ ColumnMetaData metaData = columnMetaDataList.get(columnIndex - 1);
+ if (metaData.type.id == Types.TIMESTAMP_WITH_TIMEZONE) {
+ return accessor.getTimestamp(localCalendar);
+ } else {
+ return AvaticaSite.get(accessor, metaData.type.id, localCalendar);
+ }
+ }
+
@Override
protected void cancel() {
signature.columns.clear();
diff --git
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestExtension.java
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestExtension.java
index db043805..f71114e1 100644
---
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestExtension.java
+++
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestExtension.java
@@ -130,6 +130,12 @@ public class FlightServerTestExtension
return this.createDataSource().getConnection();
}
+ public Connection getConnection(String timezone) throws SQLException {
+ setUseEncryption(false);
+ properties.put("timezone", timezone);
+ return this.createDataSource().getConnection();
+ }
+
private void setUseEncryption(boolean useEncryption) {
properties.put("useEncryption", useEncryption);
}
diff --git
a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/TimestampResultSetTest.java
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/TimestampResultSetTest.java
new file mode 100644
index 00000000..0921ae2d
--- /dev/null
+++
b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/TimestampResultSetTest.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.driver.jdbc;
+
+import com.google.common.collect.ImmutableList;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.TimeZone;
+import org.apache.arrow.driver.jdbc.utils.MockFlightSqlProducer;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.TimeStampVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.types.TimeUnit;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+/**
+ * Timestamps have a lot of nuances in JDBC. This class is here to test that
timestamp behavior is
+ * correct for different types of Timestamp vectors as well as different
methods of retrieving the
+ * timestamps in JDBC.
+ */
+public class TimestampResultSetTest {
+ private static final MockFlightSqlProducer FLIGHT_SQL_PRODUCER = new
MockFlightSqlProducer();
+
+ @RegisterExtension public static FlightServerTestExtension
FLIGHT_SERVER_TEST_EXTENSION;
+
+ static {
+ FLIGHT_SERVER_TEST_EXTENSION =
+
FlightServerTestExtension.createStandardTestExtension(FLIGHT_SQL_PRODUCER);
+ }
+
+ private static final String QUERY_STRING = "SELECT * FROM TIMESTAMPS";
+ private static final Schema QUERY_SCHEMA =
+ new Schema(
+ ImmutableList.of(
+ Field.nullable("no_tz", new
ArrowType.Timestamp(TimeUnit.MILLISECOND, null)),
+ Field.nullable("utc", new
ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")),
+ Field.nullable("utc+1", new
ArrowType.Timestamp(TimeUnit.MILLISECOND, "GMT+1")),
+ Field.nullable("utc-1", new
ArrowType.Timestamp(TimeUnit.MILLISECOND, "GMT-1"))));
+
+ @BeforeAll
+ public static void setup() throws SQLException {
+ Instant firstDay2025 = OffsetDateTime.of(2025, 1, 1, 0, 0, 0, 0,
ZoneOffset.UTC).toInstant();
+
+ FLIGHT_SQL_PRODUCER.addSelectQuery(
+ QUERY_STRING,
+ QUERY_SCHEMA,
+ Collections.singletonList(
+ listener -> {
+ try (final BufferAllocator allocator = new
RootAllocator(Long.MAX_VALUE);
+ final VectorSchemaRoot root =
VectorSchemaRoot.create(QUERY_SCHEMA, allocator)) {
+ listener.start(root);
+ root.getFieldVectors()
+ .forEach(v -> ((TimeStampVector) v).setSafe(0,
firstDay2025.toEpochMilli()));
+ root.setRowCount(1);
+ listener.putNext();
+ } catch (final Throwable throwable) {
+ listener.error(throwable);
+ } finally {
+ listener.completed();
+ }
+ }));
+ }
+
+ /**
+ * This test doesn't yet test anything other than ensuring all ResultSet
methods to retrieve a
+ * timestamp succeed.
+ *
+ * <p>This is a good starting point to add more tests to ensure the values
are correct when we
+ * change the "local calendar" either through changing the JVM default or
through the connection
+ * property.
+ */
+ @Test
+ public void test() {
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+ try (Connection connection =
FLIGHT_SERVER_TEST_EXTENSION.getConnection("UTC")) {
+ try (PreparedStatement s = connection.prepareStatement(QUERY_STRING)) {
+ try (ResultSet rs = s.executeQuery()) {
+ int numCols = rs.getMetaData().getColumnCount();
+ try {
+ rs.next();
+ for (int i = 1; i <= numCols; i++) {
+ int type = rs.getMetaData().getColumnType(i);
+ String name = rs.getMetaData().getColumnName(i);
+ System.out.println(name);
+ System.out.print("- getDate:\t\t\t\t\t\t\t");
+ System.out.print(rs.getDate(i));
+ System.out.println();
+ System.out.print("- getTimestamp:\t\t\t\t\t\t");
+ System.out.print(rs.getTimestamp(i));
+ System.out.println();
+ System.out.print("- getString:\t\t\t\t\t\t");
+ System.out.print(rs.getString(i));
+ System.out.println();
+ System.out.print("- getObject:\t\t\t\t\t\t");
+ System.out.print(rs.getObject(i));
+ System.out.println();
+ System.out.print("- getObject(Timestamp.class):\t\t");
+ System.out.print(rs.getObject(i, Timestamp.class));
+ System.out.println();
+ System.out.print("- getTimestamp(default Calendar):\t");
+ System.out.print(rs.getTimestamp(i, Calendar.getInstance()));
+ System.out.println();
+ System.out.print("- getTimestamp(UTC Calendar):\t\t");
+ System.out.print(
+ rs.getTimestamp(i,
Calendar.getInstance(TimeZone.getTimeZone("UTC"))));
+ System.out.println();
+ System.out.print("- getObject(LocalDateTime.class):\t");
+ System.out.print(rs.getObject(i, LocalDateTime.class));
+ System.out.println();
+ if (type == Types.TIMESTAMP_WITH_TIMEZONE) {
+ System.out.print("- getObject(Instant.class):\t\t\t");
+ System.out.print(rs.getObject(i, Instant.class));
+ System.out.println();
+ System.out.print("- getObject(OffsetDateTime.class):\t");
+ System.out.print(rs.getObject(i, OffsetDateTime.class));
+ System.out.println();
+ System.out.print("- getObject(ZonedDateTime.class):\t");
+ System.out.print(rs.getObject(i, ZonedDateTime.class));
+ System.out.println();
+ }
+ System.out.println();
+ }
+ System.out.println();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}