diff --git a/google-cloud-spanner-executor/src/main/java/com/google/cloud/executor/spanner/CloudClientExecutor.java b/google-cloud-spanner-executor/src/main/java/com/google/cloud/executor/spanner/CloudClientExecutor.java index 64a29ed3e61..43cd54ccf46 100644 --- a/google-cloud-spanner-executor/src/main/java/com/google/cloud/executor/spanner/CloudClientExecutor.java +++ b/google-cloud-spanner-executor/src/main/java/com/google/cloud/executor/spanner/CloudClientExecutor.java @@ -176,6 +176,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; @@ -2897,6 +2898,9 @@ private com.google.spanner.executor.v1.ValueList buildStruct(StructReader struct case DATE: value.setDateDaysValue(daysFromDate(struct.getDate(i))); break; + case UUID: + value.setStringValue(struct.getUuid(i).toString()); + break; case NUMERIC: String ascii = struct.getBigDecimal(i).toPlainString(); value.setStringValue(ascii); @@ -3043,6 +3047,25 @@ private com.google.spanner.executor.v1.ValueList buildStruct(StructReader struct com.google.spanner.v1.Type.newBuilder().setCode(TypeCode.DATE).build()); } break; + case UUID: + { + com.google.spanner.executor.v1.ValueList.Builder builder = + com.google.spanner.executor.v1.ValueList.newBuilder(); + List values = struct.getUuidList(i); + for (UUID uuidValue : values) { + com.google.spanner.executor.v1.Value.Builder valueProto = + com.google.spanner.executor.v1.Value.newBuilder(); + if (uuidValue == null) { + builder.addValue(valueProto.setIsNull(true).build()); + } else { + builder.addValue(valueProto.setStringValue(uuidValue.toString()).build()); + } + } + value.setArrayValue(builder.build()); + value.setArrayType( + com.google.spanner.v1.Type.newBuilder().setCode(TypeCode.UUID).build()); + } + break; case TIMESTAMP: { com.google.spanner.executor.v1.ValueList.Builder builder = @@ -3226,6 +3249,7 @@ private static com.google.cloud.spanner.Key keyProtoToCloudKey( case BYTES: case FLOAT64: case DATE: + case UUID: case TIMESTAMP: case NUMERIC: case JSON: @@ -3313,6 +3337,9 @@ private static com.google.cloud.spanner.Value valueProtoToCloudValue( case DATE: return com.google.cloud.spanner.Value.date( value.hasIsNull() ? null : dateFromDays(value.getDateDaysValue())); + case UUID: + return com.google.cloud.spanner.Value.uuid( + value.hasIsNull() ? null : UUID.fromString(value.getStringValue())); case NUMERIC: { if (value.hasIsNull()) { @@ -3437,6 +3464,20 @@ private static com.google.cloud.spanner.Value valueProtoToCloudValue( .collect(Collectors.toList()), CloudClientExecutor::dateFromDays)); } + case UUID: + if (value.hasIsNull()) { + return com.google.cloud.spanner.Value.uuidArray(null); + } else { + return com.google.cloud.spanner.Value.uuidArray( + unmarshallValueList( + value.getArrayValue().getValueList().stream() + .map(com.google.spanner.executor.v1.Value::getIsNull) + .collect(Collectors.toList()), + value.getArrayValue().getValueList().stream() + .map(com.google.spanner.executor.v1.Value::getStringValue) + .collect(Collectors.toList()), + UUID::fromString)); + } case NUMERIC: { if (value.hasIsNull()) { @@ -3602,6 +3643,8 @@ private static com.google.cloud.spanner.Type typeProtoToCloudType( return com.google.cloud.spanner.Type.float64(); case DATE: return com.google.cloud.spanner.Type.date(); + case UUID: + return com.google.cloud.spanner.Type.uuid(); case TIMESTAMP: return com.google.cloud.spanner.Type.timestamp(); case NUMERIC: @@ -3658,6 +3701,8 @@ private static com.google.spanner.v1.Type cloudTypeToTypeProto(@Nonnull Type clo return com.google.spanner.v1.Type.newBuilder().setCode(TypeCode.TIMESTAMP).build(); case DATE: return com.google.spanner.v1.Type.newBuilder().setCode(TypeCode.DATE).build(); + case UUID: + return com.google.spanner.v1.Type.newBuilder().setCode(TypeCode.UUID).build(); case NUMERIC: return com.google.spanner.v1.Type.newBuilder().setCode(TypeCode.NUMERIC).build(); case PG_NUMERIC: diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 5c103beca6f..ffc2f8e7d04 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -566,6 +566,48 @@ java.util.List getFloat32Array() + + + 7013 + com/google/cloud/spanner/AbstractStructReader + java.util.UUID getUuidInternal(int) + + + 7013 + com/google/cloud/spanner/AbstractStructReader + java.util.List getUuidListInternal(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.UUID getUuid(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.UUID getUuid(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getUuidList(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getUuidList(java.lang.String) + + + 7013 + com/google/cloud/spanner/Value + java.util.UUID getUuid() + + + 7013 + com/google/cloud/spanner/Value + java.util.List getUuidArray() + + 7012 diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 608ccf7544d..17142227911 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -38,6 +38,7 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -434,6 +435,11 @@ protected Date getDateInternal(int columnIndex) { return currRow().getDateInternal(columnIndex); } + @Override + protected UUID getUuidInternal(int columnIndex) { + return currRow().getUuidInternal(columnIndex); + } + @Override protected Value getValueInternal(int columnIndex) { return currRow().getValueInternal(columnIndex); @@ -526,6 +532,11 @@ protected List getDateListInternal(int columnIndex) { return currRow().getDateListInternal(columnIndex); } + @Override + protected List getUuidListInternal(int columnIndex) { + return currRow().getUuidListInternal(columnIndex); + } + @Override protected List getStructListInternal(int columnIndex) { return currRow().getStructListInternal(columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java index d13c61aaf01..8841d4a6694 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.function.Function; /** @@ -67,6 +68,8 @@ protected String getPgJsonbInternal(int columnIndex) { protected abstract Date getDateInternal(int columnIndex); + protected abstract UUID getUuidInternal(int columnIndex); + protected T getProtoMessageInternal(int columnIndex, T message) { throw new UnsupportedOperationException("Not implemented"); } @@ -128,6 +131,8 @@ protected List getPgJsonbListInternal(int columnIndex) { protected abstract List getDateListInternal(int columnIndex); + protected abstract List getUuidListInternal(int columnIndex); + protected abstract List getStructListInternal(int columnIndex); @Override @@ -299,6 +304,19 @@ public Date getDate(String columnName) { return getDateInternal(columnIndex); } + @Override + public UUID getUuid(int columnIndex) { + checkNonNullOfType(columnIndex, Type.uuid(), columnIndex); + return getUuidInternal(columnIndex); + } + + @Override + public UUID getUuid(String columnName) { + final int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.uuid(), columnName); + return getUuid(columnIndex); + } + @Override public T getProtoEnum( int columnIndex, Function method) { @@ -583,6 +601,19 @@ public List getDateList(String columnName) { return getDateListInternal(columnIndex); } + @Override + public List getUuidList(int columnIndex) { + checkNonNullOfType(columnIndex, Type.array(Type.uuid()), columnIndex); + return getUuidListInternal(columnIndex); + } + + @Override + public List getUuidList(String columnName) { + final int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.array(Type.uuid()), columnName); + return getUuidList(columnIndex); + } + @Override public List getStructList(int columnIndex) { checkNonNullArrayOfStruct(columnIndex, columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java index b3e37ffcddb..edbf0d564d2 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java @@ -26,6 +26,7 @@ import com.google.protobuf.ProtocolMessageEnum; import java.math.BigDecimal; import java.util.List; +import java.util.UUID; import java.util.function.Function; /** Forwarding implements of StructReader */ @@ -225,12 +226,24 @@ public Date getDate(int columnIndex) { return delegate.get().getDate(columnIndex); } + @Override + public UUID getUuid(int columnIndex) { + checkValidState(); + return delegate.get().getUuid(columnIndex); + } + @Override public Date getDate(String columnName) { checkValidState(); return delegate.get().getDate(columnName); } + @Override + public UUID getUuid(String columnName) { + checkValidState(); + return delegate.get().getUuid(columnName); + } + @Override public boolean[] getBooleanArray(int columnIndex) { checkValidState(); @@ -409,6 +422,18 @@ public List getDateList(String columnName) { return delegate.get().getDateList(columnName); } + @Override + public List getUuidList(int columnIndex) { + checkValidState(); + return delegate.get().getUuidList(columnIndex); + } + + @Override + public List getUuidList(String columnName) { + checkValidState(); + return delegate.get().getUuidList(columnName); + } + @Override public List getProtoMessageList(int columnIndex, T message) { checkValidState(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java index 4d07a12880c..ec052e4044e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java @@ -49,6 +49,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; @@ -131,6 +132,9 @@ private Object writeReplace() { case DATE: builder.set(fieldName).to((Date) value); break; + case UUID: + builder.set(fieldName).to((UUID) value); + break; case ARRAY: final Type elementType = fieldType.getArrayElementType(); switch (elementType.getCode()) { @@ -184,6 +188,9 @@ private Object writeReplace() { case DATE: builder.set(fieldName).toDateArray((Iterable) value); break; + case UUID: + builder.set(fieldName).toUuidArray((Iterable) value); + break; case STRUCT: builder.set(fieldName).toStructArray(elementType, (Iterable) value); break; @@ -298,6 +305,9 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot case DATE: checkType(fieldType, proto, KindCase.STRING_VALUE); return Date.parseDate(proto.getStringValue()); + case UUID: + checkType(fieldType, proto, KindCase.STRING_VALUE); + return UUID.fromString(proto.getStringValue()); case ARRAY: checkType(fieldType, proto, KindCase.LIST_VALUE); ListValue listValue = proto.getListValue(); @@ -347,6 +357,7 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) { case BYTES: case TIMESTAMP: case DATE: + case UUID: case STRUCT: case PROTO: return Lists.transform(listValue.getValuesList(), input -> decodeValue(elementType, input)); @@ -503,6 +514,12 @@ protected Date getDateInternal(int columnIndex) { return (Date) rowData.get(columnIndex); } + @Override + protected UUID getUuidInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (UUID) rowData.get(columnIndex); + } + private boolean isUnrecognizedType(int columnIndex) { return type.getStructFields().get(columnIndex).getType().getCode() == Code.UNRECOGNIZED; } @@ -624,6 +641,8 @@ protected Value getValueInternal(int columnIndex) { return Value.timestamp(isNull ? null : getTimestampInternal(columnIndex)); case DATE: return Value.date(isNull ? null : getDateInternal(columnIndex)); + case UUID: + return Value.uuid(isNull ? null : getUuidInternal(columnIndex)); case STRUCT: return Value.struct(isNull ? null : getStructInternal(columnIndex)); case UNRECOGNIZED: @@ -664,6 +683,8 @@ protected Value getValueInternal(int columnIndex) { return Value.timestampArray(isNull ? null : getTimestampListInternal(columnIndex)); case DATE: return Value.dateArray(isNull ? null : getDateListInternal(columnIndex)); + case UUID: + return Value.uuidArray(isNull ? null : getUuidListInternal(columnIndex)); case STRUCT: return Value.structArray( elementType, isNull ? null : getStructListInternal(columnIndex)); @@ -847,6 +868,12 @@ protected List getDateListInternal(int columnIndex) { return Collections.unmodifiableList((List) rowData.get(columnIndex)); } + @Override + protected List getUuidListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + @Override @SuppressWarnings("unchecked") // We know ARRAY> produces a List. protected List getStructListInternal(int columnIndex) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java index 3d12cf5ad2c..39b73fec3a5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java @@ -35,6 +35,7 @@ import com.google.spanner.v1.ResultSetStats; import java.math.BigDecimal; import java.util.List; +import java.util.UUID; import java.util.function.Function; /** Utility methods for working with {@link com.google.cloud.spanner.ResultSet}. */ @@ -321,11 +322,21 @@ public Date getDate(int columnIndex) { return getCurrentRowAsStruct().getDate(columnIndex); } + @Override + public UUID getUuid(int columnIndex) { + return getCurrentRowAsStruct().getUuid(columnIndex); + } + @Override public Date getDate(String columnName) { return getCurrentRowAsStruct().getDate(columnName); } + @Override + public UUID getUuid(String columnName) { + return getCurrentRowAsStruct().getUuid(columnName); + } + @Override public T getProtoMessage(int columnIndex, T message) { return getCurrentRowAsStruct().getProtoMessage(columnIndex, message); @@ -508,6 +519,16 @@ public List getDateList(String columnName) { return getCurrentRowAsStruct().getDateList(columnName); } + @Override + public List getUuidList(int columnIndex) { + return getCurrentRowAsStruct().getUuidList(columnIndex); + } + + @Override + public List getUuidList(String columnNameÏ) { + return getCurrentRowAsStruct().getUuidList(columnNameÏ); + } + @Override public List getProtoMessageList(int columnIndex, T message) { return getCurrentRowAsStruct().getProtoMessageList(columnIndex, message); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java index 112ecc8120c..d44a12e7291 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.function.Function; import javax.annotation.concurrent.Immutable; @@ -226,6 +227,11 @@ protected Date getDateInternal(int columnIndex) { return values.get(columnIndex).getDate(); } + @Override + protected UUID getUuidInternal(int columnIndex) { + return values.get(columnIndex).getUuid(); + } + @Override protected T getProtoMessageInternal(int columnIndex, T message) { return values.get(columnIndex).getProtoMessage(message); @@ -334,6 +340,11 @@ protected List getDateListInternal(int columnIndex) { return values.get(columnIndex).getDateArray(); } + @Override + protected List getUuidListInternal(int columnIndex) { + return values.get(columnIndex).getUuidArray(); + } + @Override protected List getStructListInternal(int columnIndex) { return values.get(columnIndex).getStructArray(); @@ -420,6 +431,8 @@ private Object getAsObject(int columnIndex) { return getTimestampInternal(columnIndex); case DATE: return getDateInternal(columnIndex); + case UUID: + return getUuidInternal(columnIndex); case STRUCT: return getStructInternal(columnIndex); case ARRAY: @@ -451,6 +464,8 @@ private Object getAsObject(int columnIndex) { return getTimestampListInternal(columnIndex); case DATE: return getDateListInternal(columnIndex); + case UUID: + return getUuidListInternal(columnIndex); case STRUCT: return getStructListInternal(columnIndex); default: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java index f9967db0451..ce2ca3209c7 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java @@ -23,6 +23,7 @@ import com.google.protobuf.ProtocolMessageEnum; import java.math.BigDecimal; import java.util.List; +import java.util.UUID; import java.util.function.Function; /** @@ -291,12 +292,16 @@ default T getProtoEnum( */ Date getDate(int columnIndex); + UUID getUuid(int columnIndex); + /** * @param columnName name of the column * @return the value of a non-{@code NULL} column with type {@link Type#date()}. */ Date getDate(String columnName); + UUID getUuid(String columnName); + /** * @param columnIndex index of the column * @return the value of a nullable column as a {@link Value}. @@ -625,6 +630,10 @@ default List getProtoEnumList( */ List getDateList(String columnName); + List getUuidList(int columnIndex); + + List getUuidList(String columnNameÏ); + /** * @param columnIndex index of the column * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.struct(...))} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java index 748cb7f87ec..85944bbc20f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java @@ -59,6 +59,7 @@ public final class Type implements Serializable { private static final Type TYPE_BYTES = new Type(Code.BYTES, null, null); private static final Type TYPE_TIMESTAMP = new Type(Code.TIMESTAMP, null, null); private static final Type TYPE_DATE = new Type(Code.DATE, null, null); + private static final Type TYPE_UUID = new Type(Code.UUID, null, null); private static final Type TYPE_ARRAY_BOOL = new Type(Code.ARRAY, TYPE_BOOL, null); private static final Type TYPE_ARRAY_INT64 = new Type(Code.ARRAY, TYPE_INT64, null); private static final Type TYPE_ARRAY_FLOAT32 = new Type(Code.ARRAY, TYPE_FLOAT32, null); @@ -72,6 +73,7 @@ public final class Type implements Serializable { private static final Type TYPE_ARRAY_BYTES = new Type(Code.ARRAY, TYPE_BYTES, null); private static final Type TYPE_ARRAY_TIMESTAMP = new Type(Code.ARRAY, TYPE_TIMESTAMP, null); private static final Type TYPE_ARRAY_DATE = new Type(Code.ARRAY, TYPE_DATE, null); + private static final Type TYPE_ARRAY_UUID = new Type(Code.ARRAY, TYPE_UUID, null); private static final int AMBIGUOUS_FIELD = -1; private static final long serialVersionUID = -3076152125004114582L; @@ -183,6 +185,10 @@ public static Type date() { return TYPE_DATE; } + public static Type uuid() { + return TYPE_UUID; + } + /** Returns a descriptor for an array of {@code elementType}. */ public static Type array(Type elementType) { Preconditions.checkNotNull(elementType); @@ -213,6 +219,8 @@ public static Type array(Type elementType) { return TYPE_ARRAY_TIMESTAMP; case DATE: return TYPE_ARRAY_DATE; + case UUID: + return TYPE_ARRAY_UUID; default: return new Type(Code.ARRAY, elementType, null); } @@ -295,6 +303,7 @@ public enum Code { BYTES(TypeCode.BYTES, "bytea"), TIMESTAMP(TypeCode.TIMESTAMP, "timestamp with time zone"), DATE(TypeCode.DATE, "date"), + UUID(TypeCode.UUID, "uuid"), ARRAY(TypeCode.ARRAY, "array"), STRUCT(TypeCode.STRUCT, "struct"); @@ -610,6 +619,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) { return timestamp(); case DATE: return date(); + case UUID: + return uuid(); case PROTO: return proto(proto.getProtoTypeFqn()); case ENUM: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index c2c851d6dd8..8378c216e8c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -47,6 +47,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -386,6 +387,10 @@ public static Value date(@Nullable Date v) { return new DateImpl(v == null, v); } + public static Value uuid(@Nullable UUID v) { + return new UuidImpl(v == null, v); + } + /** Returns a non-{@code NULL} {#code STRUCT} value. */ public static Value struct(Struct v) { Preconditions.checkNotNull(v, "Illegal call to create a NULL struct value."); @@ -776,6 +781,10 @@ public static Value dateArray(@Nullable Iterable v) { return new DateArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); } + public static Value uuidArray(@Nullable Iterable v) { + return new UuidArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); + } + /** * Returns an {@code ARRAY>} value. * @@ -915,6 +924,8 @@ public T getProtoEnum( */ public abstract Date getDate(); + public abstract UUID getUuid(); + /** * Returns the value of a {@code STRUCT}-typed instance. * @@ -1035,6 +1046,8 @@ public List getProtoEnumArray( */ public abstract List getDateArray(); + public abstract List getUuidArray(); + /** * Returns the value of an {@code ARRAY>}-typed instance. While the returned list * itself will never be {@code null}, elements of that list may be null. @@ -1314,6 +1327,11 @@ public Date getDate() { throw defaultGetter(Type.date()); } + @Override + public UUID getUuid() { + throw defaultGetter(Type.uuid()); + } + @Override public Struct getStruct() { if (getType().getCode() != Type.Code.STRUCT) { @@ -1378,6 +1396,11 @@ public List getDateArray() { throw defaultGetter(Type.array(Type.date())); } + @Override + public List getUuidArray() { + throw defaultGetter(Type.array(Type.uuid())); + } + @Override public List getStructArray() { if (getType().getCode() != Type.Code.ARRAY @@ -1795,6 +1818,24 @@ void valueToString(StringBuilder b) { } } + private static class UuidImpl extends AbstractObjectValue { + + private UuidImpl(boolean isNull, UUID value) { + super(isNull, Type.uuid(), value); + } + + @Override + public UUID getUuid() { + checkNotNull(); + return value; + } + + @Override + void valueToString(StringBuilder b) { + b.append(value); + } + } + private static class StringImpl extends AbstractObjectValue { private StringImpl(boolean isNull, @Nullable String value) { @@ -2797,6 +2838,24 @@ void appendElement(StringBuilder b, Date element) { } } + private static class UuidArrayImpl extends AbstractArrayValue { + + private UuidArrayImpl(boolean isNull, @Nullable List values) { + super(isNull, Type.uuid(), values); + } + + @Override + public List getUuidArray() { + checkNotNull(); + return value; + } + + @Override + void appendElement(StringBuilder b, UUID element) { + b.append(element); + } + } + private static class NumericArrayImpl extends AbstractArrayValue { private NumericArrayImpl(boolean isNull, @Nullable List values) { @@ -2938,6 +2997,8 @@ private Value getValue(int fieldIndex) { return Value.pgOid(value.getLong(fieldIndex)); case DATE: return Value.date(value.getDate(fieldIndex)); + case UUID: + return Value.uuid(value.getUuid(fieldIndex)); case TIMESTAMP: return Value.timestamp(value.getTimestamp(fieldIndex)); case PROTO: @@ -2976,6 +3037,8 @@ private Value getValue(int fieldIndex) { return Value.pgNumericArray(value.getStringList(fieldIndex)); case DATE: return Value.dateArray(value.getDateList(fieldIndex)); + case UUID: + return Value.uuidArray(value.getUuidList(fieldIndex)); case TIMESTAMP: return Value.timestampArray(value.getTimestampList(fieldIndex)); case STRUCT: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java index 8386bd5c213..1b297758ee3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java @@ -24,6 +24,7 @@ import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.ProtocolMessageEnum; import java.math.BigDecimal; +import java.util.UUID; import javax.annotation.Nullable; /** @@ -165,6 +166,10 @@ public R to(@Nullable Date value) { return handle(Value.date(value)); } + public R to(@Nullable UUID value) { + return handle(Value.uuid(value)); + } + /** Binds a non-{@code NULL} struct value to {@code Value.struct(value)} */ public R to(Struct value) { return handle(Value.struct(value)); @@ -323,6 +328,11 @@ public R toDateArray(@Nullable Iterable values) { return handle(Value.dateArray(values)); } + /** Binds to {@code Value.uuidArray(values)} */ + public R toUuidArray(@Nullable Iterable values) { + return handle(Value.uuidArray(values)); + } + /** Binds to {@code Value.structArray(fieldTypes, values)} */ public R toStructArray(Type elementType, @Nullable Iterable values) { return handle(Value.structArray(elementType, values)); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java index b5e4060ddd8..71451ff66a6 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java @@ -32,6 +32,7 @@ import com.google.spanner.v1.ResultSetStats; import java.math.BigDecimal; import java.util.List; +import java.util.UUID; import java.util.function.Function; /** @@ -288,6 +289,18 @@ public Date getDate(String columnName) { return delegate.getDate(columnName); } + @Override + public UUID getUuid(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getUuid(columnIndex); + } + + @Override + public UUID getUuid(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getUuid(columnName); + } + @Override public Value getValue(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); @@ -480,6 +493,18 @@ public List getDateList(String columnName) { return delegate.getDateList(columnName); } + @Override + public List getUuidList(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getUuidList(columnIndex); + } + + @Override + public List getUuidList(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getUuidList(columnName); + } + @Override public List getProtoMessageList(int columnIndex, T message) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java index bd7c794a0fa..57210495eca 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java @@ -34,6 +34,7 @@ import com.google.spanner.v1.ResultSetStats; import java.math.BigDecimal; import java.util.List; +import java.util.UUID; import java.util.function.Function; /** @@ -291,12 +292,24 @@ public Date getDate(int columnIndex) { return delegate.getDate(columnIndex); } + @Override + public UUID getUuid(int columnIndex) { + checkClosed(); + return delegate.getUuid(columnIndex); + } + @Override public Date getDate(String columnName) { checkClosed(); return delegate.getDate(columnName); } + @Override + public UUID getUuid(String columnName) { + checkClosed(); + return delegate.getUuid(columnName); + } + @Override public Value getValue(int columnIndex) { checkClosed(); @@ -489,6 +502,18 @@ public List getDateList(String columnName) { return delegate.getDateList(columnName); } + @Override + public List getUuidList(int columnIndex) { + checkClosed(); + return delegate.getUuidList(columnIndex); + } + + @Override + public List getUuidList(String columnName) { + checkClosed(); + return delegate.getUuidList(columnName); + } + @Override public List getProtoMessageList(int columnIndex, T message) { checkClosed(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java index 595bbcaf26a..c42082ad97f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java @@ -36,6 +36,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.function.Function; import javax.annotation.Nullable; import org.junit.Before; @@ -103,6 +104,11 @@ protected Date getDateInternal(int columnIndex) { return null; } + @Override + protected UUID getUuidInternal(int columnIndex) { + return null; + } + @Override protected T getProtoMessageInternal(int columnIndex, T message) { return null; @@ -206,6 +212,11 @@ protected List getDateListInternal(int columnIndex) { return null; } + @Override + protected List getUuidListInternal(int columnIndex) { + return null; + } + @Override protected List getStructListInternal(int columnIndex) { return null; @@ -301,6 +312,13 @@ public static Collection parameters() { "getDate", Collections.singletonList("getValue") }, + { + Type.uuid(), + "getUuidInternal", + UUID.randomUUID(), + "getUuid", + Collections.singletonList("getValue") + }, { Type.array(Type.bool()), "getBooleanArrayInternal", @@ -423,6 +441,13 @@ public static Collection parameters() { "getDateList", Collections.singletonList("getValue") }, + { + Type.array(Type.uuid()), + "getUuidListInternal", + Arrays.asList(UUID.randomUUID(), UUID.randomUUID()), + "getUuidList", + Collections.singletonList("getValue") + }, { Type.array(Type.struct(StructField.of("f1", Type.int64()))), "getStructListInternal", diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index 87ea5c19ce9..228544e0a9e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -4707,6 +4707,7 @@ public void testGetAllTypesAsString() { resultSet, col++); assertAsString("2023-01-11", resultSet, col++); + assertAsString("b1153a48-cd31-498e-b770-f554bce48e05", resultSet, col++); assertAsString("2023-01-11T11:55:18.123456789Z", resultSet, col++); if (dialect == Dialect.POSTGRESQL) { // Check PG_OID value @@ -4748,6 +4749,13 @@ public void testGetAllTypesAsString() { resultSet, col++); assertAsString(ImmutableList.of("2000-02-29", "NULL", "2000-01-01"), resultSet, col++); + assertAsString( + ImmutableList.of( + "b1153a48-cd31-498e-b770-f554bce48e05", + "NULL", + "11546309-8b37-4366-9a20-369381c7803a"), + resultSet, + col++); assertAsString( ImmutableList.of("2023-01-11T11:55:18.123456789Z", "NULL", "2023-01-12T11:55:18Z"), resultSet, @@ -5282,6 +5290,10 @@ private ListValue getRows(Dialect dialect) { .encodeToString("test-bytes".getBytes(StandardCharsets.UTF_8))) .build()) .addValues(com.google.protobuf.Value.newBuilder().setStringValue("2023-01-11").build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("b1153a48-cd31-498e-b770-f554bce48e05") + .build()) .addValues( com.google.protobuf.Value.newBuilder() .setStringValue("2023-01-11T11:55:18.123456789Z") @@ -5446,6 +5458,23 @@ private ListValue getRows(Dialect dialect) { .setStringValue("2000-01-01") .build()) .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("b1153a48-cd31-498e-b770-f554bce48e05") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("11546309-8b37-4366-9a20-369381c7803a") + .build()) + .build())) .addValues( com.google.protobuf.Value.newBuilder() .setListValue( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index 25c01560e92..8d405cc235b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -51,6 +51,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.UUID; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Test; @@ -552,6 +553,8 @@ public void serialization() { Value.timestamp(null), Value.date(Date.fromYearMonthDay(2017, 4, 17)), Value.date(null), + Value.uuid(UUID.randomUUID()), + Value.uuid(null), Value.stringArray(ImmutableList.of("one", "two")), Value.stringArray(null), Value.boolArray(new boolean[] {true, false}), @@ -574,6 +577,8 @@ public void serialization() { ImmutableList.of( Date.fromYearMonthDay(2017, 4, 17), Date.fromYearMonthDay(2017, 5, 18))), Value.dateArray(null), + Value.uuidArray(ImmutableList.of(UUID.randomUUID(), UUID.randomUUID())), + Value.uuidArray(null), Value.struct(s(null, 30)), Value.struct(structType, null), Value.structArray(structType, Arrays.asList(s("def", 10), null)), @@ -739,6 +744,23 @@ public void getDate() { assertThat(resultSet.getDate(0)).isEqualTo(Date.fromYearMonthDay(2018, 5, 29)); } + @Test + public void getUuid() { + final UUID uuid = UUID.randomUUID(); + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.uuid())))) + .addValues(Value.uuid(uuid).toProto()) + .build()); + consumer.onCompleted(); + + Value value = Value.uuid(uuid); + com.google.protobuf.Value diff_value = value.toProto(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getUuid(0)).isEqualTo(uuid); + } + @Test public void getTimestamp() { consumer.onPartialResultSet( @@ -992,6 +1014,22 @@ public void getDateList() { assertThat(resultSet.getDateList(0)).isEqualTo(dateList); } + @Test + public void getUuidList() { + List uuidList = Arrays.asList(UUID.randomUUID(), UUID.randomUUID()); + + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata(Type.struct(Type.StructField.of("f", Type.array(Type.uuid()))))) + .addValues(Value.uuidArray(uuidList).toProto()) + .build()); + consumer.onCompleted(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getUuidList(0)).isEqualTo(uuidList); + } + @Test public void getJsonList() { List jsonList = new ArrayList<>(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 3443be192e6..da0f86bad31 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -1328,6 +1328,9 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).toDateArray(null); break; + case UUID: + builder.bind(fieldName).toUuidArray(null); + break; case FLOAT32: builder.bind(fieldName).toFloat32Array((Iterable) null); break; @@ -1374,6 +1377,9 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).to((Date) null); break; + case UUID: + builder.bind(fieldName).to((UUID) null); + break; case FLOAT32: builder.bind(fieldName).to((Float) null); break; @@ -1442,6 +1448,14 @@ private Statement buildStatement( GrpcStruct.decodeArrayValue( com.google.cloud.spanner.Type.date(), value.getListValue())); break; + case UUID: + builder + .bind(fieldName) + .toUuidArray( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.uuid(), value.getListValue())); + break; case FLOAT32: builder .bind(fieldName) @@ -1533,6 +1547,9 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).to(Date.parseDate(value.getStringValue())); break; + case UUID: + builder.bind(fieldName).to(UUID.fromString(value.getStringValue())); + break; case FLOAT32: builder.bind(fieldName).to((float) value.getNumberValue()); break; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java index 3ca550caa2d..ff9be7fc39b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java @@ -40,6 +40,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; @@ -70,6 +71,7 @@ public void resultSetIteration() { int year = 2018; int month = 5; int day = 26; + UUID uuid = UUID.randomUUID(); boolean[] boolArray = {true, false, true, true, false}; long[] longArray = {Long.MAX_VALUE, Long.MIN_VALUE, 0, 1, -1}; double[] doubleArray = {Double.MIN_VALUE, Double.MAX_VALUE, 0, 1, -1, 1.2341}; @@ -92,6 +94,7 @@ public void resultSetIteration() { Date[] dateArray = { Date.fromYearMonthDay(1, 2, 3), Date.fromYearMonthDay(4, 5, 6), Date.fromYearMonthDay(7, 8, 9) }; + UUID[] uuidArray = {UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()}; String[] stringArray = {"abc", "def", "ghi"}; String[] jsonArray = {"{}", "{\"color\":\"red\",\"value\":\"#f00\"}", "[]"}; AbstractMessage[] protoMessageArray = { @@ -114,6 +117,7 @@ public void resultSetIteration() { Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), Type.StructField.of("date", Type.date()), + Type.StructField.of("uuid", Type.uuid()), Type.StructField.of( "protoMessage", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), Type.StructField.of( @@ -126,6 +130,7 @@ public void resultSetIteration() { Type.StructField.of("byteArray", Type.array(Type.bytes())), Type.StructField.of("timestampArray", Type.array(Type.timestamp())), Type.StructField.of("dateArray", Type.array(Type.date())), + Type.StructField.of("uuidArray", Type.array(Type.uuid())), Type.StructField.of("stringArray", Type.array(Type.string())), Type.StructField.of("jsonArray", Type.array(Type.json())), Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb())), @@ -163,6 +168,8 @@ public void resultSetIteration() { .to(Timestamp.ofTimeMicroseconds(usecs)) .set("date") .to(Date.fromYearMonthDay(year, month, day)) + .set("uuid") + .to(uuid) .set("protoMessage") .to(protoMessageVal) .set("protoEnum") @@ -183,6 +190,8 @@ public void resultSetIteration() { .to(Value.timestampArray(Arrays.asList(timestampArray))) .set("dateArray") .to(Value.dateArray(Arrays.asList(dateArray))) + .set("uuidArray") + .to(Value.uuidArray(Arrays.asList(uuidArray))) .set("stringArray") .to(Value.stringArray(Arrays.asList(stringArray))) .set("jsonArray") @@ -228,6 +237,8 @@ public void resultSetIteration() { .to(Timestamp.ofTimeMicroseconds(usecs)) .set("date") .to(Date.fromYearMonthDay(year, month, day)) + .set("uuid") + .to(uuid) .set("protoMessage") .to(protoMessageVal) .set("protoEnum") @@ -248,6 +259,8 @@ public void resultSetIteration() { .to(Value.timestampArray(Arrays.asList(timestampArray))) .set("dateArray") .to(Value.dateArray(Arrays.asList(dateArray))) + .set("uuidArray") + .to(Value.uuidArray(Arrays.asList(uuidArray))) .set("stringArray") .to(Value.stringArray(Arrays.asList(stringArray))) .set("jsonArray") @@ -339,6 +352,12 @@ public void resultSetIteration() { assertThat(rs.getDate("date")).isEqualTo(Date.fromYearMonthDay(year, month, day)); assertThat(rs.getValue("date")).isEqualTo(Value.date(Date.fromYearMonthDay(year, month, day))); + // UUID + assertThat(rs.getUuid(columnIndex)).isEqualTo(uuid); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.uuid(uuid)); + assertThat(rs.getUuid("uuid")).isEqualTo(uuid); + assertThat(rs.getValue("uuid")).isEqualTo(Value.uuid(uuid)); + assertEquals(protoMessageVal, rs.getProtoMessage(columnIndex, SingerInfo.getDefaultInstance())); assertEquals(Value.protoMessage(protoMessageVal), rs.getValue(columnIndex++)); assertEquals( @@ -400,6 +419,13 @@ public void resultSetIteration() { assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.dateArray(Arrays.asList(dateArray))); assertThat(rs.getDateList("dateArray")).isEqualTo(Arrays.asList(dateArray)); assertThat(rs.getValue("dateArray")).isEqualTo(Value.dateArray(Arrays.asList(dateArray))); + + // UUID Array + assertThat(rs.getUuidList(columnIndex)).isEqualTo(Arrays.asList(uuidArray)); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.uuidArray(Arrays.asList(uuidArray))); + assertThat(rs.getUuidList("uuidArray")).isEqualTo(Arrays.asList(uuidArray)); + assertThat(rs.getValue("uuidArray")).isEqualTo(Value.uuidArray(Arrays.asList(uuidArray))); + assertThat(rs.getStringList(columnIndex)).isEqualTo(Arrays.asList(stringArray)); assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.stringArray(Arrays.asList(stringArray))); assertThat(rs.getStringList("stringArray")).isEqualTo(Arrays.asList(stringArray)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java index aea799aa158..0d069ae5d8f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java @@ -240,6 +240,16 @@ Type newType() { }.test(); } + @Test + public void uuid() { + new ScalarTypeTester(Type.Code.UUID, TypeCode.UUID) { + @Override + Type newType() { + return Type.uuid(); + } + }.test(); + } + abstract static class ArrayTypeTester { private final Type.Code expectedElementCode; private final TypeCode expectedElementTypeCode; @@ -428,6 +438,16 @@ Type newElementType() { }.test(); } + @Test + public void uuidArray() { + new ArrayTypeTester(Type.Code.UUID, TypeCode.UUID, true) { + @Override + Type newElementType() { + return Type.uuid(); + } + }.test(); + } + @Test public void protoArray() { new ArrayTypeTester(Type.Code.PROTO, TypeCode.PROTO, "com.google.temp", false) { @@ -615,6 +635,7 @@ public void testGoogleSQLTypeNames() { assertEquals("STRING", Type.string().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("BYTES", Type.bytes().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("DATE", Type.date().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); + assertEquals("UUID", Type.uuid().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("TIMESTAMP", Type.timestamp().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("JSON", Type.json().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("NUMERIC", Type.numeric().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); @@ -632,6 +653,8 @@ public void testGoogleSQLTypeNames() { "ARRAY", Type.array(Type.bytes()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals( "ARRAY", Type.array(Type.date()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); + assertEquals( + "ARRAY", Type.array(Type.uuid()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals( "ARRAY", Type.array(Type.timestamp()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); @@ -650,6 +673,7 @@ public void testPostgreSQLTypeNames() { assertEquals("character varying", Type.string().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("bytea", Type.bytes().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("date", Type.date().getSpannerTypeName(Dialect.POSTGRESQL)); + assertEquals("uuid", Type.uuid().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals( "timestamp with time zone", Type.timestamp().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("jsonb", Type.pgJsonb().getSpannerTypeName(Dialect.POSTGRESQL)); @@ -663,6 +687,7 @@ public void testPostgreSQLTypeNames() { "character varying[]", Type.array(Type.string()).getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("bytea[]", Type.array(Type.bytes()).getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals("date[]", Type.array(Type.date()).getSpannerTypeName(Dialect.POSTGRESQL)); + assertEquals("uuid[]", Type.array(Type.uuid()).getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals( "timestamp with time zone[]", Type.array(Type.timestamp()).getSpannerTypeName(Dialect.POSTGRESQL)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java index 23128ad52b2..d39f2358cd3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java @@ -40,6 +40,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; +import java.util.UUID; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -331,6 +332,10 @@ public static Date defaultDate() { return Date.fromYearMonthDay(2016, 9, 15); } + public static UUID defaultUuid() { + return UUID.fromString("db09330e-cc05-472c-a54e-b2784deebac3"); + } + public static boolean[] defaultBooleanArray() { return new boolean[] {false, true}; } @@ -388,6 +393,12 @@ public static Iterable defaultDateIterable() { return Arrays.asList(Date.fromYearMonthDay(2016, 9, 15), Date.fromYearMonthDay(2016, 9, 14)); } + public static Iterable defaultUuidIterable() { + return Arrays.asList( + UUID.fromString("8ebe9153-2747-4c92-a462-6da13eb25ebb"), + UUID.fromString("12c154ca-6500-4be0-89c8-160bcfa8c3f6")); + } + static Object getDefault(java.lang.reflect.Type type) throws InvocationTargetException, IllegalAccessException { for (Method method : DefaultValues.class.getMethods()) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index 92b63913fdb..a91dffb2a2f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -51,6 +51,7 @@ import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.UUID; import java.util.function.Supplier; import java.util.stream.Collectors; import org.junit.Test; @@ -731,6 +732,28 @@ public void dateNull() { assertEquals("NULL", v.getAsString()); } + @Test + public void uuid() { + UUID uuid = UUID.randomUUID(); + Value v = Value.uuid(uuid); + assertThat(v.getType()).isEqualTo(Type.uuid()); + assertThat(v.isNull()).isFalse(); + assertThat(v.getUuid()).isSameInstanceAs(uuid); + assertThat(v.toString()).isEqualTo(uuid.toString()); + assertEquals(uuid.toString(), v.getAsString()); + } + + @Test + public void uuidNull() { + Value v = Value.uuid(null); + assertThat(v.getType()).isEqualTo(Type.uuid()); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = assertThrows(IllegalStateException.class, v::getUuid); + assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); + } + @Test public void protoMessage() { SingerInfo singerInfo = SingerInfo.newBuilder().setSingerId(111).setGenre(Genre.FOLK).build(); @@ -1359,6 +1382,29 @@ public void dateArrayNull() { assertEquals("NULL", v.getAsString()); } + @Test + public void uuidArray() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + + Value v = Value.uuidArray(Arrays.asList(uuid1, null, uuid2)); + assertThat(v.isNull()).isFalse(); + assertThat(v.getUuidArray()).containsExactly(uuid1, null, uuid2).inOrder(); + assertThat(v.toString()).isEqualTo("[" + uuid1.toString() + ",NULL," + uuid2.toString() + "]"); + assertEquals( + String.format("[%s,NULL,%s]", uuid1.toString(), uuid2.toString()), v.getAsString()); + } + + @Test + public void uuidArrayNull() { + Value v = Value.uuidArray(null); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = assertThrows(IllegalStateException.class, v::getUuidArray); + assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); + } + @Test public void protoMessageArray() { SingerInfo singerInfo1 = SingerInfo.newBuilder().setSingerId(111).setGenre(Genre.FOLK).build(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AllTypesMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AllTypesMockServerTest.java index 3313fa53426..1f75885aaa4 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AllTypesMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AllTypesMockServerTest.java @@ -40,6 +40,7 @@ import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.After; @@ -76,6 +77,7 @@ public static Object[] data() { public static final long PG_OID_VALUE = 1L; public static final byte[] BYTES_VALUE = "test-bytes".getBytes(StandardCharsets.UTF_8); public static final Date DATE_VALUE = Date.fromYearMonthDay(2024, 3, 2); + public static final UUID UUID_VALUE = UUID.randomUUID(); public static final Timestamp TIMESTAMP_VALUE = Timestamp.parseTimestamp("2024-03-02T07:07:00.20982735Z"); @@ -124,6 +126,9 @@ public static Object[] data() { Date.fromYearMonthDay(2024, 3, 3), Date.fromYearMonthDay(1, 1, 1), Date.fromYearMonthDay(9999, 12, 31)); + + public static final List UUID_ARRAY_VALUE = + Arrays.asList(UUID.randomUUID(), null, UUID.randomUUID()); public static final List TIMESTAMP_ARRAY_VALUE = Arrays.asList( Timestamp.parseTimestamp("2024-03-01T07:07:00.20982735Z"), @@ -157,15 +162,16 @@ private void setupAllTypesResultSet(Dialect dialect) { // COL7: JSON / PG_JSONB // COL8: BYTES // COL9: DATE - // COL10: TIMESTAMP - // COL11: PG_OID (added only for POSTGRESQL dialect) - // COL12-21: ARRAY<..> for the types above. + // COL10: UUID + // COL11: TIMESTAMP + // COL12: PG_OID (added only for POSTGRESQL dialect) + // COL13-22: ARRAY<..> for the types above. // Only for GoogleSQL: - // COL22: PROTO - // COL23: ENUM - // COL24: ARRAY - // COL25: ARRAY - // COL26: ARRAY (added only for POSTGRESQL dialect) + // COL23: PROTO + // COL24: ENUM + // COL25: ARRAY + // COL26: ARRAY + // COL27: ARRAY (added only for POSTGRESQL dialect) ListValue.Builder row1Builder = ListValue.newBuilder() .addValues(Value.newBuilder().setBoolValue(BOOL_VALUE)) @@ -183,6 +189,7 @@ private void setupAllTypesResultSet(Dialect dialect) { .addValues( Value.newBuilder().setStringValue(Base64.getEncoder().encodeToString(BYTES_VALUE))) .addValues(Value.newBuilder().setStringValue(DATE_VALUE.toString())) + .addValues(Value.newBuilder().setStringValue(UUID_VALUE.toString())) .addValues(Value.newBuilder().setStringValue(TIMESTAMP_VALUE.toString())); if (dialect == Dialect.POSTGRESQL) { row1Builder.addValues( @@ -356,6 +363,23 @@ private void setupAllTypesResultSet(Dialect dialect) { .build()) .collect(Collectors.toList())) .build())) + .addValues( + Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addAllValues( + UUID_ARRAY_VALUE.stream() + .map( + uuid -> + uuid == null + ? Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build() + : Value.newBuilder() + .setStringValue(uuid.toString()) + .build()) + .collect(Collectors.toList())) + .build())) .addValues( Value.newBuilder() .setListValue( @@ -509,6 +533,8 @@ public static Statement createInsertStatement(Dialect dialect) { .bind("p" + ++param) .to(DATE_VALUE) .bind("p" + ++param) + .to(UUID_VALUE) + .bind("p" + ++param) .to(TIMESTAMP_VALUE); if (dialect == Dialect.POSTGRESQL) { builder.bind("p" + ++param).to(PG_OID_VALUE); @@ -539,6 +565,8 @@ public static Statement createInsertStatement(Dialect dialect) { .bind("p" + ++param) .toDateArray(DATE_ARRAY_VALUE) .bind("p" + ++param) + .toUuidArray(UUID_ARRAY_VALUE) + .bind("p" + ++param) .toTimestampArray(TIMESTAMP_ARRAY_VALUE); if (dialect == Dialect.POSTGRESQL) { builder.bind("p" + ++param).toInt64Array(PG_OID_ARRAY_VALUE); @@ -573,6 +601,7 @@ public void testSelectAllTypes() { dialect == Dialect.POSTGRESQL ? resultSet.getPgJsonb(++col) : resultSet.getJson(++col)); assertArrayEquals(BYTES_VALUE, resultSet.getBytes(++col).toByteArray()); assertEquals(DATE_VALUE, resultSet.getDate(++col)); + assertEquals(UUID_VALUE, resultSet.getUuid(++col)); assertEquals(TIMESTAMP_VALUE, resultSet.getTimestamp(++col)); if (dialect == Dialect.POSTGRESQL) { assertEquals(PG_OID_VALUE, resultSet.getLong(++col)); @@ -595,6 +624,7 @@ public void testSelectAllTypes() { : resultSet.getJsonList(++col)); assertEquals(BYTES_ARRAY_VALUE, resultSet.getBytesList(++col)); assertEquals(DATE_ARRAY_VALUE, resultSet.getDateList(++col)); + assertEquals(UUID_ARRAY_VALUE, resultSet.getUuidList(++col)); assertEquals(TIMESTAMP_ARRAY_VALUE, resultSet.getTimestampList(++col)); if (dialect == Dialect.POSTGRESQL) { assertEquals(PG_OID_ARRAY_VALUE, resultSet.getLongList(++col)); @@ -613,8 +643,8 @@ public void testInsertAllTypes() { ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0); Map paramTypes = request.getParamTypesMap(); Map params = request.getParams().getFieldsMap(); - assertEquals(dialect == Dialect.POSTGRESQL ? 22 : 20, paramTypes.size()); - assertEquals(dialect == Dialect.POSTGRESQL ? 22 : 20, params.size()); + assertEquals(dialect == Dialect.POSTGRESQL ? 24 : 22, paramTypes.size()); + assertEquals(dialect == Dialect.POSTGRESQL ? 24 : 22, params.size()); // Verify param types. ImmutableList expectedTypes; @@ -630,6 +660,7 @@ public void testInsertAllTypes() { TypeCode.JSON, TypeCode.BYTES, TypeCode.DATE, + TypeCode.UUID, TypeCode.TIMESTAMP, TypeCode.INT64); } else { @@ -644,6 +675,7 @@ public void testInsertAllTypes() { TypeCode.JSON, TypeCode.BYTES, TypeCode.DATE, + TypeCode.UUID, TypeCode.TIMESTAMP); } for (int col = 0; col < expectedTypes.size(); col++) { @@ -670,6 +702,7 @@ public void testInsertAllTypes() { Base64.getEncoder().encodeToString(BYTES_VALUE), params.get("p" + ++col).getStringValue()); assertEquals(DATE_VALUE.toString(), params.get("p" + ++col).getStringValue()); + assertEquals(UUID_VALUE.toString(), params.get("p" + ++col).getStringValue()); assertEquals(TIMESTAMP_VALUE.toString(), params.get("p" + ++col).getStringValue()); if (dialect == Dialect.POSTGRESQL) { assertEquals(String.valueOf(PG_OID_VALUE), params.get("p" + ++col).getStringValue()); @@ -730,6 +763,11 @@ public void testInsertAllTypes() { params.get("p" + ++col).getListValue().getValuesList().stream() .map(value -> value.hasNullValue() ? null : Date.parseDate(value.getStringValue())) .collect(Collectors.toList())); + assertEquals( + UUID_ARRAY_VALUE, + params.get("p" + ++col).getListValue().getValuesList().stream() + .map(value -> value.hasNullValue() ? null : UUID.fromString(value.getStringValue())) + .collect(Collectors.toList())); assertEquals( TIMESTAMP_ARRAY_VALUE, params.get("p" + ++col).getListValue().getValuesList().stream() diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java index e13cfa91c1f..001bf8513eb 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java @@ -42,6 +42,7 @@ import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.UUID; import java.util.concurrent.Callable; import org.junit.Test; import org.junit.runner.RunWith; @@ -81,6 +82,8 @@ public class ChecksumResultSetTest { .to(Timestamp.parseTimestamp("2022-08-04T11:20:00.123456789Z")) .set("date") .to(Date.fromYearMonthDay(2022, 8, 3)) + .set("uuid") + .to(UUID.randomUUID()) .set("boolArray") .to(Value.boolArray(Arrays.asList(Boolean.FALSE, null, Boolean.TRUE))) .set("longArray") @@ -108,6 +111,8 @@ public class ChecksumResultSetTest { .to( Value.dateArray( Arrays.asList(Date.parseDate("2000-01-01"), null, Date.parseDate("2022-08-03")))) + .set("uuidArray") + .to(Value.uuidArray(Arrays.asList(UUID.randomUUID(), UUID.randomUUID()))) .set("stringArray") .to(Value.stringArray(Arrays.asList("test2", null, "test1"))) .set("jsonArray") @@ -150,6 +155,7 @@ public void testRetry() { Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), Type.StructField.of("date", Type.date()), + Type.StructField.of("uuid", Type.uuid()), Type.StructField.of("boolArray", Type.array(Type.bool())), Type.StructField.of("longArray", Type.array(Type.int64())), Type.StructField.of("doubleArray", Type.array(Type.float64())), @@ -159,6 +165,7 @@ public void testRetry() { Type.StructField.of("byteArray", Type.array(Type.bytes())), Type.StructField.of("timestampArray", Type.array(Type.timestamp())), Type.StructField.of("dateArray", Type.array(Type.date())), + Type.StructField.of("uuidArray", Type.array(Type.uuid())), Type.StructField.of("stringArray", Type.array(Type.string())), Type.StructField.of("jsonArray", Type.array(Type.json())), Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb())), @@ -200,6 +207,8 @@ public void testRetry() { .to(Timestamp.parseTimestamp("2022-08-04T10:19:00.123456789Z")) .set("date") .to(Date.fromYearMonthDay(2022, 8, 4)) + .set("uuid") + .to(UUID.randomUUID()) .set("boolArray") .to(Value.boolArray(Arrays.asList(Boolean.TRUE, null, Boolean.FALSE))) .set("longArray") @@ -228,6 +237,8 @@ public void testRetry() { Value.dateArray( Arrays.asList( Date.parseDate("2000-01-01"), null, Date.parseDate("2022-08-04")))) + .set("uuidArray") + .to(Value.uuidArray(Arrays.asList(UUID.randomUUID(), UUID.randomUUID()))) .set("stringArray") .to(Value.stringArray(Arrays.asList("test1", null, "test2"))) .set("jsonArray") @@ -282,6 +293,8 @@ public void testRetry() { .to((Timestamp) null) .set("date") .to((Date) null) + .set("uuid") + .to((UUID) null) .set("boolArray") .toBoolArray((Iterable) null) .set("longArray") @@ -300,6 +313,8 @@ public void testRetry() { .toTimestampArray(null) .set("dateArray") .toDateArray(null) + .set("uuidArray") + .toUuidArray(null) .set("stringArray") .toStringArray(null) .set("jsonArray") diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/MergedResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/MergedResultSetTest.java index 8a309115c71..b0465be6106 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/MergedResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/MergedResultSetTest.java @@ -150,7 +150,7 @@ public void testAllResultsAreReturned() { if (numPartitions == 0) { assertEquals(0, resultSet.getColumnCount()); } else { - assertEquals(24, resultSet.getColumnCount()); + assertEquals(26, resultSet.getColumnCount()); assertEquals(Type.bool(), resultSet.getColumnType(0)); assertEquals(Type.bool(), resultSet.getColumnType("COL0")); assertEquals(10, resultSet.getColumnIndex("COL10")); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/PartitionedQueryMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/PartitionedQueryMockServerTest.java index cdd8b15a38a..5922c436957 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/PartitionedQueryMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/PartitionedQueryMockServerTest.java @@ -415,9 +415,9 @@ public void testRunEmptyPartitionedQuery() { statement, PartitionOptions.newBuilder().setMaxPartitions(maxPartitions).build())) { assertFalse(resultSet.next()); assertNotNull(resultSet.getMetadata()); - assertEquals(24, resultSet.getMetadata().getRowType().getFieldsCount()); + assertEquals(26, resultSet.getMetadata().getRowType().getFieldsCount()); assertNotNull(resultSet.getType()); - assertEquals(24, resultSet.getType().getStructFields().size()); + assertEquals(26, resultSet.getType().getStructFields().size()); } if (isMultiplexedSessionsEnabled(connection.getSpanner())) { assertEquals(2, mockSpanner.countRequestsOfType(CreateSessionRequest.class)); @@ -447,15 +447,15 @@ public void testGetMetadataWithoutNextCall() { connection.runPartitionedQuery( statement, PartitionOptions.newBuilder().setMaxPartitions(maxPartitions).build())) { assertNotNull(resultSet.getMetadata()); - assertEquals(24, resultSet.getMetadata().getRowType().getFieldsCount()); + assertEquals(26, resultSet.getMetadata().getRowType().getFieldsCount()); assertNotNull(resultSet.getType()); - assertEquals(24, resultSet.getType().getStructFields().size()); + assertEquals(26, resultSet.getType().getStructFields().size()); assertTrue(resultSet.next()); assertNotNull(resultSet.getMetadata()); - assertEquals(24, resultSet.getMetadata().getRowType().getFieldsCount()); + assertEquals(26, resultSet.getMetadata().getRowType().getFieldsCount()); assertNotNull(resultSet.getType()); - assertEquals(24, resultSet.getType().getStructFields().size()); + assertEquals(26, resultSet.getType().getStructFields().size()); assertFalse(resultSet.next()); } @@ -482,9 +482,9 @@ public void testGetMetadataWithoutNextCallOnEmptyResultSet() { connection.runPartitionedQuery( statement, PartitionOptions.newBuilder().setMaxPartitions(maxPartitions).build())) { assertNotNull(resultSet.getMetadata()); - assertEquals(24, resultSet.getMetadata().getRowType().getFieldsCount()); + assertEquals(26, resultSet.getMetadata().getRowType().getFieldsCount()); assertNotNull(resultSet.getType()); - assertEquals(24, resultSet.getType().getStructFields().size()); + assertEquals(26, resultSet.getType().getStructFields().size()); assertFalse(resultSet.next()); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java index da4b87200c3..e21e0020f6e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java @@ -39,6 +39,7 @@ import java.util.Arrays; import java.util.List; import java.util.Random; +import java.util.UUID; /** * Utility class for generating {@link ResultSet}s containing columns with all possible data types @@ -68,6 +69,7 @@ public static Type[] generateAllTypes(Dialect dialect) { : Type.newBuilder().setCode(TypeCode.JSON).build(), Type.newBuilder().setCode(TypeCode.BYTES).build(), Type.newBuilder().setCode(TypeCode.DATE).build(), + Type.newBuilder().setCode(TypeCode.UUID).build(), Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())); if (dialect == Dialect.POSTGRESQL) { types.add( @@ -124,6 +126,10 @@ public static Type[] generateAllTypes(Dialect dialect) { .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.DATE)) .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.UUID)) + .build(), Type.newBuilder() .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.TIMESTAMP)) @@ -255,6 +261,10 @@ private void setRandomValue(Value.Builder builder, Type type) { random.nextInt(2019) + 1, random.nextInt(11) + 1, random.nextInt(28) + 1); builder.setStringValue(date.toString()); break; + case UUID: + UUID uuid = UUID.randomUUID(); + builder.setStringValue(uuid.toString()); + break; case FLOAT32: if (randomNaN()) { builder.setNumberValue(Float.NaN); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java index 18044c452b5..9d844036ee5 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java @@ -56,6 +56,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.UUID; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -423,6 +424,31 @@ public void bindDateNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void bindUuid() { + // TODO: Remove once it is enabled in emulator. + assumeFalse("Emulator does not support UUID yet", isUsingEmulator()); + // TODO: Remove once it is enabled in production universe. + assumeTrue("UUID is currently only supported in cloud-devel", isUsingCloudDevel()); + + UUID uuid = UUID.randomUUID(); + Struct row = execute(Statement.newBuilder(selectValueQuery).bind("p1").to(uuid), Type.uuid()); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getUuid(0)).isEqualTo(uuid); + } + + @Test + public void bindUuidNull() { + // TODO: Remove once it is enabled in emulator. + assumeFalse("Emulator does not support UUID yet", isUsingEmulator()); + // TODO: Remove once it is enabled in production universe. + assumeTrue("UUID is currently only supported in cloud-devel", isUsingCloudDevel()); + + Struct row = + execute(Statement.newBuilder(selectValueQuery).bind("p1").to((UUID) null), Type.uuid()); + assertThat(row.isNull(0)).isTrue(); + } + @Test public void bindNumeric() { assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator()); @@ -817,6 +843,53 @@ public void bindDateArrayNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void bindUuidArray() { + // TODO: Remove once it is enabled in emulator. + assumeFalse("Emulator does not support UUID yet", isUsingEmulator()); + // TODO: Remove once it is enabled in production universe. + assumeTrue("UUID is currently only supported in cloud-devel", isUsingCloudDevel()); + + UUID u1 = UUID.randomUUID(); + UUID u2 = UUID.randomUUID(); + + Struct row = + execute( + Statement.newBuilder(selectValueQuery).bind("p1").toUuidArray(asList(u1, u2, null)), + Type.array(Type.uuid())); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getUuidList(0)).containsExactly(u1, u2, null).inOrder(); + } + + @Test + public void bindUuidArrayEmpty() { + // TODO: Remove once it is enabled in emulator. + assumeFalse("Emulator does not support UUID yet", isUsingEmulator()); + // TODO: Remove once it is enabled in production universe. + assumeTrue("UUID is currently only supported in cloud-devel", isUsingCloudDevel()); + + Struct row = + execute( + Statement.newBuilder(selectValueQuery).bind("p1").toUuidArray(Collections.emptyList()), + Type.array(Type.uuid())); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getUuidList(0)).containsExactly(); + } + + @Test + public void bindUuidArrayNull() { + // TODO: Remove once it is enabled in emulator. + assumeFalse("Emulator does not support UUID yet", isUsingEmulator()); + // TODO: Remove once it is enabled in production universe. + assumeTrue("UUID is currently only supported in cloud-devel", isUsingCloudDevel()); + + Struct row = + execute( + Statement.newBuilder(selectValueQuery).bind("p1").toUuidArray(null), + Type.array(Type.uuid())); + assertThat(row.isNull(0)).isTrue(); + } + @Test public void bindNumericArrayGoogleStandardSQL() { assumeTrue(dialect.dialect == Dialect.GOOGLE_STANDARD_SQL); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITUuidTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITUuidTest.java new file mode 100644 index 00000000000..d303155a5f5 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITUuidTest.java @@ -0,0 +1,449 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.it; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TimestampBound; +import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.connection.ConnectionOptions; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * Class for running integration tests for UUID data type. It tests read and write operations + * involving UUID as key and non-key columns. + */ +@Category(ParallelIntegrationTest.class) +@RunWith(Parameterized.class) +public class ITUuidTest { + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + + private static boolean isUsingCloudDevel() { + String jobType = System.getenv("JOB_TYPE"); + + // Assumes that the jobType contains the string "cloud-devel" to signal that + // the environment is cloud-devel. + return !isNullOrEmpty(jobType) && jobType.contains("cloud-devel"); + } + + @Parameterized.Parameters(name = "Dialect = {0}") + public static List data() { + // TODO: Remove once it is enabled in production universe. + if (isUsingCloudDevel()) { + return Arrays.asList( + new DialectTestParameter(Dialect.GOOGLE_STANDARD_SQL), + new DialectTestParameter(Dialect.POSTGRESQL)); + } + return Collections.emptyList(); + } + + @Parameterized.Parameter() public DialectTestParameter dialect; + + private static DatabaseClient googleStandardSQLClient; + private static DatabaseClient postgreSQLClient; + + private static final String[] GOOGLE_STANDARD_SQL_SCHEMA = + new String[] { + "CREATE TABLE T (" + + " Key STRING(MAX) NOT NULL," + + " UuidValue UUID," + + " UuidArrayValue ARRAY," + + ") PRIMARY KEY (Key)", + "CREATE TABLE UK (" + " Key UUID NOT NULL," + ") PRIMARY KEY (Key)", + }; + + private static final String[] POSTGRESQL_SCHEMA = + new String[] { + "CREATE TABLE T (" + + " Key VARCHAR PRIMARY KEY," + + " UuidValue UUID," + + " UuidArrayValue UUID[]" + + ")", + "CREATE TABLE UK (" + " Key UUID PRIMARY KEY" + ")", + }; + + private static DatabaseClient client; + + private UUID uuid1 = UUID.fromString("aac68fbe-6847-48b1-8373-110950aeaf3a");; + private UUID uuid2 = UUID.fromString("f5868be9-7983-4cfa-adf3-2e9f13f2019d"); + + @BeforeClass + public static void setUpDatabase() + throws ExecutionException, InterruptedException, TimeoutException { + Database googleStandardSQLDatabase = + env.getTestHelper().createTestDatabase(GOOGLE_STANDARD_SQL_SCHEMA); + + googleStandardSQLClient = env.getTestHelper().getDatabaseClient(googleStandardSQLDatabase); + + Database postgreSQLDatabase = + env.getTestHelper() + .createTestDatabase(Dialect.POSTGRESQL, Arrays.asList(POSTGRESQL_SCHEMA)); + postgreSQLClient = env.getTestHelper().getDatabaseClient(postgreSQLDatabase); + } + + @Before + public void before() { + client = + dialect.dialect == Dialect.GOOGLE_STANDARD_SQL ? googleStandardSQLClient : postgreSQLClient; + } + + @AfterClass + public static void tearDown() throws Exception { + ConnectionOptions.closeSpanner(); + } + + /** Sequence used to generate unique keys. */ + private static int seq; + + private static String uniqueString() { + return String.format("k%04d", seq++); + } + + private String lastKey; + + private Timestamp write(Mutation m) { + return client.write(Collections.singletonList(m)); + } + + private Mutation.WriteBuilder baseInsert() { + return Mutation.newInsertOrUpdateBuilder("T").set("Key").to(lastKey = uniqueString()); + } + + private Struct readRow(String table, String key, String... columns) { + return client + .singleUse(TimestampBound.strong()) + .readRow(table, Key.of(key), Arrays.asList(columns)); + } + + private Struct readLastRow(String... columns) { + return readRow("T", lastKey, columns); + } + + private Timestamp deleteAllRows(String table) { + return write(Mutation.delete(table, KeySet.all())); + } + + @Test + public void writeUuid() { + UUID uuid = UUID.randomUUID(); + write(baseInsert().set("UuidValue").to(uuid).build()); + Struct row = readLastRow("UuidValue"); + assertFalse(row.isNull(0)); + assertEquals(uuid, row.getUuid(0)); + } + + @Test + public void writeUuidNull() { + write(baseInsert().set("UuidValue").to((UUID) null).build()); + Struct row = readLastRow("UuidValue"); + assertTrue(row.isNull(0)); + } + + @Test + public void writeUuidArrayNull() { + write(baseInsert().set("UuidArrayValue").toUuidArray(null).build()); + Struct row = readLastRow("UuidArrayValue"); + assertTrue(row.isNull(0)); + } + + @Test + public void writeUuidArrayEmpty() { + write(baseInsert().set("UuidArrayValue").toUuidArray(Collections.emptyList()).build()); + Struct row = readLastRow("UuidArrayValue"); + assertFalse(row.isNull(0)); + assertTrue(row.getUuidList(0).isEmpty()); + } + + @Test + public void writeUuidArray() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + + write( + baseInsert().set("UuidArrayValue").toUuidArray(Arrays.asList(null, uuid1, uuid2)).build()); + Struct row = readLastRow("UuidArrayValue"); + assertFalse(row.isNull(0)); + assertEquals(row.getUuidList(0), Arrays.asList(null, uuid1, uuid2)); + } + + @Test + public void writeUuidArrayNoNulls() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + + write(baseInsert().set("UuidArrayValue").toUuidArray(Arrays.asList(uuid1, uuid2)).build()); + Struct row = readLastRow("UuidArrayValue"); + assertFalse(row.isNull(0)); + assertEquals(2, row.getUuidList(0).size()); + assertEquals(uuid1, row.getUuidList(0).get(0)); + assertEquals(uuid2, row.getUuidList(0).get(1)); + } + + private String getInsertStatementWithLiterals() { + String statement = "INSERT INTO T (Key, UuidValue, UuidArrayValue) VALUES "; + + if (dialect.dialect == Dialect.POSTGRESQL) { + statement += + "('dml1', 'aac68fbe-6847-48b1-8373-110950aeaf3a', array['aac68fbe-6847-48b1-8373-110950aeaf3a'::uuid]), " + + "('dml2', 'aac68fbe-6847-48b1-8373-110950aeaf3a'::uuid, array['aac68fbe-6847-48b1-8373-110950aeaf3a'::uuid])," + + "('dml3', null, null), " + + "('dml4', 'aac68fbe-6847-48b1-8373-110950aeaf3a'::uuid, array['aac68fbe-6847-48b1-8373-110950aeaf3a'::uuid, 'f5868be9-7983-4cfa-adf3-2e9f13f2019d'::uuid, null])"; + } else { + statement += + "('dml1', 'aac68fbe-6847-48b1-8373-110950aeaf3a', [CAST('aac68fbe-6847-48b1-8373-110950aeaf3a' AS UUID)]), " + + "('dml2', CAST('aac68fbe-6847-48b1-8373-110950aeaf3a' AS UUID), [CAST('aac68fbe-6847-48b1-8373-110950aeaf3a' AS UUID)]), " + + "('dml3', null, null), " + + "('dml4', 'aac68fbe-6847-48b1-8373-110950aeaf3a', [CAST('aac68fbe-6847-48b1-8373-110950aeaf3a' AS UUID), CAST('f5868be9-7983-4cfa-adf3-2e9f13f2019d' AS UUID), null])"; + } + return statement; + } + + @Test + public void uuidLiterals() { + client + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate(Statement.of(getInsertStatementWithLiterals())); + return null; + }); + + verifyNonKeyContents("dml"); + } + + private String getInsertStatementWithParameters() { + String statement = + "INSERT INTO T (Key, UuidValue, UuidArrayValue) VALUES " + + "('param1', $1, $2), " + + "('param2', $3, $4), " + + "('param3', $5, $6), " + + "('param4', $7, $8)"; + + return (dialect.dialect == Dialect.POSTGRESQL) ? statement : statement.replace("$", "@p"); + } + + @Test + public void uuidParameter() { + client + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.newBuilder(getInsertStatementWithParameters()) + .bind("p1") + .to(Value.uuid(uuid1)) + .bind("p2") + .to(Value.uuidArray(Collections.singletonList(uuid1))) + .bind("p3") + .to(Value.uuid(uuid1)) + .bind("p4") + .to(Value.uuidArray(Collections.singletonList(uuid1))) + .bind("p5") + .to(Value.uuid(null)) + .bind("p6") + .to(Value.uuidArray(null)) + .bind("p7") + .to(Value.uuid(uuid1)) + .bind("p8") + .to(Value.uuidArray(Arrays.asList(uuid1, uuid2, null))) + .build()); + return null; + }); + + verifyNonKeyContents("param"); + } + + private String getInsertStatementForUntypedParameters() { + if (dialect.dialect == Dialect.POSTGRESQL) { + return "INSERT INTO T (key, uuidValue, uuidArrayValue) VALUES " + + "('untyped1', ($1)::uuid, ($2)::uuid[])"; + } + return "INSERT INTO T (Key, UuidValue, UuidArrayValue) VALUES " + + "('untyped1', CAST(@p1 AS UUID), CAST(@p2 AS ARRAY))"; + } + + @Test + public void uuidUntypedParameter() { + client + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.newBuilder(getInsertStatementForUntypedParameters()) + .bind("p1") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder() + .setStringValue("aac68fbe-6847-48b1-8373-110950aeaf3a") + .build())) + .bind("p2") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder() + .setListValue( + com.google.protobuf.ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue( + "aac68fbe-6847-48b1-8373-110950aeaf3a"))) + .build())) + .build()); + return null; + }); + + Struct row = readRow("T", "untyped1", "UuidValue", "UuidArrayValue"); + assertEquals(UUID.fromString("aac68fbe-6847-48b1-8373-110950aeaf3a"), row.getUuid(0)); + assertEquals( + Collections.singletonList(UUID.fromString("aac68fbe-6847-48b1-8373-110950aeaf3a")), + row.getUuidList(1)); + } + + private String getInsertStatementWithKeyLiterals(UUID uuid1, UUID uuid2) { + String statement = "INSERT INTO UK (Key) VALUES "; + if (dialect.dialect == Dialect.POSTGRESQL) { + statement += "('" + uuid1.toString() + "')," + "('" + uuid2.toString() + "'::uuid)"; + } else { + statement += "('" + uuid1.toString() + "')," + "(CAST('" + uuid2.toString() + "' AS UUID))"; + } + return statement; + } + + @Test + public void uuidAsKeyLiteral() { + deleteAllRows("UK"); + + client + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.of(getInsertStatementWithKeyLiterals(uuid1, uuid2))); + return null; + }); + + verifyKeyContents(Arrays.asList(uuid1, uuid2)); + } + + private String getInsertStatementWithKeyParameters() { + String statement = "INSERT INTO UK (Key) VALUES " + "($1)," + "($2)"; + return (dialect.dialect == Dialect.POSTGRESQL) ? statement : statement.replace("$", "@p"); + } + + @Test + public void uuidAsKeyParameter() { + deleteAllRows("UK"); + UUID uuid1 = UUID.fromString("fb907080-48a4-4615-b2c4-c8ccb5bb66a4"); + UUID uuid2 = UUID.fromString("faee3a78-cc54-42fc-baa2-53197fb89e8a"); + + client + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.newBuilder(getInsertStatementWithKeyParameters()) + .bind("p1") + .to(Value.uuid(uuid1)) + .bind("p2") + .to(Value.uuid(uuid2)) + .build()); + return null; + }); + + verifyKeyContents(Arrays.asList(uuid1, uuid2)); + } + + private void verifyKeyContents(List uuids) { + try (ResultSet resultSet = + client.singleUse().executeQuery(Statement.of("SELECT Key AS key FROM UK ORDER BY key"))) { + + for (UUID uuid : uuids) { + assertTrue(resultSet.next()); + assertEquals(uuid, resultSet.getUuid("key")); + assertEquals(Value.uuid(uuid), resultSet.getValue("key")); + } + } + } + + private void verifyNonKeyContents(String keyPrefix) { + try (ResultSet resultSet = + client + .singleUse() + .executeQuery( + Statement.of( + "SELECT Key AS key, UuidValue AS uuidvalue, UuidArrayValue AS uuidarrayvalue FROM T WHERE Key LIKE '{keyPrefix}%' ORDER BY key" + .replace("{keyPrefix}", keyPrefix)))) { + + // Row 1 + assertTrue(resultSet.next()); + assertEquals(uuid1, resultSet.getUuid("uuidvalue")); + assertEquals(Value.uuid(uuid1), resultSet.getValue("uuidvalue")); + assertEquals(Collections.singletonList(uuid1), resultSet.getUuidList("uuidarrayvalue")); + assertEquals( + Value.uuidArray(Collections.singletonList(uuid1)), resultSet.getValue("uuidarrayvalue")); + + // Row 2 + assertTrue(resultSet.next()); + assertEquals(uuid1, resultSet.getUuid("uuidvalue")); + assertEquals(Value.uuid(uuid1), resultSet.getValue("uuidvalue")); + assertEquals(Collections.singletonList(uuid1), resultSet.getUuidList("uuidarrayvalue")); + assertEquals( + Value.uuidArray(Collections.singletonList(uuid1)), resultSet.getValue("uuidarrayvalue")); + + // Row 3 + assertTrue(resultSet.next()); + assertTrue(resultSet.isNull("uuidvalue")); + assertTrue(resultSet.isNull("uuidarrayvalue")); + + // Row 4 + assertTrue(resultSet.next()); + assertEquals(uuid1, resultSet.getUuid("uuidvalue")); + assertEquals(Value.uuid(uuid1), resultSet.getValue("uuidvalue")); + assertEquals(Arrays.asList(uuid1, uuid2, null), resultSet.getUuidList("uuidarrayvalue")); + assertEquals( + Value.uuidArray(Arrays.asList(uuid1, uuid2, null)), resultSet.getValue("uuidarrayvalue")); + } + } +}