Skip to content

Commit d6935b8

Browse files
authored
feat: allow get/set Spanner Value instances (#454)
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
1 parent e638162 commit d6935b8

8 files changed

+454
-32
lines changed

Diff for: src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public void setObject(int parameterIndex, Object value, int targetSqlType) throw
198198
@Override
199199
public void setObject(int parameterIndex, Object value) throws SQLException {
200200
checkClosed();
201-
parameters.setParameter(parameterIndex, value, null);
201+
parameters.setParameter(parameterIndex, value, (SQLType) null);
202202
}
203203

204204
@Override

Diff for: src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java

+62-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.cloud.ByteArray;
2020
import com.google.cloud.spanner.Statement;
2121
import com.google.cloud.spanner.Statement.Builder;
22+
import com.google.cloud.spanner.Value;
2223
import com.google.cloud.spanner.ValueBinder;
2324
import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
2425
import com.google.common.io.CharStreams;
@@ -39,6 +40,7 @@
3940
import java.sql.PreparedStatement;
4041
import java.sql.ResultSet;
4142
import java.sql.SQLException;
43+
import java.sql.SQLType;
4244
import java.sql.Time;
4345
import java.sql.Timestamp;
4446
import java.sql.Types;
@@ -129,7 +131,8 @@ void setColumn(int parameterIndex, String column) throws SQLException {
129131
getParameter(parameterIndex),
130132
getType(parameterIndex),
131133
getScaleOrLength(parameterIndex),
132-
column);
134+
column,
135+
null);
133136
}
134137

135138
void setType(int parameterIndex, Integer type) throws SQLException {
@@ -138,26 +141,71 @@ void setType(int parameterIndex, Integer type) throws SQLException {
138141
getParameter(parameterIndex),
139142
type,
140143
getScaleOrLength(parameterIndex),
141-
getColumn(parameterIndex));
144+
getColumn(parameterIndex),
145+
null);
142146
}
143147

148+
/** Sets a parameter value. The type will be determined based on the type of the value. */
149+
void setParameter(int parameterIndex, Object value) throws SQLException {
150+
setParameter(parameterIndex, value, null, null, null, null);
151+
}
152+
153+
/** Sets a parameter value as the specified vendor-specific {@link SQLType}. */
154+
void setParameter(int parameterIndex, Object value, SQLType sqlType) throws SQLException {
155+
setParameter(parameterIndex, value, null, null, null, sqlType);
156+
}
157+
158+
/**
159+
* Sets a parameter value as the specified vendor-specific {@link SQLType} with the specified
160+
* scale or length. This method is only here to support the {@link
161+
* PreparedStatement#setObject(int, Object, SQLType, int)} method.
162+
*/
163+
void setParameter(int parameterIndex, Object value, SQLType sqlType, Integer scaleOrLength)
164+
throws SQLException {
165+
setParameter(parameterIndex, value, null, scaleOrLength, null, sqlType);
166+
}
167+
168+
/**
169+
* Sets a parameter value as the specified sql type. The type can be one of the constants in
170+
* {@link Types} or a vendor specific type code supplied by a vendor specific {@link SQLType}.
171+
*/
144172
void setParameter(int parameterIndex, Object value, Integer sqlType) throws SQLException {
145173
setParameter(parameterIndex, value, sqlType, null);
146174
}
147175

176+
/**
177+
* Sets a parameter value as the specified sql type with the specified scale or length. The type
178+
* can be one of the constants in {@link Types} or a vendor specific type code supplied by a
179+
* vendor specific {@link SQLType}.
180+
*/
148181
void setParameter(int parameterIndex, Object value, Integer sqlType, Integer scaleOrLength)
149182
throws SQLException {
150-
setParameter(parameterIndex, value, sqlType, scaleOrLength, null);
183+
setParameter(parameterIndex, value, sqlType, scaleOrLength, null, null);
151184
}
152185

