From 61d3be06ea92b3a22dae9b3046cd81f8fdac60a2 Mon Sep 17 00:00:00 2001 From: Kirill Logachev Date: Thu, 23 Jul 2026 00:30:46 +0000 Subject: [PATCH] fix(bigquery-jdbc): fix time getters --- .../bigquery/jdbc/BigQueryBaseArray.java | 8 + .../bigquery/jdbc/BigQueryTypeCoercer.java | 4 + .../jdbc/BigQueryTypeCoercionUtility.java | 165 ++++++++++++------ .../bigquery/jdbc/it/ITBigQueryJDBCTest.java | 59 +++++++ 4 files changed, 183 insertions(+), 53 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java index e001e35c6a21..62a1fe6f4f67 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java @@ -95,6 +95,14 @@ protected Object getArrayInternal(int fromIndex, int toIndexExclusive) { LOG.finestTrace("getArrayInternal"); Class targetClass = getTargetClass(); int size = toIndexExclusive - fromIndex; + if (size == 1) { + Object firstVal = getCoercedValue(fromIndex); + if (firstVal != null + && firstVal.getClass().isArray() + && firstVal.getClass().getComponentType().equals(targetClass)) { + return firstVal; + } + } Object javaArray = Array.newInstance(targetClass, size); for (int index = 0; index < size; index++) { diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercer.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercer.java index 9f968fd4b8d0..7ab680de497f 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercer.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercer.java @@ -116,6 +116,10 @@ T coerceTo(Class targetClass, Object value, BigQueryJdbcResultSetLogger l return null; } if (coercion == null) { + if (targetClass.isAssignableFrom(sourceClass) + || (sourceClass.isArray() && targetClass.isArray())) { + return (T) value; + } if (targetClass.equals(String.class)) { return (T) value.toString(); } 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 931fa0afc644..a38144032d4d 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 @@ -138,6 +138,27 @@ static Timestamp convertTimestampWithCalendar(Timestamp timestamp, Calendar cal) return adjustedTimestamp; } + static Time localTimeToTime(LocalTime lt) { + long epochMillis = + lt.atDate(LocalDate.of(1970, 1, 1)) + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + return new Time(epochMillis); + } + + static LocalDateTime parseFieldValueToLocalDateTime(FieldValue fv) { + String raw = fv.getStringValue(); + if (raw.contains("T") || raw.contains(" ")) { + return LocalDateTime.parse(raw.replace(' ', 'T')); + } + long micros = fv.getTimestampValue(); + return Instant.EPOCH + .plus(micros, ChronoUnit.MICROS) + .atOffset(ZoneOffset.UTC) + .toLocalDateTime(); + } + static BigQueryTypeCoercer INSTANCE; static { @@ -163,7 +184,7 @@ static Timestamp convertTimestampWithCalendar(Timestamp timestamp, Calendar cal) // Read API Type coercions .registerTypeCoercion( - (LocalDateTime ldt) -> Timestamp.from(ldt.toInstant(ZoneOffset.UTC)), + (LocalDateTime ldt) -> Timestamp.valueOf(ldt), LocalDateTime.class, Timestamp.class) .registerTypeCoercion(Text::toString, Text.class, String.class) @@ -172,22 +193,15 @@ static Timestamp convertTimestampWithCalendar(Timestamp timestamp, Calendar cal) .registerTypeCoercion(new LongToTime()) .registerTypeCoercion(new IntegerToDate()) .registerTypeCoercion( - (Timestamp ts) -> - Date.valueOf(ts.toInstant().atOffset(ZoneOffset.UTC).toLocalDate()), + (Timestamp ts) -> Date.valueOf(ts.toLocalDateTime().toLocalDate()), Timestamp.class, Date.class) .registerTypeCoercion( - (Timestamp ts) -> - Time.valueOf(ts.toInstant().atOffset(ZoneOffset.UTC).toLocalTime()), + (Timestamp ts) -> localTimeToTime(ts.toLocalDateTime().toLocalTime()), Timestamp.class, Time.class) .registerTypeCoercion( - (Time time) -> // Per JDBC spec, the date component should be 1970-01-01 - Timestamp.from( - LocalDateTime.of(LocalDate.ofEpochDay(0), time.toLocalTime()) - .toInstant(ZoneOffset.UTC)), - Time.class, - Timestamp.class) + (Time time) -> new Timestamp(time.getTime()), Time.class, Timestamp.class) .registerTypeCoercion( (Date date) -> new Timestamp(date.getTime()), Date.class, Timestamp.class) .registerTypeCoercion( @@ -195,35 +209,94 @@ static Timestamp convertTimestampWithCalendar(Timestamp timestamp, Calendar cal) 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 ldt) -> localTimeToTime(ldt.toLocalTime()), 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 + TimeZoneCache.getOffset(millis); - return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(localMillis)); - }, + (Date date) -> date.toLocalDate().atStartOfDay(), + Date.class, + LocalDateTime.class) + .registerTypeCoercion( + (Time time) -> + Instant.ofEpochMilli(time.getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalTime(), Time.class, LocalTime.class) .registerTypeCoercion( - (Timestamp ts) -> ts.toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime(), + (Timestamp ts) -> ts.toLocalDateTime(), Timestamp.class, LocalDateTime.class) + .registerTypeCoercion( + (Timestamp ts) -> ts.toLocalDateTime().toLocalDate(), Timestamp.class, - LocalDateTime.class) + LocalDate.class) .registerTypeCoercion( - (Timestamp ts) -> ts.toInstant().atOffset(ZoneOffset.UTC), + (Timestamp ts) -> ts.toLocalDateTime().toLocalTime(), + Timestamp.class, + LocalTime.class) + .registerTypeCoercion( + (Timestamp ts) -> ts.toLocalDateTime().atOffset(ZoneOffset.UTC), Timestamp.class, OffsetDateTime.class) - .registerTypeCoercion((Timestamp ts) -> ts.toInstant(), Timestamp.class, Instant.class) + .registerTypeCoercion( + (Timestamp ts) -> ts.toLocalDateTime().atZone(ZoneOffset.UTC), + Timestamp.class, + ZonedDateTime.class) + .registerTypeCoercion( + (Timestamp ts) -> ts.toLocalDateTime().atOffset(ZoneOffset.UTC).toInstant(), + Timestamp.class, + Instant.class) + .registerTypeCoercion( + (LocalDateTime ldt) -> ldt.toLocalDate(), LocalDateTime.class, LocalDate.class) + .registerTypeCoercion( + (LocalDateTime ldt) -> ldt.toLocalTime(), LocalDateTime.class, LocalTime.class) + .registerTypeCoercion( + (LocalDateTime ldt) -> ldt.atOffset(ZoneOffset.UTC), + LocalDateTime.class, + OffsetDateTime.class) + .registerTypeCoercion( + (LocalDateTime ldt) -> ldt.atZone(ZoneOffset.UTC), + LocalDateTime.class, + ZonedDateTime.class) + .registerTypeCoercion( + (LocalDateTime ldt) -> ldt.toInstant(ZoneOffset.UTC), + LocalDateTime.class, + Instant.class) + .registerTypeCoercion((LocalDate ld) -> Date.valueOf(ld), LocalDate.class, Date.class) + .registerTypeCoercion( + (LocalDate ld) -> Timestamp.valueOf(ld.atStartOfDay()), + LocalDate.class, + Timestamp.class) + .registerTypeCoercion( + (LocalDate ld) -> ld.atStartOfDay(), LocalDate.class, LocalDateTime.class) + .registerTypeCoercion( + BigQueryTypeCoercionUtility::localTimeToTime, + LocalTime.class, + Time.class) + .registerTypeCoercion( + (FieldValue fv) -> Date.valueOf(fv.getStringValue()).toLocalDate(), + FieldValue.class, + LocalDate.class) + .registerTypeCoercion( + (FieldValue fv) -> LocalTime.parse(fv.getStringValue()), + FieldValue.class, + LocalTime.class) + .registerTypeCoercion( + BigQueryTypeCoercionUtility::parseFieldValueToLocalDateTime, + FieldValue.class, + LocalDateTime.class) + .registerTypeCoercion( + (FieldValue fv) -> parseFieldValueToLocalDateTime(fv).atOffset(ZoneOffset.UTC), + FieldValue.class, + OffsetDateTime.class) + .registerTypeCoercion( + (FieldValue fv) -> parseFieldValueToLocalDateTime(fv).atZone(ZoneOffset.UTC), + FieldValue.class, + ZonedDateTime.class) + .registerTypeCoercion( + (FieldValue fv) -> parseFieldValueToLocalDateTime(fv).toInstant(ZoneOffset.UTC), + FieldValue.class, + Instant.class) .registerTypeCoercion(new TimestampToString()) .registerTypeCoercion(new TimeToString()) .registerTypeCoercion((Long l) -> l != 0L, Long.class, Boolean.class) @@ -276,7 +349,9 @@ private static class TimeToString implements BigQueryCoercion { @Override public String coerce(Time value) { - return FORMATTER.format(value.toLocalTime()); + LocalTime lt = + Instant.ofEpochMilli(value.getTime()).atZone(ZoneId.systemDefault()).toLocalTime(); + return FORMATTER.format(lt); } } @@ -355,10 +430,11 @@ private static class LongToTimestamp implements BigQueryCoercion result = + new ImmutableMap.Builder() + .put("dateField", LocalDate.of(2023, 7, 28)) + .put("dateTimeField", LocalDate.of(2023, 7, 28)) + .put("timestampFiled", LocalDate.of(2023, 7, 28)) + .build(); + BiFunction getter = + (s, i) -> { + try { + return s.getObject(i, LocalDate.class); + } catch (Exception e) { + return EXCEPTION_REPLACEMENT; + } + }; + validate("getLocalDate", getter, result); + } + + @Test + public void validateGetLocalTime() throws Exception { + final ImmutableMap result = + new ImmutableMap.Builder() + .put("timeField", LocalTime.of(12, 30, 0)) + .put("dateTimeField", LocalTime.of(12, 30, 0)) + .put("timestampFiled", LocalTime.of(12, 30, 0)) + .build(); + BiFunction getter = + (s, i) -> { + try { + return s.getObject(i, LocalTime.class); + } catch (Exception e) { + return EXCEPTION_REPLACEMENT; + } + }; + validate("getLocalTime", getter, result); + } + + @Test + public void validateGetLocalDateTime() throws Exception { + final ImmutableMap result = + new ImmutableMap.Builder() + .put("dateField", LocalDate.of(2023, 7, 28).atStartOfDay()) + .put("dateTimeField", LocalDateTime.of(2023, 7, 28, 12, 30, 0)) + .put("timestampFiled", LocalDateTime.of(2023, 7, 28, 12, 30, 0)) + .build(); + BiFunction getter = + (s, i) -> { + try { + return s.getObject(i, LocalDateTime.class); + } catch (Exception e) { + return EXCEPTION_REPLACEMENT; + } + }; + validate("getLocalDateTime", getter, result); + } + @Test public void validateGetByte() throws Exception { final ImmutableMap result =