Skip to content

Commit f58026e

Browse files
committed
fix(bigquery-jdbc): honor Calendar timezone and prevent ClassCastException in ResultSet temporal getters
1 parent 2106e26 commit f58026e

10 files changed

Lines changed: 388 additions & 27 deletions

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.sql.Timestamp;
3636
import java.time.LocalDate;
3737
import java.time.LocalDateTime;
38+
import java.time.ZoneId;
3839
import java.util.ArrayList;
3940
import java.util.List;
4041
import java.util.concurrent.BlockingQueue;
@@ -369,6 +370,11 @@ private Object getObjectInternal(int columnIndex) throws SQLException {
369370

370371
@Override
371372
public Object getObject(int columnIndex) throws SQLException {
373+
return getObject(columnIndex, (ZoneId) null);
374+
}
375+
376+
@Override
377+
protected Object getObject(int columnIndex, ZoneId zone) throws SQLException {
372378

373379
// columnIndex is SQL index starting at 1
374380
LOG.finestTrace("getObject");
@@ -398,7 +404,7 @@ public Object getObject(int columnIndex) throws SQLException {
398404
if (this.enableTimestampPicos && BigQueryTemporalUtility.isPicosecondTimestamp(arrayField)) {
399405
return BigQueryTemporalUtility.formatTimestampValue(value, true);
400406
}
401-
return BigQueryTypeRegistry.convert(value, arrayField.getType().getStandardType(), null);
407+
return BigQueryTypeRegistry.convert(value, arrayField.getType().getStandardType(), zone);
402408
}
403409

404410
int fieldIndex = this.isNested ? 0 : columnIndex - 1;
@@ -478,7 +484,7 @@ public Object getObject(int columnIndex) throws SQLException {
478484
if (this.enableTimestampPicos && BigQueryTemporalUtility.isPicosecondTimestamp(fieldSchema)) {
479485
return BigQueryTemporalUtility.formatTimestampValue(value, true);
480486
}
481-
return BigQueryTypeRegistry.convert(value, fieldSchema.getType().getStandardType(), null);
487+
return BigQueryTypeRegistry.convert(value, fieldSchema.getType().getStandardType(), zone);
482488
}
483489

484490
private StandardSQLTypeName getRangeElementType(Field field, Object representativeElement) {

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

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ public boolean isClosed() {
296296

297297
public abstract Object getObject(int columnIndex) throws SQLException;
298298

299+
protected Object getObject(int columnIndex, ZoneId zone) throws SQLException {
300+
return getObject(columnIndex);
301+
}
302+
299303
@Override
300304
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
301305
LOG.finestTrace("getObject");
@@ -564,13 +568,19 @@ public InputStream getBinaryStream(int columnIndex) throws SQLException {
564568
return bytes == null ? null : new ByteArrayInputStream(bytes);
565569
}
566570

571+
private static ZoneId getEffectiveZone(StandardSQLTypeName bqType, Calendar cal) {
572+
return (cal == null || bqType == StandardSQLTypeName.TIMESTAMP)
573+
? null
574+
: cal.getTimeZone().toZoneId();
575+
}
576+
567577
@Override
568578
public Date getDate(int columnIndex, Calendar cal) throws SQLException {
569579
LOG.finestTrace("getDate");
580+
StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex);
581+
ZoneId zone = getEffectiveZone(bqType, cal);
570582
try {
571-
Object value = getObject(columnIndex);
572-
StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex);
573-
ZoneId zone = (cal == null) ? null : cal.getTimeZone().toZoneId();
583+
Object value = getObject(columnIndex, zone);
574584
return BigQueryTypeRegistry.convert(value, bqType, Date.class, zone);
575585
} catch (BigQueryJdbcException e) {
576586
throw createCoercionException(columnIndex, Date.class, e);
@@ -580,10 +590,13 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException {
580590
@Override
581591
public Time getTime(int columnIndex, Calendar cal) throws SQLException {
582592
LOG.finestTrace("getTime");
593+
StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex);
594+
if (bqType == StandardSQLTypeName.INT64) {
595+
throw createCoercionException(columnIndex, Time.class, null);
596+
}
597+
ZoneId zone = getEffectiveZone(bqType, cal);
583598
try {
584-
Object value = getObject(columnIndex);
585-
StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex);
586-
ZoneId zone = (cal == null) ? null : cal.getTimeZone().toZoneId();
599+
Object value = getObject(columnIndex, zone);
587600
return BigQueryTypeRegistry.convert(value, bqType, Time.class, zone);
588601
} catch (BigQueryJdbcException e) {
589602
throw createCoercionException(columnIndex, Time.class, e);
@@ -593,10 +606,13 @@ public Time getTime(int columnIndex, Calendar cal) throws SQLException {
593606
@Override
594607
public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException {
595608
LOG.finestTrace("getTimestamp");
609+
StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex);
610+
if (bqType == StandardSQLTypeName.INT64) {
611+
throw createCoercionException(columnIndex, Timestamp.class, null);
612+
}
613+
ZoneId zone = getEffectiveZone(bqType, cal);
596614
try {
597-
Object value = getObject(columnIndex);
598-
StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex);
599-
ZoneId zone = (cal == null) ? null : cal.getTimeZone().toZoneId();
615+
Object value = getObject(columnIndex, zone);
600616
return BigQueryTypeRegistry.convert(value, bqType, Timestamp.class, zone);
601617
} catch (BigQueryJdbcException e) {
602618
throw createCoercionException(columnIndex, Timestamp.class, e);

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
3131
import java.sql.ResultSet;
3232
import java.sql.SQLException;
33+
import java.time.ZoneId;
3334
import java.util.concurrent.BlockingQueue;
3435
import java.util.concurrent.Future;
3536

@@ -250,6 +251,11 @@ public boolean next() throws SQLException {
250251

251252
@Override
252253
public Object getObject(int columnIndex) throws SQLException {
254+
return getObject(columnIndex, (ZoneId) null);
255+
}
256+
257+
@Override
258+
protected Object getObject(int columnIndex, ZoneId zone) throws SQLException {
253259
// columnIndex is SQL index starting at 1
254260
checkClosed();
255261
LOG.finestTrace("getObject");
@@ -277,7 +283,7 @@ public Object getObject(int columnIndex) throws SQLException {
277283
if (this.enableTimestampPicos && isRangeTimestamp(arrayField)) {
278284
return formatRangeTimestamp(value);
279285
}
280-
return BigQueryTypeRegistry.convert(value, arrayField.getType().getStandardType(), null);
286+
return BigQueryTypeRegistry.convert(value, arrayField.getType().getStandardType(), zone);
281287
}
282288

283289
Field fieldSchema = this.schemaFieldList.get(columnIndex - 1);
@@ -298,7 +304,7 @@ public Object getObject(int columnIndex) throws SQLException {
298304
if (this.enableTimestampPicos && BigQueryTemporalUtility.isPicosecondTimestamp(fieldSchema)) {
299305
return BigQueryTemporalUtility.formatTimestampValue(value.getStringValue(), true);
300306
}
301-
return BigQueryTypeRegistry.convert(value, fieldSchema.getType().getStandardType(), null);
307+
return BigQueryTypeRegistry.convert(value, fieldSchema.getType().getStandardType(), zone);
302308
}
303309

304310
static String formatRangeTimestamp(FieldValue value) throws SQLException {

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,46 @@ private BigQueryTemporalUtility() {}
5858
* provided timezone (or JVM default if null).
5959
*/
6060
public static Timestamp boxDateTime(String val, ZoneId zoneId) {
61-
ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault();
6261
String isoString = truncateFractionalSeconds(val.replace(' ', 'T'), 9);
63-
return Timestamp.from(LocalDateTime.parse(isoString).atZone(targetZone).toInstant());
62+
return boxDateTime(LocalDateTime.parse(isoString), zoneId);
63+
}
64+
65+
/**
66+
* Converts a civil {@link LocalDateTime} into a {@link Timestamp} by anchoring it to the provided
67+
* timezone (or JVM default if null).
68+
*/
69+
static Timestamp boxDateTime(LocalDateTime localDateTime, ZoneId zoneId) {
70+
ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault();
71+
return Timestamp.from(localDateTime.atZone(targetZone).toInstant());
6472
}
6573

6674
/**
6775
* Converts a BigQuery civil DATE string into an absolute Date by anchoring it to midnight of the
6876
* provided timezone (or JVM default if null).
6977
*/
7078
public static Date boxDate(String val, ZoneId zoneId) {
79+
return boxDate(LocalDate.parse(val), zoneId);
80+
}
81+
82+
/**
83+
* Converts a civil {@link LocalDate} into a {@link Date} by anchoring it to midnight of the
84+
* provided timezone (or JVM default if null).
85+
*/
86+
static Date boxDate(LocalDate localDate, ZoneId zoneId) {
7187
ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault();
72-
return new Date(LocalDate.parse(val).atStartOfDay(targetZone).toInstant().toEpochMilli());
88+
return new Date(localDate.atStartOfDay(targetZone).toInstant().toEpochMilli());
89+
}
90+
91+
/**
92+
* Converts a civil {@link LocalTime} into a {@link Time} anchored to 1970-01-01 in the provided
93+
* timezone (or JVM default if null).
94+
*/
95+
static Time boxTime(LocalTime localTime, ZoneId zoneId) {
96+
if (zoneId == null) {
97+
return Time.valueOf(localTime);
98+
}
99+
long millisOfDay = localTime.toNanoOfDay() / 1_000_000;
100+
return new Time(getLocalMillis(millisOfDay, zoneId));
73101
}
74102

75103
/**

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

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,17 +253,20 @@ static TypeDescriptor<?> createDateDescriptor() {
253253
if (val instanceof Date) {
254254
sqlDate = (Date) val;
255255
} else if (val instanceof Timestamp) {
256-
sqlDate = Date.valueOf(((Timestamp) val).toLocalDateTime().toLocalDate());
256+
ZoneId targetZone = zone != null ? zone : ZoneId.systemDefault();
257+
LocalDate localDate = ((Timestamp) val).toInstant().atZone(targetZone).toLocalDate();
258+
sqlDate = BigQueryTemporalUtility.boxDate(localDate, zone);
257259
} else if (val instanceof java.sql.Time) {
258260
throw new BigQueryJdbcException("Cannot convert to DATE: " + val);
259261
} else if (val instanceof java.util.Date) {
260262
sqlDate = new Date(((java.util.Date) val).getTime());
261263
} else if (val instanceof LocalDate) {
262-
sqlDate = Date.valueOf((LocalDate) val);
264+
sqlDate = BigQueryTemporalUtility.boxDate((LocalDate) val, zone);
263265
} else if (val instanceof LocalDateTime) {
264-
sqlDate = Date.valueOf(((LocalDateTime) val).toLocalDate());
266+
sqlDate = BigQueryTemporalUtility.boxDate(((LocalDateTime) val).toLocalDate(), zone);
265267
} else if (val instanceof Integer) {
266-
sqlDate = Date.valueOf(LocalDate.ofEpochDay(((Integer) val).longValue()));
268+
LocalDate localDate = LocalDate.ofEpochDay(((Integer) val).longValue());
269+
sqlDate = BigQueryTemporalUtility.boxDate(localDate, zone);
267270
} else if (val instanceof String) {
268271
sqlDate = BigQueryTemporalUtility.boxDate((String) val, zone);
269272
} else {
@@ -303,7 +306,7 @@ static TypeDescriptor<?> createDatetimeDescriptor() {
303306
} else if (val instanceof java.util.Date) {
304307
ts = new Timestamp(((java.util.Date) val).getTime());
305308
} else if (val instanceof LocalDateTime) {
306-
ts = Timestamp.valueOf((LocalDateTime) val);
309+
ts = BigQueryTemporalUtility.boxDateTime((LocalDateTime) val, zone);
307310
} else if (val instanceof String) {
308311
ts = BigQueryTemporalUtility.boxDateTime((String) val, zone);
309312
} else {
@@ -408,13 +411,16 @@ static TypeDescriptor<?> createTimeDescriptor() {
408411
Time sqlTime;
409412
if (val instanceof Time) sqlTime = (Time) val;
410413
else if (val instanceof Timestamp) {
411-
sqlTime = Time.valueOf(((Timestamp) val).toLocalDateTime().toLocalTime());
414+
ZoneId targetZone = zone != null ? zone : ZoneId.systemDefault();
415+
LocalTime localTime = ((Timestamp) val).toInstant().atZone(targetZone).toLocalTime();
416+
sqlTime = BigQueryTemporalUtility.boxTime(localTime, zone);
412417
} else if (val instanceof java.sql.Date) {
413418
throw new BigQueryJdbcException("Cannot convert to TIME: " + val);
414419
} else if (val instanceof java.util.Date)
415420
sqlTime = new Time(((java.util.Date) val).getTime());
416-
else if (val instanceof LocalTime) sqlTime = Time.valueOf((LocalTime) val);
417-
else if (val instanceof LocalDateTime) {
421+
else if (val instanceof LocalTime) {
422+
sqlTime = BigQueryTemporalUtility.boxTime((LocalTime) val, zone);
423+
} else if (val instanceof LocalDateTime) {
418424
long millisOfDay = ((LocalDateTime) val).toLocalTime().toNanoOfDay() / 1_000_000;
419425
sqlTime = new Time(BigQueryTemporalUtility.getLocalMillis(millisOfDay, zone));
420426
} else if (val instanceof Long) {
@@ -772,12 +778,28 @@ public static <T> T convert(
772778
if (input == null) {
773779
return null;
774780
}
781+
if (input instanceof FieldValue) {
782+
FieldValue fv = (FieldValue) input;
783+
if (fv.isNull()) {
784+
return null;
785+
}
786+
input = fv.getValue();
787+
}
775788
int ordinal = bqType.ordinal();
776789
if (ordinal >= DESCRIPTORS_BY_ORDINAL.length || DESCRIPTORS_BY_ORDINAL[ordinal] == null) {
777790
throw new BigQueryJdbcException("No type descriptor registered for BigQuery type: " + bqType);
778791
}
792+
ZoneId effectiveZone = (bqType == StandardSQLTypeName.TIMESTAMP) ? null : zoneId;
779793
try {
780-
return (T) DESCRIPTORS_BY_ORDINAL[ordinal].convert(input, targetClass, zoneId);
794+
Object result = DESCRIPTORS_BY_ORDINAL[ordinal].convert(input, targetClass, effectiveZone);
795+
if (!targetClass.isInstance(result)) {
796+
TypeDescriptor<?> targetDescriptor = getDescriptorForClass(targetClass);
797+
if (targetDescriptor == null) {
798+
throw new BigQueryJdbcException("Unsupported target class: " + targetClass.getName());
799+
}
800+
result = targetDescriptor.convert(result, targetClass, effectiveZone);
801+
}
802+
return (T) result;
781803
} catch (Exception e) {
782804
throw new BigQueryJdbcException(
783805
String.format(

0 commit comments

Comments
 (0)