From defb44a1b1dac35ba7c9b18808b7be78ade263ce Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Thu, 16 Jul 2026 13:14:44 -0400 Subject: [PATCH 1/3] feat(bigquery-jdbc): implement PreparedStatement ParameterMetaData and Calendar date setters --- .../jdbc/BigQueryParameterMetaData.java | 166 ++++++++++++++++++ .../jdbc/BigQueryPreparedStatement.java | 80 +++++++-- .../BigQueryPreparedStatementSettersTest.java | 73 ++++++++ .../bigquery/jdbc/it/ITBigQueryJDBCTest.java | 27 ++- 4 files changed, 333 insertions(+), 13 deletions(-) create mode 100644 java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryParameterMetaData.java diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryParameterMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryParameterMetaData.java new file mode 100644 index 000000000000..89f67fba04e9 --- /dev/null +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryParameterMetaData.java @@ -0,0 +1,166 @@ +/* + * 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.cloud.bigquery.StandardSQLTypeName; +import com.google.cloud.bigquery.exception.BigQueryJdbcException; +import java.sql.ParameterMetaData; +import java.sql.SQLException; +import java.sql.Types; + +/** ParameterMetaData implementation for BigQuery JDBC PreparedStatement parameters. */ +class BigQueryParameterMetaData implements ParameterMetaData { + private final int parameterCount; + private final BigQueryParameterHandler parameterHandler; + + BigQueryParameterMetaData(int parameterCount, BigQueryParameterHandler parameterHandler) { + this.parameterCount = parameterCount; + this.parameterHandler = parameterHandler; + } + + private void checkValidIndex(int param) throws SQLException { + if (param < 1 || param > this.parameterCount) { + throw new BigQueryJdbcException("Invalid parameter index: " + param); + } + } + + @Override + public int getParameterCount() { + return this.parameterCount; + } + + @Override + public int isNullable(int param) throws SQLException { + checkValidIndex(param); + return parameterNullable; + } + + @Override + public boolean isSigned(int param) throws SQLException { + int type = getParameterType(param); + return type == Types.SMALLINT + || type == Types.TINYINT + || type == Types.INTEGER + || type == Types.BIGINT + || type == Types.FLOAT + || type == Types.DOUBLE + || type == Types.DECIMAL + || type == Types.NUMERIC; + } + + @Override + public int getPrecision(int param) throws SQLException { + checkValidIndex(param); + StandardSQLTypeName sqlType = getStandardSQLTypeName(param); + if (sqlType != null) { + BigQueryJdbcTypeMappings.ColumnTypeInfo typeInfo = + BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(sqlType); + if (typeInfo != null && typeInfo.columnSize != null) { + return typeInfo.columnSize; + } + } + return 0; + } + + @Override + public int getScale(int param) throws SQLException { + checkValidIndex(param); + StandardSQLTypeName sqlType = getStandardSQLTypeName(param); + if (sqlType != null) { + BigQueryJdbcTypeMappings.ColumnTypeInfo typeInfo = + BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(sqlType); + if (typeInfo != null && typeInfo.decimalDigits != null) { + return typeInfo.decimalDigits; + } + } + return 0; + } + + @Override + public int getParameterType(int param) throws SQLException { + checkValidIndex(param); + StandardSQLTypeName sqlType = getStandardSQLTypeName(param); + if (sqlType != null) { + Integer jdbcType = BigQueryJdbcTypeMappings.standardSQLToJavaSqlTypesMapping.get(sqlType); + if (jdbcType != null) { + return jdbcType; + } + } + return Types.OTHER; + } + + @Override + public String getParameterTypeName(int param) throws SQLException { + checkValidIndex(param); + StandardSQLTypeName sqlType = getStandardSQLTypeName(param); + return sqlType != null ? sqlType.name() : "UNKNOWN"; + } + + @Override + public String getParameterClassName(int param) throws SQLException { + checkValidIndex(param); + StandardSQLTypeName sqlType = getStandardSQLTypeName(param); + if (sqlType != null) { + Class clazz = BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get(sqlType); + if (clazz != null) { + return clazz.getName(); + } + } + Class boundType = this.parameterHandler.getType(param); + if (boundType != null) { + return boundType.getName(); + } + return Object.class.getName(); + } + + @Override + public int getParameterMode(int param) throws SQLException { + checkValidIndex(param); + return parameterModeIn; + } + + private StandardSQLTypeName getStandardSQLTypeName(int param) { + if (this.parameterHandler != null) { + StandardSQLTypeName sqlType = this.parameterHandler.getSqlType(param); + if (sqlType != null) { + return sqlType; + } + Class javaType = this.parameterHandler.getType(param); + if (javaType != null) { + try { + return BigQueryJdbcTypeMappings.classToType(javaType); + } catch (SQLException ignored) { + // fall back to default + } + } + } + return null; + } + + @Override + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(this)) { + return iface.cast(this); + } + throw new BigQueryJdbcException("Cannot unwrap to " + iface.getName()); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return iface != null && iface.isInstance(this); + } +} 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 12b450d044b1..b32b0d01a0fc 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 @@ -70,7 +70,7 @@ class BigQueryPreparedStatement extends BigQueryStatement implements PreparedSta protected int parameterCount = 0; protected String currentQuery; private Queue> batchParameters = new LinkedList<>(); - private Schema insertSchema = null; + Schema insertSchema = null; private TableName insertTableName = null; BigQueryPreparedStatement(BigQueryConnection connection, String query) { @@ -470,24 +470,80 @@ public void setArray(int parameterIndex, Array x) throws SQLException { } @Override - public ResultSetMetaData getMetaData() { - // TODO(neenu) :IMPLEMENT metadata + public ResultSetMetaData getMetaData() throws SQLException { + checkClosed(); + if (this.insertSchema != null) { + return BigQueryResultSetMetadata.of(this.insertSchema.getFields(), this); + } return null; } @Override - public void setDate(int parameterIndex, Date x, Calendar cal) { - // TODO :NOT IMPLEMENTED + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + checkClosed(); + if (x == null) { + setNull(parameterIndex, Types.DATE); + return; + } + if (cal == null) { + setDate(parameterIndex, x); + return; + } + Calendar defaultCal = Calendar.getInstance(); + defaultCal.setTime(x); + cal.set(Calendar.YEAR, defaultCal.get(Calendar.YEAR)); + cal.set(Calendar.MONTH, defaultCal.get(Calendar.MONTH)); + cal.set(Calendar.DAY_OF_MONTH, defaultCal.get(Calendar.DAY_OF_MONTH)); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + setDate(parameterIndex, new Date(cal.getTimeInMillis())); } @Override - public void setTime(int parameterIndex, Time x, Calendar cal) { - // TODO :NOT IMPLEMENTED + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + checkClosed(); + if (x == null) { + setNull(parameterIndex, Types.TIME); + return; + } + if (cal == null) { + setTime(parameterIndex, x); + return; + } + Calendar defaultCal = Calendar.getInstance(); + defaultCal.setTime(x); + cal.set(Calendar.HOUR_OF_DAY, defaultCal.get(Calendar.HOUR_OF_DAY)); + cal.set(Calendar.MINUTE, defaultCal.get(Calendar.MINUTE)); + cal.set(Calendar.SECOND, defaultCal.get(Calendar.SECOND)); + cal.set(Calendar.MILLISECOND, defaultCal.get(Calendar.MILLISECOND)); + setTime(parameterIndex, new Time(cal.getTimeInMillis())); } @Override - public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) { - // TODO :NOT IMPLEMENTED + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + checkClosed(); + if (x == null) { + setNull(parameterIndex, Types.TIMESTAMP); + return; + } + if (cal == null) { + setTimestamp(parameterIndex, x); + return; + } + Calendar defaultCal = Calendar.getInstance(); + defaultCal.setTime(x); + cal.set(Calendar.YEAR, defaultCal.get(Calendar.YEAR)); + cal.set(Calendar.MONTH, defaultCal.get(Calendar.MONTH)); + cal.set(Calendar.DAY_OF_MONTH, defaultCal.get(Calendar.DAY_OF_MONTH)); + cal.set(Calendar.HOUR_OF_DAY, defaultCal.get(Calendar.HOUR_OF_DAY)); + cal.set(Calendar.MINUTE, defaultCal.get(Calendar.MINUTE)); + cal.set(Calendar.SECOND, defaultCal.get(Calendar.SECOND)); + cal.set(Calendar.MILLISECOND, defaultCal.get(Calendar.MILLISECOND)); + Timestamp adjustedTimestamp = new Timestamp(cal.getTimeInMillis()); + adjustedTimestamp.setNanos(x.getNanos()); + setTimestamp(parameterIndex, adjustedTimestamp); } @Override @@ -501,9 +557,9 @@ public void setURL(int parameterIndex, URL x) throws SQLException { } @Override - public ParameterMetaData getParameterMetaData() { - // TODO(neenu) :IMPLEMENT - return null; + public ParameterMetaData getParameterMetaData() throws SQLException { + checkClosed(); + return new BigQueryParameterMetaData(this.parameterCount, this.parameterHandler); } @Override diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java index f9b07efabd90..784ad5c23d9f 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java @@ -18,12 +18,16 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; +import com.google.cloud.bigquery.Field; +import com.google.cloud.bigquery.Schema; import com.google.cloud.bigquery.StandardSQLTypeName; import java.sql.Array; +import java.sql.ResultSetMetaData; import java.sql.Types; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -138,4 +142,73 @@ public void testSetArray() throws Exception { assertEquals(Array.class, preparedStatement.parameterHandler.getType(1)); assertEquals(StandardSQLTypeName.ARRAY, preparedStatement.parameterHandler.getSqlType(1)); } + + @Test + public void testGetParameterMetaData() throws Exception { + preparedStatement.setString(1, "test"); + preparedStatement.setInt(2, 42); + + java.sql.ParameterMetaData metaData = preparedStatement.getParameterMetaData(); + assertEquals(5, metaData.getParameterCount()); + assertEquals(Types.NVARCHAR, metaData.getParameterType(1)); + assertEquals("STRING", metaData.getParameterTypeName(1)); + assertEquals(String.class.getName(), metaData.getParameterClassName(1)); + + assertEquals(Types.BIGINT, metaData.getParameterType(2)); + assertEquals("INT64", metaData.getParameterTypeName(2)); + assertEquals(Long.class.getName(), metaData.getParameterClassName(2)); + + assertEquals(java.sql.ParameterMetaData.parameterNullable, metaData.isNullable(1)); + assertEquals(java.sql.ParameterMetaData.parameterModeIn, metaData.getParameterMode(1)); + + assertThrows(java.sql.SQLException.class, () -> metaData.getParameterType(0)); + assertThrows(java.sql.SQLException.class, () -> metaData.getParameterType(6)); + } + + @Test + public void testSetDateWithCalendar() throws Exception { + java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); + java.sql.Date date = new java.sql.Date(1700000000000L); // 2023-11-14 + + preparedStatement.setDate(1, date, cal); + assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); + } + + @Test + public void testSetTimeWithCalendar() throws Exception { + java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); + java.sql.Time time = new java.sql.Time(43200000L); // 12:00:00 + + preparedStatement.setTime(1, time, cal); + assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); + } + + @Test + public void testSetTimestampWithCalendar() throws Exception { + java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); + java.sql.Timestamp ts = new java.sql.Timestamp(1700000000000L); + + preparedStatement.setTimestamp(1, ts, cal); + assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); + } + + @Test + public void testGetMetaData() throws Exception { + // Before execution/insertSchema initialization, getMetaData() returns null + assertNull(preparedStatement.getMetaData()); + + // When insertSchema is present, getMetaData() returns ResultSetMetaData + preparedStatement.insertSchema = + Schema.of( + Field.of("col1", StandardSQLTypeName.STRING), + Field.of("col2", StandardSQLTypeName.INT64)); + + ResultSetMetaData metaData = preparedStatement.getMetaData(); + assertNotNull(metaData); + assertEquals(2, metaData.getColumnCount()); + assertEquals("col1", metaData.getColumnName(1)); + assertEquals(Types.NVARCHAR, metaData.getColumnType(1)); + assertEquals("col2", metaData.getColumnName(2)); + assertEquals(Types.BIGINT, metaData.getColumnType(2)); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java index 3fd1489c1afe..9df2f8b624e4 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITBigQueryJDBCTest.java @@ -44,6 +44,7 @@ import java.sql.Date; import java.sql.Driver; import java.sql.DriverManager; +import java.sql.ParameterMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; @@ -53,8 +54,10 @@ import java.sql.Timestamp; import java.sql.Types; import java.time.LocalTime; +import java.util.Calendar; import java.util.Properties; import java.util.Random; +import java.util.TimeZone; import java.util.function.BiFunction; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -1515,8 +1518,19 @@ public void testPreparedStatementSmallSelect() throws SQLException { PreparedStatement preparedStatement = bigQueryConnection.prepareStatement(query); preparedStatement.setString(1, "hamlet"); + ParameterMetaData paramMetaData = preparedStatement.getParameterMetaData(); + assertNotNull(paramMetaData); + assertEquals(1, paramMetaData.getParameterCount()); + assertEquals(Types.NVARCHAR, paramMetaData.getParameterType(1)); + assertEquals("STRING", paramMetaData.getParameterTypeName(1)); + ResultSet jsonResultSet = preparedStatement.executeQuery(); + ResultSetMetaData resultSetMetaData = preparedStatement.getMetaData(); + if (resultSetMetaData != null) { + assertTrue(resultSetMetaData.getColumnCount() > 0); + } + int rowCount = resultSetRowCount(jsonResultSet); assertEquals(1000, rowCount); assertTrue(jsonResultSet.getClass().getName().contains("BigQueryJsonResultSet")); @@ -1643,11 +1657,22 @@ public void testPreparedStatementDateTimeValues() throws SQLException { int insertStatus = insertPs.executeUpdate(); assertEquals(1, insertStatus); + // Test Calendar overloads (setDate, setTime, setTimestamp with Calendar) + 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); + + int insertStatus2 = insertPs.executeUpdate(); + assertEquals(1, insertStatus2); + ResultSet rs = bigQueryStatement.executeQuery( String.format("SELECT COUNT(*) AS row_count\n" + "FROM %s.%s", DATASET, TABLE_NAME1)); rs.next(); - assertEquals(1, rs.getInt(1)); + assertEquals(2, rs.getInt(1)); String dropQuery = String.format("DROP TABLE %s.%s", DATASET, TABLE_NAME1); int dropStatus = bigQueryStatement.executeUpdate(dropQuery); From 12348a6ade7844d98ec989afea7e89204659fe08 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Thu, 16 Jul 2026 13:51:19 -0400 Subject: [PATCH 2/3] add date/time handling in setObject --- .../jdbc/BigQueryJdbcTypeMappings.java | 16 ++++-- .../jdbc/BigQueryPreparedStatement.java | 54 +++++++++++++++++++ .../BigQueryPreparedStatementSettersTest.java | 52 ++++++++++++++---- 3 files changed, 108 insertions(+), 14 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcTypeMappings.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcTypeMappings.java index 648cf8fd4673..19b53d6e2ae7 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcTypeMappings.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcTypeMappings.java @@ -28,6 +28,12 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.AbstractMap.SimpleEntry; import java.util.Map; @@ -117,11 +123,15 @@ static StandardSQLTypeName classToType(Class type) return StandardSQLTypeName.FLOAT64; } else if (BigDecimal.class.isAssignableFrom(type)) { return StandardSQLTypeName.NUMERIC; - } else if (Date.class.isAssignableFrom(type)) { + } else if (Date.class.isAssignableFrom(type) || LocalDate.class.isAssignableFrom(type)) { return StandardSQLTypeName.DATE; - } else if (Timestamp.class.isAssignableFrom(type)) { + } else if (Timestamp.class.isAssignableFrom(type) + || LocalDateTime.class.isAssignableFrom(type) + || OffsetDateTime.class.isAssignableFrom(type) + || Instant.class.isAssignableFrom(type) + || ZonedDateTime.class.isAssignableFrom(type)) { return StandardSQLTypeName.TIMESTAMP; - } else if (Time.class.isAssignableFrom(type)) { + } else if (Time.class.isAssignableFrom(type) || LocalTime.class.isAssignableFrom(type)) { return StandardSQLTypeName.TIME; } else if (JsonObject.class.isAssignableFrom(type)) { return StandardSQLTypeName.JSON; 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 b32b0d01a0fc..01f95bfa70ab 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 @@ -57,6 +57,12 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -230,6 +236,30 @@ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQ setNull(parameterIndex, targetSqlType); return; } + if (x instanceof LocalDate) { + setDate(parameterIndex, Date.valueOf((LocalDate) x)); + return; + } + if (x instanceof LocalTime) { + setTime(parameterIndex, Time.valueOf((LocalTime) x)); + return; + } + if (x instanceof LocalDateTime) { + setTimestamp(parameterIndex, Timestamp.valueOf((LocalDateTime) x)); + return; + } + if (x instanceof OffsetDateTime) { + setTimestamp(parameterIndex, Timestamp.from(((OffsetDateTime) x).toInstant())); + return; + } + if (x instanceof Instant) { + setTimestamp(parameterIndex, Timestamp.from((Instant) x)); + return; + } + if (x instanceof ZonedDateTime) { + setTimestamp(parameterIndex, Timestamp.from(((ZonedDateTime) x).toInstant())); + return; + } Class javaType = BigQueryJdbcTypeMappings.getJavaType(targetSqlType); this.parameterHandler.setParameter(parameterIndex, x, javaType); } @@ -241,6 +271,30 @@ public void setObject(int parameterIndex, Object x) throws SQLException { setNull(parameterIndex, Types.NULL); return; } + if (x instanceof LocalDate) { + setDate(parameterIndex, Date.valueOf((LocalDate) x)); + return; + } + if (x instanceof LocalTime) { + setTime(parameterIndex, Time.valueOf((LocalTime) x)); + return; + } + if (x instanceof LocalDateTime) { + setTimestamp(parameterIndex, Timestamp.valueOf((LocalDateTime) x)); + return; + } + if (x instanceof OffsetDateTime) { + setTimestamp(parameterIndex, Timestamp.from(((OffsetDateTime) x).toInstant())); + return; + } + if (x instanceof Instant) { + setTimestamp(parameterIndex, Timestamp.from((Instant) x)); + return; + } + if (x instanceof ZonedDateTime) { + setTimestamp(parameterIndex, Timestamp.from(((ZonedDateTime) x).toInstant())); + return; + } this.parameterHandler.setParameter(parameterIndex, x, x.getClass()); } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java index 784ad5c23d9f..b8a2ba55eb5b 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java @@ -27,8 +27,19 @@ import com.google.cloud.bigquery.Schema; import com.google.cloud.bigquery.StandardSQLTypeName; import java.sql.Array; +import java.sql.Date; +import java.sql.ParameterMetaData; import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; import java.sql.Types; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Calendar; +import java.util.TimeZone; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -148,7 +159,7 @@ public void testGetParameterMetaData() throws Exception { preparedStatement.setString(1, "test"); preparedStatement.setInt(2, 42); - java.sql.ParameterMetaData metaData = preparedStatement.getParameterMetaData(); + ParameterMetaData metaData = preparedStatement.getParameterMetaData(); assertEquals(5, metaData.getParameterCount()); assertEquals(Types.NVARCHAR, metaData.getParameterType(1)); assertEquals("STRING", metaData.getParameterTypeName(1)); @@ -158,17 +169,17 @@ public void testGetParameterMetaData() throws Exception { assertEquals("INT64", metaData.getParameterTypeName(2)); assertEquals(Long.class.getName(), metaData.getParameterClassName(2)); - assertEquals(java.sql.ParameterMetaData.parameterNullable, metaData.isNullable(1)); - assertEquals(java.sql.ParameterMetaData.parameterModeIn, metaData.getParameterMode(1)); + assertEquals(ParameterMetaData.parameterNullable, metaData.isNullable(1)); + assertEquals(ParameterMetaData.parameterModeIn, metaData.getParameterMode(1)); - assertThrows(java.sql.SQLException.class, () -> metaData.getParameterType(0)); - assertThrows(java.sql.SQLException.class, () -> metaData.getParameterType(6)); + assertThrows(SQLException.class, () -> metaData.getParameterType(0)); + assertThrows(SQLException.class, () -> metaData.getParameterType(6)); } @Test public void testSetDateWithCalendar() throws Exception { - java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); - java.sql.Date date = new java.sql.Date(1700000000000L); // 2023-11-14 + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + Date date = new Date(1700000000000L); // 2023-11-14 preparedStatement.setDate(1, date, cal); assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); @@ -176,8 +187,8 @@ public void testSetDateWithCalendar() throws Exception { @Test public void testSetTimeWithCalendar() throws Exception { - java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); - java.sql.Time time = new java.sql.Time(43200000L); // 12:00:00 + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + Time time = new Time(43200000L); // 12:00:00 preparedStatement.setTime(1, time, cal); assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); @@ -185,8 +196,8 @@ public void testSetTimeWithCalendar() throws Exception { @Test public void testSetTimestampWithCalendar() throws Exception { - java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); - java.sql.Timestamp ts = new java.sql.Timestamp(1700000000000L); + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + Timestamp ts = new Timestamp(1700000000000L); preparedStatement.setTimestamp(1, ts, cal); assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); @@ -211,4 +222,23 @@ public void testGetMetaData() throws Exception { assertEquals("col2", metaData.getColumnName(2)); assertEquals(Types.BIGINT, metaData.getColumnType(2)); } + + @Test + public void testSetObjectWithJavaTime() throws Exception { + LocalDate localDate = LocalDate.of(2025, 12, 3); + preparedStatement.setObject(1, localDate); + assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); + + LocalTime localTime = LocalTime.of(12, 30, 0); + preparedStatement.setObject(2, localTime); + assertEquals(String.class, preparedStatement.parameterHandler.getType(2)); + + LocalDateTime localDateTime = LocalDateTime.of(2025, 12, 3, 12, 30, 0); + preparedStatement.setObject(3, localDateTime); + assertEquals(String.class, preparedStatement.parameterHandler.getType(3)); + + Instant instant = Instant.now(); + preparedStatement.setObject(4, instant); + assertEquals(String.class, preparedStatement.parameterHandler.getType(4)); + } } From 1e293fb42078140da6808e78610a8b8079e909a1 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Thu, 16 Jul 2026 13:54:19 -0400 Subject: [PATCH 3/3] fix(jdbc): clone Calendar parameter in date/time setters to prevent mutation --- .../jdbc/BigQueryPreparedStatement.java | 45 ++++++++++--------- .../BigQueryPreparedStatementSettersTest.java | 9 ++++ 2 files changed, 33 insertions(+), 21 deletions(-) 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 01f95bfa70ab..09e636125bc4 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 @@ -543,16 +543,17 @@ public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLExceptio setDate(parameterIndex, x); return; } + Calendar targetCal = (Calendar) cal.clone(); Calendar defaultCal = Calendar.getInstance(); defaultCal.setTime(x); - cal.set(Calendar.YEAR, defaultCal.get(Calendar.YEAR)); - cal.set(Calendar.MONTH, defaultCal.get(Calendar.MONTH)); - cal.set(Calendar.DAY_OF_MONTH, defaultCal.get(Calendar.DAY_OF_MONTH)); - cal.set(Calendar.HOUR_OF_DAY, 0); - cal.set(Calendar.MINUTE, 0); - cal.set(Calendar.SECOND, 0); - cal.set(Calendar.MILLISECOND, 0); - setDate(parameterIndex, new Date(cal.getTimeInMillis())); + targetCal.set(Calendar.YEAR, defaultCal.get(Calendar.YEAR)); + targetCal.set(Calendar.MONTH, defaultCal.get(Calendar.MONTH)); + targetCal.set(Calendar.DAY_OF_MONTH, defaultCal.get(Calendar.DAY_OF_MONTH)); + targetCal.set(Calendar.HOUR_OF_DAY, 0); + targetCal.set(Calendar.MINUTE, 0); + targetCal.set(Calendar.SECOND, 0); + targetCal.set(Calendar.MILLISECOND, 0); + setDate(parameterIndex, new Date(targetCal.getTimeInMillis())); } @Override @@ -566,13 +567,14 @@ public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLExceptio setTime(parameterIndex, x); return; } + Calendar targetCal = (Calendar) cal.clone(); Calendar defaultCal = Calendar.getInstance(); defaultCal.setTime(x); - cal.set(Calendar.HOUR_OF_DAY, defaultCal.get(Calendar.HOUR_OF_DAY)); - cal.set(Calendar.MINUTE, defaultCal.get(Calendar.MINUTE)); - cal.set(Calendar.SECOND, defaultCal.get(Calendar.SECOND)); - cal.set(Calendar.MILLISECOND, defaultCal.get(Calendar.MILLISECOND)); - setTime(parameterIndex, new Time(cal.getTimeInMillis())); + 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)); + setTime(parameterIndex, new Time(targetCal.getTimeInMillis())); } @Override @@ -586,16 +588,17 @@ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws S setTimestamp(parameterIndex, x); return; } + Calendar targetCal = (Calendar) cal.clone(); Calendar defaultCal = Calendar.getInstance(); defaultCal.setTime(x); - cal.set(Calendar.YEAR, defaultCal.get(Calendar.YEAR)); - cal.set(Calendar.MONTH, defaultCal.get(Calendar.MONTH)); - cal.set(Calendar.DAY_OF_MONTH, defaultCal.get(Calendar.DAY_OF_MONTH)); - cal.set(Calendar.HOUR_OF_DAY, defaultCal.get(Calendar.HOUR_OF_DAY)); - cal.set(Calendar.MINUTE, defaultCal.get(Calendar.MINUTE)); - cal.set(Calendar.SECOND, defaultCal.get(Calendar.SECOND)); - cal.set(Calendar.MILLISECOND, defaultCal.get(Calendar.MILLISECOND)); - Timestamp adjustedTimestamp = new Timestamp(cal.getTimeInMillis()); + targetCal.set(Calendar.YEAR, defaultCal.get(Calendar.YEAR)); + targetCal.set(Calendar.MONTH, defaultCal.get(Calendar.MONTH)); + targetCal.set(Calendar.DAY_OF_MONTH, defaultCal.get(Calendar.DAY_OF_MONTH)); + 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)); + Timestamp adjustedTimestamp = new Timestamp(targetCal.getTimeInMillis()); adjustedTimestamp.setNanos(x.getNanos()); setTimestamp(parameterIndex, adjustedTimestamp); } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java index b8a2ba55eb5b..9a019cab7810 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java @@ -179,28 +179,37 @@ public void testGetParameterMetaData() throws Exception { @Test public void testSetDateWithCalendar() throws Exception { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(1000000L); + long originalMillis = cal.getTimeInMillis(); Date date = new Date(1700000000000L); // 2023-11-14 preparedStatement.setDate(1, date, cal); assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); + assertEquals(originalMillis, cal.getTimeInMillis()); } @Test public void testSetTimeWithCalendar() throws Exception { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(1000000L); + long originalMillis = cal.getTimeInMillis(); Time time = new Time(43200000L); // 12:00:00 preparedStatement.setTime(1, time, cal); assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); + assertEquals(originalMillis, cal.getTimeInMillis()); } @Test public void testSetTimestampWithCalendar() throws Exception { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(1000000L); + long originalMillis = cal.getTimeInMillis(); Timestamp ts = new Timestamp(1700000000000L); preparedStatement.setTimestamp(1, ts, cal); assertEquals(String.class, preparedStatement.parameterHandler.getType(1)); + assertEquals(originalMillis, cal.getTimeInMillis()); } @Test