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 36447684d419..aa6b07269360 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 @@ -172,14 +172,15 @@ public String toString() { if (array == null) { return "null"; } - if (this.schema.getType().getStandardType() == StandardSQLTypeName.BYTES) { + StandardSQLTypeName type = this.schema.getType().getStandardType(); + if (type == StandardSQLTypeName.BYTES) { return Arrays.stream(array) .map( element -> element == null ? "null" : Base64.getEncoder().encodeToString((byte[]) element)) .collect(Collectors.joining(", ", "[", "]")); } - if (this.schema.getType().getStandardType() == StandardSQLTypeName.TIMESTAMP) { + if (type == StandardSQLTypeName.TIMESTAMP) { return Arrays.stream(array) .map( element -> { @@ -193,7 +194,7 @@ public String toString() { }) .collect(Collectors.joining(", ", "[", "]")); } - if (this.schema.getType().getStandardType() == StandardSQLTypeName.TIME) { + if (BigQueryTemporalUtility.isCivilTemporalType(type)) { StringBuilder sb = new StringBuilder("["); for (int i = 0; i < array.length; i++) { if (i > 0) { @@ -202,8 +203,7 @@ public String toString() { sb.append( array[i] == null ? "null" - : BigQueryTypeRegistry.convert( - getRawElement(i), StandardSQLTypeName.TIME, String.class, null)); + : BigQueryTemporalUtility.formatCivilTemporalElement(type, getRawElement(i))); } sb.append("]"); return sb.toString(); 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 d0dd7a3f3c53..97e04b6d9564 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 @@ -52,13 +52,7 @@ import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; import java.time.ZoneId; -import java.time.ZonedDateTime; import java.util.Calendar; import java.util.List; import javax.sql.rowset.serial.SerialBlob; @@ -318,28 +312,27 @@ protected Object getObject(int columnIndex, ZoneId zone) throws SQLException { abstract Object getObjectInternal(int columnIndex) throws SQLException; + private Object getTemporalValue(int columnIndex, StandardSQLTypeName bqType, ZoneId zone) + throws SQLException { + return BigQueryTemporalUtility.isCivilTemporalType(bqType) + ? getObjectInternal(columnIndex) + : getObject(columnIndex, zone); + } + @Override public T getObject(int columnIndex, Class type) throws SQLException { LOG.finestTrace("getObject"); try { - if (isRawViewTarget(type)) { - StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex); - // java.sql types can't hold every value of these (sub-ms TIME, pre-1582 DATE/DATETIME). - if (bqType == StandardSQLTypeName.TIME - || bqType == StandardSQLTypeName.DATE - || bqType == StandardSQLTypeName.DATETIME) { - return BigQueryTypeRegistry.convert(getObjectInternal(columnIndex), bqType, type, null); - } + StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex); + if (type != null && BigQueryTemporalUtility.isCivilTemporalType(bqType)) { + return BigQueryTypeRegistry.convert(getObjectInternal(columnIndex), bqType, type, null); } Object value = getObject(columnIndex); if (value == null) { return null; } - if (type != null && !type.isInstance(value)) { - StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex); - if (bqType == StandardSQLTypeName.TIMESTAMP) { - return BigQueryTypeRegistry.convert(value, bqType, type, null); - } + if (type != null && !type.isInstance(value) && bqType == StandardSQLTypeName.TIMESTAMP) { + return BigQueryTypeRegistry.convert(value, bqType, type, null); } return BigQueryTypeRegistry.convert(value, type); } catch (RuntimeException e) { @@ -347,16 +340,6 @@ public T getObject(int columnIndex, Class type) throws SQLException { } } - private static boolean isRawViewTarget(Class type) { - return type == String.class - || type == LocalDate.class - || type == LocalDateTime.class - || type == LocalTime.class - || type == Instant.class - || type == OffsetDateTime.class - || type == ZonedDateTime.class; - } - @Override public T getObject(String columnLabel, Class type) throws SQLException { return getObject(getColumnIndex(columnLabel), type); @@ -592,7 +575,7 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException { StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex); ZoneId zone = getEffectiveZone(bqType, cal); try { - Object value = getObject(columnIndex, zone); + Object value = getTemporalValue(columnIndex, bqType, zone); return BigQueryTypeRegistry.convert(value, bqType, Date.class, zone); } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Date.class, e); @@ -608,7 +591,7 @@ public Time getTime(int columnIndex, Calendar cal) throws SQLException { } ZoneId zone = getEffectiveZone(bqType, cal); try { - Object value = getObject(columnIndex, zone); + Object value = getTemporalValue(columnIndex, bqType, zone); return BigQueryTypeRegistry.convert(value, bqType, Time.class, zone); } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Time.class, e); @@ -624,7 +607,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException } ZoneId zone = getEffectiveZone(bqType, cal); try { - Object value = getObject(columnIndex, zone); + Object value = getTemporalValue(columnIndex, bqType, zone); return BigQueryTypeRegistry.convert(value, bqType, Timestamp.class, zone); } catch (BigQueryJdbcException e) { throw createCoercionException(columnIndex, Timestamp.class, e); diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java index 55a21b304932..d12977323b83 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java @@ -117,23 +117,21 @@ public String toString() { private static void appendJsonValue(StringBuilder sb, Field field, Object value, Object rawValue) throws SQLException { + StandardSQLTypeName type = field.getType().getStandardType(); if (value == null) { sb.append("null"); - } else if (field.getType().getStandardType() == StandardSQLTypeName.JSON) { + } else if (type == StandardSQLTypeName.JSON) { sb.append(value); } else if (value instanceof String || value instanceof Text) { String stringValue = value.toString().replace("\"", "\\\""); sb.append("\"").append(stringValue).append("\""); - } else if (value instanceof Timestamp - && field.getType().getStandardType() == StandardSQLTypeName.TIMESTAMP) { + } else if (value instanceof Timestamp && type == StandardSQLTypeName.TIMESTAMP) { sb.append("\"") .append(BigQueryTemporalUtility.formatTimestampToUtcString((Timestamp) value)) .append("\""); - } else if (value instanceof Time - && field.getType().getStandardType() == StandardSQLTypeName.TIME) { + } else if (BigQueryTemporalUtility.isCivilTemporalType(type)) { sb.append("\"") - .append( - BigQueryTypeRegistry.convert(rawValue, StandardSQLTypeName.TIME, String.class, null)) + .append(BigQueryTemporalUtility.formatCivilTemporalElement(type, rawValue)) .append("\""); } else if (value instanceof Timestamp || value instanceof Date || value instanceof Time) { sb.append("\"").append(value).append("\""); diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java index ea0836aba6ae..a93358008970 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java @@ -392,6 +392,28 @@ static String formatTimestampToUtcString(Timestamp ts) { return UTC_TIMESTAMP_TO_STRING_FORMATTER.format(ts.toInstant()); } + static boolean isCivilTemporalType(StandardSQLTypeName type) { + return type == StandardSQLTypeName.DATE + || type == StandardSQLTypeName.TIME + || type == StandardSQLTypeName.DATETIME; + } + + /** + * Formats a raw civil temporal value ({@code DATE}, {@code TIME}, or {@code DATETIME}) as a + * string for complex types ({@code ARRAY} and {@code STRUCT}) without boxing through {@code + * java.sql} types. + */ + static String formatCivilTemporalElement(StandardSQLTypeName type, Object rawValue) + throws BigQueryJdbcException { + if (type == StandardSQLTypeName.DATETIME) { + LocalDateTime ldt = + BigQueryTypeRegistry.convert( + rawValue, StandardSQLTypeName.DATETIME, LocalDateTime.class, null); + return ldt == null ? null : UTC_TIMESTAMP_TO_STRING_FORMATTER.format(ldt); + } + return BigQueryTypeRegistry.convert(rawValue, type, String.class, null); + } + /** * Formats a timestamp value (which may be a {@link Long} epoch microsecond, an ISO-8601 string, a * {@link java.sql.Timestamp}, or an epoch decimal string) into a standard UTC JDBC timestamp diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java index dcb34450184b..88089988ba15 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistry.java @@ -298,57 +298,54 @@ static TypeDescriptor createDatetimeDescriptor() { StandardSQLTypeName.DATETIME, Arrays.asList(Timestamp.class, LocalDateTime.class), (val, targetClass, zone) -> { - // Modern fast-path: Bypass intermediate object creation for JSR-310 targets - if (targetClass == LocalDateTime.class - || targetClass == String.class - || targetClass == LocalDate.class - || targetClass == LocalTime.class - || isInstantView(targetClass)) { - LocalDateTime localDateTime; - if (val instanceof LocalDateTime) { - localDateTime = (LocalDateTime) val; - } else if (val instanceof Timestamp) { - localDateTime = ((Timestamp) val).toLocalDateTime(); - } else if (val instanceof java.util.Date) { - localDateTime = new Timestamp(((java.util.Date) val).getTime()).toLocalDateTime(); - } else if (val instanceof String) { - String isoString = - BigQueryTemporalUtility.truncateFractionalSeconds( - ((String) val).replace(' ', 'T'), 9); - localDateTime = LocalDateTime.parse(isoString); - } else { - throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); - } - if (targetClass == LocalDateTime.class) { - return localDateTime; - } - if (targetClass == String.class) { - return TIMESTAMP_FORMATTER.format(localDateTime); - } - if (targetClass == LocalDate.class) { - return localDateTime.toLocalDate(); - } - if (targetClass == LocalTime.class) { - return localDateTime.toLocalTime(); - } - return anchorCivil(localDateTime, targetClass, zone); + if (val instanceof Timestamp && targetClass == Timestamp.class && zone == null) { + return val; + } + if (val instanceof java.util.Date + && !(val instanceof Timestamp) + && targetClass == Timestamp.class + && zone == null) { + return new Timestamp(((java.util.Date) val).getTime()); } - // Legacy path: Box values into java.sql.Timestamp - Timestamp ts; - if (val instanceof Timestamp) { - ts = (Timestamp) val; + LocalDateTime localDateTime; + if (val instanceof LocalDateTime) { + localDateTime = (LocalDateTime) val; + } else if (val instanceof Timestamp) { + localDateTime = ((Timestamp) val).toLocalDateTime(); } else if (val instanceof java.util.Date) { - ts = new Timestamp(((java.util.Date) val).getTime()); - } else if (val instanceof LocalDateTime) { - ts = BigQueryTemporalUtility.boxDateTime((LocalDateTime) val, zone); + localDateTime = new Timestamp(((java.util.Date) val).getTime()).toLocalDateTime(); } else if (val instanceof String) { - ts = BigQueryTemporalUtility.boxDateTime((String) val, zone); + String isoString = + BigQueryTemporalUtility.truncateFractionalSeconds( + ((String) val).replace(' ', 'T'), 9); + localDateTime = LocalDateTime.parse(isoString); } else { throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val); } - return ts; + if (targetClass == LocalDateTime.class) { + return localDateTime; + } + if (targetClass == String.class) { + return TIMESTAMP_FORMATTER.format(localDateTime); + } + if (targetClass == LocalDate.class) { + return localDateTime.toLocalDate(); + } + if (targetClass == LocalTime.class) { + return localDateTime.toLocalTime(); + } + if (targetClass == Date.class) { + return BigQueryTemporalUtility.boxDate(localDateTime.toLocalDate(), zone); + } + if (targetClass == Time.class) { + return BigQueryTemporalUtility.boxTime(localDateTime.toLocalTime(), zone); + } + if (isInstantView(targetClass)) { + return anchorCivil(localDateTime, targetClass, zone); + } + return BigQueryTemporalUtility.boxDateTime(localDateTime, zone); }); } 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 6f8a249513b1..2711664ddcca 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 @@ -700,16 +700,20 @@ public void testGetStringOnTimestampAndDatetimeAcrossJvmZones(String jvmZone) TimeZone originalDefault = TimeZone.getDefault(); try { TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of(jvmZone))); - Instant ref = Instant.parse("2025-01-01T12:34:56.123Z"); + Instant ref = Instant.parse("2025-03-09T02:34:56.123Z"); BigQueryArrowResultSet rs = createTemporalArrowResultSet(ref); assertThat(rs.next()).isTrue(); - assertThat(rs.getString("ts")).isEqualTo("2025-01-01 12:34:56.123000"); - assertThat(rs.getObject("ts", String.class)).isEqualTo("2025-01-01 12:34:56.123000"); - assertThat(rs.getString("dt")).isEqualTo("2025-01-01 12:34:56.123000"); + assertThat(rs.getString("ts")).isEqualTo("2025-03-09 02:34:56.123000"); + assertThat(rs.getObject("ts", String.class)).isEqualTo("2025-03-09 02:34:56.123000"); + assertThat(rs.getString("dt")).isEqualTo("2025-03-09 02:34:56.123000"); + assertThat(rs.getDate("dt").toLocalDate()).isEqualTo(LocalDate.of(2025, 3, 9)); + assertThat(rs.getObject("dt", Date.class).toLocalDate()).isEqualTo(LocalDate.of(2025, 3, 9)); + assertThat(rs.getTime("dt").toLocalTime()).isEqualTo(LocalTime.of(2, 34, 56)); + assertThat(rs.getObject("dt", Time.class).toLocalTime()).isEqualTo(LocalTime.of(2, 34, 56)); long tsMicros = ref.getEpochSecond() * 1_000_000L + ref.getNano() / 1_000L; - LocalDateTime dtLocal = LocalDateTime.of(2025, 1, 1, 12, 34, 56, 123_000_000); + LocalDateTime dtLocal = LocalDateTime.of(2025, 3, 9, 2, 34, 56, 123_000_000); Field tsArraySchema = Field.newBuilder("ts_array", StandardSQLTypeName.TIMESTAMP) @@ -718,10 +722,10 @@ public void testGetStringOnTimestampAndDatetimeAcrossJvmZones(String jvmZone) JsonStringArrayList tsList = new JsonStringArrayList<>(); tsList.add(tsMicros); BigQueryArrowArray tsArrowArray = new BigQueryArrowArray(tsArraySchema, tsList); - assertThat(tsArrowArray.toString()).isEqualTo("[2025-01-01 12:34:56.123]"); + assertThat(tsArrowArray.toString()).isEqualTo("[2025-03-09 02:34:56.123]"); try (ResultSet nestedTsRs = tsArrowArray.getResultSet()) { assertThat(nestedTsRs.next()).isTrue(); - assertThat(nestedTsRs.getString(2)).isEqualTo("2025-01-01 12:34:56.123000"); + assertThat(nestedTsRs.getString(2)).isEqualTo("2025-03-09 02:34:56.123000"); } Field dtArraySchema = @@ -731,7 +735,12 @@ public void testGetStringOnTimestampAndDatetimeAcrossJvmZones(String jvmZone) JsonStringArrayList dtList = new JsonStringArrayList<>(); dtList.add(dtLocal); BigQueryArrowArray dtArrowArray = new BigQueryArrowArray(dtArraySchema, dtList); - assertThat(dtArrowArray.toString()).isEqualTo("[2025-01-01 12:34:56.123]"); + assertThat(dtArrowArray.toString()).isEqualTo("[2025-03-09 02:34:56.123]"); + try (ResultSet nestedDtRs = dtArrowArray.getResultSet()) { + assertThat(nestedDtRs.next()).isTrue(); + assertThat(nestedDtRs.getString(2)).isEqualTo("2025-03-09 02:34:56.123000"); + assertThat(nestedDtRs.getTime(2).toLocalTime()).isEqualTo(LocalTime.of(2, 34, 56)); + } FieldList structSchema = FieldList.of( @@ -742,7 +751,7 @@ public void testGetStringOnTimestampAndDatetimeAcrossJvmZones(String jvmZone) structValues.put("dt", dtLocal); BigQueryArrowStruct arrowStruct = new BigQueryArrowStruct(structSchema, structValues); assertThat(arrowStruct.toString()) - .isEqualTo("{\"ts\":\"2025-01-01 12:34:56.123\",\"dt\":\"2025-01-01 12:34:56.123\"}"); + .isEqualTo("{\"ts\":\"2025-03-09 02:34:56.123\",\"dt\":\"2025-03-09 02:34:56.123\"}"); } finally { TimeZone.setDefault(originalDefault); } 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 50b795be9470..c47e58112e05 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 @@ -830,8 +830,8 @@ public void testGetStringOnTimestampAndDatetimeAcrossJvmZones(String jvmZone) .build(), Field.of("ts_struct", StandardSQLTypeName.STRUCT, FieldList.of(tsSubField)), Field.of("dt_struct", StandardSQLTypeName.STRUCT, FieldList.of(dtSubField))); - FieldValue tsVal = FieldValue.of(Attribute.PRIMITIVE, "1735734896.123000"); - FieldValue dtVal = FieldValue.of(Attribute.PRIMITIVE, "2025-01-01 12:34:56.123000"); + FieldValue tsVal = FieldValue.of(Attribute.PRIMITIVE, "1741487696.123000"); + FieldValue dtVal = FieldValue.of(Attribute.PRIMITIVE, "2025-03-09 02:34:56.123000"); FieldValueList row = FieldValueList.of( ImmutableList.of( @@ -849,20 +849,25 @@ public void testGetStringOnTimestampAndDatetimeAcrossJvmZones(String jvmZone) BigQueryJsonResultSet rs = createJsonResultSet(fields, row, false); assertThat(rs.next()).isTrue(); - assertThat(rs.getString("ts")).isEqualTo("2025-01-01 12:34:56.123000"); - assertThat(rs.getObject("ts", String.class)).isEqualTo("2025-01-01 12:34:56.123000"); - assertThat(rs.getString("dt")).isEqualTo("2025-01-01 12:34:56.123000"); - assertThat(rs.getString("ts_array")).isEqualTo("[2025-01-01 12:34:56.123]"); - assertThat(rs.getString("dt_array")).isEqualTo("[2025-01-01 12:34:56.123]"); - assertThat(rs.getString("ts_struct")).isEqualTo("{\"ts\":\"2025-01-01 12:34:56.123\"}"); - assertThat(rs.getString("dt_struct")).isEqualTo("{\"dt\":\"2025-01-01 12:34:56.123\"}"); + assertThat(rs.getString("ts")).isEqualTo("2025-03-09 02:34:56.123000"); + assertThat(rs.getObject("ts", String.class)).isEqualTo("2025-03-09 02:34:56.123000"); + assertThat(rs.getString("dt")).isEqualTo("2025-03-09 02:34:56.123000"); + assertThat(rs.getDate("dt").toLocalDate()).isEqualTo(LocalDate.of(2025, 3, 9)); + assertThat(rs.getObject("dt", Date.class).toLocalDate()).isEqualTo(LocalDate.of(2025, 3, 9)); + assertThat(rs.getTime("dt").toLocalTime()).isEqualTo(LocalTime.of(2, 34, 56)); + assertThat(rs.getObject("dt", Time.class).toLocalTime()).isEqualTo(LocalTime.of(2, 34, 56)); + assertThat(rs.getString("ts_array")).isEqualTo("[2025-03-09 02:34:56.123]"); + assertThat(rs.getString("dt_array")).isEqualTo("[2025-03-09 02:34:56.123]"); + assertThat(rs.getString("ts_struct")).isEqualTo("{\"ts\":\"2025-03-09 02:34:56.123\"}"); + assertThat(rs.getString("dt_struct")).isEqualTo("{\"dt\":\"2025-03-09 02:34:56.123\"}"); try (ResultSet tsArrayRs = rs.getArray("ts_array").getResultSet(); ResultSet dtArrayRs = rs.getArray("dt_array").getResultSet()) { assertThat(tsArrayRs.next()).isTrue(); - assertThat(tsArrayRs.getString(2)).isEqualTo("2025-01-01 12:34:56.123000"); + assertThat(tsArrayRs.getString(2)).isEqualTo("2025-03-09 02:34:56.123000"); assertThat(dtArrayRs.next()).isTrue(); - assertThat(dtArrayRs.getString(2)).isEqualTo("2025-01-01 12:34:56.123000"); + assertThat(dtArrayRs.getString(2)).isEqualTo("2025-03-09 02:34:56.123000"); + assertThat(dtArrayRs.getTime(2).toLocalTime()).isEqualTo(LocalTime.of(2, 34, 56)); } } finally { TimeZone.setDefault(originalDefault); diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistryTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistryTest.java index ba91b21e051f..b3d4a384481d 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistryTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTypeRegistryTest.java @@ -34,6 +34,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.Arrays; import java.util.TimeZone; import java.util.stream.Stream; import org.apache.arrow.vector.PeriodDuration; @@ -305,4 +306,23 @@ public void testTimeMicrosecondPrecision() throws BigQueryJdbcException { assertThat(sqlTime.getClass()).isEqualTo(Time.class); assertThat(sqlTime.toString()).isEqualTo("14:30:00"); } + + @ParameterizedTest(name = "JVM zone {0}") + @MethodSource("jvmZones") + public void testDatetimeReadCoercionsPreserveCivilDigits(String zone) throws Exception { + TimeZone.setDefault(TimeZone.getTimeZone(zone)); + LocalDateTime gapLdt = LocalDateTime.of(2025, 3, 9, 2, 30, 0); + ZoneId utc = ZoneId.of("UTC"); + + for (Object raw : Arrays.asList("2025-03-09 02:30:00", gapLdt)) { + assertThat(BigQueryTypeRegistry.convert(raw, StandardSQLTypeName.DATETIME, Date.class, null)) + .isEqualTo(Date.valueOf(gapLdt.toLocalDate())); + assertThat(BigQueryTypeRegistry.convert(raw, StandardSQLTypeName.DATETIME, Time.class, null)) + .isEqualTo(Time.valueOf(gapLdt.toLocalTime())); + assertThat( + BigQueryTypeRegistry.convert(raw, StandardSQLTypeName.DATETIME, Time.class, utc) + .getTime()) + .isEqualTo(Duration.ofHours(2).plusMinutes(30).toMillis()); + } + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITTimestampTimeZoneTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITTimestampTimeZoneTest.java index d246bd912379..94085f21e563 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITTimestampTimeZoneTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITTimestampTimeZoneTest.java @@ -73,6 +73,9 @@ public class ITTimestampTimeZoneTest extends ITBase { "SELECT TIMESTAMP '0001-01-01 00:00:00 UTC' AS ts_min, DATETIME '0001-01-01 00:00:00' AS" + " dt_min, DATE '0001-01-01' AS d_min, DATETIME '1850-06-01 12:00:00' AS dt_1850," + " DATE '1850-06-01' AS d_1850, DATE '1582-10-10' AS d_gap," + + " DATETIME '2025-03-09 02:30:00' AS dt_dst_gap," + + " [DATETIME '2025-03-09 02:30:00'] AS dt_dst_gap_array," + + " STRUCT(DATETIME '2025-03-09 02:30:00' AS dt) AS dt_dst_gap_struct," + " TIMESTAMP '2025-01-01 12:34:56.123 UTC' AS ts," + " DATETIME '2025-01-01 12:34:56.123' AS dt," + " DATE '2025-01-01' AS d," @@ -477,7 +480,27 @@ public void testWallClockTypes_returnDigits(String zone) throws SQLException { assertEquals( LocalDate.parse("1582-10-10"), rs.getObject("d_gap", LocalDate.class), - label(zone, "d_gap"))); + label(zone, "d_gap")), + () -> + assertEquals( + LocalDate.of(2025, 3, 9), + rs.getDate("dt_dst_gap").toLocalDate(), + label(zone, "getDate on DST-gap DATETIME")), + () -> + assertEquals( + LocalTime.of(2, 30, 0), + rs.getTime("dt_dst_gap").toLocalTime(), + label(zone, "getTime on DST-gap DATETIME")), + () -> + assertEquals( + "[2025-03-09 02:30:00.0]", + rs.getString("dt_dst_gap_array"), + label(zone, "getString on ARRAY in DST gap")), + () -> + assertEquals( + "{\"dt\":\"2025-03-09 02:30:00.0\"}", + rs.getString("dt_dst_gap_struct"), + label(zone, "getString on STRUCT in DST gap"))); } }