Skip to content

Commit

Permalink
feat: allow get/set Spanner Value instances (#454)
Browse files Browse the repository at this point in the history
Adds support for getting a value from a `java.sql.ResultSet` as a `com.google.cloud.spanner.Value` instance, and for setting a parameter on a `java.sql.PreparedStatement` using a `com.google.cloud.spanner.Value` instance.

Fixes #452
  • Loading branch information
olavloite committed May 4, 2021
1 parent e638162 commit d6935b8
Show file tree
Hide file tree
Showing 8 changed files with 454 additions and 32 deletions.
Expand Up @@ -198,7 +198,7 @@ public void setObject(int parameterIndex, Object value, int targetSqlType) throw
@Override
public void setObject(int parameterIndex, Object value) throws SQLException {
checkClosed();
parameters.setParameter(parameterIndex, value, null);
parameters.setParameter(parameterIndex, value, (SQLType) null);
}

@Override
Expand Down
72 changes: 62 additions & 10 deletions src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
Expand Up @@ -19,6 +19,7 @@
import com.google.cloud.ByteArray;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Statement.Builder;
import com.google.cloud.spanner.Value;
import com.google.cloud.spanner.ValueBinder;
import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
import com.google.common.io.CharStreams;
Expand All @@ -39,6 +40,7 @@
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLType;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
Expand Down Expand Up @@ -129,7 +131,8 @@ void setColumn(int parameterIndex, String column) throws SQLException {
getParameter(parameterIndex),
getType(parameterIndex),
getScaleOrLength(parameterIndex),
column);
column,
null);
}

void setType(int parameterIndex, Integer type) throws SQLException {
Expand All @@ -138,26 +141,71 @@ void setType(int parameterIndex, Integer type) throws SQLException {
getParameter(parameterIndex),
type,
getScaleOrLength(parameterIndex),
getColumn(parameterIndex));
getColumn(parameterIndex),
null);
}

/** Sets a parameter value. The type will be determined based on the type of the value. */
void setParameter(int parameterIndex, Object value) throws SQLException {
setParameter(parameterIndex, value, null, null, null, null);
}

/** Sets a parameter value as the specified vendor-specific {@link SQLType}. */
void setParameter(int parameterIndex, Object value, SQLType sqlType) throws SQLException {
setParameter(parameterIndex, value, null, null, null, sqlType);
}

/**
* Sets a parameter value as the specified vendor-specific {@link SQLType} with the specified
* scale or length. This method is only here to support the {@link
* PreparedStatement#setObject(int, Object, SQLType, int)} method.
*/
void setParameter(int parameterIndex, Object value, SQLType sqlType, Integer scaleOrLength)
throws SQLException {
setParameter(parameterIndex, value, null, scaleOrLength, null, sqlType);
}

/**
* Sets a parameter value as the specified sql type. The type can be one of the constants in
* {@link Types} or a vendor specific type code supplied by a vendor specific {@link SQLType}.
*/
void setParameter(int parameterIndex, Object value, Integer sqlType) throws SQLException {
setParameter(parameterIndex, value, sqlType, null);
}

/**
* Sets a parameter value as the specified sql type with the specified scale or length. The type
* can be one of the constants in {@link Types} or a vendor specific type code supplied by a
* vendor specific {@link SQLType}.
*/
void setParameter(int parameterIndex, Object value, Integer sqlType, Integer scaleOrLength)
throws SQLException {
setParameter(parameterIndex, value, sqlType, scaleOrLength, null);
setParameter(parameterIndex, value, sqlType, scaleOrLength, null, null);
}

