Skip to content

Commit 828aff6

Browse files
authored
fix: support getShort for DATA_TYPE in TypeInfo (googleapis#1691)
* fix: support getShort for DATA_TYPE in TypeInfo The ResultSet that is returned by DatabaseMetadata#getTypeInfo has a column at index 2 with the name DATA_TYPE. This field should contain one of the java.sql.Types constants, or a vendor-specific type code. The JDBC specification states that this column should be a `short` (although the constants in java.sql.Types are of type `int`). Cloud Spanner (at the time of writing) does not support any int16 fields. The type code is therefore returned as an int64. The codes that are used for vendor-specific types by Spanner exceed the max value of a `short`, and therefore resulted in an OUT_OF_RANGE exception if you tried to call `ResultSet#getShort(int)` on this column for any of the Spanner-specific types (e.g. JSON). This change fixes that by adding an additional vendor type code for these types that does fit in a `short`. This value is returned when `getShort(int)` is called on the ResultSet. Fixes googleapis#1688 * chore: cleanup
1 parent 08011a5 commit 828aff6

File tree

9 files changed

+160
-15
lines changed

9 files changed

+160
-15
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.cloud.spanner.Type.StructField;
2727
import com.google.cloud.spanner.connection.Connection.InternalMetadataQuery;
2828
import com.google.common.annotations.VisibleForTesting;
29+
import com.google.common.collect.ImmutableSet;
2930
import java.io.BufferedReader;
3031
import java.io.InputStream;
3132
import java.io.InputStreamReader;
@@ -1284,7 +1285,9 @@ public ResultSet getTypeInfo() {
12841285
.set("NUM_PREC_RADIX")
12851286
.to(10)
12861287
.build(),
1287-
getJsonType(connection.getDialect()))));
1288+
getJsonType(connection.getDialect()))),
1289+
// Allow column 2 to be cast to short without any range checks.
1290+
ImmutableSet.of(2));
12881291
}
12891292

