Skip to content

Commit 041450d

Browse files
Neenu1995mutianf
authored andcommitted
fix(bigquery-jdbc): refine temporal timezone coercion and PreparedStatement parameter setters (googleapis#13813)
b/535194644 * **Centralized Timezone Coercion**: Consolidated `Calendar` timezone conversions in `BigQueryTypeCoercionUtility` using `java.time` APIs. * **Temporal Helper Refactoring**: Extracted `setTemporalObject` helper to eliminate duplicate temporal type logic across `setObject` overloads. * **Defensive Copying & Unsupported Stubs**: Added defensive copying for `Calendar` setters and replaced silent stubs with `BigQueryJdbcSqlFeatureNotSupportedException`. * **Expanded Test Suite**: Added unit and integration tests covering temporal setters and calendar conversion edge cases.
1 parent c545730 commit 041450d

5 files changed

Lines changed: 361 additions & 19 deletions

File tree

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

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
import java.sql.Time;
5858
import java.sql.Timestamp;
5959
import java.sql.Types;
60+
import java.time.Instant;
61+
import java.time.LocalDate;
62+
import java.time.LocalDateTime;
63+
import java.time.LocalTime;
64+
import java.time.OffsetDateTime;
65+
import java.time.ZonedDateTime;
6066
import java.util.ArrayList;
6167
import java.util.Arrays;
6268
import java.util.Calendar;
@@ -70,7 +76,7 @@ class BigQueryPreparedStatement extends BigQueryStatement implements PreparedSta
7076
protected int parameterCount = 0;
7177
protected String currentQuery;
7278
private Queue<ArrayList<BigQueryJdbcParameter>> batchParameters = new LinkedList<>();
73-
private Schema insertSchema = null;
79+
Schema insertSchema = null;
7480
private TableName insertTableName = null;
7581

7682
BigQueryPreparedStatement(BigQueryConnection connection, String query) {
@@ -207,20 +213,20 @@ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
207213
}
208214

209215
@Override
210-
public void setAsciiStream(int parameterIndex, InputStream x, int length) {
211-
// TODO :NOT IMPLEMENTED
216+
public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
217+
throw new BigQueryJdbcSqlFeatureNotSupportedException("setAsciiStream is not supported.");
212218
}
213219

214220
@Override
215221
@Deprecated
216222
@SuppressWarnings("deprecation")
217-
public void setUnicodeStream(int parameterIndex, InputStream x, int length) {
218-
// TODO :NOT IMPLEMENTED
223+
public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {
224+
throw new BigQueryJdbcSqlFeatureNotSupportedException("setUnicodeStream is not supported.");
219225
}
220226

221227
@Override
222-
public void setBinaryStream(int parameterIndex, InputStream x, int length) {
223-
// TODO :NOT IMPLEMENTED
228+
public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
229+
throw new BigQueryJdbcSqlFeatureNotSupportedException("setBinaryStream is not supported.");
224230
}
225231

226232
@Override
@@ -230,6 +236,9 @@ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQ
230236
setNull(parameterIndex, targetSqlType);
231237
return;
232238
}
239+
if (setTemporalObject(parameterIndex, x)) {
240+
return;
241+
}
233242
Class<?> javaType = BigQueryJdbcTypeMappings.getJavaType(targetSqlType);
234243
this.parameterHandler.setParameter(parameterIndex, x, javaType);
235244
}
@@ -241,9 +250,40 @@ public void setObject(int parameterIndex, Object x) throws SQLException {
241250
setNull(parameterIndex, Types.NULL);
242251
return;
243252
}
253+
if (setTemporalObject(parameterIndex, x)) {
254+
return;
255+
}
244256
this.parameterHandler.setParameter(parameterIndex, x, x.getClass());
245257
}
246258

