builder()
- .put(StandardSQLTypeName.INT64, new ColumnTypeInfo(Types.BIGINT, "INT64", 19, 0, 10))
- .put(StandardSQLTypeName.BOOL, new ColumnTypeInfo(Types.BOOLEAN, "BOOL", 1, null, null))
- .put(
- StandardSQLTypeName.FLOAT64,
- new ColumnTypeInfo(Types.DOUBLE, "FLOAT64", 15, null, 10))
- .put(StandardSQLTypeName.NUMERIC, new ColumnTypeInfo(Types.NUMERIC, "NUMERIC", 38, 9, 10))
- .put(
- StandardSQLTypeName.BIGNUMERIC,
- new ColumnTypeInfo(Types.NUMERIC, "BIGNUMERIC", 77, 38, 10))
- .put(
- StandardSQLTypeName.STRING,
- new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null))
- .put(
- StandardSQLTypeName.TIMESTAMP,
- new ColumnTypeInfo(Types.TIMESTAMP, "TIMESTAMP", 26, 6, null))
- .put(
- StandardSQLTypeName.DATETIME,
- new ColumnTypeInfo(Types.TIMESTAMP, "DATETIME", 26, 6, null))
- .put(StandardSQLTypeName.DATE, new ColumnTypeInfo(Types.DATE, "DATE", 10, 0, null))
- .put(StandardSQLTypeName.TIME, new ColumnTypeInfo(Types.TIME, "TIME", 15, 6, null))
- .put(
- StandardSQLTypeName.GEOGRAPHY,
- new ColumnTypeInfo(Types.OTHER, "GEOGRAPHY", null, null, null))
- .put(StandardSQLTypeName.JSON, new ColumnTypeInfo(Types.OTHER, "JSON", null, null, null))
- .put(
- StandardSQLTypeName.INTERVAL,
- new ColumnTypeInfo(Types.OTHER, "INTERVAL", null, null, null))
- .put(
- StandardSQLTypeName.RANGE, new ColumnTypeInfo(Types.OTHER, "RANGE", null, null, null))
- .put(
- StandardSQLTypeName.BYTES,
- new ColumnTypeInfo(Types.VARBINARY, "BYTES", null, null, null))
- .put(
- StandardSQLTypeName.STRUCT,
- new ColumnTypeInfo(Types.STRUCT, "STRUCT", null, null, null))
- .build();
-}
diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java
index a5b40a8e2d34..7ba6af89c7b6 100644
--- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java
+++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java
@@ -532,13 +532,13 @@ public ResultSetMetaData getMetaData() throws SQLException {
@Override
public void setDate(int parameterIndex, Date value, Calendar calendar) throws SQLException {
checkClosed();
- setDate(parameterIndex, BigQueryTypeCoercionUtility.convertDateToCalendar(value, calendar));
+ setDate(parameterIndex, BigQueryTemporalUtility.convertDateToCalendar(value, calendar));
}
@Override
public void setTime(int parameterIndex, Time value, Calendar calendar) throws SQLException {
checkClosed();
- setTime(parameterIndex, BigQueryTypeCoercionUtility.convertTimeWithCalendar(value, calendar));
+ setTime(parameterIndex, BigQueryTemporalUtility.convertTimeWithCalendar(value, calendar));
}
@Override
@@ -546,7 +546,7 @@ public void setTimestamp(int parameterIndex, Timestamp value, Calendar calendar)
throws SQLException {
checkClosed();
setTimestamp(
- parameterIndex, BigQueryTypeCoercionUtility.convertTimestampWithCalendar(value, calendar));
+ parameterIndex, BigQueryTemporalUtility.convertTimestampWithCalendar(value, calendar));
}
@Override
diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java
index db7912ff4b6a..8880f1d1e623 100644
--- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java
+++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java
@@ -25,6 +25,7 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.Calendar;
/**
@@ -149,4 +150,114 @@ public static long getLocalMillis(long millisOfDay, ZoneId zoneId) {
.toInstant()
.toEpochMilli();
}
+
+ /** Returns a defensively cloned Calendar instance or a new default Calendar if input is null. */
+ static Calendar getSafeCalendar(Calendar cal) {
+ if (cal == null) {
+ return Calendar.getInstance();
+ }
+ Object cloned = cal.clone();
+ if (cloned instanceof Calendar) {
+ return (Calendar) cloned;
+ }
+ Calendar safeCal = Calendar.getInstance();
+ if (cal.getTimeZone() != null) {
+ safeCal.setTimeZone(cal.getTimeZone());
+ }
+ return safeCal;
+ }
+
+ /**
+ * Converts a {@link Date} for reading/outbound operations (e.g., {@code getDate(..., Calendar)})
+ * by shifting its wall-clock year, month, and day fields into the target {@link Calendar}'s
+ * timezone per the JDBC specification.
+ *
+ * @param date the date in system-default time representation
+ * @param cal the target Calendar containing the desired timezone
+ * @return the adjusted Date starting at 00:00:00 in the target Calendar's timezone
+ */
+ static Date convertDateWithCalendar(Date date, Calendar cal) {
+ if (date == null || cal == null) {
+ return date;
+ }
+ ZoneId systemZone = ZoneId.systemDefault();
+ ZoneId targetZone = cal.getTimeZone().toZoneId();
+ if (systemZone.equals(targetZone)) {
+ return date;
+ }
+ LocalDate localDate = date.toLocalDate();
+ ZonedDateTime zdt = localDate.atStartOfDay(targetZone);
+ return new Date(zdt.toInstant().toEpochMilli());
+ }
+
+ /**
+ * Converts a {@link Date} for writing/inbound parameter setting operations (e.g., {@code
+ * setDate(..., Calendar)}) by extracting its local date fields as interpreted in the target
+ * {@link Calendar}'s timezone and normalizing them back into start-of-day in the system-default
+ * timezone.
+ *
+ * @param date the date instant specified relative to the target Calendar
+ * @param cal the Calendar containing the source timezone
+ * @return the normalized Date starting at 00:00:00 in the system-default timezone
+ */
+ static Date convertDateToCalendar(Date date, Calendar cal) {
+ if (date == null || cal == null) {
+ return date;
+ }
+ ZoneId systemZone = ZoneId.systemDefault();
+ ZoneId targetZone = cal.getTimeZone().toZoneId();
+ if (systemZone.equals(targetZone)) {
+ return date;
+ }
+ LocalDate localDate = Instant.ofEpochMilli(date.getTime()).atZone(targetZone).toLocalDate();
+ ZonedDateTime zdt = localDate.atStartOfDay(systemZone);
+ return new Date(zdt.toInstant().toEpochMilli());
+ }
+
+ /**
+ * Converts a java.sql.Time by shifting its wall-clock hour, minute, second, and millisecond
+ * fields into the target Calendar's timezone per JDBC specification.
+ */
+ static Time convertTimeWithCalendar(Time time, Calendar cal) {
+ if (time == null || cal == null) {
+ return time;
+ }
+ ZoneId systemZone = ZoneId.systemDefault();
+ ZoneId targetZone = cal.getTimeZone().toZoneId();
+ 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());
+ }
+
+ /**
+ * Converts a java.sql.Timestamp by shifting its wall-clock fields into the target Calendar's
+ * timezone per JDBC specification while preserving nanosecond precision.
+ */
+ static Timestamp convertTimestampWithCalendar(Timestamp timestamp, Calendar cal) {
+ if (timestamp == null || cal == null) {
+ return timestamp;
+ }
+ ZoneId systemZone = ZoneId.systemDefault();
+ ZoneId targetZone = cal.getTimeZone().toZoneId();
+ if (systemZone.equals(targetZone)) {
+ return timestamp;
+ }
+ LocalDateTime ldt = timestamp.toLocalDateTime();
+ ZonedDateTime zdt = ldt.atZone(targetZone);
+ Timestamp adjustedTimestamp = Timestamp.from(zdt.toInstant());
+ adjustedTimestamp.setNanos(timestamp.getNanos());
+ return adjustedTimestamp;
+ }
}
diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercer.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercer.java
deleted file mode 100644
index 9f968fd4b8d0..000000000000
--- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercer.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright 2023 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 com.google.cloud.bigquery.FieldValue;
-import com.google.cloud.bigquery.exception.BigQueryJdbcCoercionException;
-import com.google.cloud.bigquery.exception.BigQueryJdbcCoercionNotFoundException;
-import java.util.Map;
-
-/**
- * Provides a declarative mechanism for coercing an object from one type to another. For example,
- * coercion of {@link String} to {@link Integer} can be achieved like this:
- *
- *
- * Integer value = BigQueryTypeCoercer.INSTANCE.coerceTo(Integer.class, "3452148");
- * System.out.println(value); // 3452148
- *
- *
- * A {@link BigQueryTypeCoercer} is baked with all the default {@link BigQueryCoercion}s from {@link
- * BigQueryDefaultCoercions} to coerce all the primitive types.
- *
- * It is also possible to extend the behaviour of {@link BigQueryTypeCoercer} to other custom
- * user defined types by creating an implementation of {@link BigQueryCoercion} and register it with
- * {@link BigQueryTypeCoercerBuilder} using it's {@link
- * BigQueryTypeCoercerBuilder#registerTypeCoercion(BigQueryCoercion)} method.
- *
- *
- * public class TextToStringCoercion extends BigQueryBigQueryCoercion{
- *
- * public TextToStringCoercion() {
- * super(Text.class, String.class);
- * }
- *
- * @Override
- * String coerce(Text text) {
- * return text.toString(); // logic to coerce from Text type to String type
- * }
- * }
- *
- *
- * and use it like this
- *
- *
- * byte[] bytesArray = {72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33};
- * Text text = new Text(bytesArray);
- *
- * BigQueryTypeCoercer typeCoercer = new BigQueryTypeCoercerBuilder()
- * .registerCoercion(new TextToStringCoercion()) // registering a custom coercion
- * .build();
- * System.out.println(typeCoercer.coerceTo(String.class, text)); // Hello World!
- *
- */
-@InternalApi
-class BigQueryTypeCoercer {
- private static final BigQueryJdbcResultSetLogger LOG =
- BigQueryJdbcResultSetLogger.getLogger(BigQueryTypeCoercer.class);
-
- /** A {@link BigQueryTypeCoercer} instance with all the inbuilt {@link BigQueryCoercion}s */
- static BigQueryTypeCoercer INSTANCE;
-
- static {
- INSTANCE = BigQueryDefaultCoercions.builder().build();
- }
-
- private final Map, Map, BigQueryCoercion, ?>>> allCoercions;
-
- BigQueryTypeCoercer(Map, Map, BigQueryCoercion, ?>>> allCoercions) {
- this.allCoercions = allCoercions;
- }
-
- /**
- * Coerce an object to the type specified.
- *
- * @param value the object that needs to be coerced.
- * @param targetClass the target class for the coercion
- * @throws BigQueryJdbcCoercionNotFoundException when coercion can not be performed to the target
- * type.
- * @throws BigQueryJdbcCoercionException when an error is encountered while performing the
- * coercion.
- */
- T coerceTo(Class targetClass, Object value) {
- return coerceTo(targetClass, value, null);
- }
-
- T coerceTo(Class targetClass, Object value, BigQueryJdbcResultSetLogger log) {
- Class> sourceClass = value == null ? Void.class : value.getClass();
- // FieldValue object for null-values requires special check
- if (sourceClass == FieldValue.class && ((FieldValue.class.cast(value)).isNull())) {
- sourceClass = Void.class;
- }
- // No coercion needed
- if (sourceClass.equals(targetClass)) {
- return targetClass.cast(value);
- }
- BigQueryCoercion