Skip to content

Commit 80d2b9d

Browse files
authored
feat: support untyped NULL value parameters (#1224)
* feat: support untyped NULL value parameters * test: add tests for untyped null parameter values * fix: update metadata test with the new table * test: add expected table for PG * refactor: move logic to ParameterStore
1 parent 8f75d26 commit 80d2b9d

10 files changed

+204
-36
lines changed

src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ public void addBatch(String sql) throws SQLException {
8181
@Override
8282
public void setNull(int parameterIndex, int sqlType) throws SQLException {
8383
checkClosed();
84-
parameters.setParameter(parameterIndex, null, sqlType, null);
84+
parameters.setParameter(
85+
parameterIndex, /* value = */ null, sqlType, /* scaleOrLength = */ null);
8586
}
8687

8788
@Override

src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.cloud.spanner.Value;
2525
import com.google.cloud.spanner.ValueBinder;
2626
import com.google.common.io.CharStreams;
27+
import com.google.protobuf.NullValue;
2728
import com.google.rpc.Code;
2829
import java.io.IOException;
2930
import java.io.InputStream;
@@ -231,6 +232,10 @@ void setParameter(
231232
}
232233

233234
private void checkTypeAndValueSupported(Object value, int sqlType) throws SQLException {
235+
if (value == null) {
236+
// null is always supported, as we will just fall back to an untyped NULL value.
237+
return;
238+
}
234239
if (!isTypeSupported(sqlType)) {
235240
throw JdbcSqlExceptionFactory.of(
236241
"Type " + sqlType + " is not supported", Code.INVALID_ARGUMENT);
@@ -775,8 +780,13 @@ private Builder setArrayValue(ValueBinder<Builder> binder, int type, Object valu
775780
case Types.LONGVARBINARY:
776781
case Types.BLOB:
777782
return binder.toBytesArray(null);
783+
default:
784+
return binder.to(
785+
Value.untyped(
786+
com.google.protobuf.Value.newBuilder()
787+
.setNullValue(NullValue.NULL_VALUE)
788+
.build()));
778789
}
779-
throw JdbcSqlExceptionFactory.unsupported("Unknown/unsupported array base type: " + type);
780790
}
781791

782792
if (boolean[].class.isAssignableFrom(value.getClass())) {
@@ -864,7 +874,9 @@ private List<Double> toDoubleList(Number[] input) {
864874
*/
865875
private Builder setNullValue(ValueBinder<Builder> binder, Integer sqlType) {
866876
if (sqlType == null) {
867-
return binder.to((String) null);
877+
return binder.to(
878+
Value.untyped(
879+
com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()));
868880
}
869881
switch (sqlType) {
870882
case Types.BIGINT:
@@ -924,8 +936,14 @@ private Builder setNullValue(ValueBinder<Builder> binder, Integer sqlType) {
924936
return binder.to((ByteArray) null);
925937
case Types.VARCHAR:
926938
return binder.to((String) null);
939+
case JsonType.VENDOR_TYPE_NUMBER:
940+
return binder.to(Value.json(null));
941+
case PgJsonbType.VENDOR_TYPE_NUMBER:
942+
return binder.to(Value.pgJsonb(null));
927943
default:
928-
throw new IllegalArgumentException("Unsupported sql type for setting to null: " + sqlType);
944+
return binder.to(
945+
Value.untyped(
946+
com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()));
929947
}
930948
}
931949
}

src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import static org.junit.Assert.assertNull;
2121
import static org.junit.Assert.assertThrows;
2222
import static org.junit.Assert.assertTrue;
23-
import static org.junit.Assert.fail;
2423
import static org.mockito.Mockito.any;
2524
import static org.mockito.Mockito.anyString;
2625
import static org.mockito.Mockito.mock;
@@ -39,15 +38,13 @@
3938
import com.google.cloud.spanner.Value;
4039
import com.google.cloud.spanner.connection.AbstractStatementParser;
4140
import com.google.cloud.spanner.connection.Connection;
42-
import com.google.rpc.Code;
4341
import java.io.ByteArrayInputStream;
4442
import java.io.StringReader;
4543
import java.math.BigDecimal;
4644
import java.net.MalformedURLException;
4745
import java.net.URL;
4846
import java.sql.Date;
4947
import java.sql.JDBCType;
50-
import java.sql.PreparedStatement;
5148
import java.sql.ResultSetMetaData;
5249
import java.sql.SQLException;
5350
import java.sql.Time;
@@ -197,7 +194,10 @@ public void testParameters() throws SQLException, MalformedURLException {
197194
ps.setObject(35, "TEST");
198195
ps.setObject(36, "TEST", Types.NVARCHAR);
199196
ps.setObject(37, "TEST", Types.NVARCHAR, 20);
197+
ps.setRef(38, null);
198+
ps.setRowId(39, null);
200199
ps.setShort(40, (short) 1);
200+
ps.setSQLXML(41, null);
201201
ps.setString(42, "TEST");
202202
ps.setTime(43, new Time(1000L));
203203
ps.setTime(44, new Time(1000L), Calendar.getInstance(TimeZone.getTimeZone("GMT")));
@@ -211,8 +211,6 @@ public void testParameters() throws SQLException, MalformedURLException {
211211
ps.setObject(52, "{}", JsonType.VENDOR_TYPE_NUMBER);
212212
ps.setObject(53, "{}", PgJsonbType.VENDOR_TYPE_NUMBER);
213213

214-
testSetUnsupportedTypes(ps);
215-
216214
JdbcParameterMetaData pmd = ps.getParameterMetaData();
217215
assertEquals(numberOfParams, pmd.getParameterCount());
218216
assertEquals(JdbcArray.class.getName(), pmd.getParameterClassName(1));
@@ -274,33 +272,9 @@ public void testParameters() throws SQLException, MalformedURLException {
274272
}
275273
}
276274

277-
private void testSetUnsupportedTypes(PreparedStatement ps) {
278-
try {
279-
ps.setRef(38, null);
280-
fail("missing expected exception");
281-
} catch (SQLException e) {
282-
assertTrue(e instanceof JdbcSqlException);
283-
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
284-
}
285-
try {
286-
ps.setRowId(39, null);
287-
fail("missing expected exception");
288-
} catch (SQLException e) {
289-
assertTrue(e instanceof JdbcSqlException);
290-
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
291-
}
292-
try {
293-
ps.setSQLXML(41, null);
294-
fail("missing expected exception");
295-
} catch (SQLException e) {
296-
assertTrue(e instanceof JdbcSqlException);
297-
assertEquals(Code.INVALID_ARGUMENT, ((JdbcSqlException) e).getCode());
298-
}
299-
}
300-
301275
@Test
302276
public void testSetNullValues() throws SQLException {
303-
final int numberOfParameters = 27;
277+
final int numberOfParameters = 31;
304278
String sql = generateSqlWithParameters(numberOfParameters);
305279
try (JdbcPreparedStatement ps = new JdbcPreparedStatement(createMockConnection(), sql)) {
306280
int index = 0;
@@ -331,6 +305,10 @@ public void testSetNullValues() throws SQLException {
331305
ps.setNull(++index, Types.BIT);
332306
ps.setNull(++index, Types.VARBINARY);
333307
ps.setNull(++index, Types.VARCHAR);
308+
ps.setNull(++index, JsonType.VENDOR_TYPE_NUMBER);
309+
ps.setNull(++index, PgJsonbType.VENDOR_TYPE_NUMBER);
310+
ps.setNull(++index, Types.OTHER);
311+
ps.setNull(++index, Types.NULL);
334312
assertEquals(numberOfParameters, index);
335313

336314
JdbcParameterMetaData pmd = ps.getParameterMetaData();

src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementWithMockedServerTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.cloud.spanner.MockSpannerServiceImpl;
2626
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
2727
import com.google.cloud.spanner.Statement;
28+
import com.google.cloud.spanner.Value;
2829
import com.google.cloud.spanner.connection.SpannerPool;
2930
import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlBatchUpdateException;
3031
import io.grpc.Server;
@@ -36,6 +37,7 @@
3637
import java.sql.DriverManager;
3738
import java.sql.PreparedStatement;
3839
import java.sql.SQLException;
40+
import java.sql.Types;
3941
import java.util.Arrays;
4042
import java.util.Collection;
4143
import org.junit.After;
@@ -193,4 +195,69 @@ public void testExecuteBatch_withException() throws SQLException {
193195
}
194196
}
195197
}
198+
199+
@Test
200+
public void testInsertUntypedNullValues() throws SQLException {
201+
mockSpanner.putStatementResult(
202+
StatementResult.update(
203+
Statement.newBuilder(
204+
"insert into all_nullable_types (ColInt64, ColFloat64, ColBool, ColString, ColBytes, ColDate, ColTimestamp, ColNumeric, ColJson, ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColBytesArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray) "
205+
+ "values (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18)")
206+
.bind("p1")
207+
.to((Value) null)
208+
.bind("p2")
209+
.to((Value) null)
210+
.bind("p3")
211+
.to((Value) null)
212+
.bind("p4")
213+
.to((Value) null)
214+
.bind("p5")
215+
.to((Value) null)
216+
.bind("p6")
217+
.to((Value) null)
218+
.bind("p7")
219+
.to((Value) null)
220+
.bind("p8")
221+
.to((Value) null)
222+
.bind("p9")
223+
.to((Value) null)
224+
.bind("p10")
225+
.to((Value) null)
226+
.bind("p11")
227+
.to((Value) null)
228+
.bind("p12")
229+
.to((Value) null)
230+
.bind("p13")
231+
.to((Value) null)
232+
.bind("p14")
233+
.to((Value) null)
234+
.bind("p15")
235+
.to((Value) null)
236+
.bind("p16")
237+
.to((Value) null)
238+
.bind("p17")
239+
.to((Value) null)
240+
.bind("p18")
241+
.to((Value) null)
242+
.build(),
243+
1L));
244+
try (Connection connection = createConnection()) {
245+
for (int type : new int[] {Types.OTHER, Types.NULL}) {
246+
try (PreparedStatement statement =
247+
connection.prepareStatement(
248+
"insert into all_nullable_types ("
249+
+ "ColInt64, ColFloat64, ColBool, ColString, ColBytes, ColDate, ColTimestamp, ColNumeric, ColJson, "
250+
+ "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColBytesArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray) "
251+
+ "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) {
252+
for (int param = 1;
253+
param <= statement.getParameterMetaData().getParameterCount();
254+
param++) {
255+
statement.setNull(param, type);
256+
}
257+
assertEquals(1, statement.executeUpdate());
258+
}
259+
mockSpanner.clearRequests();
260+
}
261+
}
262+
}
196263
}

src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,8 @@ private IndexInfo(
512512
new IndexInfo("TableWithRef", false, "PRIMARY_KEY", 1, "Id", "A"),
513513
new IndexInfo("TableWithRef", true, "FOREIGN_KEY", 1, "RefFloat", "A"),
514514
new IndexInfo("TableWithRef", true, "FOREIGN_KEY", 2, "RefString", "A"),
515-
new IndexInfo("TableWithRef", true, "FOREIGN_KEY", 3, "RefDate", "A"));
515+
new IndexInfo("TableWithRef", true, "FOREIGN_KEY", 3, "RefDate", "A"),
516+
new IndexInfo("all_nullable_types", false, "PRIMARY_KEY", 1, "ColInt64", "A"));
516517

517518
@Test
518519
public void testGetIndexInfo() throws SQLException {
@@ -860,7 +861,8 @@ private Table(String name, String type) {
860861
new Table("SingersView", "VIEW"),
861862
new Table("Songs"),
862863
new Table("TableWithAllColumnTypes"),
863-
new Table("TableWithRef"));
864+
new Table("TableWithRef"),
865+
new Table("all_nullable_types"));
864866

865867
@Test
866868
public void testGetTables() throws SQLException {

src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ private IndexInfo(
420420
new IndexInfo("albums", false, "PRIMARY_KEY", 1, "singerid", "A"),
421421
new IndexInfo("albums", false, "PRIMARY_KEY", 2, "albumid", "A"),
422422
new IndexInfo("albums", true, "albumsbyalbumtitle", 1, "albumtitle", "A"),
423+
new IndexInfo("all_nullable_types", false, "PRIMARY_KEY", 1, "colint64", "A"),
423424
new IndexInfo("concerts", false, "PRIMARY_KEY", 1, "venueid", "A"),
424425
new IndexInfo("concerts", false, "PRIMARY_KEY", 2, "singerid", "A"),
425426
new IndexInfo("concerts", false, "PRIMARY_KEY", 3, "concertdate", "A"),
@@ -790,6 +791,7 @@ private Table(String name, String type) {
790791
private static final List<Table> EXPECTED_TABLES =
791792
Arrays.asList(
792793
new Table("albums"),
794+
new Table("all_nullable_types"),
793795
new Table("concerts"),
794796
new Table("singers"),
795797
// TODO: Enable when views are supported for PostgreSQL dialect databases.

src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,39 @@ public void test12_InsertReturningTestData() throws SQLException {
12941294
}
12951295
}
12961296

1297+
@Test
1298+
public void test13_InsertUntypedNullValues() throws SQLException {
1299+
try (Connection connection = createConnection(env, database)) {
1300+
try (PreparedStatement preparedStatement =
1301+
connection.prepareStatement(
1302+
"insert into all_nullable_types ("
1303+
+ "ColInt64, ColFloat64, ColBool, ColString, ColBytes, ColDate, ColTimestamp, ColNumeric, ColJson, "
1304+
+ "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColBytesArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray) "
1305+
+ "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) {
1306+
for (int param = 1;
1307+
param <= preparedStatement.getParameterMetaData().getParameterCount();
1308+
param++) {
1309+
preparedStatement.setNull(param, Types.OTHER);
1310+
}
1311+
if (getDialect() == Dialect.POSTGRESQL) {
1312+
// PostgreSQL-dialect databases do not allow NULLs in primary keys.
1313+
preparedStatement.setLong(1, 1L);
1314+
}
1315+
assertEquals(1, preparedStatement.executeUpdate());
1316+
1317+
// Verify that calling preparedStatement.setObject(index, null) works.
1318+
for (int param = 1;
1319+
param <= preparedStatement.getParameterMetaData().getParameterCount();
1320+
param++) {
1321+
preparedStatement.setObject(param, null);
1322+
}
1323+
// We need a different primary key value to insert another row.
1324+
preparedStatement.setLong(1, 2L);
1325+
assertEquals(1, preparedStatement.executeUpdate());
1326+
}
1327+
}
1328+
}
1329+
12971330
private List<String> readValuesFromFile(String filename) {
12981331
StringBuilder builder = new StringBuilder();
12991332
try (InputStream stream = ITJdbcPreparedStatementTest.class.getResourceAsStream(filename)) {

src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,29 @@ CREATE TABLE TableWithAllColumnTypes (
9797
) PRIMARY KEY (ColInt64)
9898
;
9999

100+
CREATE TABLE all_nullable_types (
101+
ColInt64 INT64,
102+
ColFloat64 FLOAT64,
103+
ColBool BOOL,
104+
ColString STRING(100),
105+
ColBytes BYTES(100),
106+
ColDate DATE,
107+
ColTimestamp TIMESTAMP,
108+
ColNumeric NUMERIC,
109+
ColJson JSON,
110+
111+
ColInt64Array ARRAY<INT64>,
112+
ColFloat64Array ARRAY<FLOAT64>,
113+
ColBoolArray ARRAY<BOOL>,
114+
ColStringArray ARRAY<STRING(100)>,
115+
ColBytesArray ARRAY<BYTES(100)>,
116+
ColDateArray ARRAY<DATE>,
117+
ColTimestampArray ARRAY<TIMESTAMP>,
118+
ColNumericArray ARRAY<NUMERIC>,
119+
ColJsonArray ARRAY<JSON>,
120+
) PRIMARY KEY (ColInt64)
121+
;
122+
100123
CREATE TABLE TableWithRef (
101124
Id INT64 NOT NULL,
102125
RefFloat FLOAT64 NOT NULL,

src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_Emulator.sql

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ CREATE TABLE TableWithAllColumnTypes (
9292
) PRIMARY KEY (ColInt64)
9393
;
9494

95+
CREATE TABLE all_nullable_types (
96+
ColInt64 INT64,
97+
ColFloat64 FLOAT64,
98+
ColBool BOOL,
99+
ColString STRING(100),
100+
ColBytes BYTES(100),
101+
ColDate DATE,
102+
ColTimestamp TIMESTAMP,
103+
ColNumeric NUMERIC,
104+
ColJson JSON,
105+
106+
ColInt64Array ARRAY<INT64>,
107+
ColFloat64Array ARRAY<FLOAT64>,
108+
ColBoolArray ARRAY<BOOL>,
109+
ColStringArray ARRAY<STRING(100)>,
110+
ColBytesArray ARRAY<BYTES(100)>,
111+
ColDateArray ARRAY<DATE>,
112+
ColTimestampArray ARRAY<TIMESTAMP>,
113+
ColNumericArray ARRAY<NUMERIC>,
114+
ColJsonArray ARRAY<JSON>,
115+
) PRIMARY KEY (ColInt64)
116+
;
117+
95118
CREATE TABLE TableWithRef (
96119
Id INT64 NOT NULL,
97120
RefFloat FLOAT64 NOT NULL,

0 commit comments

Comments
 (0)