Skip to content

Commit 721ff45

Browse files
authored
feat: support PreparedStatement#getParameterMetaData() (#1218)
* feat: support PreparedStatement#getParameterMetaData() Add actual support for `PreparedStatement#getParameterMetaData()`. The first time this method is called for a PreparedStatement, the connection will now send the query to Cloud Spanner in analyze mode and without any parameter values. This will instruct Cloud Spanner to return the names and types of any query parameters in the statement. Fixes #35 * fix: restore previous behavior * fix: PostgreSQL string type name should be 'character varying' * fix: update type name to 'character varying' in integration test
1 parent 0e15ba1 commit 721ff45

File tree

10 files changed

+1140
-63
lines changed

10 files changed

+1140
-63
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2023 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+
* http://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.spanner;
18+
19+
import com.google.api.core.InternalApi;
20+
21+
@InternalApi
22+
public class JdbcDataTypeConverter {
23+
24+
/** Converts a protobuf type to a Spanner type. */
25+
@InternalApi
26+
public static Type toSpannerType(com.google.spanner.v1.Type proto) {
27+
return Type.fromProto(proto);
28+
}
29+
}

src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.spanner.jdbc;
1818

19+
import com.google.cloud.spanner.Dialect;
1920
import com.google.cloud.spanner.Type;
2021
import com.google.cloud.spanner.Type.Code;
2122
import com.google.common.base.Preconditions;
@@ -69,7 +70,74 @@ static int extractColumnType(Type type) {
6970
}
7071
}
7172

