Skip to content

Commit 3d67dba

Browse files
authored
feat(bigquery-jdbc): implement BigQueryParameterMetaData and dynamic type mappings (#13812)
b/535194644 * **ParameterMetaData Implementation**: Implemented `BigQueryParameterMetaData` to enable JDBC parameter metadata introspection. * **Dynamic Parameter Modes**: Added support for dynamic `IN`, `OUT`, and `INOUT` parameter modes. * **Accurate Type Mapping**: Mapped `LocalDateTime` to `StandardSQLTypeName.DATETIME` and expanded standard SQL to Java type mappings. * **Null-Safety Guarding**: Added null checks for `parameterHandler` to prevent `NullPointerException` during metadata resolution.
1 parent 9943b2b commit 3d67dba

2 files changed

Lines changed: 228 additions & 15 deletions

File tree

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

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
import java.sql.Time;
2929
import java.sql.Timestamp;
3030
import java.sql.Types;
31+
import java.time.Instant;
32+
import java.time.LocalDate;
33+
import java.time.LocalDateTime;
34+
import java.time.LocalTime;
35+
import java.time.OffsetDateTime;
36+
import java.time.ZonedDateTime;
3137
import java.util.AbstractMap.SimpleEntry;
3238
import java.util.Map;
3339

@@ -103,35 +109,56 @@ static StandardSQLTypeName classToType(Class type)
103109
throws BigQueryJdbcSqlFeatureNotSupportedException {
104110
if (Boolean.class.isAssignableFrom(type)) {
105111
return StandardSQLTypeName.BOOL;
106-
} else if (String.class.isAssignableFrom(type)) {
112+
}
113+
if (String.class.isAssignableFrom(type)) {
107114
return StandardSQLTypeName.STRING;
108-
} else if (Integer.class.isAssignableFrom(type)) {
115+
}
116+
if (Integer.class.isAssignableFrom(type)) {
109117
return StandardSQLTypeName.INT64;
110-
} else if (Long.class.isAssignableFrom(type)) {
118+
}
119+
if (Long.class.isAssignableFrom(type)) {
111120
return StandardSQLTypeName.INT64;
112-
} else if (Short.class.isAssignableFrom(type)) {
121+
}
122+
if (Short.class.isAssignableFrom(type)) {
113123
return StandardSQLTypeName.INT64;
114-
} else if (Double.class.isAssignableFrom(type)) {
124+
}
125+
if (Double.class.isAssignableFrom(type)) {
115126
return StandardSQLTypeName.FLOAT64;
116-
} else if (Float.class.isAssignableFrom(type)) {
127+
}
128+
if (Float.class.isAssignableFrom(type)) {
117129
return StandardSQLTypeName.FLOAT64;
118-
} else if (BigDecimal.class.isAssignableFrom(type)) {
130+
}
131+
if (BigDecimal.class.isAssignableFrom(type)) {
119132
return StandardSQLTypeName.NUMERIC;
120-
} else if (Date.class.isAssignableFrom(type)) {
133+
}
134+
if (Date.class.isAssignableFrom(type) || LocalDate.class.isAssignableFrom(type)) {
121135
return StandardSQLTypeName.DATE;
122-
} else if (Timestamp.class.isAssignableFrom(type)) {
136+
}
137+
if (LocalDateTime.class.isAssignableFrom(type)) {
138+
return StandardSQLTypeName.DATETIME;
139+
}
140+
if (Timestamp.class.isAssignableFrom(type)
141+
|| OffsetDateTime.class.isAssignableFrom(type)
142+
|| Instant.class.isAssignableFrom(type)
143+
|| ZonedDateTime.class.isAssignableFrom(type)) {
123144
return StandardSQLTypeName.TIMESTAMP;
124-
} else if (Time.class.isAssignableFrom(type)) {
145+
}
146+
if (Time.class.isAssignableFrom(type) || LocalTime.class.isAssignableFrom(type)) {
125147
return StandardSQLTypeName.TIME;
126-
} else if (JsonObject.class.isAssignableFrom(type)) {
148+
}
149+
if (JsonObject.class.isAssignableFrom(type)) {
127150
return StandardSQLTypeName.JSON;
128-
} else if (Byte.class.isAssignableFrom(type)) {
151+
}
152+
if (Byte.class.isAssignableFrom(type)) {
129153
return StandardSQLTypeName.INT64;
130-
} else if (Array.class.isAssignableFrom(type)) {
154+
}
155+
if (Array.class.isAssignableFrom(type)) {
131156
return StandardSQLTypeName.ARRAY;
132-
} else if (Struct.class.isAssignableFrom(type)) {
157+
}
158+
if (Struct.class.isAssignableFrom(type)) {
133159
return StandardSQLTypeName.STRUCT;
134-
} else if (byte[].class.isAssignableFrom(type)) {
160+
}
161+
if (byte[].class.isAssignableFrom(type)) {
135162
return StandardSQLTypeName.BYTES;
136163
}
137164
throw new BigQueryJdbcSqlFeatureNotSupportedException(
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigquery.jdbc;
18+
19+
import com.google.cloud.bigquery.StandardSQLTypeName;
20+
import com.google.cloud.bigquery.exception.BigQueryJdbcException;
21+
import java.sql.ParameterMetaData;
22+
import java.sql.SQLException;
23+
import java.sql.Types;
24+
25+
/** ParameterMetaData implementation for BigQuery JDBC PreparedStatement parameters. */
26+
class BigQueryParameterMetaData implements ParameterMetaData {
27+
private final int parameterCount;
28+
private final BigQueryParameterHandler parameterHandler;
29+
30+
BigQueryParameterMetaData(int parameterCount, BigQueryParameterHandler parameterHandler) {
31+
this.parameterCount = parameterCount;
32+
this.parameterHandler = parameterHandler;
33+
}
34+
35+
private void checkValidIndex(int param) throws SQLException {
36+
if (param < 1 || param > this.parameterCount) {
37+
throw new BigQueryJdbcException("Invalid parameter index: " + param);
38+
}
39+
}
40+
41+
@Override
42+
public int getParameterCount() {
43+
return this.parameterCount;
44+
}
45+
46+
@Override
47+
public int isNullable(int param) throws SQLException {
48+
checkValidIndex(param);
49+
return parameterNullable;
50+
}
51+
52+
@Override
53+
public boolean isSigned(int param) throws SQLException {
54+
int type = getParameterType(param);
55+
return type == Types.SMALLINT
56+
|| type == Types.TINYINT
57+
|| type == Types.INTEGER
58+
|| type == Types.BIGINT
59+
|| type == Types.FLOAT
60+
|| type == Types.DOUBLE
61+
|| type == Types.DECIMAL
62+
|| type == Types.NUMERIC;
63+
}
64+
65+
@Override
66+
public int getPrecision(int param) throws SQLException {
67+
checkValidIndex(param);
68+
StandardSQLTypeName sqlType = getStandardSQLTypeName(param);
69+
if (sqlType == null) {
70+
return 0;
71+
}
72+
BigQueryJdbcTypeMappings.ColumnTypeInfo typeInfo =
73+
BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(sqlType);
74+
if (typeInfo != null && typeInfo.columnSize != null) {
75+
return typeInfo.columnSize;
76+
}
77+
return 0;
78+
}
79+
80+
@Override
81+
public int getScale(int param) throws SQLException {
82+
checkValidIndex(param);
83+
StandardSQLTypeName sqlType = getStandardSQLTypeName(param);
84+
if (sqlType == null) {
85+
return 0;
86+
}
87+
BigQueryJdbcTypeMappings.ColumnTypeInfo typeInfo =
88+
BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(sqlType);
89+
if (typeInfo != null && typeInfo.decimalDigits != null) {
90+
return typeInfo.decimalDigits;
91+
}
92+
return 0;
93+
}
94+
95+
@Override
96+
public int getParameterType(int param) throws SQLException {
97+
checkValidIndex(param);
98+
StandardSQLTypeName sqlType = getStandardSQLTypeName(param);
99+
if (sqlType == null) {
100+
return Types.OTHER;
101+
}
102+
Integer jdbcType = BigQueryJdbcTypeMappings.standardSQLToJavaSqlTypesMapping.get(sqlType);
103+
if (jdbcType != null) {
104+
return jdbcType;
105+
}
106+
return Types.OTHER;
107+
}
108+
109+
@Override
110+
public String getParameterTypeName(int param) throws SQLException {
111+
checkValidIndex(param);
112+
StandardSQLTypeName sqlType = getStandardSQLTypeName(param);
113+
return sqlType != null ? sqlType.name() : "UNKNOWN";
114+
}
115+
116+
@Override
117+
public String getParameterClassName(int param) throws SQLException {
118+
checkValidIndex(param);
119+
StandardSQLTypeName sqlType = getStandardSQLTypeName(param);
120+
if (sqlType != null) {
121+
Class<?> clazz = BigQueryJdbcTypeMappings.standardSQLToJavaTypeMapping.get(sqlType);
122+
if (clazz != null) {
123+
return clazz.getName();
124+
}
125+
}
126+
if (this.parameterHandler == null) {
127+
return Object.class.getName();
128+
}
129+
Class<?> boundType = this.parameterHandler.getType(param);
130+
if (boundType != null) {
131+
return boundType.getName();
132+
}
133+
return Object.class.getName();
134+
}
135+
136+
@Override
137+
public int getParameterMode(int param) throws SQLException {
138+
checkValidIndex(param);
139+
if (this.parameterHandler == null) {
140+
return parameterModeIn;
141+
}
142+
BigQueryParameterHandler.BigQueryStatementParameterType paramType =
143+
this.parameterHandler.getParameterType(param);
144+
if (paramType == BigQueryParameterHandler.BigQueryStatementParameterType.OUT) {
145+
return parameterModeOut;
146+
}
147+
if (paramType == BigQueryParameterHandler.BigQueryStatementParameterType.INOUT) {
148+
return parameterModeInOut;
149+
}
150+
return parameterModeIn;
151+
}
152+
153+
private StandardSQLTypeName getStandardSQLTypeName(int param) {
154+
if (this.parameterHandler == null) {
155+
return null;
156+
}
157+
StandardSQLTypeName sqlType = this.parameterHandler.getSqlType(param);
158+
if (sqlType != null) {
159+
return sqlType;
160+
}
161+
Class<?> javaType = this.parameterHandler.getType(param);
162+
if (javaType == null) {
163+
return null;
164+
}
165+
try {
166+
return BigQueryJdbcTypeMappings.classToType(javaType);
167+
} catch (SQLException ignored) {
168+
// fall back to default
169+
return null;
170+
}
171+
}
172+
173+
@Override
174+
public <T> T unwrap(Class<T> iface) throws SQLException {
175+
if (!isWrapperFor(iface)) {
176+
throw new BigQueryJdbcException(
177+
"Cannot unwrap to " + (iface != null ? iface.getName() : "null"));
178+
}
179+
return iface.cast(this);
180+
}
181+
182+
@Override
183+
public boolean isWrapperFor(Class<?> iface) throws SQLException {
184+
return iface != null && iface.isInstance(this);
185+
}
186+
}

0 commit comments

Comments
 (0)