diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java index fdfcaefe2361..a455560f1af7 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseResultSet.java @@ -246,6 +246,25 @@ public boolean isClosed() { public abstract Object getObject(int columnIndex) throws SQLException; + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + LOG.finestTrace("getObject"); + try { + Object value = getObject(columnIndex); + if (value == null) { + return null; + } + return this.bigQueryTypeCoercer.coerceTo(type, value, this.LOG); + } catch (RuntimeException e) { + throw createCoercionException(columnIndex, type, e); + } + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + return getObject(getColumnIndex(columnLabel), type); + } + protected int getColumnIndex(String columnLabel) throws SQLException { LOG.finestTrace("getColumnIndex"); checkClosed(); diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryNoOpsResultSet.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryNoOpsResultSet.java index e4b29f7cd566..e6437ae940fb 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryNoOpsResultSet.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryNoOpsResultSet.java @@ -655,16 +655,6 @@ public void updateNClob(String columnLabel, Reader reader) throws SQLException { throw new BigQueryJdbcSqlFeatureNotSupportedException(METHOD_NOT_IMPLEMENTED); } - @Override - public T getObject(int columnIndex, Class type) throws SQLException { - throw new BigQueryJdbcSqlFeatureNotSupportedException(METHOD_NOT_IMPLEMENTED); - } - - @Override - public T getObject(String columnLabel, Class type) throws SQLException { - throw new BigQueryJdbcSqlFeatureNotSupportedException(METHOD_NOT_IMPLEMENTED); - } - @Override public T unwrap(Class iface) throws SQLException { throw new BigQueryJdbcSqlFeatureNotSupportedException(METHOD_NOT_IMPLEMENTED); diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercionUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercionUtility.java index ae51736473dd..13f035cee830 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercionUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercionUtility.java @@ -29,9 +29,12 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.Period; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.apache.arrow.vector.PeriodDuration; import org.apache.arrow.vector.util.Text; @@ -65,27 +68,68 @@ class BigQueryTypeCoercionUtility { .registerTypeCoercion(new BytesArrayToString()) // Read API Type coercions - .registerTypeCoercion(Timestamp::valueOf, LocalDateTime.class, Timestamp.class) + .registerTypeCoercion( + (LocalDateTime ldt) -> Timestamp.from(ldt.toInstant(ZoneOffset.UTC)), + LocalDateTime.class, + Timestamp.class) .registerTypeCoercion(Text::toString, Text.class, String.class) .registerTypeCoercion(new TextToInteger()) .registerTypeCoercion(new LongToTimestamp()) .registerTypeCoercion(new LongToTime()) .registerTypeCoercion(new IntegerToDate()) .registerTypeCoercion( - (Timestamp ts) -> Date.valueOf(ts.toLocalDateTime().toLocalDate()), + (Timestamp ts) -> + Date.valueOf(ts.toInstant().atOffset(ZoneOffset.UTC).toLocalDate()), Timestamp.class, Date.class) .registerTypeCoercion( - (Timestamp ts) -> Time.valueOf(ts.toLocalDateTime().toLocalTime()), + (Timestamp ts) -> + Time.valueOf(ts.toInstant().atOffset(ZoneOffset.UTC).toLocalTime()), Timestamp.class, Time.class) .registerTypeCoercion( (Time time) -> // Per JDBC spec, the date component should be 1970-01-01 - Timestamp.valueOf(LocalDateTime.of(LocalDate.ofEpochDay(0), time.toLocalTime())), + Timestamp.from( + LocalDateTime.of(LocalDate.ofEpochDay(0), time.toLocalTime()) + .toInstant(ZoneOffset.UTC)), Time.class, Timestamp.class) .registerTypeCoercion( (Date date) -> new Timestamp(date.getTime()), Date.class, Timestamp.class) + .registerTypeCoercion( + (LocalDateTime ldt) -> Date.valueOf(ldt.toLocalDate()), + LocalDateTime.class, + Date.class) + .registerTypeCoercion( + (LocalDateTime ldt) -> { + // Custom conversion is used to preserve sub-second (millisecond) precision, + // as standard java.sql.Time.valueOf(LocalTime) truncates milliseconds. + long millisOfDay = TimeUnit.NANOSECONDS.toMillis(ldt.toLocalTime().toNanoOfDay()); + long localMillis = TimeZoneCache.getLocalMillis(millisOfDay); + return new Time(localMillis); + }, + LocalDateTime.class, + Time.class) + .registerTypeCoercion((Date date) -> date.toLocalDate(), Date.class, LocalDate.class) + .registerTypeCoercion( + (Time time) -> { + // Custom conversion is used to preserve sub-second (millisecond) precision, + // as standard java.sql.Time.toLocalTime() truncates milliseconds. + long millis = time.getTime(); + long localMillis = millis + TimeZone.getDefault().getOffset(millis); + return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(localMillis)); + }, + Time.class, + LocalTime.class) + .registerTypeCoercion( + (Timestamp ts) -> ts.toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime(), + Timestamp.class, + LocalDateTime.class) + .registerTypeCoercion( + (Timestamp ts) -> ts.toInstant().atOffset(ZoneOffset.UTC), + Timestamp.class, + OffsetDateTime.class) + .registerTypeCoercion((Timestamp ts) -> ts.toInstant(), Timestamp.class, Instant.class) .registerTypeCoercion(new TimestampToString()) .registerTypeCoercion(new TimeToString()) .registerTypeCoercion((Long l) -> l != 0L, Long.class, Boolean.class) @@ -106,6 +150,13 @@ class BigQueryTypeCoercionUtility { (Boolean b) -> b ? BigDecimal.ONE : BigDecimal.ZERO, Boolean.class, BigDecimal.class) + .registerTypeCoercion( + (Integer i) -> BigDecimal.valueOf(i), Integer.class, BigDecimal.class) + .registerTypeCoercion((Long l) -> BigDecimal.valueOf(l), Long.class, BigDecimal.class) + .registerTypeCoercion( + (Double d) -> BigDecimal.valueOf(d), Double.class, BigDecimal.class) + .registerTypeCoercion((Float f) -> BigDecimal.valueOf(f), Float.class, BigDecimal.class) + .registerTypeCoercion((String s) -> new BigDecimal(s), String.class, BigDecimal.class) .registerTypeCoercion(new PeriodDurationToString()) .registerTypeCoercion(unused -> (byte) 0, Void.class, Byte.class) .registerTypeCoercion(unused -> 0, Void.class, Integer.class) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSetTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSetTest.java index 442ebff3cd3c..1bae7d10b6d5 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSetTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryArrowResultSetTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.apache.arrow.vector.types.Types.MinorType.INT; import static org.apache.arrow.vector.types.Types.MinorType.VARCHAR; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import com.google.cloud.bigquery.Field; @@ -32,14 +33,19 @@ import com.google.cloud.bigquery.storage.v1.ArrowSchema; import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.math.BigDecimal; import java.sql.Array; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Struct; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Arrays; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; +import java.util.stream.Stream; import org.apache.arrow.memory.RootAllocator; import org.apache.arrow.vector.BitVector; import org.apache.arrow.vector.DateMilliVector; @@ -59,6 +65,9 @@ import org.apache.arrow.vector.util.Text; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class BigQueryArrowResultSetTest { @@ -110,7 +119,7 @@ private VectorSchemaRoot getTestVectorSchemaRoot() { Float8Vector float64Field = new Float8Vector("float64Field", allocator); // Mapped with StandardSQLTypeName.FLOAT64 float64Field.allocateNew(2); - float64Field.set(0, 1.1f); + float64Field.set(0, 1.1); float64Field.setValueCount(1); VarCharVector stringField = new VarCharVector("stringField", allocator); // Mapped with StandardSQLTypeName.STRING @@ -346,6 +355,60 @@ public void testIterationNested() throws SQLException { assertThat(bigQueryArrowResultSetNested.isAfterLast()).isTrue(); } + public static Stream successfulCoercionCases() { + return Stream.of( + Arguments.of("dateField", LocalDate.class, LocalDate.of(1970, 1, 1)), + Arguments.of(11, LocalDate.class, LocalDate.of(1970, 1, 1)), + Arguments.of("timeField", LocalTime.class, LocalTime.of(0, 0, 1, 234_000_000)), + Arguments.of(10, LocalTime.class, LocalTime.of(0, 0, 1, 234_000_000)), + Arguments.of( + "timeStampField", + LocalDateTime.class, + LocalDateTime.of(1970, 1, 1, 0, 0, 0, 10_000_000)), + Arguments.of(5, LocalDateTime.class, LocalDateTime.of(1970, 1, 1, 0, 0, 0, 10_000_000)), + Arguments.of("boolField", Boolean.class, false), + Arguments.of(1, Boolean.class, false), + Arguments.of("int64Filed", Long.class, 1L), + Arguments.of(2, Integer.class, 1), + Arguments.of(2, String.class, "1"), + Arguments.of("float64Field", Double.class, 1.1), + Arguments.of("stringField", String.class, "text1"), + Arguments.of("numericField", BigDecimal.class, BigDecimal.ONE), + Arguments.of(9, Long.class, 1L)); + } + + public static Stream failingCoercionCases() { + return Stream.of( + Arguments.of("boolField", LocalDate.class), + Arguments.of("dateField", Boolean.class), + Arguments.of("stringField", BigDecimal.class)); + } + + @ParameterizedTest + @MethodSource("successfulCoercionCases") + public void testGetObjectWithType_success(Object column, Class type, Object expectedValue) + throws SQLException { + assertThat(bigQueryArrowResultSet.next()).isTrue(); + if (column instanceof String) { + assertThat(bigQueryArrowResultSet.getObject((String) column, type)).isEqualTo(expectedValue); + } else { + assertThat(bigQueryArrowResultSet.getObject((Integer) column, type)).isEqualTo(expectedValue); + } + } + + @ParameterizedTest + @MethodSource("failingCoercionCases") + public void testGetObjectWithType_failure(Object column, Class type) throws SQLException { + assertThat(bigQueryArrowResultSet.next()).isTrue(); + if (column instanceof String) { + assertThrows( + SQLException.class, () -> bigQueryArrowResultSet.getObject((String) column, type)); + } else { + assertThrows( + SQLException.class, () -> bigQueryArrowResultSet.getObject((Integer) column, type)); + } + } + private int resultSetRowCount(BigQueryArrowResultSet resultSet) throws SQLException { int rowCount = 0; while (resultSet.next()) { diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java index 252d252588bc..8cf8b330ac49 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java @@ -16,7 +16,11 @@ package com.google.cloud.bigquery.jdbc; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; import com.google.api.gax.rpc.HeaderProvider; diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSetTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSetTest.java index 6f0ade90c3a4..a4d12adeb60d 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSetTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJsonResultSetTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static java.time.Month.MARCH; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import com.google.cloud.bigquery.Field; @@ -47,6 +48,7 @@ import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Calendar; @@ -54,9 +56,13 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class BigQueryJsonResultSetTest { @@ -466,6 +472,62 @@ public void testDate() throws SQLException { .isEqualTo(bigQueryJsonResultSet.getDate("fourteenth", calendar).getTime()); } + public static Stream successfulCoercionCases() { + return Stream.of( + Arguments.of("fourteenth", LocalDate.class, LocalDate.of(2020, 1, 15)), + Arguments.of(14, LocalDate.class, LocalDate.of(2020, 1, 15)), + Arguments.of("twelfth", LocalTime.class, LocalTime.of(11, 14, 19, 820000000)), + Arguments.of(12, LocalTime.class, LocalTime.of(11, 14, 19, 820000000)), + Arguments.of( + "fifth", LocalDateTime.class, LocalDateTime.of(2023, 3, 30, 11, 14, 19, 820000000)), + Arguments.of(5, LocalDateTime.class, LocalDateTime.of(2023, 3, 30, 11, 14, 19, 820000000)), + Arguments.of("first", Boolean.class, false), + Arguments.of(1, Boolean.class, false), + Arguments.of("second", Long.class, 1L), + Arguments.of(2, Integer.class, 1), + Arguments.of(2, Short.class, (short) 1), + Arguments.of(2, String.class, "1"), + Arguments.of("third", Double.class, 1.5D), + Arguments.of(3, Float.class, 1.5F), + Arguments.of("fourth", String.class, STRING_VAL), + Arguments.of("tenth", BigDecimal.class, new BigDecimal("12345678")), + Arguments.of(10, Long.class, 12345678L), + Arguments.of("eleventh", BigDecimal.class, new BigDecimal("12345678.99")), + Arguments.of(11, Double.class, 12345678.99D)); + } + + public static Stream failingCoercionCases() { + return Stream.of( + Arguments.of("first", LocalDate.class), + Arguments.of("fourteenth", Boolean.class), + Arguments.of("fourth", BigDecimal.class)); + } + + @ParameterizedTest + @MethodSource("successfulCoercionCases") + public void testGetObjectWithType_success(Object column, Class type, Object expectedValue) + throws SQLException { + assertThat(resetResultSet()).isTrue(); + if (column instanceof String) { + assertThat(bigQueryJsonResultSet.getObject((String) column, type)).isEqualTo(expectedValue); + } else { + assertThat(bigQueryJsonResultSet.getObject((Integer) column, type)).isEqualTo(expectedValue); + } + } + + @ParameterizedTest + @MethodSource("failingCoercionCases") + public void testGetObjectWithType_failure(Object column, Class type) throws SQLException { + assertThat(resetResultSet()).isTrue(); + if (column instanceof String) { + assertThrows( + SQLException.class, () -> bigQueryJsonResultSet.getObject((String) column, type)); + } else { + assertThrows( + SQLException.class, () -> bigQueryJsonResultSet.getObject((Integer) column, type)); + } + } + private int resultSetRowCount(BigQueryJsonResultSet resultSet) throws SQLException { int rowCount = 0; while (resultSet.next()) {