diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 28e22e4a86c..e79cac4fc78 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -566,6 +566,38 @@ java.util.List getFloat32Array() + + + 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 49df1167bd5..fc3a5609bb1 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 Interval getIntervalInternal(int columnIndex) { return currRow().getIntervalInternal(columnIndex); @@ -531,6 +537,11 @@ protected List getDateListInternal(int columnIndex) { return currRow().getDateListInternal(columnIndex); } + @Override + protected List getUuidListInternal(int columnIndex) { + return currRow().getUuidListInternal(columnIndex); + } + @Override protected List getIntervalListInternal(int columnIndex) { return currRow().getIntervalListInternal(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 e5b9fa00123..60ff4fd330e 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,10 @@ protected String getPgJsonbInternal(int columnIndex) { protected abstract Date getDateInternal(int columnIndex); + protected UUID getUuidInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + protected Interval getIntervalInternal(int columnIndex) { throw new UnsupportedOperationException("Not implemented"); } @@ -132,6 +137,10 @@ protected List getPgJsonbListInternal(int columnIndex) { protected abstract List getDateListInternal(int columnIndex); + protected List getUuidListInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + protected List getIntervalListInternal(int columnIndex) { throw new UnsupportedOperationException("Not implemented"); } @@ -307,6 +316,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 getUuidInternal(columnIndex); + } + @Override public Interval getInterval(int columnIndex) { checkNonNullOfType(columnIndex, Type.interval(), columnIndex); @@ -604,6 +626,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 getUuidListInternal(columnIndex); + } + @Override public List getIntervalList(int columnIndex) { checkNonNullOfType(columnIndex, Type.array(Type.interval()), 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 c747c2a5908..839202bb9fe 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 */ @@ -231,6 +232,18 @@ public Date getDate(String columnName) { return delegate.get().getDate(columnName); } + @Override + public UUID getUuid(int columnIndex) { + checkValidState(); + return delegate.get().getUuid(columnIndex); + } + + @Override + public UUID getUuid(String columnName) { + checkValidState(); + return delegate.get().getUuid(columnName); + } + @Override public Interval getInterval(int columnIndex) { checkValidState(); @@ -421,6 +434,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 getIntervalList(int columnIndex) { 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 bd783366997..49629f09f57 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 INTERVAL: builder.set(fieldName).to((Interval) value); break; @@ -187,6 +191,9 @@ private Object writeReplace() { case DATE: builder.set(fieldName).toDateArray((Iterable) value); break; + case UUID: + builder.set(fieldName).toUuidArray((Iterable) value); + break; case INTERVAL: builder.set(fieldName).toIntervalArray((Iterable) value); break; @@ -304,6 +311,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 INTERVAL: checkType(fieldType, proto, KindCase.STRING_VALUE); return Interval.parseFromString(proto.getStringValue()); @@ -356,6 +366,7 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) { case BYTES: case TIMESTAMP: case DATE: + case UUID: case INTERVAL: case STRUCT: case PROTO: @@ -513,6 +524,12 @@ protected Date getDateInternal(int columnIndex) { return (Date) rowData.get(columnIndex); } + @Override + protected UUID getUuidInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (UUID) rowData.get(columnIndex); + } + @Override protected Interval getIntervalInternal(int columnIndex) { ensureDecoded(columnIndex); @@ -640,6 +657,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 INTERVAL: return Value.interval(isNull ? null : getIntervalInternal(columnIndex)); case STRUCT: @@ -682,6 +701,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 INTERVAL: return Value.intervalArray(isNull ? null : getIntervalListInternal(columnIndex)); case STRUCT: @@ -867,6 +888,13 @@ protected List getDateListInternal(int columnIndex) { return Collections.unmodifiableList((List) rowData.get(columnIndex)); } + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + 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 getIntervalListInternal(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 d91bc2104af..92a12286ae2 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}. */ @@ -326,6 +327,16 @@ public Date getDate(String columnName) { return getCurrentRowAsStruct().getDate(columnName); } + @Override + public UUID getUuid(int columnIndex) { + return getCurrentRowAsStruct().getUuid(columnIndex); + } + + @Override + public UUID getUuid(String columnName) { + return getCurrentRowAsStruct().getUuid(columnName); + } + @Override public Interval getInterval(int columnIndex) { return getCurrentRowAsStruct().getInterval(columnIndex); @@ -518,6 +529,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 getIntervalList(int columnIndex) { return getCurrentRowAsStruct().getIntervalList(columnIndex); 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 7944c7f409d..38a47e99dff 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 Interval getIntervalInternal(int columnIndex) { return values.get(columnIndex).getInterval(); @@ -339,6 +345,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 getIntervalListInternal(int columnIndex) { return values.get(columnIndex).getIntervalArray(); @@ -430,6 +441,8 @@ private Object getAsObject(int columnIndex) { return getTimestampInternal(columnIndex); case DATE: return getDateInternal(columnIndex); + case UUID: + return getUuidInternal(columnIndex); case INTERVAL: return getIntervalInternal(columnIndex); case STRUCT: @@ -463,6 +476,8 @@ private Object getAsObject(int columnIndex) { return getTimestampListInternal(columnIndex); case DATE: return getDateListInternal(columnIndex); + case UUID: + return getUuidListInternal(columnIndex); case INTERVAL: return getIntervalListInternal(columnIndex); case STRUCT: 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 33e88c39d38..b235cb2db62 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 non-{@code NULL} column with type {@link Type#interval()}. @@ -637,6 +642,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.interval())}. 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 7cb3f131f73..71120a0f420 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_INTERVAL = new Type(Code.INTERVAL, 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); @@ -73,6 +74,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 Type TYPE_ARRAY_INTERVAL = new Type(Code.ARRAY, TYPE_INTERVAL, null); private static final int AMBIGUOUS_FIELD = -1; @@ -185,6 +187,11 @@ public static Type date() { return TYPE_DATE; } + /** Returns the descriptor for the {@code UUID} type. */ + public static Type uuid() { + return TYPE_UUID; + } + /** * Returns the descriptor for the {@code INTERVAL} type: an interval which represents a time * duration as a tuple of 3 values (months, days, nanoseconds). [Interval(months:-120000, days: @@ -225,6 +232,8 @@ public static Type array(Type elementType) { return TYPE_ARRAY_TIMESTAMP; case DATE: return TYPE_ARRAY_DATE; + case UUID: + return TYPE_ARRAY_UUID; case INTERVAL: return TYPE_ARRAY_INTERVAL; default: @@ -309,6 +318,7 @@ public enum Code { BYTES(TypeCode.BYTES, "bytea"), TIMESTAMP(TypeCode.TIMESTAMP, "timestamp with time zone"), DATE(TypeCode.DATE, "date"), + UUID(TypeCode.UUID, "uuid"), INTERVAL(TypeCode.INTERVAL, "interval"), ARRAY(TypeCode.ARRAY, "array"), STRUCT(TypeCode.STRUCT, "struct"); @@ -625,6 +635,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) { return timestamp(); case DATE: return date(); + case UUID: + return uuid(); case INTERVAL: return interval(); case PROTO: 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 5befba04e57..49a692aa0e2 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 @@ -60,6 +60,7 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -409,6 +410,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."); @@ -799,6 +804,16 @@ public static Value dateArray(@Nullable Iterable v) { return new DateArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); } + /** + * Returns an {@code ARRAY} value. + * + * @param v the source of element values. This may be {@code null} to produce a value for which + * {@code isNull()} is {@code true}. Individual elements may also be {@code null}. + */ + public static Value uuidArray(@Nullable Iterable v) { + return new UuidArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); + } + /** * Returns an {@code ARRAY} value. * @@ -869,6 +884,9 @@ static Value toValue(Object value) { if (value instanceof Date) { return Value.date((Date) value); } + if (value instanceof UUID) { + return Value.uuid((UUID) value); + } if (value instanceof LocalDate) { return Value.date(convertLocalDateToSpannerDate((LocalDate) value)); } @@ -937,6 +955,9 @@ static Value toValue(Object value) { if (object instanceof Date) { return Value.dateArray(convertToTypedIterable((Date) object, iterator)); } + if (object instanceof UUID) { + return Value.uuidArray(convertToTypedIterable((UUID) object, iterator)); + } if (object instanceof LocalDate) { return Value.dateArray( SpannerTypeConverter.convertToTypedIterable( @@ -1101,6 +1122,13 @@ public T getProtoEnum( */ public abstract Date getDate(); + /** + * Returns the value of a {@code UUID}-typed instance. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public abstract UUID getUuid(); + /** * Returns the value of a {@code INTERVAL}-typed instance. * @@ -1228,6 +1256,14 @@ public List getProtoEnumArray( */ public abstract List getDateArray(); + /** + * 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. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + 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. @@ -1515,6 +1551,11 @@ public Date getDate() { throw defaultGetter(Type.date()); } + @Override + public UUID getUuid() { + throw defaultGetter(Type.uuid()); + } + @Override public Interval getInterval() { throw defaultGetter(Type.interval()); @@ -1584,6 +1625,11 @@ public List getDateArray() { throw defaultGetter(Type.array(Type.date())); } + @Override + public List getUuidArray() { + throw defaultGetter(Type.array(Type.uuid())); + } + @Override public List getIntervalArray() { throw defaultGetter(Type.array(Type.interval())); @@ -2006,6 +2052,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 IntervalImpl extends AbstractObjectValue { private IntervalImpl(boolean isNull, Interval value) { @@ -3037,6 +3101,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 IntervalArrayImpl extends AbstractArrayValue { private IntervalArrayImpl(boolean isNull, @Nullable List values) { @@ -3201,6 +3283,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 INTERVAL: @@ -3241,6 +3325,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 INTERVAL: 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 45b08ca582d..e0b420e07ab 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,11 @@ public R to(@Nullable Date value) { return handle(Value.date(value)); } + /** Binds to {@code Value.uuid(value)} */ + public R to(@Nullable UUID value) { + return handle(Value.uuid(value)); + } + /** Binds to {@code Value.interval(value)} */ public R to(@Nullable Interval value) { return handle(Value.interval(value)); @@ -328,6 +334,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.intervalArray(values)} */ public R toIntervalArray(@Nullable Iterable values) { return handle(Value.intervalArray(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 5339c4b69f6..f0c289a6d80 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 @@ -33,6 +33,7 @@ import com.google.spanner.v1.ResultSetStats; import java.math.BigDecimal; import java.util.List; +import java.util.UUID; import java.util.function.Function; /** @@ -289,6 +290,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 Interval getInterval(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); @@ -493,6 +506,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 getIntervalList(int columnIndex) { 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 a6198b7d633..8a73318c880 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 @@ -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; /** @@ -292,12 +293,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 Interval getInterval(int columnIndex) { checkClosed(); @@ -502,6 +515,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 getIntervalList(int columnIndex) { 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 b6aaf8471f6..66596cacb92 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; @@ -104,6 +105,10 @@ protected Date getDateInternal(int columnIndex) { } @Override + protected UUID getUuidInternal(int columnIndex) { + return null; + } + protected Interval getIntervalInternal(int columnIndex) { return null; } @@ -211,6 +216,11 @@ protected List getDateListInternal(int columnIndex) { return null; } + @Override + protected List getUuidListInternal(int columnIndex) { + return null; + } + @Override protected List getIntervalListInternal(int columnIndex) { return null; @@ -311,6 +321,13 @@ public static Collection parameters() { "getDate", Collections.singletonList("getValue") }, + { + Type.uuid(), + "getUuidInternal", + UUID.randomUUID(), + "getUuid", + Collections.singletonList("getValue") + }, { Type.interval(), "getIntervalInternal", @@ -440,6 +457,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.interval()), "getIntervalListInternal", 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 70209917f0b..33ed10a465b 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 @@ -4596,6 +4596,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 @@ -4637,6 +4638,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, @@ -5309,6 +5317,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") @@ -5473,6 +5485,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 1a2cdf6eb68..eed80cc8eb7 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 @@ -53,6 +53,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; @@ -554,6 +555,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.interval( Interval.builder() .setMonths(100) @@ -583,6 +586,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.intervalArray( ImmutableList.of( Interval.parseFromString("P0Y"), @@ -753,6 +758,19 @@ 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(); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getUuid(0)).isEqualTo(uuid); + } + @Test public void getInterval() { consumer.onPartialResultSet( @@ -1022,6 +1040,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 getIntervalList() { List intervalList = 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 1c370caee9d..dc369b2909b 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 INTERVAL: builder.bind(fieldName).toIntervalArray(null); break; @@ -1377,6 +1380,9 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).to((Date) null); break; + case UUID: + builder.bind(fieldName).to((UUID) null); + break; case INTERVAL: builder.bind(fieldName).to((Interval) null); break; @@ -1448,6 +1454,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 INTERVAL: builder .bind(fieldName) @@ -1547,6 +1561,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 INTERVAL: builder.bind(fieldName).to(Interval.parseFromString(value.getStringValue())); 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 8b2d18ce963..082cf30b8c2 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,7 +71,9 @@ public void resultSetIteration() { int year = 2018; int month = 5; int day = 26; + UUID uuid = UUID.randomUUID(); Interval interval = Interval.parseFromString("P1Y2M3DT5H7M8.967589762S"); + 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}; @@ -93,9 +96,13 @@ 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()}; + Interval[] intervalArray = { Interval.parseFromString("P0Y"), Interval.parseFromString("P1Y2M3DT-5H-7M8.9675S") }; + String[] stringArray = {"abc", "def", "ghi"}; String[] jsonArray = {"{}", "{\"color\":\"red\",\"value\":\"#f00\"}", "[]"}; AbstractMessage[] protoMessageArray = { @@ -118,6 +125,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("interval", Type.interval()), Type.StructField.of( "protoMessage", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), @@ -131,6 +139,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("intervalArray", Type.array(Type.interval())), Type.StructField.of("stringArray", Type.array(Type.string())), Type.StructField.of("jsonArray", Type.array(Type.json())), @@ -169,6 +178,8 @@ public void resultSetIteration() { .to(Timestamp.ofTimeMicroseconds(usecs)) .set("date") .to(Date.fromYearMonthDay(year, month, day)) + .set("uuid") + .to(uuid) .set("interval") .to(interval) .set("protoMessage") @@ -191,6 +202,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("intervalArray") .to(Value.intervalArray(Arrays.asList(intervalArray))) .set("stringArray") @@ -238,6 +251,8 @@ public void resultSetIteration() { .to(Timestamp.ofTimeMicroseconds(usecs)) .set("date") .to(Date.fromYearMonthDay(year, month, day)) + .set("uuid") + .to(uuid) .set("interval") .to(Value.interval(interval)) .set("protoMessage") @@ -260,6 +275,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("intervalArray") .to(Value.intervalArray(Arrays.asList(intervalArray))) .set("stringArray") @@ -353,6 +370,13 @@ 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)); + + // INTERVAL assertThat(rs.getInterval(columnIndex)).isEqualTo(interval); assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.interval(interval)); assertThat(rs.getInterval("interval")).isEqualTo(interval); @@ -419,12 +443,21 @@ 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))); + + // INTERVAL Array assertThat(rs.getIntervalList(columnIndex)).isEqualTo(Arrays.asList(intervalArray)); assertThat(rs.getValue(columnIndex++)) .isEqualTo(Value.intervalArray(Arrays.asList(intervalArray))); assertThat(rs.getIntervalList("intervalArray")).isEqualTo(Arrays.asList(intervalArray)); assertThat(rs.getValue("intervalArray")) .isEqualTo(Value.intervalArray(Arrays.asList(intervalArray))); + 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 d7fcb38050e..8fc168eae96 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(); + } + @Test public void interval() { new ScalarTypeTester(Code.INTERVAL, TypeCode.INTERVAL) { @@ -438,6 +448,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 intervalArray() { new ArrayTypeTester(Type.Code.INTERVAL, TypeCode.INTERVAL, true) { @@ -635,6 +655,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("INTERVAL", Type.interval().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("TIMESTAMP", Type.timestamp().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); assertEquals("JSON", Type.json().getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); @@ -653,6 +674,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.interval()).getSpannerTypeName(Dialect.GOOGLE_STANDARD_SQL)); @@ -674,6 +697,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("interval", Type.interval().getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals( "timestamp with time zone", Type.timestamp().getSpannerTypeName(Dialect.POSTGRESQL)); @@ -688,6 +712,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("interval[]", Type.array(Type.interval()).getSpannerTypeName(Dialect.POSTGRESQL)); assertEquals( "timestamp with time zone[]", 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 9cf3a5634a2..d85f816f147 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 @@ -41,6 +41,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; @@ -332,6 +333,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 Interval defaultInterval() { return Interval.parseFromString("P0Y"); } @@ -393,6 +398,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")); + } + public static Interval[] defaultIntervalArray() { return new Interval[] { Interval.builder() 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 f88d7683967..4e9296e307d 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 @@ -64,6 +64,7 @@ import java.util.Random; import java.util.Set; import java.util.TimeZone; +import java.util.UUID; import java.util.function.Supplier; import java.util.stream.Collectors; import org.junit.Test; @@ -745,6 +746,25 @@ public void dateNull() { } @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); + } + public void interval() { String interval = "P1Y2M3DT67H45M5.123478678S"; Interval t = Interval.parseFromString(interval); @@ -1395,6 +1415,27 @@ 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); + } + @Test public void intervalArray() { Interval interval1 = Interval.parseFromString("P123Y34M678DT478H345M345.76857863S"); @@ -1715,6 +1756,15 @@ public void testValueToProto() { com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), Value.date(null).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder() + .setStringValue("e0d8a283-29d8-49ce-8d4c-e1d8cb0ea047") + .build(), + Value.uuid(UUID.fromString("e0d8a283-29d8-49ce-8d4c-e1d8cb0ea047")).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), + Value.uuid(null).toProto()); + assertEquals( com.google.protobuf.Value.newBuilder().setStringValue("P1Y2M3DT5H6M3.624567878S").build(), Value.interval(Interval.fromMonthsDaysNanos(14, 3, BigInteger.valueOf(18363624567878L))) @@ -1860,6 +1910,23 @@ public void testValueToProto() { .build(), Value.dateArray(Arrays.asList(Date.fromYearMonthDay(2010, 2, 28), null)).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addAllValues( + Arrays.asList( + com.google.protobuf.Value.newBuilder() + .setStringValue("3fb10ff0-4a9a-428a-bc20-a947181fd76d") + .build(), + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()))) + .build(), + Value.uuidArray( + Arrays.asList(UUID.fromString("3fb10ff0-4a9a-428a-bc20-a947181fd76d"), null)) + .toProto()); + assertEquals( com.google.protobuf.Value.newBuilder() .setListValue( @@ -2089,6 +2156,35 @@ public void testValueToProto() { .add(Value.dateArray(Arrays.asList(Date.fromYearMonthDay(2010, 2, 28), null))) .build()) .toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addAllValues( + Arrays.asList( + com.google.protobuf.Value.newBuilder() + .setStringValue( + "9e2f9eac-8d6f-45c1-ac1d-c589daad8821") + .build(), + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build())) + .build()) + .build()) + .build()) + .build(), + Value.struct( + Struct.newBuilder() + .add( + Value.uuidArray( + Arrays.asList( + UUID.fromString("9e2f9eac-8d6f-45c1-ac1d-c589daad8821"), null))) + .build()) + .toProto()); assertEquals( com.google.protobuf.Value.newBuilder() .setListValue( @@ -2220,6 +2316,11 @@ public void testEqualsHashCode() { Value.date(Date.fromYearMonthDay(2018, 2, 26))); tester.addEqualityGroup(Value.date(Date.fromYearMonthDay(2018, 2, 27))); + UUID uuid = UUID.randomUUID(); + tester.addEqualityGroup(Value.uuid(null), Value.uuid(null)); + tester.addEqualityGroup(Value.uuid(uuid), Value.uuid(uuid)); + tester.addEqualityGroup(Value.uuid(UUID.randomUUID())); + Struct structValue1 = Struct.newBuilder().set("f1").to(20).set("f2").to("def").build(); Struct structValue2 = Struct.newBuilder().set("f1").to(20).set("f2").to("def").build(); assertThat(Value.struct(structValue1).equals(Value.struct(structValue2))).isTrue(); @@ -2310,6 +2411,10 @@ public void testEqualsHashCode() { Value.dateArray(Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26)))); tester.addEqualityGroup(Value.dateArray(null)); + tester.addEqualityGroup( + Value.uuidArray(Arrays.asList(null, uuid)), Value.uuidArray(Arrays.asList(null, uuid))); + tester.addEqualityGroup(Value.uuidArray(null)); + tester.addEqualityGroup( Value.intervalArray( Arrays.asList(null, Interval.fromMonthsDaysNanos(14, 3, BigInteger.valueOf(0)))), @@ -2371,6 +2476,9 @@ public void testGetAsString() { "2023-01-10T18:59:00Z", Value.timestamp(Timestamp.parseTimestamp("2023-01-10T18:59:00Z")).getAsString()); assertEquals("2023-01-10", Value.date(Date.parseDate("2023-01-10")).getAsString()); + assertEquals( + "4ef8ba78-3bb5-4a8f-ae39-bf59a89a491d", + Value.uuid(UUID.fromString("4ef8ba78-3bb5-4a8f-ae39-bf59a89a491d")).getAsString()); assertEquals( "P1Y2M3DT4H5M6.789123456S", Value.interval(Interval.parseFromString("P1Y2M3DT4H5M6.789123456S")).getAsString()); @@ -2476,6 +2584,12 @@ public void serialization() { reserializeAndAssert(Value.date(Date.fromYearMonthDay(2018, 2, 26))); reserializeAndAssert(Value.dateArray(Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26)))); + reserializeAndAssert(Value.uuid(null)); + reserializeAndAssert(Value.uuid(UUID.fromString("20d55f8b-5cd4-46ae-81bc-38f6b53c243b"))); + reserializeAndAssert( + Value.uuidArray( + Arrays.asList(null, UUID.fromString("20d55f8b-5cd4-46ae-81bc-38f6b53c243b")))); + reserializeAndAssert(Value.interval(null)); reserializeAndAssert( Value.interval(Interval.fromMonthsDaysNanos(15, 7, BigInteger.valueOf(1234567891)))); @@ -2584,6 +2698,11 @@ public void testToValue() { assertEquals(Type.date(), value.getType()); assertEquals(date, value.getDate()); + UUID uuid = UUID.randomUUID(); + value = Value.toValue(uuid); + assertEquals(Type.uuid(), value.getType()); + assertEquals(uuid, value.getUuid()); + LocalDate localDate = LocalDate.of(2018, 2, 26); value = Value.toValue(localDate); assertEquals(Type.date(), value.getType()); @@ -2768,6 +2887,11 @@ public void testToValueIterable() { assertEquals(Type.array(Type.date()), value.getType()); assertEquals(dates, value.getDateArray()); + List uuids = Arrays.asList(UUID.randomUUID(), UUID.randomUUID()); + value = Value.toValue(uuids); + assertEquals(Type.array(Type.uuid()), value.getType()); + assertEquals(uuids, value.getUuidArray()); + List localDates = Arrays.asList(LocalDate.of(2024, 8, 23), LocalDate.of(2024, 12, 27)); value = Value.toValue(localDates); 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 4c101153972..6201200ec07 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 @@ -43,6 +43,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; @@ -82,6 +83,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("interval") .to(Interval.parseFromString("P8Y2M3DT4H5M6.789123456S")) .set("boolArray") @@ -111,6 +114,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("intervalArray") .to( Value.intervalArray( @@ -160,6 +165,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("interval", Type.interval()), Type.StructField.of("boolArray", Type.array(Type.bool())), Type.StructField.of("longArray", Type.array(Type.int64())), @@ -170,6 +176,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("intervalArray", Type.array(Type.interval())), Type.StructField.of("stringArray", Type.array(Type.string())), Type.StructField.of("jsonArray", Type.array(Type.json())), @@ -212,6 +219,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("interval") .to(Interval.parseFromString("P1Y2M3DT4H5M6.789123456S")) .set("boolArray") @@ -242,6 +251,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("intervalArray") .to( Value.intervalArray( @@ -303,6 +314,8 @@ public void testRetry() { .to((Timestamp) null) .set("date") .to((Date) null) + .set("uuid") + .to((UUID) null) .set("interval") .to((Interval) null) .set("boolArray") @@ -323,6 +336,8 @@ public void testRetry() { .toTimestampArray(null) .set("dateArray") .toDateArray(null) + .set("uuidArray") + .toUuidArray(null) .set("intervalArray") .toIntervalArray(null) .set("stringArray") 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 ec5b3e77036..d24a3ca1012 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 @@ -57,6 +57,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; @@ -415,6 +416,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 bindInterval() { Interval d = Interval.parseFromString("P1Y2M3DT4H5M6.789123S"); @@ -810,6 +836,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 bindIntervalArray() { Interval d1 = Interval.parseFromString("P-1Y-2M-3DT4H5M6.789123S"); 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")); + } + } +}