12901293
private Struct getJsonType(Dialect dialect) {

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,13 @@ private boolean isTypeSupported(int sqlType) {
284284
case Types.NUMERIC:
285285
case Types.DECIMAL:
286286
case JsonType.VENDOR_TYPE_NUMBER:
287+
case JsonType.SHORT_VENDOR_TYPE_NUMBER:
287288
case PgJsonbType.VENDOR_TYPE_NUMBER:
289+
case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
288290
case ProtoMessageType.VENDOR_TYPE_NUMBER:
291+
case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
289292
case ProtoEnumType.VENDOR_TYPE_NUMBER:
293+
case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
290294
return true;
291295
}
292296
return false;
@@ -348,19 +352,23 @@ private boolean isValidTypeAndValue(Object value, int sqlType) {
348352
case Types.NCLOB:
349353
return value instanceof NClob || value instanceof Reader;
350354
case JsonType.VENDOR_TYPE_NUMBER:
355+
case JsonType.SHORT_VENDOR_TYPE_NUMBER:
351356
return value instanceof String
352357
|| value instanceof InputStream
353358
|| value instanceof Reader
354359
|| (value instanceof Value && ((Value) value).getType().getCode() == Type.Code.JSON);
355360
case PgJsonbType.VENDOR_TYPE_NUMBER:
361+
case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
356362
return value instanceof String
357363
|| value instanceof InputStream
358364
|| value instanceof Reader
359365
|| (value instanceof Value
360366
&& ((Value) value).getType().getCode() == Type.Code.PG_JSONB);
361367
case ProtoMessageType.VENDOR_TYPE_NUMBER:
368+
case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
362369
return value instanceof AbstractMessage || value instanceof byte[];
363370
case ProtoEnumType.VENDOR_TYPE_NUMBER:
371+
case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
364372
return value instanceof ProtocolMessageEnum || value instanceof Number;
365373
}
366374
return false;
@@ -449,7 +457,12 @@ private Builder setSingleValue(ValueBinder<Builder> binder, Object value, Intege
449457
/** Set a JDBC parameter value on a Spanner {@link Statement} with a known SQL type. */
450458
private Builder setParamWithKnownType(ValueBinder<Builder> binder, Object value, Integer sqlType)
451459
throws SQLException {
452-
switch (sqlType) {
460+
if (sqlType == null) {
461+
return null;
462+
}
463+
int type = sqlType;
464+
465+
switch (type) {
453466
case Types.BIT:
454467
case Types.BOOLEAN:
455468
if (value instanceof Boolean) {
@@ -522,7 +535,9 @@ private Builder setParamWithKnownType(ValueBinder<Builder> binder, Object value,
522535
}
523536
return binder.to(stringValue);
524537
case JsonType.VENDOR_TYPE_NUMBER:
538+
case JsonType.SHORT_VENDOR_TYPE_NUMBER:
525539
case PgJsonbType.VENDOR_TYPE_NUMBER:
540+
case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
526541
String jsonValue;
527542
if (value instanceof String) {
528543
jsonValue = (String) value;
@@ -534,7 +549,8 @@ private Builder setParamWithKnownType(ValueBinder<Builder> binder, Object value,
534549
throw JdbcSqlExceptionFactory.of(
535550
value + " is not a valid JSON value", Code.INVALID_ARGUMENT);
536551
}
537-
if (sqlType == PgJsonbType.VENDOR_TYPE_NUMBER) {
552+
if (type == PgJsonbType.VENDOR_TYPE_NUMBER
553+
|| type == PgJsonbType.SHORT_VENDOR_TYPE_NUMBER) {
538554
return binder.to(Value.pgJsonb(jsonValue));
539555
}
540556
return binder.to(Value.json(jsonValue));
@@ -631,6 +647,7 @@ private Builder setParamWithKnownType(ValueBinder<Builder> binder, Object value,
631647
}
632648
throw JdbcSqlExceptionFactory.of(value + " is not a valid clob", Code.INVALID_ARGUMENT);
633649
case ProtoMessageType.VENDOR_TYPE_NUMBER:
650+
case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
634651
if (value instanceof AbstractMessage) {
635652
return binder.to((AbstractMessage) value);
636653
} else if (value instanceof byte[]) {
@@ -640,6 +657,7 @@ private Builder setParamWithKnownType(ValueBinder<Builder> binder, Object value,
640657
value + " is not a valid PROTO value", Code.INVALID_ARGUMENT);
641658
}
642659
case ProtoEnumType.VENDOR_TYPE_NUMBER:
660+
case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
643661
if (value instanceof ProtocolMessageEnum) {
644662
return binder.to((ProtocolMessageEnum) value);
645663
} else if (value instanceof Number) {
@@ -809,8 +827,10 @@ private Builder setArrayValue(ValueBinder<Builder> binder, int type, Object valu
809827
case Types.NCLOB:
810828
return binder.toStringArray(null);
811829
case JsonType.VENDOR_TYPE_NUMBER:
830+
case JsonType.SHORT_VENDOR_TYPE_NUMBER:
812831
return binder.toJsonArray(null);
813832
case PgJsonbType.VENDOR_TYPE_NUMBER:
833+
case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
814834
return binder.toPgJsonbArray(null);
815835
case Types.DATE:
816836
return binder.toDateArray(null);
@@ -825,7 +845,9 @@ private Builder setArrayValue(ValueBinder<Builder> binder, int type, Object valu
825845
case Types.BLOB:
826846
return binder.toBytesArray(null);
827847
case ProtoMessageType.VENDOR_TYPE_NUMBER:
848+
case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
828849
case ProtoEnumType.VENDOR_TYPE_NUMBER:
850+
case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
829851
return binder.to(
830852
Value.untyped(
831853
com.google.protobuf.Value.newBuilder()
@@ -886,9 +908,10 @@ private Builder setArrayValue(ValueBinder<Builder> binder, int type, Object valu
886908
} else if (Timestamp[].class.isAssignableFrom(value.getClass())) {
887909
return binder.toTimestampArray(JdbcTypeConverter.toGoogleTimestamps((Timestamp[]) value));
888910
} else if (String[].class.isAssignableFrom(value.getClass())) {
889-
if (type == JsonType.VENDOR_TYPE_NUMBER) {
911+
if (type == JsonType.VENDOR_TYPE_NUMBER || type == JsonType.SHORT_VENDOR_TYPE_NUMBER) {
890912
return binder.toJsonArray(Arrays.asList((String[]) value));
891-
} else if (type == PgJsonbType.VENDOR_TYPE_NUMBER) {
913+
} else if (type == PgJsonbType.VENDOR_TYPE_NUMBER
914+
|| type == PgJsonbType.SHORT_VENDOR_TYPE_NUMBER) {
892915
return binder.toPgJsonbArray(Arrays.asList((String[]) value));
893916
} else {
894917
return binder.toStringArray(Arrays.asList((String[]) value));
@@ -992,11 +1015,13 @@ private Builder setNullValue(ValueBinder<Builder> binder, Integer sqlType) {
9921015
Value.untyped(
9931016
com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()));
9941017
}
995-
switch (sqlType) {
1018+
int type = sqlType;
1019+
switch (type) {
9961020
case Types.BIGINT:
9971021
return binder.to((Long) null);
9981022
case Types.BINARY:
9991023
case ProtoMessageType.VENDOR_TYPE_NUMBER:
1024+
case ProtoEnumType.SHORT_VENDOR_TYPE_NUMBER:
10001025
return binder.to((ByteArray) null);
10011026
case Types.BLOB:
10021027
return binder.to((ByteArray) null);
@@ -1021,6 +1046,7 @@ private Builder setNullValue(ValueBinder<Builder> binder, Integer sqlType) {
10211046
return binder.to((Double) null);
10221047
case Types.INTEGER:
10231048
case ProtoEnumType.VENDOR_TYPE_NUMBER:
1049+
case ProtoMessageType.SHORT_VENDOR_TYPE_NUMBER:
10241050
return binder.to((Long) null);
10251051
case Types.LONGNVARCHAR:
10261052
return binder.to((String) null);
@@ -1052,8 +1078,10 @@ private Builder setNullValue(ValueBinder<Builder> binder, Integer sqlType) {
10521078
case Types.VARCHAR:
10531079
return binder.to((String) null);
10541080
case JsonType.VENDOR_TYPE_NUMBER:
1081+
case JsonType.SHORT_VENDOR_TYPE_NUMBER:
10551082
return binder.to(Value.json(null));
10561083
case PgJsonbType.VENDOR_TYPE_NUMBER:
1084+
case PgJsonbType.SHORT_VENDOR_TYPE_NUMBER:
10571085
return binder.to(Value.pgJsonb(null));
10581086
default:
10591087
return binder.to(

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.cloud.spanner.connection.PartitionedQueryResultSet;
2727
import com.google.common.base.Preconditions;
2828
import com.google.common.collect.ImmutableList;
29+
import com.google.common.collect.ImmutableSet;
2930
import java.io.ByteArrayInputStream;
3031
import java.io.InputStream;
3132
import java.io.Reader;
@@ -57,8 +58,16 @@
5758
class JdbcResultSet extends AbstractJdbcResultSet {
5859

5960
static JdbcResultSet of(com.google.cloud.spanner.ResultSet resultSet) {
60-
Preconditions.checkNotNull(resultSet);
61-
return new JdbcResultSet(null, resultSet);
61+
return of(resultSet, ImmutableSet.of());
62+
}
63+
64+
static JdbcResultSet of(
65+
com.google.cloud.spanner.ResultSet resultSet,
66+
ImmutableSet<Integer> columnsAllowedUncheckedLongCastToShort) {
67+
return new JdbcResultSet(
68+
null,
69+
Preconditions.checkNotNull(resultSet),
70+
Preconditions.checkNotNull(columnsAllowedUncheckedLongCastToShort));
6271
}
6372

6473
static JdbcResultSet of(Statement statement, com.google.cloud.spanner.ResultSet resultSet) {
@@ -129,10 +138,19 @@ public Struct next() {
129138
private boolean nextCalledForMetaData = false;
130139
private boolean nextCalledForMetaDataResult = false;
131140
private long currentRow = 0L;
141+
private final ImmutableSet<Integer> columnsAllowedUncheckedLongCastToShort;
132142

133143
JdbcResultSet(Statement statement, com.google.cloud.spanner.ResultSet spanner) {
144+
this(statement, spanner, ImmutableSet.of());
145+
}
146+
147+
JdbcResultSet(
148+
Statement statement,
149+
com.google.cloud.spanner.ResultSet spanner,
150+
ImmutableSet<Integer> columnsAllowedUncheckedLongCastToShort) {
134151
super(spanner);
135152
this.statement = statement;
153+
this.columnsAllowedUncheckedLongCastToShort = columnsAllowedUncheckedLongCastToShort;
136154
}
137155

138156
void checkClosedAndValidRow() throws SQLException {
@@ -327,6 +345,12 @@ public short getShort(int columnIndex) throws SQLException {
327345
: checkedCastToShort(Double.valueOf(spanner.getDouble(spannerIndex)).longValue());
328346
case INT64:
329347
case ENUM:
348+
if (this.columnsAllowedUncheckedLongCastToShort.contains(columnIndex)) {
349+
// This is used to allow frameworks that call getShort(int) on the ResultSet that is
350+
// returned by DatabaseMetadata#getTypeInfo() to get the type code as a short, even when
351+
// the value is out of range for a short.
352+
return isNull ? 0 : (short) spanner.getLong(spannerIndex);
353+
}
330354
return isNull ? 0 : checkedCastToShort(spanner.getLong(spannerIndex));
331355
case NUMERIC:
332356
return isNull ? 0 : checkedCastToShort(spanner.getBigDecimal(spannerIndex));

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.jdbc;
1818

1919
import com.google.spanner.v1.TypeCode;
20+
import java.sql.DatabaseMetaData;
2021
import java.sql.PreparedStatement;
2122
import java.sql.SQLType;
2223

@@ -31,6 +32,11 @@ public class JsonType implements SQLType {
3132
* conflicts with the type numbers in java.sql.Types.
3233
*/
3334
public static final int VENDOR_TYPE_NUMBER = 100_000 + TypeCode.JSON_VALUE;
35+
/**
36+
* Define a short type number as well, as this is what is expected to be returned in {@link
37+
* DatabaseMetaData#getTypeInfo()}.
38+
*/
39+
public static final short SHORT_VENDOR_TYPE_NUMBER = (short) VENDOR_TYPE_NUMBER;
3440

3541
private JsonType() {}
3642

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.jdbc;
1818

1919
import com.google.spanner.v1.TypeCode;
20+
import java.sql.DatabaseMetaData;
2021
import java.sql.SQLType;
2122

2223
public class PgJsonbType implements SQLType {
@@ -27,6 +28,11 @@ public class PgJsonbType implements SQLType {
2728
* the range starting at 100,000 (see {@link JsonType}).
2829
*/
2930
public static final int VENDOR_TYPE_NUMBER = 200_000 + TypeCode.JSON_VALUE;
31+
/**
32+
* Define a short type number as well, as this is what is expected to be returned in {@link
33+
* DatabaseMetaData#getTypeInfo()}.
34+
*/
35+
public static final short SHORT_VENDOR_TYPE_NUMBER = (short) VENDOR_TYPE_NUMBER;
3036

3137
private PgJsonbType() {}
3238

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.jdbc;
1818

1919
import com.google.spanner.v1.TypeCode;
20+
import java.sql.DatabaseMetaData;
2021
import java.sql.PreparedStatement;
2122
import java.sql.SQLType;
2223

@@ -31,6 +32,11 @@ public class ProtoEnumType implements SQLType {
3132
* conflicts with the type numbers in java.sql.Types.
3233
*/
3334
public static final int VENDOR_TYPE_NUMBER = 100_000 + TypeCode.ENUM_VALUE;
35+
/**
36+
* Define a short type number as well, as this is what is expected to be returned in {@link
37+
* DatabaseMetaData#getTypeInfo()}.
38+
*/
39+
public static final short SHORT_VENDOR_TYPE_NUMBER = (short) VENDOR_TYPE_NUMBER;
3440

3541
private ProtoEnumType() {}
3642

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.jdbc;
1818

1919
import com.google.spanner.v1.TypeCode;
20+
import java.sql.DatabaseMetaData;
2021
import java.sql.PreparedStatement;
2122
import java.sql.SQLType;
2223

@@ -31,6 +32,11 @@ public class ProtoMessageType implements SQLType {
3132
* conflicts with the type numbers in java.sql.Types.
3233
*/
3334
public static final int VENDOR_TYPE_NUMBER = 100_000 + TypeCode.PROTO_VALUE;
35+
/**
36+
* Define a short type number as well, as this is what is expected to be returned in {@link
37+
* DatabaseMetaData#getTypeInfo()}.
38+
*/
39+
public static final short SHORT_VENDOR_TYPE_NUMBER = (short) VENDOR_TYPE_NUMBER;
3440

3541
private ProtoMessageType() {}
3642

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,24 +475,51 @@ public void testGetTypeInfo() throws SQLException {
475475
try (ResultSet rs = meta.getTypeInfo()) {
476476
assertTrue(rs.next());
477477
assertEquals("STRING", rs.getString("TYPE_NAME"));
478+
assertEquals(Types.NVARCHAR, rs.getInt("DATA_TYPE"));
479+
assertEquals(Types.NVARCHAR, rs.getShort("DATA_TYPE"));
478480
assertTrue(rs.next());
479481
assertEquals("INT64", rs.getString("TYPE_NAME"));
482+
assertEquals(Types.BIGINT, rs.getInt("DATA_TYPE"));
483+
assertEquals(Types.BIGINT, rs.getShort("DATA_TYPE"));
480484
assertTrue(rs.next());
481485
assertEquals("BYTES", rs.getString("TYPE_NAME"));
486+
assertEquals(Types.BINARY, rs.getInt("DATA_TYPE"));
487+
assertEquals(Types.BINARY, rs.getShort("DATA_TYPE"));
482488
assertTrue(rs.next());
483489
assertEquals("FLOAT32", rs.getString("TYPE_NAME"));
490+
assertEquals(Types.REAL, rs.getInt("DATA_TYPE"));
491+
assertEquals(Types.REAL, rs.getShort("DATA_TYPE"));
484492
assertTrue(rs.next());
485493
assertEquals("FLOAT64", rs.getString("TYPE_NAME"));
494+
assertEquals(Types.DOUBLE, rs.getInt("DATA_TYPE"));
495+
assertEquals(Types.DOUBLE, rs.getShort("DATA_TYPE"));
486496
assertTrue(rs.next());
487497
assertEquals("BOOL", rs.getString("TYPE_NAME"));
498+
assertEquals(Types.BOOLEAN, rs.getInt("DATA_TYPE"));
499+
assertEquals(Types.BOOLEAN, rs.getShort("DATA_TYPE"));
488500
assertTrue(rs.next());
489501
assertEquals("DATE", rs.getString("TYPE_NAME"));
502+
assertEquals(Types.DATE, rs.getInt("DATA_TYPE"));
503+
assertEquals(Types.DATE, rs.getShort("DATA_TYPE"));
490504
assertTrue(rs.next());
491505
assertEquals("TIMESTAMP", rs.getString("TYPE_NAME"));
506+
assertEquals(Types.TIMESTAMP, rs.getInt("DATA_TYPE"));
507+
assertEquals(Types.TIMESTAMP, rs.getShort("DATA_TYPE"));
492508
assertTrue(rs.next());
493509
assertEquals("NUMERIC", rs.getString("TYPE_NAME"));
510+
assertEquals(Types.NUMERIC, rs.getInt("DATA_TYPE"));
511+
assertEquals(Types.NUMERIC, rs.getShort("DATA_TYPE"));
494512
assertTrue(rs.next());
495-
assertEquals(dialect == Dialect.POSTGRESQL ? "JSONB" : "JSON", rs.getString("TYPE_NAME"));
513+
if (dialect == Dialect.POSTGRESQL) {
514+
assertEquals("JSONB", rs.getString("TYPE_NAME"));
515+
assertEquals(PgJsonbType.VENDOR_TYPE_NUMBER, rs.getInt("DATA_TYPE"));
516+
assertEquals(PgJsonbType.SHORT_VENDOR_TYPE_NUMBER, rs.getShort("DATA_TYPE"));
517+
} else {
518+
assertEquals("JSON", rs.getString("TYPE_NAME"));
519+
assertEquals(JsonType.VENDOR_TYPE_NUMBER, rs.getInt("DATA_TYPE"));
520+
assertEquals(JsonType.SHORT_VENDOR_TYPE_NUMBER, rs.getShort("DATA_TYPE"));
521+
}
522+
496523
assertFalse(rs.next());
497524
ResultSetMetaData rsmd = rs.getMetaData();
498525
assertEquals(18, rsmd.getColumnCount());

0 commit comments

Comments
 (0)