-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support PreparedStatement#getParameterMetaData() #1218
Changes from all commits
d2d9fef
2cd594d
8189ef9
ae4c518
9d33376
ffdbb64
0cbcaee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* 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 | ||
* | ||
* http://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.spanner; | ||
|
||
import com.google.api.core.InternalApi; | ||
|
||
@InternalApi | ||
public class JdbcDataTypeConverter { | ||
|
||
/** Converts a protobuf type to a Spanner type. */ | ||
@InternalApi | ||
public static Type toSpannerType(com.google.spanner.v1.Type proto) { | ||
return Type.fromProto(proto); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,13 @@ | |
|
||
package com.google.cloud.spanner.jdbc; | ||
|
||
import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo; | ||
import com.google.cloud.spanner.JdbcDataTypeConverter; | ||
import com.google.cloud.spanner.ResultSet; | ||
import com.google.rpc.Code; | ||
import com.google.spanner.v1.StructType; | ||
import com.google.spanner.v1.StructType.Field; | ||
import com.google.spanner.v1.Type; | ||
import com.google.spanner.v1.TypeCode; | ||
import java.math.BigDecimal; | ||
import java.sql.Date; | ||
import java.sql.ParameterMetaData; | ||
|
@@ -29,9 +35,23 @@ | |
class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMetaData { | ||
private final JdbcPreparedStatement statement; | ||
|
||
JdbcParameterMetaData(JdbcPreparedStatement statement) throws SQLException { | ||
private final StructType parameters; | ||
|
||
JdbcParameterMetaData(JdbcPreparedStatement statement, ResultSet resultSet) { | ||
this.statement = statement; | ||
statement.getParameters().fetchMetaData(statement.getConnection()); | ||
this.parameters = resultSet.getMetadata().getUndeclaredParameters(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on my understanding of this field from Cloud Spanner code, UndeclaredParameters field only returns types for untyped parameters, i.e. when the types of the parameters are not passed in from the client and they are sent as proto.value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case the name of this field in the proto definition is a bit unfortunate. It says 'undeclared parameters', but it actually returns information about all parameters, including the ones that included a type and/or value when it was sent by the client. So if the user included type information for one or more parameters, it will also show up in this map. |
||
} | ||
|
||
private Field getField(int param) throws SQLException { | ||
JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param); | ||
String paramName = "p" + param; | ||
return parameters.getFieldsList().stream() | ||
.filter(field -> field.getName().equals(paramName)) | ||
.findAny() | ||
.orElseThrow( | ||
() -> | ||
JdbcSqlExceptionFactory.of( | ||
"Unknown parameter: " + paramName, Code.INVALID_ARGUMENT)); | ||
} | ||
|
||
@Override | ||
|
@@ -41,8 +61,7 @@ public boolean isClosed() { | |
|
||
@Override | ||
public int getParameterCount() { | ||
ParametersInfo info = statement.getParametersInfo(); | ||
return info.numberOfParameters; | ||
return parameters.getFieldsCount(); | ||
} | ||
|
||
@Override | ||
|
@@ -53,7 +72,7 @@ public int isNullable(int param) { | |
} | ||
|
||
@Override | ||
public boolean isSigned(int param) { | ||
public boolean isSigned(int param) throws SQLException { | ||
int type = getParameterType(param); | ||
return type == Types.DOUBLE | ||
|| type == Types.FLOAT | ||
|
@@ -77,9 +96,34 @@ public int getScale(int param) { | |
} | ||
|
||
@Override | ||
public int getParameterType(int param) { | ||
public int getParameterType(int param) throws SQLException { | ||
JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param); | ||
int typeFromValue = getParameterTypeFromValue(param); | ||
if (typeFromValue != Types.OTHER) { | ||
return typeFromValue; | ||
} | ||
|
||
Type type = getField(param).getType(); | ||
// JDBC only has a generic ARRAY type. | ||
if (type.getCode() == TypeCode.ARRAY) { | ||
return Types.ARRAY; | ||
} | ||
JdbcDataType jdbcDataType = | ||
JdbcDataType.getType(JdbcDataTypeConverter.toSpannerType(type).getCode()); | ||
return jdbcDataType == null ? Types.OTHER : jdbcDataType.getSqlType(); | ||
} | ||
|
||
/** | ||
* This method returns the parameter type based on the parameter value that has been set. This was | ||
* previously the only way to get the parameter types of a statement. Cloud Spanner can now return | ||
* the types and names of parameters in a SQL string, which is what this method should return. | ||
*/ | ||
// TODO: Remove this method for the next major version bump. | ||
private int getParameterTypeFromValue(int param) { | ||
Integer type = statement.getParameters().getType(param); | ||
if (type != null) return type; | ||
if (type != null) { | ||
return type; | ||
} | ||
|
||
Object value = statement.getParameters().getParameter(param); | ||
if (value == null) { | ||
|
@@ -116,16 +160,49 @@ public int getParameterType(int param) { | |
} | ||
|
||
@Override | ||
public String getParameterTypeName(int param) { | ||
return getSpannerTypeName(getParameterType(param)); | ||
public String getParameterTypeName(int param) throws SQLException { | ||
JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param); | ||
String typeNameFromValue = getParameterTypeNameFromValue(param); | ||
if (typeNameFromValue != null) { | ||
return typeNameFromValue; | ||
} | ||
|
||
com.google.cloud.spanner.Type type = | ||
JdbcDataTypeConverter.toSpannerType(getField(param).getType()); | ||
return getSpannerTypeName(type, statement.getConnection().getDialect()); | ||
} | ||
|
||
private String getParameterTypeNameFromValue(int param) { | ||
int type = getParameterTypeFromValue(param); | ||
if (type != Types.OTHER) { | ||
return getSpannerTypeName(type); | ||
} | ||
return null; | ||
} | ||
|
||
@Override | ||
public String getParameterClassName(int param) { | ||
public String getParameterClassName(int param) throws SQLException { | ||
JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param); | ||
String classNameFromValue = getParameterClassNameFromValue(param); | ||
if (classNameFromValue != null) { | ||
return classNameFromValue; | ||
} | ||
|
||
com.google.cloud.spanner.Type type = | ||
JdbcDataTypeConverter.toSpannerType(getField(param).getType()); | ||
return getClassName(type); | ||
} | ||
|
||
// TODO: Remove this method for the next major version bump. | ||
private String getParameterClassNameFromValue(int param) { | ||
Object value = statement.getParameters().getParameter(param); | ||
if (value != null) return value.getClass().getName(); | ||
if (value != null) { | ||
return value.getClass().getName(); | ||
} | ||
Integer type = statement.getParameters().getType(param); | ||
if (type != null) return getClassName(type); | ||
if (type != null) { | ||
return getClassName(type); | ||
} | ||
return null; | ||
} | ||
|
||
|
@@ -136,22 +213,26 @@ public int getParameterMode(int param) { | |
|
||
@Override | ||
public String toString() { | ||
StringBuilder res = new StringBuilder(); | ||
res.append("CloudSpannerPreparedStatementParameterMetaData, parameter count: ") | ||
.append(getParameterCount()); | ||
for (int param = 1; param <= getParameterCount(); param++) { | ||
res.append("\nParameter ") | ||
.append(param) | ||
.append(":\n\t Class name: ") | ||
.append(getParameterClassName(param)); | ||
res.append(",\n\t Parameter type name: ").append(getParameterTypeName(param)); | ||
res.append(",\n\t Parameter type: ").append(getParameterType(param)); | ||
res.append(",\n\t Parameter precision: ").append(getPrecision(param)); | ||
res.append(",\n\t Parameter scale: ").append(getScale(param)); | ||
res.append(",\n\t Parameter signed: ").append(isSigned(param)); | ||
res.append(",\n\t Parameter nullable: ").append(isNullable(param)); | ||
res.append(",\n\t Parameter mode: ").append(getParameterMode(param)); | ||
try { | ||
StringBuilder res = new StringBuilder(); | ||
res.append("CloudSpannerPreparedStatementParameterMetaData, parameter count: ") | ||
.append(getParameterCount()); | ||
for (int param = 1; param <= getParameterCount(); param++) { | ||
res.append("\nParameter ") | ||
.append(param) | ||
.append(":\n\t Class name: ") | ||
.append(getParameterClassName(param)); | ||
res.append(",\n\t Parameter type name: ").append(getParameterTypeName(param)); | ||
res.append(",\n\t Parameter type: ").append(getParameterType(param)); | ||
res.append(",\n\t Parameter precision: ").append(getPrecision(param)); | ||
res.append(",\n\t Parameter scale: ").append(getScale(param)); | ||
res.append(",\n\t Parameter signed: ").append(isSigned(param)); | ||
res.append(",\n\t Parameter nullable: ").append(isNullable(param)); | ||
res.append(",\n\t Parameter mode: ").append(getParameterMode(param)); | ||
} | ||
return res.toString(); | ||
} catch (SQLException exception) { | ||
return "Failed to get parameter metadata: " + exception; | ||
} | ||
return res.toString(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would creating enum here( with both names for each type) improve readability?
In general another possibility could be to have a structure like a type manager for mapping pg, googlesql, java types, so the type checking and conversions like in functions such as ParameterTypeFromValue can also be simplified from one place?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My plan was to add that to the Java client library instead of directly in the JDBC driver, so we can use it in more places. I've expedited that and added a PR for it here: googleapis/java-spanner#2763
When that has been merged, a new release of the client library has been cut, and the dependency in the JDBC driver updated to the new version, then we can simplify this implementation.