Skip to content

Commit 3b02a44

Browse files
fix(bigquery-jdbc): preserve Timestamp instant in PreparedStatement and CallableStatement Calendar overloads (#14640)
b/565785264 ### Summary Fixes an issue where passing a `Calendar` to `PreparedStatement.setTimestamp` or `CallableStatement.setTimestamp`/`getTimestamp` corrupted the underlying UTC instant by shifting it by `JVM_offset - Calendar_offset` (or `2 * (JVM_offset - Calendar_offset)` on a `CallableStatement` setter-to-getter round trip). ### Root Cause A `java.sql.Timestamp` already represents an absolute UTC epoch instant (`milliseconds + nanos`), and BigQuery's `TIMESTAMP` type is a timezone-aware absolute instant. Previously, the `Calendar` overloads called `BigQueryTemporalUtility.convertTimestampWithCalendar(ts, cal)`, which extracted the JVM-local wall-clock fields (`ts.toLocalDateTime()`) and re-anchored them in `cal.getTimeZone()`, shifting the epoch instant whenever the `Calendar` timezone differed from the JVM default timezone. ### Changes - **`BigQueryPreparedStatement`**: Delegate `setTimestamp(int, Timestamp, Calendar)` directly to `setTimestamp(int, Timestamp)`. - **`BigQueryCallableStatement`**: Delegate `getTimestamp(int, Calendar)`, `getTimestamp(String, Calendar)`, and `setTimestamp(String, Timestamp, Calendar)` directly to their non-`Calendar` counterparts. - **`BigQueryTemporalUtility`**: Remove unused `convertTimestampWithCalendar(Timestamp, Calendar)` helper. - **Tests**: Update unit and integration tests (`BigQueryPreparedStatementSettersTest`, `BigQueryCallableStatementTest`, `BigQueryTemporalUtilityTest`, `ITCallableStatementTest`) to verify `Timestamp` preservation across distinct JVM (`America/New_York`) and `Calendar` (`Asia/Tokyo`) timezones.
1 parent a7a0098 commit 3b02a44

7 files changed

Lines changed: 32 additions & 85 deletions

File tree

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryCallableStatement.java‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -480,14 +480,12 @@ public Timestamp getTimestamp(String parameterName) throws SQLException {
480480

481481
@Override
482482
public Timestamp getTimestamp(int parameterIndex, Calendar calendar) throws SQLException {
483-
Timestamp ts = getTimestamp(parameterIndex);
484-
return BigQueryTemporalUtility.convertTimestampWithCalendar(ts, calendar);
483+
return getTimestamp(parameterIndex);
485484
}
486485

487486
@Override
488487
public Timestamp getTimestamp(String parameterName, Calendar calendar) throws SQLException {
489-
Timestamp ts = getTimestamp(parameterName);
490-
return BigQueryTemporalUtility.convertTimestampWithCalendar(ts, calendar);
488+
return getTimestamp(parameterName);
491489
}
492490

493491
@Override
@@ -877,12 +875,7 @@ public void setTimestamp(String parameterName, Timestamp value) throws SQLExcept
877875
public void setTimestamp(String parameterName, Timestamp value, Calendar calendar)
878876
throws SQLException {
879877
checkClosed();
880-
this.parameterHandler.setParameter(
881-
parameterName,
882-
BigQueryTemporalUtility.convertTimestampWithCalendar(value, calendar),
883-
Timestamp.class,
884-
BigQueryStatementParameterType.IN,
885-
0);
878+
setTimestamp(parameterName, value);
886879
}
887880

888881
@Override

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,8 +644,7 @@ public void setTime(int parameterIndex, Time value, Calendar calendar) throws SQ
644644
public void setTimestamp(int parameterIndex, Timestamp value, Calendar calendar)
645645
throws SQLException {
646646
checkClosed();
647-
setTimestamp(
648-
parameterIndex, BigQueryTemporalUtility.convertTimestampWithCalendar(value, calendar));
647+
setTimestamp(parameterIndex, value);
649648
}
650649

651650
@Override

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -478,24 +478,4 @@ static Time convertTimeWithCalendar(Time time, Calendar cal) {
478478
targetCal.set(Calendar.MILLISECOND, defaultCal.get(Calendar.MILLISECOND));
479479
return new Time(targetCal.getTimeInMillis());
480480
}
481-
482-
/**
483-
* Converts a java.sql.Timestamp by shifting its wall-clock fields into the target Calendar's
484-
* timezone per JDBC specification while preserving nanosecond precision.
485-
*/
486-
static Timestamp convertTimestampWithCalendar(Timestamp timestamp, Calendar cal) {
487-
if (timestamp == null || cal == null) {
488-
return timestamp;
489-
}
490-
ZoneId systemZone = ZoneId.systemDefault();
491-
ZoneId targetZone = cal.getTimeZone().toZoneId();
492-
if (systemZone.equals(targetZone)) {
493-
return timestamp;
494-
}
495-
LocalDateTime ldt = timestamp.toLocalDateTime();
496-
ZonedDateTime zdt = ldt.atZone(targetZone);
497-
Timestamp adjustedTimestamp = Timestamp.from(zdt.toInstant());
498-
adjustedTimestamp.setNanos(timestamp.getNanos());
499-
return adjustedTimestamp;
500-
}
501481
}

‎java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryCallableStatementTest.java‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@
3333
import java.util.Calendar;
3434
import java.util.HashMap;
3535
import java.util.Map;
36+
import java.util.TimeZone;
3637
import org.junit.jupiter.api.BeforeEach;
3738
import org.junit.jupiter.api.Test;
39+
import org.junit.jupiter.params.ParameterizedTest;
40+
import org.junit.jupiter.params.provider.ValueSource;
3841

3942
public class BigQueryCallableStatementTest {
4043

@@ -820,26 +823,28 @@ public void testGetTimestampParamByName() throws SQLException {
820823
assertEquals(expected, actual);
821824
}
822825

823-
@Test
824-
public void testGetTimestampParamWithCalByIndex() throws SQLException {
826+
@ParameterizedTest
827+
@ValueSource(strings = {"America/New_York", "Asia/Tokyo"})
828+
public void testGetTimestampParamWithCalByIndex(String zoneId) throws SQLException {
825829
BigQueryCallableStatement statement =
826830
new BigQueryCallableStatement(bigQueryConnection, "call testProc('?')");
827831
assertNotNull(statement);
828-
Calendar cal = Calendar.getInstance();
829-
Timestamp expected = new Timestamp(cal.getTimeInMillis());
832+
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(zoneId));
833+
Timestamp expected = new Timestamp(1700000000000L);
830834

831835
statement.getParameterHandler().setParameter(1, expected, Timestamp.class);
832836
Timestamp actual = statement.getTimestamp(1, cal);
833837
assertEquals(expected, actual);
834838
}
835839

836-
@Test
837-
public void testGetTimestampParamWithCalByName() throws SQLException {
840+
@ParameterizedTest
841+
@ValueSource(strings = {"America/New_York", "Asia/Tokyo"})
842+
public void testGetTimestampParamWithCalByName(String zoneId) throws SQLException {
838843
BigQueryCallableStatement statement =
839844
new BigQueryCallableStatement(bigQueryConnection, "call testProc('?')");
840845
assertNotNull(statement);
841-
Calendar cal = Calendar.getInstance();
842-
Timestamp expected = new Timestamp(cal.getTimeInMillis());
846+
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(zoneId));
847+
Timestamp expected = new Timestamp(1700000000000L);
843848

844849
statement
845850
.getParameterHandler()
@@ -1050,22 +1055,24 @@ public void testSetTimestampParamByName() throws SQLException {
10501055
assertEquals(expected, actual);
10511056
}
10521057

1053-
@Test
1054-
public void testSetTimestampCalParamByName() throws SQLException {
1058+
@ParameterizedTest
1059+
@ValueSource(strings = {"America/New_York", "Asia/Tokyo"})
1060+
public void testSetTimestampCalParamByName(String zoneId) throws SQLException {
10551061
BigQueryCallableStatement statement =
10561062
new BigQueryCallableStatement(bigQueryConnection, "call testProc('?')");
10571063
assertNotNull(statement);
1058-
Timestamp expectedTimestamp = Timestamp.valueOf("2026-07-17 12:34:56.789");
1059-
Calendar expectedCal = Calendar.getInstance();
1064+
Timestamp expectedTimestamp = new Timestamp(1700000000000L);
1065+
Calendar expectedCal = Calendar.getInstance(TimeZone.getTimeZone(zoneId));
10601066

10611067
statement.setTimestamp(PARAM_KEY, expectedTimestamp, expectedCal);
10621068
Timestamp actual = statement.getTimestamp(PARAM_KEY);
1063-
assertNotNull(actual);
1069+
assertEquals(expectedTimestamp, actual);
10641070
actual = statement.getTimestamp(PARAM_KEY, expectedCal);
1065-
assertNotNull(actual);
1071+
assertEquals(expectedTimestamp, actual);
10661072
}
10671073

10681074
////////// Private helper methods ////////////////////////
1075+
10691076
private void registerOutParamIndexHelper(
10701077
int paramIndex, int javaSqlType, Class<?> javaType, StandardSQLTypeName sqlType, int scale)
10711078
throws SQLException {

‎java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
import java.util.TimeZone;
5454
import org.junit.jupiter.api.BeforeEach;
5555
import org.junit.jupiter.api.Test;
56+
import org.junit.jupiter.params.ParameterizedTest;
57+
import org.junit.jupiter.params.provider.ValueSource;
5658

5759
public class BigQueryPreparedStatementSettersTest {
5860

@@ -234,15 +236,17 @@ public void testSetTimeWithCalendar() throws Exception {
234236
assertEquals(originalMillis, cal.getTimeInMillis());
235237
}
236238

237-
@Test
238-
public void testSetTimestampWithCalendar() throws Exception {
239-
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
239+
@ParameterizedTest
240+
@ValueSource(strings = {"America/New_York", "Asia/Tokyo"})
241+
public void testSetTimestampWithCalendar(String zoneId) throws Exception {
242+
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(zoneId));
240243
cal.setTimeInMillis(1000000L);
241244
long originalMillis = cal.getTimeInMillis();
242245
Timestamp ts = new Timestamp(1700000000000L);
243246

244247
preparedStatement.setTimestamp(1, ts, cal);
245248
assertEquals(Timestamp.class, preparedStatement.parameterHandler.getType(1));
249+
assertEquals(ts, preparedStatement.parameterHandler.getParameter(1));
246250
assertEquals(originalMillis, cal.getTimeInMillis());
247251
}
248252

‎java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtilityTest.java‎

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -148,31 +148,6 @@ public void testConvertTimeWithCalendar() {
148148
assertThat(resultCal.get(Calendar.SECOND)).isEqualTo(45);
149149
}
150150

151-
@Test
152-
public void testConvertTimestampWithCalendar() {
153-
Timestamp ts = Timestamp.valueOf("2026-08-24 15:30:45.123456789");
154-
Calendar cal = Calendar.getInstance();
155-
156-
// Null checks
157-
assertThat(BigQueryTemporalUtility.convertTimestampWithCalendar(null, cal)).isNull();
158-
assertThat(BigQueryTemporalUtility.convertTimestampWithCalendar(ts, null)).isSameInstanceAs(ts);
159-
160-
// Same timezone fast-path
161-
Calendar sameZoneCal = Calendar.getInstance(TimeZone.getDefault());
162-
assertThat(BigQueryTemporalUtility.convertTimestampWithCalendar(ts, sameZoneCal))
163-
.isSameInstanceAs(ts);
164-
165-
// Different timezone preserves wall-clock shift and nanoseconds
166-
Calendar tokyoCal = Calendar.getInstance(TimeZone.getTimeZone("Asia/Tokyo"));
167-
Timestamp result = BigQueryTemporalUtility.convertTimestampWithCalendar(ts, tokyoCal);
168-
Instant expectedInstant =
169-
LocalDateTime.of(2026, 8, 24, 15, 30, 45, 123456789)
170-
.atZone(ZoneId.of("Asia/Tokyo"))
171-
.toInstant();
172-
assertThat(result.getTime()).isEqualTo(expectedInstant.toEpochMilli());
173-
assertThat(result.getNanos()).isEqualTo(123456789);
174-
}
175-
176151
@Test
177152
public void testBoxDateTimeAndDate() {
178153
Timestamp ts =

‎java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITCallableStatementTest.java‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -444,17 +444,6 @@ public void testSetterGetterTimestamp() throws SQLException {
444444
assertEquals(expected, actual);
445445
}
446446

447-
@Test
448-
public void testSetterGetterTimestampCal() throws SQLException {
449-
CallableStatement callableStatement = this.bigQueryConnection.prepareCall("call testProc('?')");
450-
assertNotNull(callableStatement);
451-
Timestamp expected = new Timestamp(1L);
452-
Calendar cal = Calendar.getInstance();
453-
callableStatement.setTimestamp(CALLABLE_STMT_PARAM_KEY, expected, cal);
454-
Timestamp actual = callableStatement.getTimestamp(CALLABLE_STMT_PARAM_KEY, cal);
455-
assertEquals(expected, actual);
456-
}
457-
458447
// Block B Tests
459448
@Test
460449
public void testCallableStatementScriptExecuteUpdate() throws SQLException {

0 commit comments

Comments
 (0)