72-
/** Extract Spanner type name from {@link java.sql.Types} code. */
73+
static String getSpannerTypeName(Type type, Dialect dialect) {
74+
// TODO: Use com.google.cloud.spanner.Type#getSpannerTypeName() when available.
75+
Preconditions.checkNotNull(type);
76+
switch (type.getCode()) {
77+
case BOOL:
78+
return dialect == Dialect.POSTGRESQL ? "boolean" : "BOOL";
79+
case BYTES:
80+
return dialect == Dialect.POSTGRESQL ? "bytea" : "BYTES";
81+
case DATE:
82+
return dialect == Dialect.POSTGRESQL ? "date" : "DATE";
83+
case FLOAT64:
84+
return dialect == Dialect.POSTGRESQL ? "double precision" : "FLOAT64";
85+
case INT64:
86+
return dialect == Dialect.POSTGRESQL ? "bigint" : "INT64";
87+
case NUMERIC:
88+
return "NUMERIC";
89+
case PG_NUMERIC:
90+
return "numeric";
91+
case STRING:
92+
return dialect == Dialect.POSTGRESQL ? "character varying" : "STRING";
93+
case JSON:
94+
return "JSON";
95+
case PG_JSONB:
96+
return "jsonb";
97+
case TIMESTAMP:
98+
return dialect == Dialect.POSTGRESQL ? "timestamp with time zone" : "TIMESTAMP";
99+
case STRUCT:
100+
return "STRUCT";
101+
case ARRAY:
102+
switch (type.getArrayElementType().getCode()) {
103+
case BOOL:
104+
return dialect == Dialect.POSTGRESQL ? "boolean[]" : "ARRAY<BOOL>";
105+
case BYTES:
106+
return dialect == Dialect.POSTGRESQL ? "bytea[]" : "ARRAY<BYTES>";
107+
case DATE:
108+
return dialect == Dialect.POSTGRESQL ? "date[]" : "ARRAY<DATE>";
109+
case FLOAT64:
110+
return dialect == Dialect.POSTGRESQL ? "double precision[]" : "ARRAY<FLOAT64>";
111+
case INT64:
112+
return dialect == Dialect.POSTGRESQL ? "bigint[]" : "ARRAY<INT64>";
113+
case NUMERIC:
114+
return "ARRAY<NUMERIC>";
115+
case PG_NUMERIC:
116+
return "numeric[]";
117+
case STRING:
118+
return dialect == Dialect.POSTGRESQL ? "character varying[]" : "ARRAY<STRING>";
119+
case JSON:
120+
return "ARRAY<JSON>";
121+
case PG_JSONB:
122+
return "jsonb[]";
123+
case TIMESTAMP:
124+
return dialect == Dialect.POSTGRESQL
125+
? "timestamp with time zone[]"
126+
: "ARRAY<TIMESTAMP>";
127+
case STRUCT:
128+
return "ARRAY<STRUCT>";
129+
}
130+
default:
131+
return null;
132+
}
133+
}
134+
135+
/**
136+
* Extract Spanner type name from {@link java.sql.Types} code.
137+
*
138+
* @deprecated Use {@link #getSpannerTypeName(Type, Dialect)} instead.
139+
*/
140+
@Deprecated
73141
static String getSpannerTypeName(int sqlType) {
74142
if (sqlType == Types.BOOLEAN) return Type.bool().getCode().name();
75143
if (sqlType == Types.BINARY) return Type.bytes().getCode().name();
@@ -89,7 +157,12 @@ static String getSpannerTypeName(int sqlType) {
89157
return OTHER_NAME;
90158
}
91159

92-
/** Get corresponding Java class name from {@link java.sql.Types} code. */
160+
/**
161+
* Get corresponding Java class name from {@link java.sql.Types} code.
162+
*
163+
* @deprecated Use {@link #getClassName(Type)} instead.
164+
*/
165+
@Deprecated
93166
static String getClassName(int sqlType) {
94167
if (sqlType == Types.BOOLEAN) return Boolean.class.getName();
95168
if (sqlType == Types.BINARY) return Byte[].class.getName();

src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,14 +390,18 @@ public Set<? extends Class<?>> getSupportedJavaClasses() {
390390

391391
public static JdbcDataType getType(Class<?> clazz) {
392392
for (JdbcDataType type : JdbcDataType.values()) {
393-
if (type.getSupportedJavaClasses().contains(clazz)) return type;
393+
if (type.getSupportedJavaClasses().contains(clazz)) {
394+
return type;
395+
}
394396
}
395397
return null;
396398
}
397399

398400
public static JdbcDataType getType(Code code) {
399401
for (JdbcDataType type : JdbcDataType.values()) {
400-
if (type.getCode() == code) return type;
402+
if (type.getCode() == code) {
403+
return type;
404+
}
401405
}
402406
return null;
403407
}

src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java

Lines changed: 110 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616

1717
package com.google.cloud.spanner.jdbc;
1818

19-
import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo;
19+
import com.google.cloud.spanner.JdbcDataTypeConverter;
20+
import com.google.cloud.spanner.ResultSet;
21+
import com.google.rpc.Code;
22+
import com.google.spanner.v1.StructType;
23+
import com.google.spanner.v1.StructType.Field;
24+
import com.google.spanner.v1.Type;
25+
import com.google.spanner.v1.TypeCode;
2026
import java.math.BigDecimal;
2127
import java.sql.Date;
2228
import java.sql.ParameterMetaData;
@@ -29,9 +35,23 @@
2935
class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMetaData {
3036
private final JdbcPreparedStatement statement;
3137

32-
JdbcParameterMetaData(JdbcPreparedStatement statement) throws SQLException {
38+
private final StructType parameters;
39+
40+
JdbcParameterMetaData(JdbcPreparedStatement statement, ResultSet resultSet) {
3341
this.statement = statement;
34-
statement.getParameters().fetchMetaData(statement.getConnection());
42+
this.parameters = resultSet.getMetadata().getUndeclaredParameters();
43+
}
44+
45+
private Field getField(int param) throws SQLException {
46+
JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param);
47+
String paramName = "p" + param;
48+
return parameters.getFieldsList().stream()
49+
.filter(field -> field.getName().equals(paramName))
50+
.findAny()
51+
.orElseThrow(
52+
() ->
53+
JdbcSqlExceptionFactory.of(
54+
"Unknown parameter: " + paramName, Code.INVALID_ARGUMENT));
3555
}
3656

3757
@Override
@@ -41,8 +61,7 @@ public boolean isClosed() {
4161

4262
@Override
4363
public int getParameterCount() {
44-
ParametersInfo info = statement.getParametersInfo();
45-
return info.numberOfParameters;
64+
return parameters.getFieldsCount();
4665
}
4766

4867
@Override
@@ -53,7 +72,7 @@ public int isNullable(int param) {
5372
}
5473

5574
@Override
56-
public boolean isSigned(int param) {
75+
public boolean isSigned(int param) throws SQLException {
5776
int type = getParameterType(param);
5877
return type == Types.DOUBLE
5978
|| type == Types.FLOAT
@@ -77,9 +96,34 @@ public int getScale(int param) {
7796
}
7897

7998
@Override
80-
public int getParameterType(int param) {
99+
public int getParameterType(int param) throws SQLException {
100+
JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param);
101+
int typeFromValue = getParameterTypeFromValue(param);
102+
if (typeFromValue != Types.OTHER) {
103+
return typeFromValue;
104+
}
105+
106+
Type type = getField(param).getType();
107+
// JDBC only has a generic ARRAY type.
108+
if (type.getCode() == TypeCode.ARRAY) {
109+
return Types.ARRAY;
110+
}
111+
JdbcDataType jdbcDataType =
112+
JdbcDataType.getType(JdbcDataTypeConverter.toSpannerType(type).getCode());
113+
return jdbcDataType == null ? Types.OTHER : jdbcDataType.getSqlType();
114+
}
115+
116+
/**
117+
* This method returns the parameter type based on the parameter value that has been set. This was
118+
* previously the only way to get the parameter types of a statement. Cloud Spanner can now return
119+
* the types and names of parameters in a SQL string, which is what this method should return.
120+
*/
121+
// TODO: Remove this method for the next major version bump.
122+
private int getParameterTypeFromValue(int param) {
81123
Integer type = statement.getParameters().getType(param);
82-
if (type != null) return type;
124+
if (type != null) {
125+
return type;
126+
}
83127

84128
Object value = statement.getParameters().getParameter(param);
85129
if (value == null) {
@@ -116,16 +160,49 @@ public int getParameterType(int param) {
116160
}
117161

118162
@Override
119-
public String getParameterTypeName(int param) {
120-
return getSpannerTypeName(getParameterType(param));
163+
public String getParameterTypeName(int param) throws SQLException {
164+
JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param);
165+
String typeNameFromValue = getParameterTypeNameFromValue(param);
166+
if (typeNameFromValue != null) {
167+
return typeNameFromValue;
168+
}
169+
170+
com.google.cloud.spanner.Type type =
171+
JdbcDataTypeConverter.toSpannerType(getField(param).getType());
172+
return getSpannerTypeName(type, statement.getConnection().getDialect());
173+
}
174+
175+
private String getParameterTypeNameFromValue(int param) {
176+
int type = getParameterTypeFromValue(param);
177+
if (type != Types.OTHER) {
178+
return getSpannerTypeName(type);
179+
}
180+
return null;
121181
}
122182

123183
@Override
124-
public String getParameterClassName(int param) {
184+
public String getParameterClassName(int param) throws SQLException {
185+
JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param);
186+
String classNameFromValue = getParameterClassNameFromValue(param);
187+
if (classNameFromValue != null) {
188+
return classNameFromValue;
189+
}
190+
191+
com.google.cloud.spanner.Type type =
192+
JdbcDataTypeConverter.toSpannerType(getField(param).getType());
193+
return getClassName(type);
194+
}
195+
196+
// TODO: Remove this method for the next major version bump.
197+
private String getParameterClassNameFromValue(int param) {
125198
Object value = statement.getParameters().getParameter(param);
126-
if (value != null) return value.getClass().getName();
199+
if (value != null) {
200+
return value.getClass().getName();
201+
}
127202
Integer type = statement.getParameters().getType(param);
128-
if (type != null) return getClassName(type);
203+
if (type != null) {
204+
return getClassName(type);
205+
}
129206
return null;
130207
}
131208

@@ -136,22 +213,26 @@ public int getParameterMode(int param) {
136213

137214
@Override
138215
public String toString() {
139-
StringBuilder res = new StringBuilder();
140-
res.append("CloudSpannerPreparedStatementParameterMetaData, parameter count: ")
141-
.append(getParameterCount());
142-
for (int param = 1; param <= getParameterCount(); param++) {
143-
res.append("\nParameter ")
144-
.append(param)
145-
.append(":\n\t Class name: ")
146-
.append(getParameterClassName(param));
147-
res.append(",\n\t Parameter type name: ").append(getParameterTypeName(param));
148-
res.append(",\n\t Parameter type: ").append(getParameterType(param));
149-
res.append(",\n\t Parameter precision: ").append(getPrecision(param));
150-
res.append(",\n\t Parameter scale: ").append(getScale(param));
151-
res.append(",\n\t Parameter signed: ").append(isSigned(param));
152-
res.append(",\n\t Parameter nullable: ").append(isNullable(param));
153-
res.append(",\n\t Parameter mode: ").append(getParameterMode(param));
216+
try {
217+
StringBuilder res = new StringBuilder();
218+
res.append("CloudSpannerPreparedStatementParameterMetaData, parameter count: ")
219+
.append(getParameterCount());
220+
for (int param = 1; param <= getParameterCount(); param++) {
221+
res.append("\nParameter ")
222+
.append(param)
223+
.append(":\n\t Class name: ")
224+
.append(getParameterClassName(param));
225+
res.append(",\n\t Parameter type name: ").append(getParameterTypeName(param));
226+
res.append(",\n\t Parameter type: ").append(getParameterType(param));
227+
res.append(",\n\t Parameter precision: ").append(getPrecision(param));
228+
res.append(",\n\t Parameter scale: ").append(getScale(param));
229+
res.append(",\n\t Parameter signed: ").append(isSigned(param));
230+
res.append(",\n\t Parameter nullable: ").append(isNullable(param));
231+
res.append(",\n\t Parameter mode: ").append(getParameterMode(param));
232+
}
233+
return res.toString();
234+
} catch (SQLException exception) {
235+
return "Failed to get parameter metadata: " + exception;
154236
}
155-
return res.toString();
156237
}
157238
}

src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class JdbcPreparedStatement extends AbstractJdbcPreparedStatement
4040
private static final char POS_PARAM_CHAR = '?';
4141
private final String sql;
4242
private final ParametersInfo parameters;
43+
private JdbcParameterMetaData cachedParameterMetadata;
4344
private final ImmutableList<String> generatedKeysColumns;
4445

4546
JdbcPreparedStatement(
@@ -118,7 +119,34 @@ public void addBatch() throws SQLException {
118119
@Override
119120
public JdbcParameterMetaData getParameterMetaData() throws SQLException {
120121
checkClosed();
121-
return new JdbcParameterMetaData(this);
122+
if (cachedParameterMetadata == null) {
123+
if (getConnection().getParser().isUpdateStatement(sql)
124+
&& !getConnection().getParser().checkReturningClause(sql)) {
125+
cachedParameterMetadata = getParameterMetadataForUpdate();
126+
} else {
127+
cachedParameterMetadata = getParameterMetadataForQuery();
128+
}
129+
}
130+
return cachedParameterMetadata;
131+
}
132+
133+
private JdbcParameterMetaData getParameterMetadataForUpdate() {
134+
try (com.google.cloud.spanner.ResultSet resultSet =
135+
getConnection()
136+
.getSpannerConnection()
137+
.analyzeUpdateStatement(
138+
Statement.of(parameters.sqlWithNamedParameters), QueryAnalyzeMode.PLAN)) {
139+
return new JdbcParameterMetaData(this, resultSet);
140+
}
141+
}
142+
143+
private JdbcParameterMetaData getParameterMetadataForQuery() {
144+
try (com.google.cloud.spanner.ResultSet resultSet =
145+
getConnection()
146+
.getSpannerConnection()
147+
.analyzeQuery(Statement.of(parameters.sqlWithNamedParameters), QueryAnalyzeMode.PLAN)) {
148+
return new JdbcParameterMetaData(this, resultSet);
149+
}
122150
}
123151

124152
@Override

0 commit comments

Comments
 (0)