Skip to content

Commit 5678fc2

Browse files
authored
fix(bqjdbc): resolve JDBC metadata compliance gaps for wrappers, searchability, and type mappings (#13242)
b/514445832 ### Key Changes 1. **Align DatabaseMetaData Column Data Types and DB-Specific Type Names**: * Updated `DatabaseMetaData.getColumns()` type resolution (via `mapBigQueryTypeToJdbc`) to map BigQuery types and database-specific type names compliantly (e.g., `STRING`, `RANGE`, `GEOGRAPHY`, `JSON`, `INTERVAL`). * Aligned type codes with JDBC conventions (e.g., `StandardSQLTypeName.STRING` maps to `Types.NVARCHAR` to support Unicode/UTF-8 string reporting). * Ensured that unknown/fallback types default to `Types.NVARCHAR` / `STRING` instead of `Types.VARCHAR` to keep mapping behavior consistent across the driver. * Documented design rationale for mapping `StandardSQLTypeName.STRING` to `Types.NVARCHAR` (UTF-8 character encoding reporting). 2. **Wrapper Pattern Implementation (JDBC 4.0 Compliance)**: * Implemented standard JDBC wrapper methods (`unwrap` and `isWrapperFor`) in `BigQueryDatabaseMetaData` and `BigQueryResultSetMetadata`. * Integrated proxy resolution via `BigQueryJdbcContextProxy` to correctly handle unwrapping in connection proxy environments. 3. **ResultSetMetaData Searchability**: * Refactored `BigQueryResultSetMetadata.isSearchable` to return `true` for all column types. This fixes compliance issues where advanced types (`GEOGRAPHY`, `JSON`, `INTERVAL`, `RANGE` under `Types.OTHER`) were incorrectly reported as not searchable, even though they are fully queryable.
1 parent d08691a commit 5678fc2

5 files changed

Lines changed: 175 additions & 72 deletions

File tree

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

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.google.cloud.bigquery.TableDefinition;
4343
import com.google.cloud.bigquery.TableId;
4444
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
45+
import com.google.common.collect.ImmutableMap;
4546
import java.io.BufferedReader;
4647
import java.io.IOException;
4748
import java.io.InputStream;
@@ -59,6 +60,7 @@
5960
import java.util.Comparator;
6061
import java.util.HashSet;
6162
import java.util.List;
63+
import java.util.Map;
6264
import java.util.Properties;
6365
import java.util.Scanner;
6466
import java.util.Set;
@@ -1650,8 +1652,8 @@ ColumnTypeInfo determineTypeInfoFromDataType(
16501652
String columnName,
16511653
int ordinalPosition) {
16521654

1653-
ColumnTypeInfo defaultVarcharTypeInfo =
1654-
new ColumnTypeInfo(Types.VARCHAR, "VARCHAR", null, null, null);
1655+
ColumnTypeInfo defaultStringTypeInfo =
1656+
new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null);
16551657
try {
16561658
String typeKind = argumentDataType.getTypeKind();
16571659
if (typeKind != null && !typeKind.isEmpty()) {
@@ -1664,10 +1666,10 @@ ColumnTypeInfo determineTypeInfoFromDataType(
16641666
} catch (Exception e) {
16651667
LOG.warning(
16661668
"Proc: %s, Arg: %s (Pos %d) - Caught an unexpected Exception during type"
1667-
+ " determination. Defaulting type to VARCHAR. Error: %s",
1669+
+ " determination. Defaulting type to STRING. Error: %s",
16681670
procedureName, columnName, ordinalPosition, e.getMessage());
16691671
}
1670-
return defaultVarcharTypeInfo;
1672+
return defaultStringTypeInfo;
16711673
}
16721674

16731675
Comparator<FieldValueList> defineGetProcedureColumnsComparator(FieldList resultSchemaFields) {
@@ -4750,13 +4752,16 @@ public boolean generatedKeyAlwaysReturned() {
47504752
}
47514753

47524754
@Override
4753-
public <T> T unwrap(Class<T> iface) {
4754-
return null;
4755+
public <T> T unwrap(Class<T> iface) throws SQLException {
4756+
if (iface.isInstance(this)) {
4757+
return iface.cast(this);
4758+
}
4759+
throw new SQLException("Cannot unwrap to " + iface.getName());
47554760
}
47564761

47574762
@Override
4758-
public boolean isWrapperFor(Class<?> iface) {
4759-
return false;
4763+
public boolean isWrapperFor(Class<?> iface) throws SQLException {
4764+
return iface != null && iface.isInstance(this);
47604765
}
47614766

47624767
// --- Helper Methods ---
@@ -4828,47 +4833,58 @@ private Tuple<String, String> determineEffectiveCatalogAndSchema(
48284833
return Tuple.of(effectiveCatalog, effectiveSchemaPattern);
48294834
}
48304835

4836+
// BigQuery STRING represents Unicode character strings (UTF-8).
4837+
// Standard JDBC maps UTF-8/Unicode data to Types.NVARCHAR rather than Types.VARCHAR.
4838+
private static final Map<StandardSQLTypeName, ColumnTypeInfo> STANDARD_TYPE_INFO =
4839+
ImmutableMap.<StandardSQLTypeName, ColumnTypeInfo>builder()
4840+
.put(StandardSQLTypeName.INT64, new ColumnTypeInfo(Types.BIGINT, "INT64", 19, 0, 10))
4841+
.put(StandardSQLTypeName.BOOL, new ColumnTypeInfo(Types.BOOLEAN, "BOOL", 1, null, null))
4842+
.put(
4843+
StandardSQLTypeName.FLOAT64,
4844+
new ColumnTypeInfo(Types.DOUBLE, "FLOAT64", 15, null, 10))
4845+
.put(StandardSQLTypeName.NUMERIC, new ColumnTypeInfo(Types.NUMERIC, "NUMERIC", 38, 9, 10))
4846+
.put(
4847+
StandardSQLTypeName.BIGNUMERIC,
4848+
new ColumnTypeInfo(Types.NUMERIC, "BIGNUMERIC", 77, 38, 10))
4849+
.put(
4850+
StandardSQLTypeName.STRING,
4851+
new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null))
4852+
.put(
4853+
StandardSQLTypeName.TIMESTAMP,
4854+
new ColumnTypeInfo(Types.TIMESTAMP, "TIMESTAMP", 29, null, null))
4855+
.put(
4856+
StandardSQLTypeName.DATETIME,
4857+
new ColumnTypeInfo(Types.TIMESTAMP, "DATETIME", 29, null, null))
4858+
.put(StandardSQLTypeName.DATE, new ColumnTypeInfo(Types.DATE, "DATE", 10, null, null))
4859+
.put(StandardSQLTypeName.TIME, new ColumnTypeInfo(Types.TIME, "TIME", 15, null, null))
4860+
.put(
4861+
StandardSQLTypeName.GEOGRAPHY,
4862+
new ColumnTypeInfo(Types.OTHER, "GEOGRAPHY", null, null, null))
4863+
.put(StandardSQLTypeName.JSON, new ColumnTypeInfo(Types.OTHER, "JSON", null, null, null))
4864+
.put(
4865+
StandardSQLTypeName.INTERVAL,
4866+
new ColumnTypeInfo(Types.OTHER, "INTERVAL", null, null, null))
4867+
.put(
4868+
StandardSQLTypeName.RANGE, new ColumnTypeInfo(Types.OTHER, "RANGE", null, null, null))
4869+
.put(
4870+
StandardSQLTypeName.BYTES,
4871+
new ColumnTypeInfo(Types.VARBINARY, "BYTES", null, null, null))
4872+
.put(
4873+
StandardSQLTypeName.STRUCT,
4874+
new ColumnTypeInfo(Types.STRUCT, "STRUCT", null, null, null))
4875+
.build();
4876+
48314877
private ColumnTypeInfo getColumnTypeInfoForSqlType(StandardSQLTypeName bqType) {
48324878
if (bqType == null) {
4833-
LOG.warning("Null BigQuery type encountered: " + bqType.name() + ". Mapping to VARCHAR.");
4834-
return new ColumnTypeInfo(Types.VARCHAR, bqType.name(), null, null, null);
4879+
LOG.warning("Null BigQuery type encountered. Mapping to STRING.");
4880+
return new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null);
48354881
}
4836-
4837-
switch (bqType) {
4838-
case INT64:
4839-
return new ColumnTypeInfo(Types.BIGINT, "BIGINT", 19, 0, 10);
4840-
case BOOL:
4841-
return new ColumnTypeInfo(Types.BOOLEAN, "BOOLEAN", 1, null, null);
4842-
case FLOAT64:
4843-
return new ColumnTypeInfo(Types.DOUBLE, "DOUBLE", 15, null, 10);
4844-
case NUMERIC:
4845-
return new ColumnTypeInfo(Types.NUMERIC, "NUMERIC", 38, 9, 10);
4846-
case BIGNUMERIC:
4847-
return new ColumnTypeInfo(Types.NUMERIC, "NUMERIC", 77, 38, 10);
4848-
case STRING:
4849-
return new ColumnTypeInfo(Types.NVARCHAR, "NVARCHAR", null, null, null);
4850-
case TIMESTAMP:
4851-
case DATETIME:
4852-
return new ColumnTypeInfo(Types.TIMESTAMP, "TIMESTAMP", 29, null, null);
4853-
case DATE:
4854-
return new ColumnTypeInfo(Types.DATE, "DATE", 10, null, null);
4855-
case TIME:
4856-
return new ColumnTypeInfo(Types.TIME, "TIME", 15, null, null);
4857-
case GEOGRAPHY:
4858-
return new ColumnTypeInfo(Types.OTHER, "GEOGRAPHY", null, null, null);
4859-
case JSON:
4860-
return new ColumnTypeInfo(Types.OTHER, "JSON", null, null, null);
4861-
case INTERVAL:
4862-
return new ColumnTypeInfo(Types.OTHER, "INTERVAL", null, null, null);
4863-
case BYTES:
4864-
return new ColumnTypeInfo(Types.VARBINARY, "VARBINARY", null, null, null);
4865-
case STRUCT:
4866-
return new ColumnTypeInfo(Types.STRUCT, "STRUCT", null, null, null);
4867-
default:
4868-
LOG.warning(
4869-
"Unknown BigQuery type encountered: " + bqType.name() + ". Mapping to VARCHAR.");
4870-
return new ColumnTypeInfo(Types.VARCHAR, bqType.name(), null, null, null);
4882+
ColumnTypeInfo info = STANDARD_TYPE_INFO.get(bqType);
4883+
if (info == null) {
4884+
LOG.warning("Unknown BigQuery type encountered: " + bqType.name() + ". Mapping to STRING.");
4885+
return new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null);
48714886
}
4887+
return info;
48724888
}
48734889

48744890
<T> List<T> findMatchingBigQueryObjects(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
109109
// Support standard JDBC Wrapper unwrap operations
110110
if (methodName.equals("unwrap") && args != null && args.length == 1) {
111111
Class<?> iface = (Class<?>) args[0];
112-
if (iface.isInstance(target)) {
112+
if (iface != null && iface.isInstance(target)) {
113113
return target;
114114
}
115115
try {
@@ -120,7 +120,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
120120
}
121121
if (methodName.equals("isWrapperFor") && args != null && args.length == 1) {
122122
Class<?> iface = (Class<?>) args[0];
123-
if (iface.isInstance(target)) {
123+
if (iface != null && iface.isInstance(target)) {
124124
return true;
125125
}
126126
try {

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import com.google.cloud.bigquery.Field.Mode;
2121
import com.google.cloud.bigquery.FieldList;
2222
import com.google.cloud.bigquery.StandardSQLTypeName;
23-
import com.google.cloud.bigquery.exception.BigQueryJdbcSqlFeatureNotSupportedException;
2423
import java.sql.ResultSetMetaData;
2524
import java.sql.SQLException;
2625
import java.sql.Statement;
@@ -74,8 +73,7 @@ public boolean isCaseSensitive(int column) {
7473

7574
@Override
7675
public boolean isSearchable(int column) {
77-
int colType = getColumnType(column);
78-
return colType != Types.OTHER;
76+
return true;
7977
}
8078

8179
@Override
@@ -205,14 +203,17 @@ public String getColumnClassName(int column) {
205203
.getName();
206204
}
207205

208-
// Unsupported methods:
206+
// Wrapper methods:
209207
@Override
210208
public <T> T unwrap(Class<T> iface) throws SQLException {
211-
throw new BigQueryJdbcSqlFeatureNotSupportedException("unwrap is not implemented");
209+
if (iface.isInstance(this)) {
210+
return iface.cast(this);
211+
}
212+
throw new SQLException("Cannot unwrap to " + iface.getName());
212213
}
213214

214215
@Override
215216
public boolean isWrapperFor(Class<?> iface) throws SQLException {
216-
throw new BigQueryJdbcSqlFeatureNotSupportedException("isWrapperFor is not implemented");
217+
return iface != null && iface.isInstance(this);
217218
}
218219
}

0 commit comments

Comments
 (0)