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 b19fbe4e8014..516fd839ea7c 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 @@ -150,14 +150,12 @@ public static Time boxTime(String val, ZoneId zoneId) { * the Calendar timezone is explicitly ignored per JDBC 4.2 spec. */ public static Timestamp boxTimestamp(String val) { - // Check if the value is a numeric float string (e.g. "1680174859.8202269" from JSON API) - try { - if (val.indexOf('-') < 0 - || (val.startsWith("-") - && val.indexOf('-', 1) < 0)) { // Quick check to ensure it's not a date string + // Epoch decimals never contain ':' but may contain '-' (e.g. "1.0E-6"), so key on ':'. + if (val.indexOf(':') < 0) { + try { return Timestamp.from(parseEpochDecimalToInstant(val)); + } catch (NumberFormatException ignored) { } - } catch (NumberFormatException ignored) { } String iso = val; diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtilityTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtilityTest.java index 45f3d49c2fe8..814cad844c30 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtilityTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtilityTest.java @@ -272,6 +272,16 @@ public void testBoxTimestamp() { Timestamp picosIsoTs = BigQueryTemporalUtility.boxTimestamp("2026-08-24T15:30:45.123456789012Z"); assertThat(picosIsoTs.getNanos()).isEqualTo(123456789); + + // JSON API uses scientific notation for |t| < 1 ms + assertThat(BigQueryTemporalUtility.boxTimestamp("1.0E-6").toInstant()) + .isEqualTo(Instant.parse("1970-01-01T00:00:00.000001Z")); + assertThat(BigQueryTemporalUtility.boxTimestamp("9.99E-4").toInstant()) + .isEqualTo(Instant.parse("1970-01-01T00:00:00.000999Z")); + assertThat(BigQueryTemporalUtility.boxTimestamp("-1.0E-6").toInstant()) + .isEqualTo(Instant.parse("1969-12-31T23:59:59.999999Z")); + assertThat(BigQueryTemporalUtility.boxTimestamp("-1.0E-4").toInstant()) + .isEqualTo(Instant.parse("1969-12-31T23:59:59.999900Z")); } @Test 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 58ddd8f9462b..505157119ca7 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,7 +73,10 @@ public class ITTimestampTimeZoneTest extends ITBase { + " DATE '2025-01-01' AS d," + " TIME '12:34:56' AS t," + " [TIMESTAMP '2025-01-01 12:34:56.123 UTC'] AS ts_array," - + " STRUCT(TIMESTAMP '2025-01-01 12:34:56.123 UTC' AS ts) AS ts_struct"; + + " STRUCT(TIMESTAMP '2025-01-01 12:34:56.123 UTC' AS ts) AS ts_struct," + // REST API sends these as scientific notation ("1.0E-6", "-1.0E-6"). + + " TIMESTAMP '1970-01-01 00:00:00.000001 UTC' AS ts_post_epoch," + + " TIMESTAMP '1969-12-31 23:59:59.999999 UTC' AS ts_pre_epoch"; // Same coercion as an INSERT into a TIMESTAMP column, for TIMESTAMP and STRING parameters. private static final String PROBE_TIMESTAMP = "SELECT UNIX_MILLIS(CAST(? AS TIMESTAMP))"; @@ -321,7 +324,17 @@ public void testGetTimestampAndGetObject_returnMoment(String zone) throws SQLExc assertEquals( Timestamp.from(REF), rs.getObject("ts"), - label(zone, "getObject must return a Timestamp of the stored moment"))); + label(zone, "getObject must return a Timestamp of the stored moment")), + () -> + assertEquals( + Instant.parse("1970-01-01T00:00:00.000001Z"), + rs.getTimestamp("ts_post_epoch").toInstant(), + label(zone, "getTimestamp just after the epoch")), + () -> + assertEquals( + Instant.parse("1969-12-31T23:59:59.999999Z"), + rs.getTimestamp("ts_pre_epoch").toInstant(), + label(zone, "getTimestamp just before the epoch"))); } } @@ -500,7 +513,17 @@ public void testGetString_returnsUtcDigits(String zone) throws SQLException { assertEquals( "{\"ts\":\"2025-01-01 12:34:56.123\"}", rs.getString("ts_struct"), - label(zone, "getString on STRUCT must return UTC digits"))); + label(zone, "getString on STRUCT must return UTC digits")), + () -> + assertEquals( + "1970-01-01 00:00:00.000001", + rs.getString("ts_post_epoch"), + label(zone, "getString just after the epoch")), + () -> + assertEquals( + "1969-12-31 23:59:59.999999", + rs.getString("ts_pre_epoch"), + label(zone, "getString just before the epoch"))); } }