Skip to content

Commit 3d4989b

Browse files
committed
feat: add support for UUID data type
1 parent ff9a244 commit 3d4989b

File tree

9 files changed

+198
-37
lines changed

9 files changed

+198
-37
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.HashSet;
3030
import java.util.List;
3131
import java.util.Set;
32+
import java.util.UUID;
3233

3334
/** Enum for mapping Cloud Spanner data types to Java classes and JDBC SQL {@link Types}. */
3435
enum JdbcDataType {
@@ -379,6 +380,32 @@ public Type getSpannerType() {
379380
return Type.timestamp();
380381
}
381382
},
383+
UUID {
384+
@Override
385+
public int getSqlType() {
386+
return UuidType.VENDOR_TYPE_NUMBER;
387+
}
388+
389+
@Override
390+
public Class<UUID> getJavaClass() {
391+
return UUID.class;
392+
}
393+
394+
@Override
395+
public Code getCode() {
396+
return Code.UUID;
397+
}
398+
399+
@Override
400+
public List<UUID> getArrayElements(ResultSet rs, int columnIndex) {
401+
return rs.getUuidList(columnIndex);
402+
}
403+
404+
@Override
405+
public Type getSpannerType() {
406+
return Type.uuid();
407+
}
408+
},
382409
STRUCT {
383410
@Override
384411
public int getSqlType() {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,8 @@ private Builder setParamWithUnknownType(ValueBinder<Builder> binder, Object valu
729729
} else if (Time.class.isAssignableFrom(value.getClass())) {
730730
Time timeValue = (Time) value;
731731
return binder.to(JdbcTypeConverter.toGoogleTimestamp(new Timestamp(timeValue.getTime())));
732+
} else if (UUID.class.isAssignableFrom(value.getClass())) {
733+
return binder.to((UUID) value);
732734
} else if (String.class.isAssignableFrom(value.getClass())) {
733735
String stringVal = (String) value;
734736
return binder.to(stringVal);
@@ -853,6 +855,9 @@ private Builder setArrayValue(ValueBinder<Builder> binder, int type, Object valu
853855
com.google.protobuf.Value.newBuilder()
854856
.setNullValue(NullValue.NULL_VALUE)
855857
.build()));
858+
case UuidType.VENDOR_TYPE_NUMBER:
859+
case UuidType.SHORT_VENDOR_TYPE_NUMBER:
860+
return binder.toUuidArray(null);
856861
default:
857862
return binder.to(
858863
Value.untyped(
@@ -907,6 +912,8 @@ private Builder setArrayValue(ValueBinder<Builder> binder, int type, Object valu
907912
return binder.toDateArray(JdbcTypeConverter.toGoogleDates((Date[]) value));
908913
} else if (Timestamp[].class.isAssignableFrom(value.getClass())) {
909914
return binder.toTimestampArray(JdbcTypeConverter.toGoogleTimestamps((Timestamp[]) value));
915+
} else if (UUID[].class.isAssignableFrom(value.getClass())) {
916+
return binder.toUuidArray(Arrays.asList((UUID[]) value));
910917
} else if (String[].class.isAssignableFrom(value.getClass())) {
911918
if (type == JsonType.VENDOR_TYPE_NUMBER || type == JsonType.SHORT_VENDOR_TYPE_NUMBER) {
912919
return binder.toJsonArray(Arrays.asList((String[]) value));

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

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.util.List;
5353
import java.util.Map;
5454
import java.util.NoSuchElementException;
55+
import java.util.UUID;
5556
import javax.annotation.Nonnull;
5657

5758
/** Implementation of {@link ResultSet} for Cloud Spanner */
@@ -623,6 +624,38 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException {
623624
}
624625
}
625626

627+
public UUID getUUID(int columnIndex) throws SQLException {
628+
checkClosedAndValidRow();
629+
if (isNull(columnIndex)) {
630+
return null;
631+
}
632+
int spannerIndex = columnIndex - 1;
633+
Code type = getMainTypeCode(spanner.getColumnType(spannerIndex));
634+
switch (type) {
635+
case UUID:
636+
return spanner.getUuid(spannerIndex);
637+
case STRING:
638+
return UUID.fromString(spanner.getString(spannerIndex));
639+
case BYTES:
640+
case DATE:
641+
case TIMESTAMP:
642+
case BOOL:
643+
case FLOAT32:
644+
case FLOAT64:
645+
case INT64:
646+
case NUMERIC:
647+
case PG_NUMERIC:
648+
case JSON:
649+
case PG_JSONB:
650+
case STRUCT:
651+
case PROTO:
652+
case ENUM:
653+
case ARRAY:
654+
default:
655+
throw createInvalidToGetAs("uuid", type);
656+
}
657+
}
658+
626659
private InputStream getInputStream(String val, Charset charset) {
627660
if (val == null) return null;
628661
byte[] b = val.getBytes(charset);
@@ -764,35 +797,46 @@ public Object getObject(int columnIndex) throws SQLException {
764797
}
765798

766799
private Object getObject(Type type, int columnIndex) throws SQLException {
767-
// TODO: Refactor to check based on type code.
768-
if (type == Type.bool()) return getBoolean(columnIndex);
769-
if (type == Type.bytes()) return getBytes(columnIndex);
770-
if (type == Type.date()) return getDate(columnIndex);
771-
if (type == Type.float32()) {
772-
return getFloat(columnIndex);
773-
}
774-
if (type == Type.float64()) return getDouble(columnIndex);
775-
if (type == Type.int64() || type == Type.pgOid()) {
776-
return getLong(columnIndex);
777-
}
778-
if (type == Type.numeric()) return getBigDecimal(columnIndex);
779-
if (type == Type.pgNumeric()) {
780-
final String value = getString(columnIndex);
781-
try {
782-
return parseBigDecimal(value);
783-
} catch (Exception e) {
784-
return parseDouble(value);
785-
}
786-
}
787-
if (type == Type.string()) return getString(columnIndex);
788-
if (type == Type.json() || type == Type.pgJsonb()) {
789-
return getString(columnIndex);
800+
JdbcPreconditions.checkArgument(type != null, "type is null");
801+
switch (type.getCode()) {
802+
case BOOL:
803+
return getBoolean(columnIndex);
804+
case BYTES:
805+
case PROTO:
806+
return getBytes(columnIndex);
807+
case DATE:
808+
return getDate(columnIndex);
809+
case FLOAT32:
810+
return getFloat(columnIndex);
811+
case FLOAT64:
812+
return getDouble(columnIndex);
813+
case INT64:
814+
case PG_OID:
815+
case ENUM:
816+
return getLong(columnIndex);
817+
case NUMERIC:
818+
return getBigDecimal(columnIndex);
819+
case PG_NUMERIC:
820+
final String value = getString(columnIndex);
821+
try {
822+
return parseBigDecimal(value);
823+
} catch (Exception e) {
824+
return parseDouble(value);
825+
}
826+
case STRING:
827+
case JSON:
828+
case PG_JSONB:
829+
return getString(columnIndex);
830+
case TIMESTAMP:
831+
return getTimestamp(columnIndex);
832+
case UUID:
833+
return getUUID(columnIndex);
834+
case ARRAY:
835+
return getArray(columnIndex);
836+
default:
837+
throw JdbcSqlExceptionFactory.of(
838+
"Unknown type: " + type, com.google.rpc.Code.INVALID_ARGUMENT);
790839
}
791-
if (type == Type.timestamp()) return getTimestamp(columnIndex);
792-
if (type.getCode() == Code.PROTO) return getBytes(columnIndex);
793-
if (type.getCode() == Code.ENUM) return getLong(columnIndex);
794-
if (type.getCode() == Code.ARRAY) return getArray(columnIndex);
795-
throw JdbcSqlExceptionFactory.of("Unknown type: " + type, com.google.rpc.Code.INVALID_ARGUMENT);
796840
}
797841

798842
@Override

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ static Object convert(Object value, Type type, Class<?> targetType) throws SQLEx
8282
if (value == null) {
8383
return null;
8484
}
85+
if (value.getClass().equals(targetType)) {
86+
return value;
87+
}
8588
try {
8689
if (targetType.equals(Value.class)) {
8790
return convertToSpannerValue(value, type);
@@ -370,7 +373,9 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx
370373

371374
private static void checkValidTypeAndValueForConvert(Type type, Object value)
372375
throws SQLException {
373-
if (value == null) return;
376+
if (value == null) {
377+
return;
378+
}
374379
JdbcPreconditions.checkArgument(
375380
type.getCode() != Code.ARRAY || Array.class.isAssignableFrom(value.getClass()),
376381
"input type is array, but input value is not an instance of java.sql.Array");
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.jdbc;
18+
19+
import com.google.spanner.v1.TypeCode;
20+
import java.sql.DatabaseMetaData;
21+
import java.sql.PreparedStatement;
22+
import java.sql.SQLType;
23+
24+
/**
25+
* Custom SQL type for Spanner UUID data type. This type (or the vendor type number) must be used
26+
* when setting a UUID parameter using {@link PreparedStatement#setObject(int, Object, SQLType)}.
27+
*/
28+
public class UuidType implements SQLType {
29+
public static final UuidType INSTANCE = new UuidType();
30+
/**
31+
* Spanner does not have any type numbers, but the code values are unique. Add 100,000 to avoid
32+
* conflicts with the type numbers in java.sql.Types.
33+
*/
34+
public static final int VENDOR_TYPE_NUMBER = 100_000 + TypeCode.UUID_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;
40+
41+
private UuidType() {}
42+
43+
@Override
44+
public String getName() {
45+
return "UUID";
46+
}
47+
48+
@Override
49+
public String getVendor() {
50+
return UuidType.class.getPackage().getName();
51+
}
52+
53+
@Override
54+
public Integer getVendorTypeNumber() {
55+
return VENDOR_TYPE_NUMBER;
56+
}
57+
58+
public String toString() {
59+
return getName();
60+
}
61+
}

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.Arrays;
4141
import java.util.Base64;
4242
import java.util.Map;
43+
import java.util.UUID;
4344
import java.util.stream.Collectors;
4445
import org.junit.Test;
4546
import org.junit.runner.RunWith;
@@ -74,6 +75,7 @@ public void testSelectAllTypes() {
7475
new java.sql.Date(
7576
DATE_VALUE.getYear() - 1900, DATE_VALUE.getMonth() - 1, DATE_VALUE.getDayOfMonth()),
7677
resultSet.getDate(++col));
78+
assertEquals(UUID_VALUE, resultSet.getObject(++col, UUID.class));
7779
assertEquals(TIMESTAMP_VALUE.toSqlTimestamp(), resultSet.getTimestamp(++col));
7880
if (dialect == Dialect.POSTGRESQL) {
7981
assertEquals(PG_OID_VALUE, resultSet.getLong(++col));
@@ -120,6 +122,8 @@ public void testSelectAllTypes() {
120122
date.getYear() - 1900, date.getMonth() - 1, date.getDayOfMonth()))
121123
.collect(Collectors.toList()),
122124
Arrays.asList((Date[]) resultSet.getArray(++col).getArray()));
125+
assertEquals(
126+
UUID_ARRAY_VALUE, Arrays.asList((UUID[]) resultSet.getArray(++col).getArray()));
123127
assertEquals(
124128
TIMESTAMP_ARRAY_VALUE.stream()
125129
.map(timestamp -> timestamp == null ? null : timestamp.toSqlTimestamp())
@@ -146,7 +150,7 @@ public void testInsertAllTypes() {
146150
insertStatement =
147151
insertStatement.toBuilder()
148152
.replace(insertStatement.getSql().replaceAll("@p", "\\$"))
149-
.bind("p16")
153+
.bind("p17")
150154
.to(
151155
com.google.cloud.spanner.Value.pgNumericArray(
152156
NUMERIC_ARRAY_VALUE.stream()
@@ -184,6 +188,7 @@ public void testInsertAllTypes() {
184188
DATE_VALUE.getYear() - 1900,
185189
DATE_VALUE.getMonth() - 1,
186190
DATE_VALUE.getDayOfMonth()));
191+
statement.setObject(++param, UUID_VALUE);
187192
statement.setTimestamp(++param, TIMESTAMP_VALUE.toSqlTimestamp());
188193
if (dialect == Dialect.POSTGRESQL) {
189194
statement.setLong(++param, PG_OID_VALUE);
@@ -243,6 +248,8 @@ public void testInsertAllTypes() {
243248
date.getMonth() - 1,
244249
date.getDayOfMonth()))
245250
.toArray(Date[]::new)));
251+
statement.setArray(
252+
++param, connection.createArrayOf("UUID", UUID_ARRAY_VALUE.toArray(new UUID[0])));
246253
statement.setArray(
247254
++param,
248255
connection.createArrayOf(
@@ -262,8 +269,8 @@ public void testInsertAllTypes() {
262269
ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
263270
Map<String, Type> paramTypes = request.getParamTypesMap();
264271
Map<String, Value> params = request.getParams().getFieldsMap();
265-
assertEquals(dialect == Dialect.POSTGRESQL ? 22 : 20, paramTypes.size());
266-
assertEquals(dialect == Dialect.POSTGRESQL ? 22 : 20, params.size());
272+
assertEquals(dialect == Dialect.POSTGRESQL ? 24 : 22, paramTypes.size());
273+
assertEquals(dialect == Dialect.POSTGRESQL ? 24 : 22, params.size());
267274

268275
// Verify param types.
269276
ImmutableList<TypeCode> expectedTypes =
@@ -277,6 +284,7 @@ public void testInsertAllTypes() {
277284
TypeCode.JSON,
278285
TypeCode.BYTES,
279286
TypeCode.DATE,
287+
TypeCode.UUID,
280288
TypeCode.TIMESTAMP);
281289
if (dialect == Dialect.POSTGRESQL) {
282290
expectedTypes =
@@ -306,6 +314,7 @@ public void testInsertAllTypes() {
306314
Base64.getEncoder().encodeToString(BYTES_VALUE),
307315
params.get("p" + ++col).getStringValue());
308316
assertEquals(DATE_VALUE.toString(), params.get("p" + ++col).getStringValue());
317+
assertEquals(UUID_VALUE.toString(), params.get("p" + ++col).getStringValue());
309318
assertEquals(TIMESTAMP_VALUE.toString(), params.get("p" + ++col).getStringValue());
310319
if (dialect == Dialect.POSTGRESQL) {
311320
assertEquals(String.valueOf(PG_OID_VALUE), params.get("p" + ++col).getStringValue());
@@ -376,6 +385,11 @@ public void testInsertAllTypes() {
376385
? null
377386
: com.google.cloud.Date.parseDate(value.getStringValue()))
378387
.collect(Collectors.toList()));
388+
assertEquals(
389+
UUID_ARRAY_VALUE,
390+
params.get("p" + ++col).getListValue().getValuesList().stream()
391+
.map(value -> value.hasNullValue() ? null : UUID.fromString(value.getStringValue()))
392+
.collect(Collectors.toList()));
379393
assertEquals(
380394
TIMESTAMP_ARRAY_VALUE,
381395
params.get("p" + ++col).getListValue().getValuesList().stream()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ public void testSetParameterWithoutType() throws SQLException {
729729
verifyParameter(params, Value.string("test"));
730730
params.setParameter(1, UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), (Integer) null);
731731
assertEquals(UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), params.getParameter(1));
732-
verifyParameter(params, Value.string("83b988cf-1f4e-428a-be3d-cc712621942e"));
732+
verifyParameter(params, Value.uuid(UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e")));
733733

734734
String jsonString = "{\"test\": \"value\"}";
735735
params.setParameter(1, Value.json(jsonString), (Integer) null);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public void clearRequests() {
7474
private int getExpectedColumnCount(Dialect dialect) {
7575
// GoogleSQL also adds 4 PROTO columns.
7676
// PostgreSQL adds 2 OID columns.
77-
return dialect == Dialect.GOOGLE_STANDARD_SQL ? 24 : 22;
77+
return dialect == Dialect.GOOGLE_STANDARD_SQL ? 26 : 24;
7878
}
7979

8080
private String createUrl() {

0 commit comments

Comments
 (0)