259+
private boolean setTemporalObject(int parameterIndex, Object x) throws SQLException {
260+
if (x instanceof LocalDate) {
261+
setDate(parameterIndex, Date.valueOf((LocalDate) x));
262+
return true;
263+
}
264+
if (x instanceof LocalTime) {
265+
setTime(parameterIndex, Time.valueOf((LocalTime) x));
266+
return true;
267+
}
268+
if (x instanceof LocalDateTime) {
269+
setTimestamp(parameterIndex, Timestamp.valueOf((LocalDateTime) x));
270+
return true;
271+
}
272+
if (x instanceof OffsetDateTime) {
273+
setTimestamp(parameterIndex, Timestamp.from(((OffsetDateTime) x).toInstant()));
274+
return true;
275+
}
276+
if (x instanceof Instant) {
277+
setTimestamp(parameterIndex, Timestamp.from((Instant) x));
278+
return true;
279+
}
280+
if (x instanceof ZonedDateTime) {
281+
setTimestamp(parameterIndex, Timestamp.from(((ZonedDateTime) x).toInstant()));
282+
return true;
283+
}
284+
return false;
285+
}
286+
247287
@Override
248288
public void addBatch() {
249289
ArrayList<BigQueryJdbcParameter> currentParameterList =
@@ -470,24 +510,42 @@ public void setArray(int parameterIndex, Array x) throws SQLException {
470510
}
471511

472512
@Override
473-
public ResultSetMetaData getMetaData() {
474-
// TODO(neenu) :IMPLEMENT metadata
513+
public ResultSetMetaData getMetaData() throws SQLException {
514+
checkClosed();
515+
if (this.insertSchema != null) {
516+
return BigQueryResultSetMetadata.of(this.insertSchema.getFields(), this);
517+
}
475518
return null;
476519
}
477520

478521
@Override
479-
public void setDate(int parameterIndex, Date x, Calendar cal) {
480-
// TODO :NOT IMPLEMENTED
522+
public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
523+
checkClosed();
524+
if (x == null) {
525+
setNull(parameterIndex, Types.DATE);
526+
return;
527+
}
528+
setDate(parameterIndex, BigQueryTypeCoercionUtility.convertDateWithCalendar(x, cal));
481529
}
482530

483531
@Override
484-
public void setTime(int parameterIndex, Time x, Calendar cal) {
485-
// TODO :NOT IMPLEMENTED
532+
public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
533+
checkClosed();
534+
if (x == null) {
535+
setNull(parameterIndex, Types.TIME);
536+
return;
537+
}
538+
setTime(parameterIndex, BigQueryTypeCoercionUtility.convertTimeWithCalendar(x, cal));
486539
}
487540

488541
@Override
489-
public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) {
490-
// TODO :NOT IMPLEMENTED
542+
public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
543+
checkClosed();
544+
if (x == null) {
545+
setNull(parameterIndex, Types.TIMESTAMP);
546+
return;
547+
}
548+
setTimestamp(parameterIndex, BigQueryTypeCoercionUtility.convertTimestampWithCalendar(x, cal));
491549
}
492550

493551
@Override
@@ -501,9 +559,9 @@ public void setURL(int parameterIndex, URL x) throws SQLException {
501559
}
502560

503561
@Override
504-
public ParameterMetaData getParameterMetaData() {
505-
// TODO(neenu) :IMPLEMENT
506-
return null;
562+
public ParameterMetaData getParameterMetaData() throws SQLException {
563+
checkClosed();
564+
return new BigQueryParameterMetaData(this.parameterCount, this.parameterHandler);
507565
}
508566

509567
@Override

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercionUtility.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@
3131
import java.time.LocalTime;
3232
import java.time.OffsetDateTime;
3333
import java.time.Period;
34+
import java.time.ZoneId;
3435
import java.time.ZoneOffset;
36+
import java.time.ZonedDateTime;
3537
import java.time.format.DateTimeFormatter;
3638
import java.time.temporal.ChronoUnit;
39+
import java.util.Calendar;
3740
import java.util.concurrent.TimeUnit;
3841
import org.apache.arrow.vector.PeriodDuration;
3942
import org.apache.arrow.vector.util.Text;
@@ -43,6 +46,98 @@ class BigQueryTypeCoercionUtility {
4346
private static final BigQueryJdbcCustomLogger LOG =
4447
new BigQueryJdbcCustomLogger(BigQueryTypeCoercionUtility.class.getName());
4548

49+
/** Returns a defensively cloned Calendar instance or a new default Calendar if input is null. */
50+
static Calendar getSafeCalendar(Calendar cal) {
51+
if (cal == null) {
52+
return Calendar.getInstance();
53+
}
54+
Object cloned = cal.clone();
55+
if (cloned instanceof Calendar) {
56+
return (Calendar) cloned;
57+
}
58+
Calendar safeCal = Calendar.getInstance();
59+
if (cal.getTimeZone() != null) {
60+
safeCal.setTimeZone(cal.getTimeZone());
61+
}
62+
return safeCal;
63+
}
64+
65+
/**
66+
* Converts a java.sql.Date by shifting its wall-clock year, month, and day fields into the target
67+
* Calendar's timezone per JDBC specification.
68+
*/
69+
static Date convertDateWithCalendar(Date date, Calendar cal) {
70+
if (date == null || cal == null) {
71+
return date;
72+
}
73+
ZoneId systemZone = ZoneId.systemDefault();
74+
ZoneId targetZone = cal.getTimeZone().toZoneId();
75+
if (systemZone.equals(targetZone)) {
76+
return date;
77+
}
78+
LocalDate localDate = date.toLocalDate();
79+
ZonedDateTime zdt = localDate.atStartOfDay(targetZone);
80+
return new Date(zdt.toInstant().toEpochMilli());
81+
}
82+
83+
static Date convertDateToCalendar(Date date, Calendar cal) {
84+
if (date == null || cal == null) {
85+
return date;
86+
}
87+
ZoneId systemZone = ZoneId.systemDefault();
88+
ZoneId targetZone = cal.getTimeZone().toZoneId();
89+
if (systemZone.equals(targetZone)) {
90+
return date;
91+
}
92+
LocalDate localDate = Instant.ofEpochMilli(date.getTime()).atZone(targetZone).toLocalDate();
93+
ZonedDateTime zdt = localDate.atStartOfDay(systemZone);
94+
return new Date(zdt.toInstant().toEpochMilli());
95+
}
96+
97+
/**
98+
* Converts a java.sql.Time by shifting its wall-clock hour, minute, second, and millisecond
99+
* fields into the target Calendar's timezone per JDBC specification.
100+
*/
101+
static Time convertTimeWithCalendar(Time time, Calendar cal) {
102+
if (time == null || cal == null) {
103+
return time;
104+
}
105+
ZoneId systemZone = ZoneId.systemDefault();
106+
ZoneId targetZone = cal.getTimeZone().toZoneId();
107+
if (systemZone.equals(targetZone)) {
108+
return time;
109+
}
110+
Calendar defaultCal = Calendar.getInstance();
111+
defaultCal.setTime(time);
112+
113+
Calendar targetCal = getSafeCalendar(cal);
114+
targetCal.set(Calendar.HOUR_OF_DAY, defaultCal.get(Calendar.HOUR_OF_DAY));
115+
targetCal.set(Calendar.MINUTE, defaultCal.get(Calendar.MINUTE));
116+
targetCal.set(Calendar.SECOND, defaultCal.get(Calendar.SECOND));
117+
targetCal.set(Calendar.MILLISECOND, defaultCal.get(Calendar.MILLISECOND));
118+
return new Time(targetCal.getTimeInMillis());
119+
}
120+
121+
/**
122+
* Converts a java.sql.Timestamp by shifting its wall-clock fields into the target Calendar's
123+
* timezone per JDBC specification while preserving nanosecond precision.
124+
*/
125+
static Timestamp convertTimestampWithCalendar(Timestamp timestamp, Calendar cal) {
126+
if (timestamp == null || cal == null) {
127+
return timestamp;
128+
}
129+
ZoneId systemZone = ZoneId.systemDefault();
130+
ZoneId targetZone = cal.getTimeZone().toZoneId();
131+
if (systemZone.equals(targetZone)) {
132+
return timestamp;
133+
}
134+
LocalDateTime ldt = timestamp.toLocalDateTime();
135+
ZonedDateTime zdt = ldt.atZone(targetZone);
136+
Timestamp adjustedTimestamp = Timestamp.from(zdt.toInstant());
137+
adjustedTimestamp.setNanos(timestamp.getNanos());
138+
return adjustedTimestamp;
139+
}
140+
46141
static BigQueryTypeCoercer INSTANCE;
47142

48143
static {

0 commit comments

Comments
 (0)