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: allow get/set Spanner Value instances #454

Merged
merged 2 commits into from May 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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