186+
/**
187+
* Sets a parameter value as the specified sql type with the specified scale or length. Any {@link
188+
* SQLType} instance will take precedence over sqlType. The type can be one of the constants in
189+
* {@link Types} or a vendor specific type code supplied by a vendor specific {@link SQLType}.
190+
*/
153191
void setParameter(
154-
int parameterIndex, Object value, Integer sqlType, Integer scaleOrLength, String column)
192+
int parameterIndex,
193+
Object value,
194+
Integer sqlType,
195+
Integer scaleOrLength,
196+
String column,
197+
SQLType sqlTypeObject)
155198
throws SQLException {
156-
// check that only valid type/value combinations are entered
157-
if (sqlType != null) {
158-
checkTypeAndValueSupported(value, sqlType);
159-
}
160-
// set the parameter
199+
// Ignore the sql type if the application has created a Spanner Value object.
200+
if (!(value instanceof Value)) {
201+
// check that only valid type/value combinations are entered
202+
if (sqlTypeObject != null && sqlType == null) {
203+
sqlType = sqlTypeObject.getVendorTypeNumber();
204+
}
205+
if (sqlType != null) {
206+
checkTypeAndValueSupported(value, sqlType);
207+
}
208+
} // set the parameter
161209
highestIndex = Math.max(parameterIndex, highestIndex);
162210
int arrayIndex = parameterIndex - 1;
163211
if (arrayIndex >= parametersList.size() || parametersList.get(arrayIndex) == null) {
@@ -416,7 +464,11 @@ Builder bindParameterValue(ValueBinder<Builder> binder, int index) throws SQLExc
416464
/** Set a value from a JDBC parameter on a Spanner {@link Statement}. */
417465
Builder setValue(ValueBinder<Builder> binder, Object value, Integer sqlType) throws SQLException {
418466
Builder res;
419-
if (sqlType != null && sqlType == Types.ARRAY) {
467+
if (value instanceof Value) {
468+
// If a Value has been constructed, then that should override any sqlType that might have been
469+
// supplied.
470+
res = binder.to((Value) value);
471+
} else if (sqlType != null && sqlType == Types.ARRAY) {
420472
if (value instanceof Array) {
421473
Array array = (Array) value;
422474
value = array.getArray();

Diff for: src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.cloud.spanner.Type;
2525
import com.google.cloud.spanner.connection.StatementParser;
2626
import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo;
27+
import com.google.common.annotations.VisibleForTesting;
2728
import com.google.common.collect.ImmutableList;
2829
import java.sql.PreparedStatement;
2930
import java.sql.ResultSet;
@@ -48,7 +49,8 @@ ParametersInfo getParametersInfo() throws SQLException {
4849
return parameters;
4950
}
5051

51-
private Statement createStatement() throws SQLException {
52+
@VisibleForTesting
53+
Statement createStatement() throws SQLException {
5254
ParametersInfo paramInfo = getParametersInfo();
5355
Statement.Builder builder = Statement.newBuilder(paramInfo.sqlWithNamedParameters);
5456
for (int index = 1; index <= getParameters().getHighestIndex(); index++) {

Diff for: src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java

+60-1
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
import com.google.cloud.Timestamp;
2222
import com.google.cloud.spanner.Type;
2323
import com.google.cloud.spanner.Type.Code;
24+
import com.google.cloud.spanner.Value;
2425
import java.math.BigDecimal;
2526
import java.math.BigInteger;
2627
import java.nio.charset.Charset;
2728
import java.sql.Array;
2829
import java.sql.SQLException;
2930
import java.sql.Time;
3031
import java.util.ArrayList;
32+
import java.util.Arrays;
3133
import java.util.Calendar;
3234
import java.util.List;
3335
import java.util.concurrent.TimeUnit;
@@ -59,8 +61,13 @@ static Object convert(Object value, Type type, Class<?> targetType) throws SQLEx
5961
JdbcPreconditions.checkArgument(targetType != null, "targetType may not be null");
6062
checkValidTypeAndValueForConvert(type, value);
6163

62-
if (value == null) return null;
64+
if (value == null) {
65+
return null;
66+
}
6367
try {
68+
if (targetType.equals(Value.class)) {
69+
return convertToSpannerValue(value, type);
70+
}
6471
if (targetType.equals(String.class)) {
6572
if (type.getCode() == Code.BYTES) return new String((byte[]) value, UTF8);
6673
if (type.getCode() == Code.TIMESTAMP) {
@@ -155,6 +162,58 @@ static Object convert(Object value, Type type, Class<?> targetType) throws SQLEx
155162
com.google.rpc.Code.INVALID_ARGUMENT);
156163
}
157164

165+
private static Value convertToSpannerValue(Object value, Type type) throws SQLException {
166+
switch (type.getCode()) {
167+
case ARRAY:
168+
switch (type.getArrayElementType().getCode()) {
169+
case BOOL:
170+
return Value.boolArray(Arrays.asList((Boolean[]) ((java.sql.Array) value).getArray()));
171+
case BYTES:
172+
return Value.bytesArray(toGoogleBytes((byte[][]) ((java.sql.Array) value).getArray()));
173+
case DATE:
174+
return Value.dateArray(
175+
toGoogleDates((java.sql.Date[]) ((java.sql.Array) value).getArray()));
176+
case FLOAT64:
177+
return Value.float64Array(
178+
Arrays.asList((Double[]) ((java.sql.Array) value).getArray()));
179+
case INT64:
180+
return Value.int64Array(Arrays.asList((Long[]) ((java.sql.Array) value).getArray()));
181+
case NUMERIC:
182+
return Value.numericArray(
183+
Arrays.asList((BigDecimal[]) ((java.sql.Array) value).getArray()));
184+
case STRING:
185+
return Value.stringArray(Arrays.asList((String[]) ((java.sql.Array) value).getArray()));
186+
case TIMESTAMP:
187+
return Value.timestampArray(
188+
toGoogleTimestamps((java.sql.Timestamp[]) ((java.sql.Array) value).getArray()));
189+
case STRUCT:
190+
default:
191+
throw JdbcSqlExceptionFactory.of(
192+
"invalid argument: " + value, com.google.rpc.Code.INVALID_ARGUMENT);
193+
}
194+
case BOOL:
195+
return Value.bool((Boolean) value);
196+
case BYTES:
197+
return Value.bytes(ByteArray.copyFrom((byte[]) value));
198+
case DATE:
199+
return Value.date(toGoogleDate((java.sql.Date) value));
200+
case FLOAT64:
201+
return Value.float64((Double) value);
202+
case INT64:
203+
return Value.int64((Long) value);
204+
case NUMERIC:
205+
return Value.numeric((BigDecimal) value);
206+
case STRING:
207+
return Value.string((String) value);
208+
case TIMESTAMP:
209+
return Value.timestamp(toGoogleTimestamp((java.sql.Timestamp) value));
210+
case STRUCT:
211+
default:
212+
throw JdbcSqlExceptionFactory.of(
213+
"invalid argument: " + value, com.google.rpc.Code.INVALID_ARGUMENT);
214+
}
215+
}
216+
158217
private static void checkValidTypeAndValueForConvert(Type type, Object value)
159218
throws SQLException {
160219
if (value == null) return;

Diff for: src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java

+72-16
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import java.sql.Timestamp;
4545
import java.sql.Types;
4646
import java.util.Arrays;
47+
import java.util.Collections;
4748
import java.util.UUID;
4849
import org.junit.Test;
4950
import org.junit.runner.RunWith;
@@ -52,6 +53,61 @@
5253
@RunWith(JUnit4.class)
5354
public class JdbcParameterStoreTest {
5455

56+
/**
57+
* Tests setting a {@link Value} as a parameter value.
58+
*
59+
* @throws SQLException
60+
*/
61+
@Test
62+
public void testSetValueAsParameter() throws SQLException {
63+
JdbcParameterStore params = new JdbcParameterStore();
64+
params.setParameter(1, Value.bool(true));
65+
verifyParameter(params, Value.bool(true));
66+
params.setParameter(1, Value.bytes(ByteArray.copyFrom("test")));
67+
verifyParameter(params, Value.bytes(ByteArray.copyFrom("test")));
68+
params.setParameter(1, Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3)));
69+
verifyParameter(params, Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3)));
70+
params.setParameter(1, Value.float64(3.14d));
71+
verifyParameter(params, Value.float64(3.14d));
72+
params.setParameter(1, Value.int64(1L));
73+
verifyParameter(params, Value.int64(1L));
74+
params.setParameter(1, Value.numeric(BigDecimal.TEN));
75+
verifyParameter(params, Value.numeric(BigDecimal.TEN));
76+
params.setParameter(1, Value.string("test"));
77+
verifyParameter(params, Value.string("test"));
78+
params.setParameter(
79+
1, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(9999L, 101)));
80+
verifyParameter(
81+
params, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(9999L, 101)));
82+
83+
params.setParameter(1, Value.boolArray(new boolean[] {true, false}));
84+
verifyParameter(params, Value.boolArray(new boolean[] {true, false}));
85+
params.setParameter(1, Value.bytesArray(Collections.singleton(ByteArray.copyFrom("test"))));
86+
verifyParameter(params, Value.bytesArray(Collections.singleton(ByteArray.copyFrom("test"))));
87+
params.setParameter(
88+
1,
89+
Value.dateArray(Collections.singleton(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3))));
90+
verifyParameter(
91+
params,
92+
Value.dateArray(Collections.singleton(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3))));
93+
params.setParameter(1, Value.float64Array(Collections.singleton(3.14d)));
94+
verifyParameter(params, Value.float64Array(Collections.singleton(3.14d)));
95+
params.setParameter(1, Value.int64Array(Collections.singleton(1L)));
96+
verifyParameter(params, Value.int64Array(Collections.singleton(1L)));
97+
params.setParameter(1, Value.numericArray(Collections.singleton(BigDecimal.TEN)));
98+
verifyParameter(params, Value.numericArray(Collections.singleton(BigDecimal.TEN)));
99+
params.setParameter(1, Value.stringArray(Collections.singleton("test")));
100+
verifyParameter(params, Value.stringArray(Collections.singleton("test")));
101+
params.setParameter(
102+
1,
103+
Value.timestampArray(
104+
Collections.singleton(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(9999L, 101))));
105+
verifyParameter(
106+
params,
107+
Value.timestampArray(
108+
Collections.singleton(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(9999L, 101))));
109+
}
110+
55111
/** Tests setting a parameter value together with a sql type */
56112
@SuppressWarnings("deprecation")
57113
@Test
@@ -422,55 +478,55 @@ private void assertInvalidParameter(JdbcParameterStore params, Object value, int
422478
@Test
423479
public void testSetParameterWithoutType() throws SQLException {
424480
JdbcParameterStore params = new JdbcParameterStore();
425-
params.setParameter(1, (byte) 1, null);
481+
params.setParameter(1, (byte) 1, (Integer) null);
426482
assertEquals(1, ((Byte) params.getParameter(1)).byteValue());
427483
verifyParameter(params, Value.int64(1));
428-
params.setParameter(1, (short) 1, null);
484+
params.setParameter(1, (short) 1, (Integer) null);
429485
assertEquals(1, ((Short) params.getParameter(1)).shortValue());
430486
verifyParameter(params, Value.int64(1));
431-
params.setParameter(1, 1, null);
487+
params.setParameter(1, 1, (Integer) null);
432488
assertEquals(1, ((Integer) params.getParameter(1)).intValue());
433489
verifyParameter(params, Value.int64(1));
434-
params.setParameter(1, 1L, null);
490+
params.setParameter(1, 1L, (Integer) null);
435491
assertEquals(1, ((Long) params.getParameter(1)).longValue());
436492
verifyParameter(params, Value.int64(1));
437-
params.setParameter(1, (float) 1, null);
493+
params.setParameter(1, (float) 1, (Integer) null);
438494
assertEquals(1.0f, ((Float) params.getParameter(1)).floatValue(), 0.0f);
439495
verifyParameter(params, Value.float64(1));
440-
params.setParameter(1, (double) 1, null);
496+
params.setParameter(1, (double) 1, (Integer) null);
441497
assertEquals(1.0d, ((Double) params.getParameter(1)).doubleValue(), 0.0d);
442498
verifyParameter(params, Value.float64(1));
443-
params.setParameter(1, new Date(1970 - 1900, 0, 1), null);
499+
params.setParameter(1, new Date(1970 - 1900, 0, 1), (Integer) null);
444500
assertEquals(new Date(1970 - 1900, 0, 1), params.getParameter(1));
445501
verifyParameter(params, Value.date(com.google.cloud.Date.fromYearMonthDay(1970, 1, 1)));
446-
params.setParameter(1, new Time(0L), null);
502+
params.setParameter(1, new Time(0L), (Integer) null);
447503
assertEquals(new Time(0L), params.getParameter(1));
448504
verifyParameter(
449505
params, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(0L, 0)));
450-
params.setParameter(1, new Timestamp(0L), null);
506+
params.setParameter(1, new Timestamp(0L), (Integer) null);
451507
assertEquals(new Timestamp(0L), params.getParameter(1));
452508
verifyParameter(
453509
params, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(0L, 0)));
454-
params.setParameter(1, new byte[] {1, 2, 3}, null);
510+
params.setParameter(1, new byte[] {1, 2, 3}, (Integer) null);
455511
assertArrayEquals(new byte[] {1, 2, 3}, (byte[]) params.getParameter(1));
456512
verifyParameter(params, Value.bytes(ByteArray.copyFrom(new byte[] {1, 2, 3})));
457513

458-
params.setParameter(1, new JdbcBlob(new byte[] {1, 2, 3}), null);
514+
params.setParameter(1, new JdbcBlob(new byte[] {1, 2, 3}), (Integer) null);
459515
assertEquals(new JdbcBlob(new byte[] {1, 2, 3}), params.getParameter(1));
460516
verifyParameter(params, Value.bytes(ByteArray.copyFrom(new byte[] {1, 2, 3})));
461-
params.setParameter(1, new JdbcClob("test"), null);
517+
params.setParameter(1, new JdbcClob("test"), (Integer) null);
462518
assertEquals(new JdbcClob("test"), params.getParameter(1));
463519
verifyParameter(params, Value.string("test"));
464-
params.setParameter(1, true, null);
520+
params.setParameter(1, true, (Integer) null);
465521
assertTrue((Boolean) params.getParameter(1));
466522
verifyParameter(params, Value.bool(true));
467-
params.setParameter(1, "test", null);
523+
params.setParameter(1, "test", (Integer) null);
468524
assertEquals("test", params.getParameter(1));
469525
verifyParameter(params, Value.string("test"));
470-
params.setParameter(1, new JdbcClob("test"), null);
526+
params.setParameter(1, new JdbcClob("test"), (Integer) null);
471527
assertEquals(new JdbcClob("test"), params.getParameter(1));
472528
verifyParameter(params, Value.string("test"));
473-
params.setParameter(1, UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), null);
529+
params.setParameter(1, UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), (Integer) null);
474530
assertEquals(UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), params.getParameter(1));
475531
verifyParameter(params, Value.string("83b988cf-1f4e-428a-be3d-cc712621942e"));
476532
}

0 commit comments

Comments
 (0)