Skip to content
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

Merged
merged 7 commits into from Dec 22, 2023
@@ -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);
}
}
Expand Up @@ -16,6 +16,7 @@

package com.google.cloud.spanner.jdbc;

import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.Code;
import com.google.common.base.Preconditions;
Expand Down Expand Up @@ -69,7 +70,74 @@ static int extractColumnType(Type type) {
}
}

/** Extract Spanner type name from {@link java.sql.Types} code. */
static String getSpannerTypeName(Type type, Dialect dialect) {
Copy link
Collaborator

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?

Copy link
Collaborator Author

@olavloite olavloite Dec 20, 2023

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.

// TODO: Move this into the Java Connection API.
Preconditions.checkNotNull(type);
switch (type.getCode()) {
case BOOL:
return dialect == Dialect.POSTGRESQL ? "boolean" : "BOOL";
case BYTES:
return dialect == Dialect.POSTGRESQL ? "bytea" : "BYTES";
case DATE:
return dialect == Dialect.POSTGRESQL ? "date" : "DATE";
case FLOAT64:
return dialect == Dialect.POSTGRESQL ? "double precision" : "FLOAT64";
case INT64:
return dialect == Dialect.POSTGRESQL ? "bigint" : "INT64";
case NUMERIC:
return "NUMERIC";
case PG_NUMERIC:
return "numeric";
case STRING:
return dialect == Dialect.POSTGRESQL ? "varchar" : "STRING";
case JSON:
return "JSON";
case PG_JSONB:
return "jsonb";
case TIMESTAMP:
return dialect == Dialect.POSTGRESQL ? "timestamp with time zone" : "TIMESTAMP";
case STRUCT:
return "STRUCT";
case ARRAY:
switch (type.getArrayElementType().getCode()) {
case BOOL:
return dialect == Dialect.POSTGRESQL ? "boolean[]" : "ARRAY<BOOL>";
case BYTES:
return dialect == Dialect.POSTGRESQL ? "bytea[]" : "ARRAY<BYTES>";
case DATE:
return dialect == Dialect.POSTGRESQL ? "date[]" : "ARRAY<DATE>";
case FLOAT64:
return dialect == Dialect.POSTGRESQL ? "double precision[]" : "ARRAY<FLOAT64>";
case INT64:
return dialect == Dialect.POSTGRESQL ? "bigint[]" : "ARRAY<INT64>";
case NUMERIC:
return "ARRAY<NUMERIC>";
case PG_NUMERIC:
return "numeric[]";
case STRING:
return dialect == Dialect.POSTGRESQL ? "varchar[]" : "ARRAY<STRING>";
case JSON:
return "ARRAY<JSON>";
case PG_JSONB:
return "jsonb[]";
case TIMESTAMP:
return dialect == Dialect.POSTGRESQL
? "timestamp with time zone[]"
: "ARRAY<TIMESTAMP>";
case STRUCT:
return "ARRAY<STRUCT>";
}
default:
return null;
}
}

/**
* Extract Spanner type name from {@link java.sql.Types} code.
*
* @deprecated Use {@link #getSpannerTypeName(Type, Dialect)} instead.
*/
@Deprecated
static String getSpannerTypeName(int sqlType) {
if (sqlType == Types.BOOLEAN) return Type.bool().getCode().name();
if (sqlType == Types.BINARY) return Type.bytes().getCode().name();
Expand All @@ -89,7 +157,12 @@ static String getSpannerTypeName(int sqlType) {
return OTHER_NAME;
}

/** Get corresponding Java class name from {@link java.sql.Types} code. */
/**
* Get corresponding Java class name from {@link java.sql.Types} code.
*
* @deprecated Use {@link #getClassName(Type)} instead.
*/
@Deprecated
static String getClassName(int sqlType) {
if (sqlType == Types.BOOLEAN) return Boolean.class.getName();
if (sqlType == Types.BINARY) return Byte[].class.getName();
Expand Down
Expand Up @@ -390,14 +390,18 @@ public Set<? extends Class<?>> getSupportedJavaClasses() {

public static JdbcDataType getType(Class<?> clazz) {
for (JdbcDataType type : JdbcDataType.values()) {
if (type.getSupportedJavaClasses().contains(clazz)) return type;
if (type.getSupportedJavaClasses().contains(clazz)) {
return type;
}
}
return null;
}

public static JdbcDataType getType(Code code) {
for (JdbcDataType type : JdbcDataType.values()) {
if (type.getCode() == code) return type;
if (type.getCode() == code) {
return type;
}
}
return null;
}
Expand Down
Expand Up @@ -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;
Expand All @@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
So is this the case for all the prepared statements we execute from the JDBC driver?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Expand All @@ -41,8 +61,7 @@ public boolean isClosed() {

@Override
public int getParameterCount() {
ParametersInfo info = statement.getParametersInfo();
return info.numberOfParameters;
return parameters.getFieldsCount();
}

@Override
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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();
}
}
Expand Up @@ -40,6 +40,7 @@ class JdbcPreparedStatement extends AbstractJdbcPreparedStatement
private static final char POS_PARAM_CHAR = '?';
private final String sql;
private final ParametersInfo parameters;
private JdbcParameterMetaData cachedParameterMetadata;
private final ImmutableList<String> generatedKeysColumns;

JdbcPreparedStatement(
Expand Down Expand Up @@ -118,7 +119,34 @@ public void addBatch() throws SQLException {
@Override
public JdbcParameterMetaData getParameterMetaData() throws SQLException {
checkClosed();
return new JdbcParameterMetaData(this);
if (cachedParameterMetadata == null) {
if (getConnection().getParser().isUpdateStatement(sql)
&& !getConnection().getParser().checkReturningClause(sql)) {
cachedParameterMetadata = getParameterMetadataForUpdate();
} else {
cachedParameterMetadata = getParameterMetadataForQuery();
}
}
return cachedParameterMetadata;
}

private JdbcParameterMetaData getParameterMetadataForUpdate() {
try (com.google.cloud.spanner.ResultSet resultSet =
getConnection()
.getSpannerConnection()
.analyzeUpdateStatement(
Statement.of(parameters.sqlWithNamedParameters), QueryAnalyzeMode.PLAN)) {
return new JdbcParameterMetaData(this, resultSet);
}
}

private JdbcParameterMetaData getParameterMetadataForQuery() {
try (com.google.cloud.spanner.ResultSet resultSet =
getConnection()
.getSpannerConnection()
.analyzeQuery(Statement.of(parameters.sqlWithNamedParameters), QueryAnalyzeMode.PLAN)) {
return new JdbcParameterMetaData(this, resultSet);
}
}

@Override
Expand Down