Skip to content
Open
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
1 change: 1 addition & 0 deletions java-bigquery/google-cloud-bigquery-jdbc/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ drivers/**
target-it/**
*logs*/**
**/ITBigQueryJDBCLocalTest.java
**/BigQueryStatementE2EBenchmark.java

tools/**/*.class
tools/**/*.jfr
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ public double getDouble(int columnIndex) throws SQLException {
}

@Override
@Deprecated
@SuppressWarnings("deprecation")
public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
LOG.finest("++enter++");
try {
Expand Down Expand Up @@ -470,6 +472,8 @@ public InputStream getAsciiStream(int columnIndex) throws SQLException {
}

@Override
@Deprecated
@SuppressWarnings("deprecation")
public InputStream getUnicodeStream(int columnIndex) throws SQLException {
LOG.finest("++enter++");
return getInputStream(getString(columnIndex), StandardCharsets.UTF_16LE);
Expand Down Expand Up @@ -567,6 +571,8 @@ public double getDouble(String columnLabel) throws SQLException {
}

@Override
@Deprecated
@SuppressWarnings("deprecation")
public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException {
return getBigDecimal(getColumnIndex(columnLabel), scale);
}
Expand Down Expand Up @@ -597,6 +603,8 @@ public InputStream getAsciiStream(String columnLabel) throws SQLException {
}

@Override
@Deprecated
@SuppressWarnings("deprecation")
public InputStream getUnicodeStream(String columnLabel) throws SQLException {
return getUnicodeStream(getColumnIndex(columnLabel));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ public BigDecimal getBigDecimal(String arg0) throws SQLException {
}

@Override
@Deprecated
@SuppressWarnings("deprecation")
public BigDecimal getBigDecimal(int arg0, int arg1) throws SQLException {
LOG.finest("++enter++");
return getBigDecimal(arg0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ public void setAsciiStream(int parameterIndex, InputStream x, int length) {
}

@Override
@Deprecated
@SuppressWarnings("deprecation")
public void setUnicodeStream(int parameterIndex, InputStream x, int length) {
// TODO :NOT IMPLEMENTED
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -198,14 +197,12 @@ private static class LongToTime implements BigQueryCoercion<Long, Time> {

@Override
public Time coerce(Long value) {

int HH = (int) TimeUnit.MICROSECONDS.toHours(value);
int MM = (int) (TimeUnit.MICROSECONDS.toMinutes(value) % 60);
int SS = (int) (TimeUnit.MICROSECONDS.toSeconds(value) % 60);

// Note: BQ Time has a precision of up to six fractional digits (microsecond precision)
// but java.sql.Time do not. So data after seconds is not returned.
return new Time(HH, MM, SS);
// but java.sql.Time only supports up to millisecond precision. So data after milliseconds is
// truncated.
long millisOfDay = value / 1000;
long localMillis = TimeZoneCache.getLocalMillis(millisOfDay);
return new Time(localMillis);
}
}

Expand All @@ -214,10 +211,9 @@ private static class LongToTimestamp implements BigQueryCoercion<Long, Timestamp
@Override
public Timestamp coerce(Long value) {
// Long value is in microseconds. All further calculations should account for the unit.
Instant instant = Instant.ofEpochMilli(value / 1000).plusNanos((value % 1000) * 1000);
// JDBC is defaulting to UTC because BQ UI defaults to UTC.
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
return Timestamp.valueOf(localDateTime);
Instant instant = Instant.EPOCH.plus(value, ChronoUnit.MICROS);
// Timezone-agnostic conversion preserving exact point in time as mandated by JDBC spec
return Timestamp.from(instant);
}
}

Expand Down Expand Up @@ -255,8 +251,11 @@ public Time coerce(FieldValue fieldValue) {
LocalTime localTime = LocalTime.parse(strTime);
// Convert LocalTime to milliseconds of the day. This correctly preserves millisecond
// precision and truncates anything smaller
long millis = TimeUnit.NANOSECONDS.toMillis(localTime.toNanoOfDay());
return new Time(millis);
long millisOfDay = TimeUnit.NANOSECONDS.toMillis(localTime.toNanoOfDay());
// Adjust by local timezone offset to ensure correct wall-clock representation with
// millisecond precision
long localMillis = TimeZoneCache.getLocalMillis(millisOfDay);
return new Time(localMillis);
} catch (java.time.format.DateTimeParseException e) {
IllegalArgumentException ex =
new IllegalArgumentException(
Expand All @@ -282,9 +281,8 @@ public Timestamp coerce(FieldValue fieldValue) {
// It's a TIMESTAMP numeric string.
long microseconds = fieldValue.getTimestampValue();
Instant instant = Instant.EPOCH.plus(microseconds, ChronoUnit.MICROS);
// JDBC is defaulting to UTC because BQ UI defaults to UTC.
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
return Timestamp.valueOf(localDateTime);
// Timezone-agnostic conversion preserving exact point in time as mandated by JDBC spec
return Timestamp.from(instant);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.bigquery.jdbc;

import com.google.api.core.InternalApi;
import java.util.TimeZone;

@InternalApi
public class TimeZoneCache {
private static volatile TimeZone defaultTimeZone = TimeZone.getDefault();

public static void reset() {
defaultTimeZone = TimeZone.getDefault();
}

public static long getLocalMillis(long millisOfDay) {
return millisOfDay - defaultTimeZone.getOffset(millisOfDay);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,28 @@ public void nullToTime() {

@Test
public void longToTime() {
assertThat(INSTANCE.coerceTo(Time.class, 1408452095220000L))
.isEqualTo(new Time(1408452095000L));
long value = 1408452095220000L;
// 1408452095220000 microseconds is 1408452095220 milliseconds.
// Since the test runs under UTC timezone by TimeZoneRule, expected localMillis is
// 1408452095220L.
assertThat(INSTANCE.coerceTo(Time.class, value)).isEqualTo(new Time(1408452095220L));
}

@Test
public void longToTimeInNonUTCTimeZone() {
java.util.TimeZone originalTimeZone = java.util.TimeZone.getDefault();
try {
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("America/Los_Angeles"));
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
long value = 1408452095220000L;
// 1408452095220000 microseconds is 1408452095220 milliseconds.
// Under America/Los_Angeles (PDT, -7 hours offset in Aug 2014), the subtracted offset
// results in 1408452095220 - (-25200000) = 1408477295220L.
assertThat(INSTANCE.coerceTo(Time.class, value)).isEqualTo(new Time(1408477295220L));
} finally {
java.util.TimeZone.setDefault(originalTimeZone);
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public static Collection<Object[]> data() {
timeZoneRule.enforce();
LocalDateTime aTimeStamp = LocalDateTime.of(2023, MARCH, 30, 11, 14, 19, 820227000);
LocalDate aDate = LocalDate.of(2023, MARCH, 30);
LocalTime aTime = LocalTime.of(11, 14, 19, 820227);
LocalTime aTime = LocalTime.of(11, 14, 19, 820227000);
return Arrays.asList(
new Object[][] {
{
Expand Down Expand Up @@ -176,10 +176,16 @@ STRING, new Text("one"), new Text("two"), new Text("three"), new Text("four")),
Long.valueOf("40461820227"),
Long.valueOf("40462820227")),
new Time[] {
Time.valueOf(aTime),
Time.valueOf(aTime.plusSeconds(1)),
Time.valueOf(aTime.plusSeconds(2)),
Time.valueOf(aTime.plusSeconds(3))
new Time(java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(aTime.toNanoOfDay())),
new Time(
java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(
aTime.plusSeconds(1).toNanoOfDay())),
new Time(
java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(
aTime.plusSeconds(2).toNanoOfDay())),
new Time(
java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(
aTime.plusSeconds(3).toNanoOfDay()))
},
Types.TIME
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ public void structOfPrimitives() throws SQLException {
"one",
Timestamp.valueOf(LocalDateTime.of(2023, MARCH, 30, 11, 14, 19, 820227000)),
Date.valueOf(LocalDate.of(2023, MARCH, 30)),
Time.valueOf(LocalTime.of(11, 14, 19, 820227)),
new Time(
java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(
LocalTime.of(11, 14, 19, 820227000).toNanoOfDay())),
Timestamp.valueOf("2023-03-30 11:14:19.820227"),
"POINT(-122 47)",
"one".getBytes())
Expand All @@ -117,7 +119,7 @@ public void structOfPrimitives() throws SQLException {
public void structOfArrays() throws SQLException {
LocalDateTime aTimeStamp = LocalDateTime.of(2023, MARCH, 30, 11, 14, 19, 820227000);
LocalDate aDate = LocalDate.of(2023, MARCH, 30);
LocalTime aTime = LocalTime.of(11, 14, 19, 820227);
LocalTime aTime = LocalTime.of(11, 14, 19, 820227000);
List<Tuple<Field, JsonStringArrayList<Object>>> schemaAndValues =
Arrays.asList(
arrowArraySchemaAndValue(INT64, 10L, 20L),
Expand Down Expand Up @@ -165,7 +167,13 @@ GEOGRAPHY, new Text("POINT(-122 47)"), new Text("POINT(-122 48)")),
assertThat(((Array) attributes[7]).getArray())
.isEqualTo(new Date[] {Date.valueOf(aDate), Date.valueOf(aDate.plusDays(1))});
assertThat(((Array) attributes[8]).getArray())
.isEqualTo(new Time[] {Time.valueOf(aTime), Time.valueOf(aTime.plusSeconds(1))});
.isEqualTo(
new Time[] {
new Time(java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(aTime.toNanoOfDay())),
new Time(
java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(
aTime.plusSeconds(1).toNanoOfDay()))
});
assertThat(((Array) attributes[9]).getArray()) // DATETIME
.isEqualTo(
new Timestamp[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,22 @@
import com.google.cloud.bigquery.FieldValueList;
import com.google.cloud.bigquery.Range;
import com.google.cloud.bigquery.exception.BigQueryJdbcCoercionException;
import com.google.cloud.bigquery.jdbc.rules.TimeZoneRule;
import com.google.common.collect.ImmutableList;
import java.math.BigDecimal;
import java.sql.Date;
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.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class FieldValueTypeBigQueryCoercionUtilityTest {
@RegisterExtension public final TimeZoneRule timeZoneRule = new TimeZoneRule("UTC");

private static final FieldValue STRING_VALUE = FieldValue.of(PRIMITIVE, "sample-string");
private static final FieldValue INTEGER_VALUE = FieldValue.of(PRIMITIVE, "345");
private static final FieldValue FLOAT_VALUE = FieldValue.of(PRIMITIVE, "345.21");
Expand Down Expand Up @@ -299,9 +300,8 @@ public void fieldValueToBytesArrayWhenInnerValueIsNull() {
@Test
public void fieldValueToTimestamp() {
Instant instant = Instant.EPOCH.plus(TIMESTAMP_VALUE.getTimestampValue(), ChronoUnit.MICROS);
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
assertThat(INSTANCE.coerceTo(Timestamp.class, TIMESTAMP_VALUE))
.isEqualTo(Timestamp.valueOf(localDateTime));
.isEqualTo(Timestamp.from(instant));
}

@Test
Expand All @@ -317,11 +317,28 @@ public void fieldValueToTimestampWhenInnerValueIsNull() {
@Test
public void fieldValueToTime() {
LocalTime expectedTime = LocalTime.of(23, 59, 59);
assertThat(INSTANCE.coerceTo(Time.class, TIME_VALUE))
.isEqualTo(new Time(TimeUnit.NANOSECONDS.toMillis(expectedTime.toNanoOfDay())));
LocalTime expectedTimeWithNanos = LocalTime.parse("23:59:59.99999");
assertThat(INSTANCE.coerceTo(Time.class, TIME_VALUE)).isEqualTo(Time.valueOf(expectedTime));
// expectedTimeWithNanos has 999 milliseconds, giving 86399999 ms of day.
// Since the test runs under UTC timezone by TimeZoneRule, expected localMillis is 86399999L.
assertThat(INSTANCE.coerceTo(Time.class, TIME_WITH_NANOSECOND_VALUE))
.isEqualTo(new Time(TimeUnit.NANOSECONDS.toMillis(expectedTimeWithNanos.toNanoOfDay())));
.isEqualTo(new Time(86399999L));
}

@Test
public void fieldValueToTimeInNonUTCTimeZone() {
java.util.TimeZone originalTimeZone = java.util.TimeZone.getDefault();
try {
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("America/Los_Angeles"));
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
// 23:59:59.99999 yields 86399999 milliseconds.
// Under America/Los_Angeles on 1970-01-01 (PST, -8 hours offset),
// the subtracted offset results in 86399999 - (-28800000) = 115199999L.
assertThat(INSTANCE.coerceTo(Time.class, TIME_WITH_NANOSECOND_VALUE))
.isEqualTo(new Time(115199999L));
} finally {
java.util.TimeZone.setDefault(originalTimeZone);
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ public TimeZoneRule(String timeZoneId) {
public void beforeAll(ExtensionContext context) {
defaultTimeZone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId));
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
}

@Override
public void afterAll(ExtensionContext context) {
TimeZone.setDefault(defaultTimeZone);
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
}

@Override
Expand All @@ -50,17 +52,20 @@ public void beforeEach(ExtensionContext context) {
defaultTimeZone = TimeZone.getDefault();
}
TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId));
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
}

@Override
public void afterEach(ExtensionContext context) {
if (defaultTimeZone != null) {
TimeZone.setDefault(defaultTimeZone);
}
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
}

/** Public method to enforce the rule manually */
public void enforce() {
TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId));
com.google.cloud.bigquery.jdbc.TimeZoneCache.reset();
}
}
Loading