Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ public void setTime(String parameterName, Time value, Calendar calendar) throws
checkClosed();
this.parameterHandler.setParameter(
parameterName,
value == null ? null : BigQueryTemporalUtility.convertTimeWithCalendar(value, calendar),
value == null ? null : BigQueryTemporalUtility.convertTimeToCalendar(value, calendar),
Time.class,
BigQueryStatementParameterType.IN,
0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ public void setDate(int parameterIndex, Date value, Calendar calendar) throws SQ
@Override
public void setTime(int parameterIndex, Time value, Calendar calendar) throws SQLException {
checkClosed();
setTime(parameterIndex, BigQueryTemporalUtility.convertTimeWithCalendar(value, calendar));
setTime(parameterIndex, BigQueryTemporalUtility.convertTimeToCalendar(value, calendar));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,12 @@ public static Time boxTime(String val, ZoneId zoneId) {
// Legacy JDBC 3.0 API (Calendar provided):
// Use legacy Calendar manipulation to intentionally replicate old JVM historical DST quirks
// for January 1, 1970, ensuring strict backwards compatibility for legacy ORMs.
Calendar targetCal = Calendar.getInstance(TimeZone.getTimeZone(zoneId));
targetCal.set(Calendar.YEAR, 1970);
targetCal.set(Calendar.MONTH, Calendar.JANUARY);
targetCal.set(Calendar.DAY_OF_MONTH, 1);
targetCal.set(Calendar.HOUR_OF_DAY, localTime.getHour());
targetCal.set(Calendar.MINUTE, localTime.getMinute());
targetCal.set(Calendar.SECOND, localTime.getSecond());
targetCal.set(Calendar.MILLISECOND, localTime.getNano() / 1_000_000);

return new Time(targetCal.getTimeInMillis());
return createTimeInCalendar(
Calendar.getInstance(TimeZone.getTimeZone(zoneId)),
localTime.getHour(),
localTime.getMinute(),
localTime.getSecond(),
localTime.getNano() / 1_000_000);
}

/**
Expand Down Expand Up @@ -488,22 +484,55 @@ static Time convertTimeWithCalendar(Time time, Calendar cal) {
if (time == null || cal == null) {
return time;
}
ZoneId systemZone = ZoneId.systemDefault();
ZoneId targetZone = cal.getTimeZone().toZoneId();
TimeZone systemZone = TimeZone.getDefault();
TimeZone targetZone = cal.getTimeZone();
if (systemZone.equals(targetZone)) {
return time;
}
Calendar defaultCal = Calendar.getInstance();
defaultCal.setTime(time);

Calendar targetCal = getSafeCalendar(cal);
targetCal.set(Calendar.YEAR, 1970);
targetCal.set(Calendar.MONTH, Calendar.JANUARY);
targetCal.set(Calendar.DAY_OF_MONTH, 1);
targetCal.set(Calendar.HOUR_OF_DAY, defaultCal.get(Calendar.HOUR_OF_DAY));
targetCal.set(Calendar.MINUTE, defaultCal.get(Calendar.MINUTE));
targetCal.set(Calendar.SECOND, defaultCal.get(Calendar.SECOND));
targetCal.set(Calendar.MILLISECOND, defaultCal.get(Calendar.MILLISECOND));
return new Time(targetCal.getTimeInMillis());
return shiftTimeBetweenCalendars(time, Calendar.getInstance(), getSafeCalendar(cal));
}

/**
* Converts a {@link Time} for writing/inbound parameter setting operations (e.g., {@code
* setTime(..., Calendar)}) by extracting its local time fields as interpreted in the target
* {@link Calendar}'s timezone and normalizing them back into 1970-01-01 in the system-default
* timezone.
*
* @param time the time instant specified relative to the target Calendar
* @param cal the Calendar containing the source timezone
* @return the normalized Time on 1970-01-01 in the system-default timezone
*/
static Time convertTimeToCalendar(Time time, Calendar cal) {
if (time == null || cal == null) {
return time;
}
TimeZone systemZone = TimeZone.getDefault();
TimeZone targetZone = cal.getTimeZone();
if (systemZone.equals(targetZone)) {
return time;
}
return shiftTimeBetweenCalendars(time, getSafeCalendar(cal), Calendar.getInstance());
}

private static Time shiftTimeBetweenCalendars(Time time, Calendar sourceCal, Calendar destCal) {
sourceCal.setTime(time);
return createTimeInCalendar(
destCal,
sourceCal.get(Calendar.HOUR_OF_DAY),
sourceCal.get(Calendar.MINUTE),
sourceCal.get(Calendar.SECOND),
sourceCal.get(Calendar.MILLISECOND));
}

private static Time createTimeInCalendar(
Calendar destCal, int hour, int minute, int second, int millisecond) {
destCal.set(Calendar.YEAR, 1970);
destCal.set(Calendar.MONTH, Calendar.JANUARY);
destCal.set(Calendar.DAY_OF_MONTH, 1);
destCal.set(Calendar.HOUR_OF_DAY, hour);
destCal.set(Calendar.MINUTE, minute);
destCal.set(Calendar.SECOND, second);
destCal.set(Calendar.MILLISECOND, millisecond);
return new Time(destCal.getTimeInMillis());
}
Comment thread
keshavdandeva marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -1029,19 +1029,22 @@ public void testSetTimeParamByName() throws SQLException {
assertEquals(expected, actual);
}

@Test
public void testSetTimeCalParamByName() throws SQLException {
@ParameterizedTest
@ValueSource(strings = {"UTC", "America/New_York", "Asia/Tokyo"})
public void testSetTimeCalParamByName(String zoneId) throws SQLException {
BigQueryCallableStatement statement =
new BigQueryCallableStatement(bigQueryConnection, "call testProc('?')");
assertNotNull(statement);
Time expectedTime = Time.valueOf("12:34:56");
Calendar expectedCal = Calendar.getInstance();
Calendar expectedCal = Calendar.getInstance(TimeZone.getTimeZone(zoneId));
expectedCal.clear();
expectedCal.set(1970, Calendar.JANUARY, 1, 12, 34, 56);
Time expectedTime = new Time(expectedCal.getTimeInMillis());

statement.setTime(PARAM_KEY, expectedTime, expectedCal);
Time actual = statement.getTime(PARAM_KEY);
assertNotNull(actual);
assertEquals("12:34:56", actual.toString());
actual = statement.getTime(PARAM_KEY, expectedCal);
assertNotNull(actual);
assertEquals(expectedTime, actual);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,15 +226,20 @@ public void testSetDateWithCalendar() throws Exception {
assertEquals(originalMillis, cal.getTimeInMillis());
}

@Test
public void testSetTimeWithCalendar() throws Exception {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
@ParameterizedTest
@ValueSource(strings = {"UTC", "America/New_York", "Asia/Tokyo"})
public void testSetTimeWithCalendar(String zoneId) throws Exception {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(zoneId));
cal.setTimeInMillis(1000000L);
long originalMillis = cal.getTimeInMillis();
Time time = new Time(43200000L); // 12:00:00
Calendar sourceCal = Calendar.getInstance(TimeZone.getTimeZone(zoneId));
sourceCal.clear();
sourceCal.set(1970, Calendar.JANUARY, 1, 12, 34, 56);
Time time = new Time(sourceCal.getTimeInMillis());

preparedStatement.setTime(1, time, cal);
assertEquals(Time.class, preparedStatement.parameterHandler.getType(1));
assertEquals("12:34:56", preparedStatement.parameterHandler.getParameter(1).toString());
assertEquals(originalMillis, cal.getTimeInMillis());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,65 @@ public void testConvertTimeWithCalendar() {
assertThat(resultCal.get(Calendar.SECOND)).isEqualTo(45);
}

@Test
public void testConvertTimeToCalendar() {
Time time = Time.valueOf("15:30:45");
Calendar cal = Calendar.getInstance();

// Null checks
assertThat(BigQueryTemporalUtility.convertTimeToCalendar(null, cal)).isNull();
assertThat(BigQueryTemporalUtility.convertTimeToCalendar(time, null)).isSameInstanceAs(time);

// Same timezone fast-path
Calendar sameZoneCal = Calendar.getInstance(TimeZone.getDefault());
assertThat(BigQueryTemporalUtility.convertTimeToCalendar(time, sameZoneCal))
.isSameInstanceAs(time);

// Different timezone
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
Calendar tokyoCal = Calendar.getInstance(TimeZone.getTimeZone(tokyoZone));
long tokyoTimeMillis =
LocalTime.of(15, 30, 45)
.atDate(LocalDate.of(1970, 1, 1))
.atZone(tokyoZone)
.toInstant()
.toEpochMilli();
Time result =
BigQueryTemporalUtility.convertTimeToCalendar(new Time(tokyoTimeMillis), tokyoCal);
long expectedSystemMillis =
LocalTime.of(15, 30, 45)
.atDate(LocalDate.of(1970, 1, 1))
.atZone(ZoneId.systemDefault())
.toInstant()
.toEpochMilli();
assertThat(result.getTime()).isEqualTo(expectedSystemMillis);
assertThat(result.toString()).isEqualTo("15:30:45");
}

@Test
public void testCrossTimezoneTimeRoundtrip() {
ZoneId writeZone = ZoneId.of("America/Los_Angeles");
ZoneId readZone = ZoneId.of("Asia/Tokyo");
Calendar writeCal = Calendar.getInstance(TimeZone.getTimeZone(writeZone));
Calendar readCal = Calendar.getInstance(TimeZone.getTimeZone(readZone));

Time inputTime =
new Time(
LocalTime.of(23, 15, 30)
.atDate(LocalDate.of(1970, 1, 1))
.atZone(writeZone)
.toInstant()
.toEpochMilli());
Time normalizedTime = BigQueryTemporalUtility.convertTimeToCalendar(inputTime, writeCal);
Time readTime = BigQueryTemporalUtility.convertTimeWithCalendar(normalizedTime, readCal);

Calendar verifyCal = Calendar.getInstance(TimeZone.getTimeZone(readZone));
verifyCal.setTime(readTime);
assertThat(verifyCal.get(Calendar.HOUR_OF_DAY)).isEqualTo(23);
assertThat(verifyCal.get(Calendar.MINUTE)).isEqualTo(15);
assertThat(verifyCal.get(Calendar.SECOND)).isEqualTo(30);
}

@Test
public void testBoxDateTimeAndDate() {
Timestamp ts =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.cloud.bigquery.jdbc.it;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand Down Expand Up @@ -57,6 +58,7 @@
import java.time.Instant;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Properties;
Expand Down Expand Up @@ -1739,12 +1741,16 @@ public void testPreparedStatementDateTimeValues() throws SQLException {

bigQueryStatement.execute(String.format(createTableQuery, DATASET, TABLE_NAME1));

Instant moment = Instant.parse("2025-12-03T12:34:56.123Z");
Time noon = Time.valueOf(LocalTime.NOON);
Date date = Date.valueOf("2025-12-03");

PreparedStatement insertPs = bigQueryConnection.prepareStatement(insertQuery);
insertPs.setString(1, "dishwasher");
insertPs.setInt(2, 1);
insertPs.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
insertPs.setTime(4, Time.valueOf(LocalTime.NOON));
insertPs.setDate(5, Date.valueOf("2025-12-3"));
insertPs.setTimestamp(3, Timestamp.from(moment));
insertPs.setTime(4, noon);
insertPs.setDate(5, date);

int insertStatus = insertPs.executeUpdate();
assertEquals(1, insertStatus);
Expand All @@ -1753,22 +1759,65 @@ public void testPreparedStatementDateTimeValues() throws SQLException {
Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
insertPs.setString(1, "refrigerator");
insertPs.setInt(2, 2);
insertPs.setTimestamp(3, new Timestamp(System.currentTimeMillis()), utcCal);
insertPs.setTime(4, Time.valueOf(LocalTime.NOON), utcCal);
insertPs.setDate(5, Date.valueOf("2025-12-03"), utcCal);
insertPs.setTimestamp(3, Timestamp.from(moment), utcCal);
insertPs.setTime(4, noon, utcCal);
insertPs.setDate(5, date, utcCal);

int insertStatus2 = insertPs.executeUpdate();
assertEquals(1, insertStatus2);

// Read the stored values server-side so no driver temporal conversion is involved.
ResultSet rs =
bigQueryStatement.executeQuery(
String.format("SELECT COUNT(*) AS row_count\n" + "FROM %s.%s", DATASET, TABLE_NAME1));
rs.next();
String.format(
"SELECT IntegerField, UNIX_MILLIS(TimestampField), CAST(TimeField AS STRING),"
+ " CAST(DateField AS STRING) FROM %s.%s ORDER BY IntegerField",
DATASET, TABLE_NAME1));

assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
long plainTimestamp = rs.getLong(2);
String plainTime = rs.getString(3);
String plainDate = rs.getString(4);
assertTrue(rs.next());
assertEquals(2, rs.getInt(1));
long calTimestamp = rs.getLong(2);
String calTime = rs.getString(3);
String calDate = rs.getString(4);
assertFalse(rs.next());

// Drop before asserting so a failure does not leak the table.
String dropQuery = String.format("DROP TABLE %s.%s", DATASET, TABLE_NAME1);
int dropStatus = bigQueryStatement.executeUpdate(dropQuery);
assertEquals(0, dropStatus);

assertAll(
// Row 1: plain setters.
() ->
assertEquals(
moment.toEpochMilli(), plainTimestamp, "setTimestamp must store the moment"),
() -> assertEquals("12:00:00", plainTime, "setTime must send its digits"),
() -> assertEquals("2025-12-03", plainDate, "setDate must send its digits"),
// Row 2: Calendar setters. The Calendar cannot change a TIMESTAMP moment; for TIME and DATE
// it is the zone the value is rendered in.
() ->
assertEquals(
moment.toEpochMilli(), calTimestamp, "setTimestamp(ts, cal) must store the moment"),
() ->
assertEquals(
Instant.ofEpochMilli(noon.getTime())
.atZone(ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("HH:mm:ss")),
calTime,
"setTime(t, cal) must send the time of t in the Calendar's zone"),
() ->
assertEquals(
Instant.ofEpochMilli(date.getTime())
.atZone(ZoneOffset.UTC)
.toLocalDate()
.toString(),
calDate,
"setDate(d, cal) must send the date of d in the Calendar's zone"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ public void testSetObjectZonedDateTime_storesSameMoment(String zone) throws SQLE
*/
@ParameterizedTest(name = "JVM zone {0}")
@MethodSource("jvmZones")
@Disabled
public void testSetTimeWithCalendar_sendsTimeInCalendarZone(String zone) throws SQLException {
useJvmZone(zone);
Time time = Time.valueOf("12:34:56");
Expand Down
Loading