/**
* Sets a parameter value as the specified sql type with the specified scale or length. Any {@link
* SQLType} instance will take precedence over sqlType. The type can be one of the constants in
* {@link Types} or a vendor specific type code supplied by a vendor specific {@link SQLType}.
*/
void setParameter(
int parameterIndex, Object value, Integer sqlType, Integer scaleOrLength, String column)
int parameterIndex,
Object value,
Integer sqlType,
Integer scaleOrLength,
String column,
SQLType sqlTypeObject)
throws SQLException {
// check that only valid type/value combinations are entered
if (sqlType != null) {
checkTypeAndValueSupported(value, sqlType);
}
// set the parameter
// Ignore the sql type if the application has created a Spanner Value object.
if (!(value instanceof Value)) {
// check that only valid type/value combinations are entered
if (sqlTypeObject != null && sqlType == null) {
sqlType = sqlTypeObject.getVendorTypeNumber();
}
if (sqlType != null) {
checkTypeAndValueSupported(value, sqlType);
}
} // set the parameter
highestIndex = Math.max(parameterIndex, highestIndex);
int arrayIndex = parameterIndex - 1;
if (arrayIndex >= parametersList.size() || parametersList.get(arrayIndex) == null) {
Expand Down Expand Up @@ -416,7 +464,11 @@ Builder bindParameterValue(ValueBinder<Builder> binder, int index) throws SQLExc
/** Set a value from a JDBC parameter on a Spanner {@link Statement}. */
Builder setValue(ValueBinder<Builder> binder, Object value, Integer sqlType) throws SQLException {
Builder res;
if (sqlType != null && sqlType == Types.ARRAY) {
if (value instanceof Value) {
// If a Value has been constructed, then that should override any sqlType that might have been
// supplied.
res = binder.to((Value) value);
} else if (sqlType != null && sqlType == Types.ARRAY) {
if (value instanceof Array) {
Array array = (Array) value;
value = array.getArray();
Expand Down
Expand Up @@ -24,6 +24,7 @@
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.connection.StatementParser;
import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
Expand All @@ -48,7 +49,8 @@ ParametersInfo getParametersInfo() throws SQLException {
return parameters;
}

private Statement createStatement() throws SQLException {
@VisibleForTesting
Statement createStatement() throws SQLException {
ParametersInfo paramInfo = getParametersInfo();
Statement.Builder builder = Statement.newBuilder(paramInfo.sqlWithNamedParameters);
for (int index = 1; index <= getParameters().getHighestIndex(); index++) {
Expand Down
Expand Up @@ -21,13 +21,15 @@
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.Code;
import com.google.cloud.spanner.Value;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.sql.Array;
import java.sql.SQLException;
import java.sql.Time;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -59,8 +61,13 @@ static Object convert(Object value, Type type, Class<?> targetType) throws SQLEx
JdbcPreconditions.checkArgument(targetType != null, "targetType may not be null");
checkValidTypeAndValueForConvert(type, value);

if (value == null) return null;
if (value == null) {
return null;
}
try {
if (targetType.equals(Value.class)) {
return convertToSpannerValue(value, type);
}
if (targetType.equals(String.class)) {
if (type.getCode() == Code.BYTES) return new String((byte[]) value, UTF8);
if (type.getCode() == Code.TIMESTAMP) {
Expand Down Expand Up @@ -155,6 +162,58 @@ static Object convert(Object value, Type type, Class<?> targetType) throws SQLEx
com.google.rpc.Code.INVALID_ARGUMENT);
}

private static Value convertToSpannerValue(Object value, Type type) throws SQLException {
switch (type.getCode()) {
case ARRAY:
switch (type.getArrayElementType().getCode()) {
case BOOL:
return Value.boolArray(Arrays.asList((Boolean[]) ((java.sql.Array) value).getArray()));
case BYTES:
return Value.bytesArray(toGoogleBytes((byte[][]) ((java.sql.Array) value).getArray()));
case DATE:
return Value.dateArray(
toGoogleDates((java.sql.Date[]) ((java.sql.Array) value).getArray()));
case FLOAT64:
return Value.float64Array(
Arrays.asList((Double[]) ((java.sql.Array) value).getArray()));
case INT64:
return Value.int64Array(Arrays.asList((Long[]) ((java.sql.Array) value).getArray()));
case NUMERIC:
return Value.numericArray(
Arrays.asList((BigDecimal[]) ((java.sql.Array) value).getArray()));
case STRING:
return Value.stringArray(Arrays.asList((String[]) ((java.sql.Array) value).getArray()));
case TIMESTAMP:
return Value.timestampArray(
toGoogleTimestamps((java.sql.Timestamp[]) ((java.sql.Array) value).getArray()));
case STRUCT:
default:
throw JdbcSqlExceptionFactory.of(
"invalid argument: " + value, com.google.rpc.Code.INVALID_ARGUMENT);
}
case BOOL:
return Value.bool((Boolean) value);
case BYTES:
return Value.bytes(ByteArray.copyFrom((byte[]) value));
case DATE:
return Value.date(toGoogleDate((java.sql.Date) value));
case FLOAT64:
return Value.float64((Double) value);
case INT64:
return Value.int64((Long) value);
case NUMERIC:
return Value.numeric((BigDecimal) value);
case STRING:
return Value.string((String) value);
case TIMESTAMP:
return Value.timestamp(toGoogleTimestamp((java.sql.Timestamp) value));
case STRUCT:
default:
throw JdbcSqlExceptionFactory.of(
"invalid argument: " + value, com.google.rpc.Code.INVALID_ARGUMENT);
}
}

private static void checkValidTypeAndValueForConvert(Type type, Object value)
throws SQLException {
if (value == null) return;
Expand Down
Expand Up @@ -44,6 +44,7 @@
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -52,6 +53,61 @@
@RunWith(JUnit4.class)
public class JdbcParameterStoreTest {

/**
* Tests setting a {@link Value} as a parameter value.
*
* @throws SQLException
*/
@Test
public void testSetValueAsParameter() throws SQLException {
JdbcParameterStore params = new JdbcParameterStore();
params.setParameter(1, Value.bool(true));
verifyParameter(params, Value.bool(true));
params.setParameter(1, Value.bytes(ByteArray.copyFrom("test")));
verifyParameter(params, Value.bytes(ByteArray.copyFrom("test")));
params.setParameter(1, Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3)));
verifyParameter(params, Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3)));
params.setParameter(1, Value.float64(3.14d));
verifyParameter(params, Value.float64(3.14d));
params.setParameter(1, Value.int64(1L));
verifyParameter(params, Value.int64(1L));
params.setParameter(1, Value.numeric(BigDecimal.TEN));
verifyParameter(params, Value.numeric(BigDecimal.TEN));
params.setParameter(1, Value.string("test"));
verifyParameter(params, Value.string("test"));
params.setParameter(
1, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(9999L, 101)));
verifyParameter(
params, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(9999L, 101)));

params.setParameter(1, Value.boolArray(new boolean[] {true, false}));
verifyParameter(params, Value.boolArray(new boolean[] {true, false}));
params.setParameter(1, Value.bytesArray(Collections.singleton(ByteArray.copyFrom("test"))));
verifyParameter(params, Value.bytesArray(Collections.singleton(ByteArray.copyFrom("test"))));
params.setParameter(
1,
Value.dateArray(Collections.singleton(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3))));
verifyParameter(
params,
Value.dateArray(Collections.singleton(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3))));
params.setParameter(1, Value.float64Array(Collections.singleton(3.14d)));
verifyParameter(params, Value.float64Array(Collections.singleton(3.14d)));
params.setParameter(1, Value.int64Array(Collections.singleton(1L)));
verifyParameter(params, Value.int64Array(Collections.singleton(1L)));
params.setParameter(1, Value.numericArray(Collections.singleton(BigDecimal.TEN)));
verifyParameter(params, Value.numericArray(Collections.singleton(BigDecimal.TEN)));
params.setParameter(1, Value.stringArray(Collections.singleton("test")));
verifyParameter(params, Value.stringArray(Collections.singleton("test")));
params.setParameter(
1,
Value.timestampArray(
Collections.singleton(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(9999L, 101))));
verifyParameter(
params,
Value.timestampArray(
Collections.singleton(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(9999L, 101))));
}

/** Tests setting a parameter value together with a sql type */
@SuppressWarnings("deprecation")
@Test
Expand Down Expand Up @@ -422,55 +478,55 @@ private void assertInvalidParameter(JdbcParameterStore params, Object value, int
@Test
public void testSetParameterWithoutType() throws SQLException {
JdbcParameterStore params = new JdbcParameterStore();
params.setParameter(1, (byte) 1, null);
params.setParameter(1, (byte) 1, (Integer) null);
assertEquals(1, ((Byte) params.getParameter(1)).byteValue());
verifyParameter(params, Value.int64(1));
params.setParameter(1, (short) 1, null);
params.setParameter(1, (short) 1, (Integer) null);
assertEquals(1, ((Short) params.getParameter(1)).shortValue());
verifyParameter(params, Value.int64(1));
params.setParameter(1, 1, null);
params.setParameter(1, 1, (Integer) null);
assertEquals(1, ((Integer) params.getParameter(1)).intValue());
verifyParameter(params, Value.int64(1));
params.setParameter(1, 1L, null);
params.setParameter(1, 1L, (Integer) null);
assertEquals(1, ((Long) params.getParameter(1)).longValue());
verifyParameter(params, Value.int64(1));
params.setParameter(1, (float) 1, null);
params.setParameter(1, (float) 1, (Integer) null);
assertEquals(1.0f, ((Float) params.getParameter(1)).floatValue(), 0.0f);
verifyParameter(params, Value.float64(1));
params.setParameter(1, (double) 1, null);
params.setParameter(1, (double) 1, (Integer) null);
assertEquals(1.0d, ((Double) params.getParameter(1)).doubleValue(), 0.0d);
verifyParameter(params, Value.float64(1));
params.setParameter(1, new Date(1970 - 1900, 0, 1), null);
params.setParameter(1, new Date(1970 - 1900, 0, 1), (Integer) null);
assertEquals(new Date(1970 - 1900, 0, 1), params.getParameter(1));
verifyParameter(params, Value.date(com.google.cloud.Date.fromYearMonthDay(1970, 1, 1)));
params.setParameter(1, new Time(0L), null);
params.setParameter(1, new Time(0L), (Integer) null);
assertEquals(new Time(0L), params.getParameter(1));
verifyParameter(
params, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(0L, 0)));
params.setParameter(1, new Timestamp(0L), null);
params.setParameter(1, new Timestamp(0L), (Integer) null);
assertEquals(new Timestamp(0L), params.getParameter(1));
verifyParameter(
params, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(0L, 0)));
params.setParameter(1, new byte[] {1, 2, 3}, null);
params.setParameter(1, new byte[] {1, 2, 3}, (Integer) null);
assertArrayEquals(new byte[] {1, 2, 3}, (byte[]) params.getParameter(1));
verifyParameter(params, Value.bytes(ByteArray.copyFrom(new byte[] {1, 2, 3})));

params.setParameter(1, new JdbcBlob(new byte[] {1, 2, 3}), null);
params.setParameter(1, new JdbcBlob(new byte[] {1, 2, 3}), (Integer) null);
assertEquals(new JdbcBlob(new byte[] {1, 2, 3}), params.getParameter(1));
verifyParameter(params, Value.bytes(ByteArray.copyFrom(new byte[] {1, 2, 3})));
params.setParameter(1, new JdbcClob("test"), null);
params.setParameter(1, new JdbcClob("test"), (Integer) null);
assertEquals(new JdbcClob("test"), params.getParameter(1));
verifyParameter(params, Value.string("test"));
params.setParameter(1, true, null);
params.setParameter(1, true, (Integer) null);
assertTrue((Boolean) params.getParameter(1));
verifyParameter(params, Value.bool(true));
params.setParameter(1, "test", null);
params.setParameter(1, "test", (Integer) null);
assertEquals("test", params.getParameter(1));
verifyParameter(params, Value.string("test"));
params.setParameter(1, new JdbcClob("test"), null);
params.setParameter(1, new JdbcClob("test"), (Integer) null);
assertEquals(new JdbcClob("test"), params.getParameter(1));
verifyParameter(params, Value.string("test"));
params.setParameter(1, UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), null);
params.setParameter(1, UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), (Integer) null);
assertEquals(UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), params.getParameter(1));
verifyParameter(params, Value.string("83b988cf-1f4e-428a-be3d-cc712621942e"));
}
Expand Down

0 comments on commit d6935b8

Please sign in to comment.