diff --git a/README.md b/README.md index 1086d0b69af..93619201701 100644 --- a/README.md +++ b/README.md @@ -49,20 +49,20 @@ If you are using Maven without BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies: ```Groovy -implementation platform('com.google.cloud:libraries-bom:26.4.0') +implementation platform('com.google.cloud:libraries-bom:26.5.0') implementation 'com.google.cloud:google-cloud-spanner' ``` If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-spanner:6.35.1' +implementation 'com.google.cloud:google-cloud-spanner:6.35.2' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.35.1" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.35.2" ``` ## Authentication 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 789e0945e17..4d9ec1cda04 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 @@ -32,12 +32,14 @@ import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.cloud.spanner.v1.stub.SpannerStubSettings; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.AbstractIterator; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Uninterruptibles; import com.google.protobuf.ByteString; import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; import com.google.protobuf.Value.KindCase; import com.google.spanner.v1.PartialResultSet; import com.google.spanner.v1.ResultSetMetadata; @@ -55,11 +57,13 @@ import java.math.BigDecimal; import java.util.AbstractList; import java.util.ArrayList; +import java.util.Base64; import java.util.BitSet; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -67,11 +71,15 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** Implementation of {@link ResultSet}. */ abstract class AbstractResultSet extends AbstractStructReader implements ResultSet { private static final Tracer tracer = Tracing.getTracer(); + private static final com.google.protobuf.Value NULL_VALUE = + com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); interface Listener { /** @@ -353,6 +361,79 @@ private boolean isMergeable(KindCase kind) { } } + static final class LazyByteArray implements Serializable { + private static final Base64.Encoder ENCODER = Base64.getEncoder(); + private static final Base64.Decoder DECODER = Base64.getDecoder(); + private final String base64String; + private transient AbstractLazyInitializer byteArray; + + LazyByteArray(@Nonnull String base64String) { + this.base64String = Preconditions.checkNotNull(base64String); + this.byteArray = defaultInitializer(); + } + + LazyByteArray(@Nonnull ByteArray byteArray) { + this.base64String = + ENCODER.encodeToString(Preconditions.checkNotNull(byteArray).toByteArray()); + this.byteArray = + new AbstractLazyInitializer() { + @Override + protected ByteArray initialize() { + return byteArray; + } + }; + } + + private AbstractLazyInitializer defaultInitializer() { + return new AbstractLazyInitializer() { + @Override + protected ByteArray initialize() { + return ByteArray.copyFrom(DECODER.decode(base64String)); + } + }; + } + + private void readObject(java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + byteArray = defaultInitializer(); + } + + ByteArray getByteArray() { + try { + return byteArray.get(); + } catch (Throwable t) { + throw SpannerExceptionFactory.asSpannerException(t); + } + } + + String getBase64String() { + return base64String; + } + + @Override + public String toString() { + return getBase64String(); + } + + @Override + public int hashCode() { + return base64String.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof LazyByteArray) { + return lazyByteArraysEqual((LazyByteArray) o); + } + return false; + } + + private boolean lazyByteArraysEqual(LazyByteArray other) { + return Objects.equals(getBase64String(), other.getBase64String()); + } + } + static class GrpcStruct extends Struct implements Serializable { private final Type type; private final List rowData; @@ -395,7 +476,11 @@ private Object writeReplace() { builder.set(fieldName).to(Value.pgJsonb((String) value)); break; case BYTES: - builder.set(fieldName).to((ByteArray) value); + builder + .set(fieldName) + .to( + Value.bytesFromBase64( + value == null ? null : ((LazyByteArray) value).getBase64String())); break; case TIMESTAMP: builder.set(fieldName).to((Timestamp) value); @@ -431,7 +516,17 @@ private Object writeReplace() { builder.set(fieldName).toPgJsonbArray((Iterable) value); break; case BYTES: - builder.set(fieldName).toBytesArray((Iterable) value); + builder + .set(fieldName) + .toBytesArrayFromBase64( + value == null + ? null + : ((List) value) + .stream() + .map( + element -> + element == null ? null : element.getBase64String()) + .collect(Collectors.toList())); break; case TIMESTAMP: builder.set(fieldName).toTimestampArray((Iterable) value); @@ -511,7 +606,7 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot return proto.getStringValue(); case BYTES: checkType(fieldType, proto, KindCase.STRING_VALUE); - return ByteArray.fromBase64(proto.getStringValue()); + return new LazyByteArray(proto.getStringValue()); case TIMESTAMP: checkType(fieldType, proto, KindCase.STRING_VALUE); return Timestamp.parseTimestamp(proto.getStringValue()); @@ -526,6 +621,8 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot checkType(fieldType, proto, KindCase.LIST_VALUE); ListValue structValue = proto.getListValue(); return decodeStructValue(fieldType, structValue); + case UNRECOGNIZED: + return proto; default: throw new AssertionError("Unhandled type code: " + fieldType.getCode()); } @@ -634,7 +731,11 @@ protected String getPgJsonbInternal(int columnIndex) { @Override protected ByteArray getBytesInternal(int columnIndex) { - return (ByteArray) rowData.get(columnIndex); + return getLazyBytesInternal(columnIndex).getByteArray(); + } + + LazyByteArray getLazyBytesInternal(int columnIndex) { + return (LazyByteArray) rowData.get(columnIndex); } @Override @@ -647,6 +748,10 @@ protected Date getDateInternal(int columnIndex) { return (Date) rowData.get(columnIndex); } + protected com.google.protobuf.Value getProtoValueInternal(int columnIndex) { + return (com.google.protobuf.Value) rowData.get(columnIndex); + } + @Override protected Value getValueInternal(int columnIndex) { final List structFields = getType().getStructFields(); @@ -671,13 +776,16 @@ protected Value getValueInternal(int columnIndex) { case PG_JSONB: return Value.pgJsonb(isNull ? null : getPgJsonbInternal(columnIndex)); case BYTES: - return Value.bytes(isNull ? null : getBytesInternal(columnIndex)); + return Value.internalBytes(isNull ? null : getLazyBytesInternal(columnIndex)); case TIMESTAMP: return Value.timestamp(isNull ? null : getTimestampInternal(columnIndex)); case DATE: return Value.date(isNull ? null : getDateInternal(columnIndex)); case STRUCT: return Value.struct(isNull ? null : getStructInternal(columnIndex)); + case UNRECOGNIZED: + return Value.unrecognized( + isNull ? NULL_VALUE : getProtoValueInternal(columnIndex), columnType); case ARRAY: final Type elementType = columnType.getArrayElementType(); switch (elementType.getCode()) { @@ -785,9 +893,10 @@ protected List getPgJsonbListInternal(int columnIndex) { } @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. + @SuppressWarnings("unchecked") // We know ARRAY produces a List. protected List getBytesListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); + return Lists.transform( + (List) rowData.get(columnIndex), l -> l == null ? null : l.getByteArray()); } @Override 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 1e897636245..c868678f109 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 @@ -251,7 +251,6 @@ public Date getDate(String columnName) { @Override public Value getValue(int columnIndex) { - checkNonNull(columnIndex, columnIndex); return getValueInternal(columnIndex); } 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 7ba6b9a41e4..24a81f09c6c 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 @@ -33,6 +33,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.TreeMap; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -71,6 +72,10 @@ public final class Type implements Serializable { private static final int AMBIGUOUS_FIELD = -1; private static final long serialVersionUID = -3076152125004114582L; + static Type unrecognized(com.google.spanner.v1.Type proto) { + return new Type(proto); + } + /** Returns the descriptor for the {@code BOOL type}. */ public static Type bool() { return TYPE_BOOL; @@ -190,6 +195,7 @@ public static Type struct(StructField... fields) { return new Type(Code.STRUCT, null, ImmutableList.copyOf(fields)); } + private final com.google.spanner.v1.Type proto; private final Code code; private final Type arrayElementType; private final ImmutableList structFields; @@ -201,9 +207,26 @@ public static Type struct(StructField... fields) { private Map fieldsByName; private Type( - Code code, + @Nonnull Code code, @Nullable Type arrayElementType, @Nullable ImmutableList structFields) { + this(null, Preconditions.checkNotNull(code), arrayElementType, structFields); + } + + private Type(@Nonnull com.google.spanner.v1.Type proto) { + this( + Preconditions.checkNotNull(proto), + Code.UNRECOGNIZED, + proto.hasArrayElementType() ? new Type(proto.getArrayElementType()) : null, + null); + } + + private Type( + com.google.spanner.v1.Type proto, + Code code, + Type arrayElementType, + ImmutableList structFields) { + this.proto = proto; this.code = code; this.arrayElementType = arrayElementType; this.structFields = structFields; @@ -211,6 +234,7 @@ private Type( /** Enumerates the categories of types. */ public enum Code { + UNRECOGNIZED(TypeCode.UNRECOGNIZED), BOOL(TypeCode.BOOL), INT64(TypeCode.INT64), NUMERIC(TypeCode.NUMERIC), @@ -258,8 +282,7 @@ TypeAnnotationCode getTypeAnnotationCode() { static Code fromProto(TypeCode typeCode, TypeAnnotationCode typeAnnotationCode) { Code code = protoToCode.get(new SimpleEntry<>(typeCode, typeAnnotationCode)); - checkArgument(code != null, "Invalid code: %s<%s>", typeCode, typeAnnotationCode); - return code; + return code == null ? Code.UNRECOGNIZED : code; } @Override @@ -325,7 +348,7 @@ public Code getCode() { * @throws IllegalStateException if {@code code() != Code.ARRAY} */ public Type getArrayElementType() { - Preconditions.checkState(code == Code.ARRAY, "Illegal call for non-ARRAY type"); + Preconditions.checkState(arrayElementType != null, "Illegal call for non-ARRAY type"); return arrayElementType; } @@ -378,8 +401,14 @@ public int getFieldIndex(String fieldName) { } void toString(StringBuilder b) { - if (code == Code.ARRAY) { - b.append("ARRAY<"); + if (code == Code.ARRAY || (proto != null && proto.hasArrayElementType())) { + if (code == Code.ARRAY) { + b.append("ARRAY<"); + } else { + // This is very unlikely to happen. It would mean that we have introduced a type that + // is not an ARRAY, but does have an array element type. + b.append("UNRECOGNIZED<"); + } arrayElementType.toString(b); b.append('>'); } else if (code == Code.STRUCT) { @@ -393,6 +422,11 @@ void toString(StringBuilder b) { f.getType().toString(b); } b.append('>'); + } else if (proto != null) { + b.append(proto.getCode().name()); + if (proto.getTypeAnnotation() != TYPE_ANNOTATION_CODE_UNSPECIFIED) { + b.append("<").append(proto.getTypeAnnotation().name()).append(">"); + } } else { b.append(code.toString()); } @@ -414,6 +448,9 @@ public boolean equals(Object o) { return false; } Type that = (Type) o; + if (proto != null) { + return Objects.equals(proto, that.proto); + } return code == that.code && Objects.equals(arrayElementType, that.arrayElementType) && Objects.equals(structFields, that.structFields); @@ -421,10 +458,16 @@ public boolean equals(Object o) { @Override public int hashCode() { + if (proto != null) { + return proto.hashCode(); + } return Objects.hash(code, arrayElementType, structFields); } com.google.spanner.v1.Type toProto() { + if (proto != null) { + return proto; + } com.google.spanner.v1.Type.Builder proto = com.google.spanner.v1.Type.newBuilder(); proto.setCode(code.getTypeCode()); proto.setTypeAnnotation(code.getTypeAnnotationCode()); @@ -490,8 +533,9 @@ static Type fromProto(com.google.spanner.v1.Type proto) { fields.add(StructField.of(name, fromProto(field.getType()))); } return struct(fields); + case UNRECOGNIZED: default: - throw new AssertionError("Unimplemented case: " + type); + return unrecognized(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 82f03e859e1..c30847d6fe3 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 @@ -19,13 +19,17 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AbstractResultSet.LazyByteArray; import com.google.cloud.spanner.Type.Code; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; +import com.google.protobuf.Value.KindCase; +import java.io.IOException; import java.io.Serializable; import java.math.BigDecimal; import java.util.ArrayList; @@ -36,6 +40,7 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -95,7 +100,16 @@ public abstract class Value implements Serializable { * @param value the non-null proto value (a {@link NullValue} is allowed) */ public static Value untyped(com.google.protobuf.Value value) { - return new UntypedValueImpl(Preconditions.checkNotNull(value)); + return new ProtoBackedValueImpl(Preconditions.checkNotNull(value), null); + } + + /** Returns a generic Value backed by a protobuf value. This is used for unrecognized types. */ + static Value unrecognized(com.google.protobuf.Value value, Type type) { + Preconditions.checkArgument( + type.getCode() == Code.UNRECOGNIZED + || type.getCode() == Code.ARRAY + && type.getArrayElementType().getCode() == Code.UNRECOGNIZED); + return new ProtoBackedValueImpl(Preconditions.checkNotNull(value), type); } /** @@ -223,7 +237,22 @@ public static Value pgJsonb(@Nullable String v) { * @param v the value, which may be null */ public static Value bytes(@Nullable ByteArray v) { - return new BytesImpl(v == null, v); + return new LazyBytesImpl(v == null, v); + } + + /** + * Returns a {@code BYTES} value. + * + * @param base64String the value in Base64 encoding, which may be null. This value must be a valid + * base64 string. + */ + public static Value bytesFromBase64(@Nullable String base64String) { + return new LazyBytesImpl( + base64String == null, base64String == null ? null : new LazyByteArray(base64String)); + } + + static Value internalBytes(@Nullable LazyByteArray bytes) { + return new LazyBytesImpl(bytes == null, bytes); } /** Returns a {@code TIMESTAMP} value. */ @@ -428,7 +457,37 @@ public static Value pgJsonbArray(@Nullable Iterable v) { * {@code isNull()} is {@code true}. Individual elements may also be {@code null}. */ public static Value bytesArray(@Nullable Iterable v) { - return new BytesArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); + return new LazyBytesArrayImpl(v == null, v == null ? null : byteArraysToLazyByteArrayList(v)); + } + + private static List byteArraysToLazyByteArrayList(Iterable byteArrays) { + List list = new ArrayList<>(); + for (ByteArray byteArray : byteArrays) { + list.add(byteArray == null ? null : new LazyByteArray(byteArray)); + } + return Collections.unmodifiableList(list); + } + + /** + * Returns an {@code ARRAY} value. + * + * @param base64Strings 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}. + * Non-null values must be a valid Base64 string. + */ + public static Value bytesArrayFromBase64(@Nullable Iterable base64Strings) { + return new LazyBytesArrayImpl( + base64Strings == null, + base64Strings == null ? null : base64StringsToLazyByteArrayList(base64Strings)); + } + + private static List base64StringsToLazyByteArrayList( + Iterable base64Strings) { + List list = new ArrayList<>(); + for (String base64 : base64Strings) { + list.add(base64 == null ? null : new LazyByteArray(base64)); + } + return Collections.unmodifiableList(list); } /** @@ -672,12 +731,41 @@ public String toString() { return b.toString(); } + /** + * Returns this value as a raw string representation. This is guaranteed to work for all values, + * regardless of the underlying data type, and is guaranteed not to be truncated. + * + *

Returns the string "NULL" for null values. + */ + @Nonnull + public String getAsString() { + return toString(); + } + + /** + * Returns this value as a list of raw string representations. This is guaranteed to work for all + * values, regardless of the underlying data type, and the strings are guaranteed not to be + * truncated. The method returns a singleton list for non-array values and a list containing as + * many elements as there are in the array for array values. This method can be used instead of + * the {@link #getAsString()} method if you need to quote the individual elements in an array. + * + *

Returns the string "NULL" for null values. + */ + @Nonnull + public ImmutableList getAsStringList() { + return ImmutableList.of(toString()); + } + // END OF PUBLIC API. static com.google.protobuf.Value toProto(Value value) { return value == null ? NULL_PROTO : value.toProto(); } + /** + * Appends a string representation of this value to the given builder. The string representation + * can be truncated. + */ abstract void toString(StringBuilder b); abstract com.google.protobuf.Value toProto(); @@ -948,7 +1036,7 @@ final void toString(StringBuilder b) { /** * Appends a representation of {@code this} to {@code b}. {@code this} is guaranteed to - * represent a non-null value. + * represent a non-null value. This value could be truncated if the underlying value is long. */ abstract void valueToString(StringBuilder b); @@ -1022,11 +1110,16 @@ final void checkNotNull() { } } - private static class UntypedValueImpl extends AbstractValue { + /** + * This {@link Value} implementation is backed by a generic protobuf Value instance. It is used + * for untyped Values that are created by users, and for values with an unrecognized types that + * coming from the backend. + */ + private static class ProtoBackedValueImpl extends AbstractValue { private final com.google.protobuf.Value value; - private UntypedValueImpl(com.google.protobuf.Value value) { - super(value.hasNullValue(), null); + private ProtoBackedValueImpl(com.google.protobuf.Value value, @Nullable Type type) { + super(value.hasNullValue(), type); this.value = value; } @@ -1053,6 +1146,44 @@ public double getFloat64() { return value.getNumberValue(); } + @Nonnull + @Override + public String getAsString() { + switch (value.getKindCase()) { + case NULL_VALUE: + return NULL_STRING; + case NUMBER_VALUE: + return Double.toString(value.getNumberValue()); + case STRING_VALUE: + return value.getStringValue(); + case BOOL_VALUE: + return Boolean.toString(value.getBoolValue()); + case LIST_VALUE: + return value.getListValue().getValuesList().stream() + .map(element -> Value.untyped(element).getAsString()) + .collect(Collectors.joining(",", "[", "]")); + case STRUCT_VALUE: + throw new IllegalArgumentException( + "Struct value with unrecognized type is not supported"); + case KIND_NOT_SET: + default: + throw new IllegalArgumentException("Kind of value is not set or unknown"); + } + } + + @Nonnull + @Override + public ImmutableList getAsStringList() { + if (value.getKindCase() == KindCase.LIST_VALUE) { + ImmutableList.Builder builder = ImmutableList.builder(); + value.getListValue().getValuesList().stream() + .map(v -> Value.untyped(v).getAsString()) + .forEach(builder::add); + return builder.build(); + } + return ImmutableList.of(getAsString()); + } + @Override void valueToString(StringBuilder b) { b.append(value); @@ -1065,7 +1196,7 @@ com.google.protobuf.Value valueToProto() { @Override boolean valueEquals(Value v) { - return ((UntypedValueImpl) v).value.equals(value); + return ((ProtoBackedValueImpl) v).value.equals(value); } @Override @@ -1234,6 +1365,12 @@ public String getString() { return value; } + @Nonnull + @Override + public String getAsString() { + return isNull() ? NULL_STRING : value; + } + @Override void valueToString(StringBuilder b) { if (value.length() > MAX_DEBUG_STRING_LENGTH) { @@ -1256,6 +1393,12 @@ public String getJson() { return value; } + @Nonnull + @Override + public String getAsString() { + return isNull() ? NULL_STRING : value; + } + @Override public String getString() { return getJson(); @@ -1283,6 +1426,12 @@ public String getPgJsonb() { return value; } + @Nonnull + @Override + public String getAsString() { + return isNull() ? NULL_STRING : value; + } + @Override public String getString() { return getPgJsonb(); @@ -1298,26 +1447,36 @@ void valueToString(StringBuilder b) { } } - private static class BytesImpl extends AbstractObjectValue { + private static class LazyBytesImpl extends AbstractObjectValue { - private BytesImpl(boolean isNull, ByteArray value) { + private LazyBytesImpl(boolean isNull, LazyByteArray value) { super(isNull, Type.bytes(), value); } + private LazyBytesImpl(boolean isNull, ByteArray value) { + super(isNull, Type.bytes(), value == null ? null : new LazyByteArray(value)); + } + @Override public ByteArray getBytes() { checkNotNull(); - return value; + return value.getByteArray(); } @Override com.google.protobuf.Value valueToProto() { - return com.google.protobuf.Value.newBuilder().setStringValue(value.toBase64()).build(); + return com.google.protobuf.Value.newBuilder().setStringValue(value.getBase64String()).build(); + } + + @Nonnull + @Override + public String getAsString() { + return value == null ? NULL_STRING : value.getBase64String(); } @Override void valueToString(StringBuilder b) { - b.append(value.toString()); + b.append(value == null ? null : value.toString()); } } @@ -1483,6 +1642,16 @@ List getArray() { abstract com.google.protobuf.Value getValueAsProto(int i); + @Nonnull + @Override + public ImmutableList getAsStringList() { + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < size(); i++) { + builder.add(isElementNull(i) ? NULL_STRING : String.valueOf(getValue(i))); + } + return builder.build(); + } + @Override void valueToString(StringBuilder b) { b.append(LIST_OPEN); @@ -1661,6 +1830,16 @@ com.google.protobuf.Value valueToProto() { return com.google.protobuf.Value.newBuilder().setListValue(list).build(); } + @Nonnull + @Override + public ImmutableList getAsStringList() { + ImmutableList.Builder builder = ImmutableList.builder(); + for (T element : value) { + builder.add(element == null ? NULL_STRING : elementToString(element)); + } + return builder.build(); + } + String elementToString(T element) { return element.toString(); } @@ -1749,25 +1928,48 @@ void appendElement(StringBuilder b, String element) { } } - private static class BytesArrayImpl extends AbstractArrayValue { - private BytesArrayImpl(boolean isNull, @Nullable List values) { + private static class LazyBytesArrayImpl extends AbstractArrayValue { + private transient AbstractLazyInitializer> bytesArray = defaultInitializer(); + + private LazyBytesArrayImpl(boolean isNull, @Nullable List values) { super(isNull, Type.bytes(), values); } + private AbstractLazyInitializer> defaultInitializer() { + return new AbstractLazyInitializer>() { + @Override + protected List initialize() { + return value.stream() + .map(element -> element == null ? null : element.getByteArray()) + .collect(Collectors.toList()); + } + }; + } + + private void readObject(java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + bytesArray = defaultInitializer(); + } + @Override public List getBytesArray() { checkNotNull(); - return value; + try { + return bytesArray.get(); + } catch (Exception e) { + throw SpannerExceptionFactory.asSpannerException(e); + } } @Override - String elementToString(ByteArray element) { - return element.toBase64(); + String elementToString(LazyByteArray element) { + return element.getBase64String(); } @Override - void appendElement(StringBuilder b, ByteArray element) { - b.append(element.toString()); + void appendElement(StringBuilder b, LazyByteArray element) { + b.append(elementToString(element)); } } 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 ec9e5a43d8f..e16b858faa8 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 @@ -95,7 +95,11 @@ public R to(@Nullable String value) { return handle(Value.string(value)); } - /** Binds to {@code Value.bytes(value)} */ + /** + * Binds to {@code Value.bytes(value)}. Use {@link #to(Value)} in combination with {@link + * Value#bytesFromBase64(String)} if you already have the value that you want to bind in base64 + * format. This prevents unnecessary decoding and encoding of base64 strings. + */ public R to(@Nullable ByteArray value) { return handle(Value.bytes(value)); } @@ -198,6 +202,15 @@ public R toBytesArray(@Nullable Iterable values) { return handle(Value.bytesArray(values)); } + /** + * Binds to {@code Value.bytesArray(values)}. The given strings must be valid base64 encoded + * strings. Use this method instead of {@link #toBytesArray(Iterable)} if you already have the + * values in base64 format to prevent unnecessary decoding and encoding to/from base64. + */ + public R toBytesArrayFromBase64(@Nullable Iterable valuesAsBase64Strings) { + return handle(Value.bytesArrayFromBase64(valuesAsBase64Strings)); + } + /** Binds to {@code Value.timestampArray(values)} */ public R toTimestampArray(@Nullable Iterable values) { return handle(Value.timestampArray(values)); 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 666152eeca4..ac0f0a98d29 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 @@ -37,6 +37,7 @@ import com.google.api.core.ApiFutures; import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.api.gax.retrying.RetrySettings; +import com.google.cloud.ByteArray; import com.google.cloud.NoCredentials; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbstractResultSet.GrpcStreamIterator; @@ -50,10 +51,15 @@ import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.cloud.spanner.SpannerOptions.SpannerCallContextTimeoutConfigurator; +import com.google.cloud.spanner.Type.Code; +import com.google.cloud.spanner.connection.RandomResultSetGenerator; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; +import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.DeleteSessionRequest; import com.google.spanner.v1.ExecuteBatchDmlRequest; @@ -67,6 +73,7 @@ import com.google.spanner.v1.StructType; import com.google.spanner.v1.StructType.Field; import com.google.spanner.v1.Type; +import com.google.spanner.v1.TypeAnnotationCode; import com.google.spanner.v1.TypeCode; import io.grpc.Context; import io.grpc.Server; @@ -74,9 +81,12 @@ import io.grpc.StatusRuntimeException; import io.grpc.inprocess.InProcessServerBuilder; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Random; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -87,6 +97,7 @@ import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -1661,7 +1672,8 @@ public void testBackendPartitionQueryOptions() { @Test public void testAsyncQuery() throws Exception { final int EXPECTED_ROW_COUNT = 10; - RandomResultSetGenerator generator = new RandomResultSetGenerator(EXPECTED_ROW_COUNT); + com.google.cloud.spanner.connection.RandomResultSetGenerator generator = + new RandomResultSetGenerator(EXPECTED_ROW_COUNT); com.google.spanner.v1.ResultSet resultSet = generator.generate(); mockSpanner.putStatementResult( StatementResult.query(Statement.of("SELECT * FROM RANDOM"), resultSet)); @@ -2381,4 +2393,565 @@ public void testAnalyzeUpdateStatement() { ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0); assertEquals(QueryMode.PLAN, request.getQueryMode()); } + + @Test + public void testByteArray() { + Random random = new Random(); + byte[] bytes = new byte[random.nextInt(200)]; + int numRows = 5; + List rows = new ArrayList<>(numRows); + for (int i = 0; i < numRows; i++) { + random.nextBytes(bytes); + rows.add( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue( + // Use both the Guava and the JDK encoder to encode the values to ensure + // that encoding/decoding using both of them works. + i % 2 == 0 + ? Base64.getEncoder().encodeToString(bytes) + : BaseEncoding.base64().encode(bytes)) + .build()) + .build()); + } + Statement statement = Statement.of("select * from foo"); + mockSpanner.putStatementResult( + StatementResult.query( + statement, + com.google.spanner.v1.ResultSet.newBuilder() + .setMetadata( + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setType(Type.newBuilder().setCode(TypeCode.BYTES).build()) + .setName("f1") + .build()) + .build()) + .build()) + .addAllRows(rows) + .build())); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + while (resultSet.next()) { + String base64String = resultSet.getValue(0).getAsString(); + ByteArray byteArray = resultSet.getBytes(0); + // Use the 'old' ByteArray.fromBase64(..) method that uses the Guava encoder to ensure that + // the two encoders (JDK and Guava) return the same values. + assertEquals(ByteArray.fromBase64(base64String), byteArray); + } + } + } + + @Test + public void testGetAllTypesAsString() { + for (Dialect dialect : Dialect.values()) { + Statement statement = Statement.of("select * from all_types"); + mockSpanner.putStatementResult( + StatementResult.query( + statement, + com.google.spanner.v1.ResultSet.newBuilder() + .setMetadata( + RandomResultSetGenerator.generateAllTypesMetadata( + RandomResultSetGenerator.generateAllTypes(dialect))) + .addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder().setBoolValue(true).build()) + .addValues( + com.google.protobuf.Value.newBuilder().setStringValue("100").build()) + .addValues( + com.google.protobuf.Value.newBuilder().setNumberValue(3.14d).build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("6.626") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("test-string") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("{\"key1\": \"value1\"}") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue( + Base64.getEncoder() + .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("2023-01-11T11:55:18.123456789Z") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setBoolValue(true) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setBoolValue(false) + .build()) + .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue(String.valueOf(Long.MAX_VALUE)) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue(String.valueOf(Long.MIN_VALUE)) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNumberValue(-12345.6789d) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNumberValue(3.14d) + .build()) + .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("6.626") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("-8.9123") + .build()) + .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("test-string1") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("test-string2") + .build()) + .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("{\"key\": \"value1\"}") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("{\"key\": \"value2\"}") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue( + Base64.getEncoder() + .encodeToString( + "test-bytes1" + .getBytes( + StandardCharsets.UTF_8))) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue( + Base64.getEncoder() + .encodeToString( + "test-bytes2" + .getBytes( + StandardCharsets.UTF_8))) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("2000-02-29") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("2000-01-01") + .build()) + .build())) + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("2023-01-11T11:55:18.123456789Z") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("2023-01-12T11:55:18Z") + .build()) + .build())) + .build()) + .build())); + + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet resultSet = client.singleUse().executeQuery(statement)) { + assertTrue(resultSet.next()); + int col = 0; + assertAsString("true", resultSet, col++); + assertAsString("100", resultSet, col++); + assertAsString("3.14", resultSet, col++); + assertAsString("6.626", resultSet, col++); + assertAsString("test-string", resultSet, col++); + assertAsString("{\"key1\": \"value1\"}", resultSet, col++); + assertAsString( + Base64.getEncoder().encodeToString("test-bytes".getBytes(StandardCharsets.UTF_8)), + resultSet, + col++); + assertAsString("2023-01-11", resultSet, col++); + assertAsString("2023-01-11T11:55:18.123456789Z", resultSet, col++); + + assertAsString(ImmutableList.of("true", "NULL", "false"), resultSet, col++); + assertAsString( + ImmutableList.of( + String.format("%d", Long.MAX_VALUE), String.format("%d", Long.MIN_VALUE), "NULL"), + resultSet, + col++); + assertAsString(ImmutableList.of("NULL", "-12345.6789", "3.14"), resultSet, col++); + assertAsString(ImmutableList.of("6.626", "NULL", "-8.9123"), resultSet, col++); + assertAsString(ImmutableList.of("test-string1", "NULL", "test-string2"), resultSet, col++); + assertAsString( + ImmutableList.of("{\"key\": \"value1\"}", "{\"key\": \"value2\"}", "NULL"), + resultSet, + col++); + assertAsString( + ImmutableList.of( + String.format( + "%s", + Base64.getEncoder() + .encodeToString("test-bytes1".getBytes(StandardCharsets.UTF_8))), + String.format( + "%s", + Base64.getEncoder() + .encodeToString("test-bytes2".getBytes(StandardCharsets.UTF_8))), + "NULL"), + resultSet, + col++); + assertAsString(ImmutableList.of("2000-02-29", "NULL", "2000-01-01"), resultSet, col++); + assertAsString( + ImmutableList.of("2023-01-11T11:55:18.123456789Z", "NULL", "2023-01-12T11:55:18Z"), + resultSet, + col++); + + assertFalse(resultSet.next()); + } + } + } + + @Test + public void testSelectUnknownType() { + mockSpanner.putStatementResult( + StatementResult.query( + Statement.of("SELECT * FROM foo"), + com.google.spanner.v1.ResultSet.newBuilder() + .setMetadata( + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("c") + .setType( + Type.newBuilder() + .setCodeValue(Integer.MAX_VALUE) + .build()) + .build()) + .build()) + .build()) + .addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder().setStringValue("bar").build()) + .build()) + .addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder().setBoolValue(true).build()) + .build()) + .addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder().setNumberValue(3.14d).build()) + .build()) + .addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .build()) + .addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("baz") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setBoolValue(false) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNumberValue(6.626) + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()) + .build()) + .build()) + .build()) + .build())); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet resultSet = client.singleUse().executeQuery(Statement.of("SELECT * FROM foo"))) { + assertTrue(resultSet.next()); + assertAsString("bar", resultSet, 0); + + assertTrue(resultSet.next()); + assertAsString("true", resultSet, 0); + + assertTrue(resultSet.next()); + assertAsString("3.14", resultSet, 0); + + assertTrue(resultSet.next()); + assertAsString("NULL", resultSet, 0); + + assertTrue(resultSet.next()); + assertAsString(ImmutableList.of("baz", "false", "6.626", "NULL"), resultSet, 0); + + assertFalse(resultSet.next()); + } + } + + @Test + public void testMetadataUnknownTypes() { + mockSpanner.putStatementResult( + StatementResult.query( + Statement.of("SELECT * FROM foo"), + com.google.spanner.v1.ResultSet.newBuilder() + .setMetadata( + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("c1") + .setType( + Type.newBuilder() + .setCodeValue(Integer.MAX_VALUE) + .build()) + .build()) + .addFields( + Field.newBuilder() + .setName("c2") + .setType( + Type.newBuilder() + .setCode(TypeCode.STRING) + .setTypeAnnotationValue(Integer.MAX_VALUE) + .build()) + .build()) + .addFields( + Field.newBuilder() + .setName("c3") + .setType( + Type.newBuilder() + .setCodeValue(Integer.MAX_VALUE) + .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) + .build()) + .build()) + .addFields( + Field.newBuilder() + .setName("c4") + .setType( + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + Type.newBuilder() + .setCodeValue(Integer.MAX_VALUE) + .build()) + .build()) + .build()) + .addFields( + Field.newBuilder() + .setName("c5") + .setType( + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + Type.newBuilder() + .setCode(TypeCode.STRING) + .setTypeAnnotationValue(Integer.MAX_VALUE) + .build()) + .build()) + .build()) + .addFields( + Field.newBuilder() + .setName("c6") + .setType( + Type.newBuilder() + // Set an unrecognized type with an array element + // type. The client should recognize this as an + // array. + .setCodeValue(Integer.MAX_VALUE) + .setArrayElementType( + Type.newBuilder() + .setCodeValue(Integer.MAX_VALUE) + .build()) + .build()) + .build()) + .addFields( + Field.newBuilder() + .setName("c7") + .setType( + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + Type.newBuilder() + .setCodeValue(Integer.MAX_VALUE) + .setTypeAnnotation( + TypeAnnotationCode.PG_NUMERIC) + .build()) + .build()) + .build()) + .build()) + .build()) + .build())); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet resultSet = client.singleUse().executeQuery(Statement.of("SELECT * FROM foo"))) { + // There are no rows, but we need to call resultSet.next() before we can get the metadata. + assertFalse(resultSet.next()); + assertEquals( + "STRUCT, c3 UNRECOGNIZED, c4 ARRAY, c5 ARRAY>, c6 UNRECOGNIZED, c7 ARRAY>>", + resultSet.getType().toString()); + assertEquals( + "UNRECOGNIZED", resultSet.getType().getStructFields().get(0).getType().toString()); + assertEquals( + "STRING", + resultSet.getType().getStructFields().get(1).getType().toString()); + assertEquals( + "UNRECOGNIZED", + resultSet.getType().getStructFields().get(2).getType().toString()); + assertEquals( + "ARRAY", resultSet.getType().getStructFields().get(3).getType().toString()); + assertEquals(Code.ARRAY, resultSet.getType().getStructFields().get(3).getType().getCode()); + assertEquals( + Code.UNRECOGNIZED, + resultSet.getType().getStructFields().get(3).getType().getArrayElementType().getCode()); + assertEquals( + "ARRAY>", + resultSet.getType().getStructFields().get(4).getType().toString()); + assertEquals(Code.ARRAY, resultSet.getType().getStructFields().get(4).getType().getCode()); + assertEquals( + Code.UNRECOGNIZED, + resultSet.getType().getStructFields().get(4).getType().getArrayElementType().getCode()); + assertEquals( + "UNRECOGNIZED", + resultSet.getType().getStructFields().get(5).getType().toString()); + assertEquals( + Code.UNRECOGNIZED, resultSet.getType().getStructFields().get(5).getType().getCode()); + assertEquals( + Code.UNRECOGNIZED, + resultSet.getType().getStructFields().get(5).getType().getArrayElementType().getCode()); + assertEquals( + "ARRAY>", + resultSet.getType().getStructFields().get(6).getType().toString()); + assertEquals(Code.ARRAY, resultSet.getType().getStructFields().get(6).getType().getCode()); + assertEquals( + Code.UNRECOGNIZED, + resultSet.getType().getStructFields().get(6).getType().getArrayElementType().getCode()); + } + } + + static void assertAsString(String expected, ResultSet resultSet, int col) { + assertEquals(expected, resultSet.getValue(col).getAsString()); + assertEquals(ImmutableList.of(expected), resultSet.getValue(col).getAsStringList()); + } + + static void assertAsString(ImmutableList expected, ResultSet resultSet, int col) { + assertEquals(expected, resultSet.getValue(col).getAsStringList()); + assertEquals( + expected.stream().collect(Collectors.joining(",", "[", "]")), + resultSet.getValue(col).getAsString()); + } } 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 ff4e92a5215..490eff3aca8 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 @@ -38,8 +38,10 @@ import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.Transaction; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; @@ -517,7 +519,10 @@ public void serialization() { Value.float64(1.0), Value.float64(null), Value.bytes(ByteArray.fromBase64("abcd")), + Value.bytesFromBase64( + Base64.getEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8))), Value.bytes(null), + Value.bytesFromBase64(null), Value.timestamp(Timestamp.ofTimeSecondsAndNanos(1, 2)), Value.timestamp(null), Value.date(Date.fromYearMonthDay(2017, 4, 17)), @@ -528,6 +533,14 @@ public void serialization() { Value.boolArray((boolean[]) null), Value.int64Array(new long[] {1, 2, 3}), Value.int64Array((long[]) null), + Value.float64Array(new double[] {1.1, 2.2, 3.3}), + Value.float64Array((double[]) null), + Value.bytesArray(Arrays.asList(ByteArray.fromBase64("abcd"), null)), + Value.bytesArrayFromBase64( + Arrays.asList( + Base64.getEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8)), null)), + Value.bytesArray(null), + Value.bytesArrayFromBase64(null), Value.timestampArray(ImmutableList.of(Timestamp.MAX_VALUE, Timestamp.MAX_VALUE)), Value.timestampArray(null), Value.dateArray( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazyByteArrayTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazyByteArrayTest.java new file mode 100644 index 00000000000..a36b3463439 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazyByteArrayTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import com.google.cloud.spanner.AbstractResultSet.LazyByteArray; +import java.util.Base64; +import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class LazyByteArrayTest { + + @Test + public void testEqualsAndHashCode() { + Random random = new Random(); + byte[] bytes1 = new byte[random.nextInt(300) + 300]; + // Make sure the second byte array has a different length than the first to be absolutely sure + // that they can never contain the same value. + byte[] bytes2 = new byte[bytes1.length + 1]; + random.nextBytes(bytes1); + random.nextBytes(bytes2); + + LazyByteArray lazyByteArray1 = new LazyByteArray(Base64.getEncoder().encodeToString(bytes1)); + LazyByteArray lazyByteArray2 = new LazyByteArray(Base64.getEncoder().encodeToString(bytes2)); + LazyByteArray lazyByteArray3 = new LazyByteArray(Base64.getEncoder().encodeToString(bytes1)); + + assertEquals(lazyByteArray1, lazyByteArray3); + assertNotEquals(lazyByteArray1, lazyByteArray2); + assertNotEquals(lazyByteArray2, lazyByteArray3); + + assertEquals(lazyByteArray1.hashCode(), lazyByteArray3.hashCode()); + } +} 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 3ed6fc6c577..eb17a7a8c62 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 @@ -19,6 +19,8 @@ import static com.google.cloud.spanner.Type.StructField; import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; @@ -428,9 +430,7 @@ public void structFieldIndexAmbiguous() { @Test public void parseErrorMissingTypeCode() { com.google.spanner.v1.Type proto = com.google.spanner.v1.Type.newBuilder().build(); - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> Type.fromProto(proto)); - assertNotNull(e.getMessage()); + assertEquals(Code.UNRECOGNIZED, Type.fromProto(proto).getCode()); } @Test @@ -442,6 +442,81 @@ public void parseErrorMissingArrayElementTypeProto() { assertNotNull(e.getMessage()); } + @Test + public void testUnrecognized() { + Type unrecognized = Type.fromProto(com.google.spanner.v1.Type.newBuilder().build()); + assertEquals("TYPE_CODE_UNSPECIFIED", unrecognized.toString()); + assertEquals(unrecognized, Type.fromProto(com.google.spanner.v1.Type.newBuilder().build())); + assertNotEquals(unrecognized, Type.int64()); + } + + @Test + public void testUnrecognizedWithAnnotation() { + Type unrecognized = + Type.fromProto( + com.google.spanner.v1.Type.newBuilder() + .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) + .build()); + assertEquals("TYPE_CODE_UNSPECIFIED", unrecognized.toString()); + assertEquals( + unrecognized, + Type.fromProto( + com.google.spanner.v1.Type.newBuilder() + .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) + .build())); + assertNotEquals( + unrecognized, + Type.fromProto( + com.google.spanner.v1.Type.newBuilder() + .setTypeAnnotation(TypeAnnotationCode.PG_JSONB) + .build())); + assertNotEquals(unrecognized, Type.int64()); + } + + @Test + public void testUnrecognizedArray() { + Type unrecognizedArray = + Type.fromProto( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(com.google.spanner.v1.Type.newBuilder().build()) + .build()); + assertEquals("ARRAY", unrecognizedArray.toString()); + assertEquals( + unrecognizedArray, + Type.fromProto( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(com.google.spanner.v1.Type.newBuilder().build()) + .build())); + assertNotEquals(unrecognizedArray, Type.array(Type.int64())); + } + + @Test + public void testUnrecognizedArrayWithAnnotation() { + Type unrecognizedArray = + Type.fromProto( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + com.google.spanner.v1.Type.newBuilder() + .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) + .build()) + .build()); + assertEquals("ARRAY>", unrecognizedArray.toString()); + assertEquals( + unrecognizedArray, + Type.fromProto( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + com.google.spanner.v1.Type.newBuilder() + .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) + .build()) + .build())); + assertNotEquals(unrecognizedArray, Type.array(Type.int64())); + } + private static void assertProtoEquals(com.google.spanner.v1.Type proto, String expected) { MatcherAssert.assertThat( proto, SpannerMatchers.matchesProto(com.google.spanner.v1.Type.class, expected)); 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 91263457baf..8da5c0322dc 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 @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import static com.google.cloud.spanner.ValueBinderTest.DefaultValues.defaultBytesBase64; import static com.google.cloud.spanner.ValueBinderTest.DefaultValues.defaultJson; import static com.google.cloud.spanner.ValueBinderTest.DefaultValues.defaultPgJsonb; import static com.google.common.truth.Truth.assertThat; @@ -28,7 +29,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,6 +43,7 @@ public class ValueBinderTest { private static final String JSON_METHOD_NAME = "json"; private static final String PG_JSONB_METHOD_NAME = "pgJsonb"; private static final String PG_NUMERIC_METHOD_NAME = "pgNumeric"; + private static final String BYTES_BASE64_METHOD_NAME = "bytesFromBase64"; public static final String DEFAULT_PG_NUMERIC = "1.23"; private Value lastValue; @@ -134,6 +138,10 @@ public void reflection() binderMethod = ValueBinder.class.getMethod("to", Value.class); assertThat(binderMethod.invoke(binder, Value.pgNumeric(null))) .isEqualTo(lastReturnValue); + } else if (method.getName().equalsIgnoreCase(BYTES_BASE64_METHOD_NAME)) { + binderMethod = ValueBinder.class.getMethod("to", Value.class); + assertThat(binderMethod.invoke(binder, Value.bytesFromBase64(null))) + .isEqualTo(lastReturnValue); } else { assertThat(binderMethod.invoke(binder, (Object) null)).isEqualTo(lastReturnValue); } @@ -160,6 +168,11 @@ public void reflection() binderMethod = ValueBinder.class.getMethod("to", Value.class); assertThat(binderMethod.invoke(binder, Value.pgNumeric(DEFAULT_PG_NUMERIC))) .isEqualTo(lastReturnValue); + } else if (method.getName().equalsIgnoreCase(BYTES_BASE64_METHOD_NAME)) { + defaultObject = defaultBytesBase64(); + binderMethod = ValueBinder.class.getMethod("to", Value.class); + assertThat(binderMethod.invoke(binder, Value.bytesFromBase64(defaultBytesBase64()))) + .isEqualTo(lastReturnValue); } else { defaultObject = DefaultValues.getDefault(method.getGenericParameterTypes()[0]); assertThat(binderMethod.invoke(binder, defaultObject)).isEqualTo(lastReturnValue); @@ -246,6 +259,10 @@ public static String defaultPgJsonb() { return "{\"color\":\"red\",\"value\":\"#f00\"}"; } + public static String defaultBytesBase64() { + return Base64.getEncoder().encodeToString("test-bytes".getBytes(StandardCharsets.UTF_8)); + } + public static ByteArray defaultByteArray() { return ByteArray.copyFrom(new byte[] {'x'}); } 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 a466fab1ab4..bf806795fa6 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 @@ -32,6 +32,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AbstractResultSet.LazyByteArray; import com.google.cloud.spanner.Type.StructField; import com.google.common.base.Strings; import com.google.common.collect.ForwardingList; @@ -41,11 +42,15 @@ import com.google.protobuf.NullValue; import java.io.Serializable; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Random; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -91,6 +96,8 @@ public void untyped() { assertNotEquals( Value.untyped(com.google.protobuf.Value.newBuilder().setBoolValue(false).build()), Value.untyped(com.google.protobuf.Value.newBuilder().setBoolValue(true).build())); + + assertEquals("test", v.getAsString()); } @Test @@ -100,6 +107,7 @@ public void bool() { assertThat(v.isNull()).isFalse(); assertThat(v.getBool()).isTrue(); assertThat(v.toString()).isEqualTo("true"); + assertEquals("true", v.getAsString()); } @Test @@ -109,6 +117,7 @@ public void boolWrapper() { assertThat(v.isNull()).isFalse(); assertThat(v.getBool()).isFalse(); assertThat(v.toString()).isEqualTo("false"); + assertEquals("false", v.getAsString()); } @Test @@ -119,6 +128,7 @@ public void boolWrapperNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getBool); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -128,6 +138,7 @@ public void int64() { assertThat(v.isNull()).isFalse(); assertThat(v.getInt64()).isEqualTo(123); assertThat(v.toString()).isEqualTo("123"); + assertEquals("123", v.getAsString()); } @Test @@ -158,6 +169,7 @@ public void int64Wrapper() { assertThat(v.isNull()).isFalse(); assertThat(v.getInt64()).isEqualTo(123); assertThat(v.toString()).isEqualTo("123"); + assertEquals("123", v.getAsString()); } @Test @@ -168,6 +180,7 @@ public void int64WrapperNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getInt64); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -177,6 +190,7 @@ public void float64() { assertThat(v.isNull()).isFalse(); assertThat(v.getFloat64()).isWithin(0.0001).of(1.23); assertThat(v.toString()).isEqualTo("1.23"); + assertEquals("1.23", v.getAsString()); } @Test @@ -186,6 +200,7 @@ public void float64Wrapper() { assertThat(v.isNull()).isFalse(); assertThat(v.getFloat64()).isWithin(0.0001).of(1.23); assertThat(v.toString()).isEqualTo("1.23"); + assertEquals("1.23", v.getAsString()); } @Test @@ -196,6 +211,7 @@ public void float64WrapperNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat64); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -205,6 +221,7 @@ public void numeric() { assertThat(v.isNull()).isFalse(); assertThat(v.getNumeric()).isEqualTo(BigDecimal.valueOf(123, 2)); assertThat(v.toString()).isEqualTo("1.23"); + assertEquals("1.23", v.getAsString()); } @Test @@ -216,6 +233,7 @@ public void pgNumeric() { assertEquals(BigDecimal.valueOf(12345678, 4), value.getNumeric()); assertEquals(1234.5678D, value.getFloat64(), 0.00001); assertEquals("1234.5678", value.toString()); + assertEquals("1234.5678", value.getAsString()); } @Test @@ -227,6 +245,7 @@ public void pgNumericNaN() { assertThrows(NumberFormatException.class, value::getNumeric); assertEquals(Double.NaN, value.getFloat64(), 0.00001); assertEquals("NaN", value.toString()); + assertEquals("NaN", value.getAsString()); } @Test @@ -384,6 +403,7 @@ public void numericNull() { IllegalStateException e = assertThrows(IllegalStateException.class, v::getNumeric); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -399,6 +419,7 @@ public void pgNumericNull() { assertTrue("exception should mention value is null", e2.getMessage().contains("null value")); final IllegalStateException e3 = assertThrows(IllegalStateException.class, value::getFloat64); assertTrue("exception should mention value is null", e3.getMessage().contains("null value")); + assertEquals("NULL", value.getAsString()); } @Test @@ -411,6 +432,7 @@ public void pgNumericInvalid() { assertEquals("INVALID", value.getString()); assertThrows(NumberFormatException.class, value::getNumeric); assertThrows(NumberFormatException.class, value::getFloat64); + assertEquals("INVALID", value.getAsString()); } @Test @@ -419,6 +441,7 @@ public void string() { assertThat(v.getType()).isEqualTo(Type.string()); assertThat(v.isNull()).isFalse(); assertThat(v.getString()).isEqualTo("abc"); + assertEquals("abc", v.getAsString()); } @Test @@ -429,6 +452,7 @@ public void stringNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getString); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -439,6 +463,7 @@ public void stringLong() { assertThat(v.toString()).hasLength(36); assertThat(v.toString()).startsWith(str.substring(0, 36 - 3)); assertThat(v.toString()).endsWith("..."); + assertEquals(str, v.getAsString()); } @Test @@ -449,6 +474,7 @@ public void json() { assertFalse(v.isNull()); assertEquals(json, v.getJson()); assertEquals(json, v.getString()); + assertEquals(json, v.getAsString()); } @Test @@ -459,6 +485,7 @@ public void jsonNull() { assertEquals(NULL_STRING, v.toString()); assertThrowsWithMessage(v::getJson, "null value"); assertThrowsWithMessage(v::getString, "null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -466,6 +493,7 @@ public void jsonEmpty() { String json = "{}"; Value v = Value.json(json); assertEquals(json, v.getJson()); + assertEquals(json, v.getAsString()); } @Test @@ -473,6 +501,7 @@ public void jsonWithEmptyArray() { String json = "[]"; Value v = Value.json(json); assertEquals(json, v.getJson()); + assertEquals(json, v.getAsString()); } @Test @@ -481,6 +510,7 @@ public void jsonWithArray() { "[{\"color\":\"red\",\"value\":\"#f00\"},{\"color\":\"green\",\"value\":\"#0f0\"},{\"color\":\"blue\",\"value\":\"#00f\"},{\"color\":\"cyan\",\"value\":\"#0ff\"},{\"color\":\"magenta\",\"value\":\"#f0f\"},{\"color\":\"yellow\",\"value\":\"#ff0\"},{\"color\":\"black\",\"value\":\"#000\"}]"; Value v = Value.json(json); assertEquals(json, v.getJson()); + assertEquals(json, v.getAsString()); } @Test @@ -489,6 +519,7 @@ public void jsonNested() { "[{\"id\":\"0001\",\"type\":\"donut\",\"name\":\"Cake\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"},{\"id\":\"1003\",\"type\":\"Blueberry\"},{\"id\":\"1004\",\"type\":\"Devil's Food\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5007\",\"type\":\"Powdered Sugar\"},{\"id\":\"5006\",\"type\":\"Chocolate with Sprinkles\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]},{\"id\":\"0002\",\"type\":\"donut\",\"name\":\"Raised\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]},{\"id\":\"0003\",\"type\":\"donut\",\"name\":\"Old Fashioned\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]}]"; Value v = Value.json(json); assertEquals(json, v.getJson()); + assertEquals(json, v.getAsString()); } @Test @@ -499,6 +530,7 @@ public void testPgJsonb() { assertFalse(v.isNull()); assertEquals(json, v.getPgJsonb()); assertEquals(json, v.getString()); + assertEquals(json, v.getAsString()); } @Test @@ -509,6 +541,7 @@ public void testPgJsonbNull() { assertEquals(NULL_STRING, v.toString()); assertThrowsWithMessage(v::getPgJsonb, "null value"); assertThrowsWithMessage(v::getString, "null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -516,6 +549,7 @@ public void testPgJsonbEmpty() { String json = "{}"; Value v = Value.pgJsonb(json); assertEquals(json, v.getPgJsonb()); + assertEquals(json, v.getAsString()); } @Test @@ -523,6 +557,7 @@ public void testPgJsonbWithEmptyArray() { String json = "[]"; Value v = Value.pgJsonb(json); assertEquals(json, v.getPgJsonb()); + assertEquals(json, v.getAsString()); } @Test @@ -531,6 +566,7 @@ public void testPgJsonbWithArray() { "[{\"color\":\"red\",\"value\":\"#f00\"},{\"color\":\"green\",\"value\":\"#0f0\"},{\"color\":\"blue\",\"value\":\"#00f\"},{\"color\":\"cyan\",\"value\":\"#0ff\"},{\"color\":\"magenta\",\"value\":\"#f0f\"},{\"color\":\"yellow\",\"value\":\"#ff0\"},{\"color\":\"black\",\"value\":\"#000\"}]"; Value v = Value.pgJsonb(json); assertEquals(json, v.getPgJsonb()); + assertEquals(json, v.getAsString()); } @Test @@ -539,6 +575,7 @@ public void testPgJsonbNested() { "[{\"id\":\"0001\",\"type\":\"donut\",\"name\":\"Cake\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"},{\"id\":\"1003\",\"type\":\"Blueberry\"},{\"id\":\"1004\",\"type\":\"Devil's Food\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5007\",\"type\":\"Powdered Sugar\"},{\"id\":\"5006\",\"type\":\"Chocolate with Sprinkles\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]},{\"id\":\"0002\",\"type\":\"donut\",\"name\":\"Raised\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]},{\"id\":\"0003\",\"type\":\"donut\",\"name\":\"Old Fashioned\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]}]"; Value v = Value.pgJsonb(json); assertEquals(json, v.getPgJsonb()); + assertEquals(json, v.getAsString()); } @Test @@ -548,7 +585,8 @@ public void bytes() { assertThat(v.getType()).isEqualTo(Type.bytes()); assertThat(v.isNull()).isFalse(); assertThat(v.getBytes()).isSameInstanceAs(bytes); - assertThat(v.toString()).isEqualTo(bytes.toString()); + assertThat(v.toString()).isEqualTo(bytes.toBase64()); + assertEquals(Base64.getEncoder().encodeToString(bytes.toByteArray()), v.getAsString()); } @Test @@ -556,7 +594,8 @@ public void bytesUnprintable() { ByteArray bytes = ByteArray.copyFrom(new byte[] {'a', 0, 15, -1, 'e'}); Value v = Value.bytes(bytes); assertThat(v.getBytes()).isSameInstanceAs(bytes); - assertThat(v.toString()).isEqualTo(bytes.toString()); + assertThat(v.toString()).isEqualTo(bytes.toBase64()); + assertEquals(Base64.getEncoder().encodeToString(bytes.toByteArray()), v.getAsString()); } @Test @@ -567,6 +606,7 @@ public void bytesNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getBytes); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -579,6 +619,7 @@ public void timestamp() { assertThat(v.isCommitTimestamp()).isFalse(); assertThat(v.getTimestamp()).isSameInstanceAs(t); assertThat(v.toString()).isEqualTo(timestamp); + assertEquals(timestamp, v.getAsString()); } @Test @@ -590,6 +631,7 @@ public void timestampNull() { assertThat(v.isCommitTimestamp()).isFalse(); IllegalStateException e = assertThrows(IllegalStateException.class, v::getTimestamp); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -606,6 +648,7 @@ public void commitTimestamp() { .build()); IllegalStateException e = assertThrows(IllegalStateException.class, v::getTimestamp); assertThat(e.getMessage()).contains("Commit timestamp value"); + assertEquals("spanner.commit_timestamp()", v.getAsString()); } @Test @@ -617,6 +660,7 @@ public void date() { assertThat(v.isNull()).isFalse(); assertThat(v.getDate()).isSameInstanceAs(t); assertThat(v.toString()).isEqualTo(date); + assertEquals(date, v.getAsString()); } @Test @@ -627,6 +671,7 @@ public void dateNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getDate); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -635,6 +680,7 @@ public void boolArray() { assertThat(v.isNull()).isFalse(); assertThat(v.getBoolArray()).containsExactly(true, false).inOrder(); assertThat(v.toString()).isEqualTo("[true,false]"); + assertEquals("[true,false]", v.getAsString()); } @Test @@ -643,6 +689,7 @@ public void boolArrayRange() { assertThat(v.isNull()).isFalse(); assertThat(v.getBoolArray()).containsExactly(false, false, true).inOrder(); assertThat(v.toString()).isEqualTo("[false,false,true]"); + assertEquals("[false,false,true]", v.getAsString()); } @Test @@ -652,6 +699,7 @@ public void boolArrayNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getBoolArray); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -660,6 +708,7 @@ public void boolArrayFromList() { assertThat(v.isNull()).isFalse(); assertThat(v.getBoolArray()).containsExactly(true, null, false).inOrder(); assertThat(v.toString()).isEqualTo("[true,NULL,false]"); + assertEquals("[true,NULL,false]", v.getAsString()); } @Test @@ -669,6 +718,7 @@ public void boolArrayFromListNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getBoolArray); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -685,6 +735,11 @@ public void boolArrayFromPlainIterable() { Value v = Value.boolArray(plainIterable(data)); assertWithMessage(name).that(v.isNull()).isFalse(); assertWithMessage(name).that(v.getBoolArray()).containsExactly((Object[]) data).inOrder(); + assertEquals( + Arrays.stream(data) + .map(element -> String.valueOf(element).replace("null", "NULL")) + .collect(Collectors.joining(",", "[", "]")), + v.getAsString()); } } @@ -701,6 +756,7 @@ public void int64Array() { assertThat(v.isNull()).isFalse(); assertThat(v.getInt64Array()).containsExactly(1L, 2L).inOrder(); assertThat(v.toString()).isEqualTo("[1,2]"); + assertEquals("[1,2]", v.getAsString()); } @Test @@ -709,6 +765,7 @@ public void int64ArrayRange() { assertThat(v.isNull()).isFalse(); assertThat(v.getInt64Array()).containsExactly(2L, 3L, 4L).inOrder(); assertThat(v.toString()).isEqualTo("[2,3,4]"); + assertEquals("[2,3,4]", v.getAsString()); } @Test @@ -718,6 +775,7 @@ public void int64ArrayNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getInt64Array); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -726,6 +784,7 @@ public void int64ArrayWrapper() { assertThat(v.isNull()).isFalse(); assertThat(v.getInt64Array()).containsExactly(1L, null, 3L).inOrder(); assertThat(v.toString()).isEqualTo("[1,NULL,3]"); + assertEquals("[1,NULL,3]", v.getAsString()); } @Test @@ -735,6 +794,7 @@ public void int64ArrayWrapperNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getInt64Array); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -757,6 +817,7 @@ public void float64Array() { assertThat(v.isNull()).isFalse(); assertThat(v.getFloat64Array()).containsExactly(.1d, .2d).inOrder(); assertThat(v.toString()).isEqualTo("[0.1,0.2]"); + assertEquals("[0.1,0.2]", v.getAsString()); } @Test @@ -765,6 +826,7 @@ public void float64ArrayRange() { assertThat(v.isNull()).isFalse(); assertThat(v.getFloat64Array()).containsExactly(.2d, .3d, .4d).inOrder(); assertThat(v.toString()).isEqualTo("[0.2,0.3,0.4]"); + assertEquals("[0.2,0.3,0.4]", v.getAsString()); } @Test @@ -774,6 +836,7 @@ public void float64ArrayNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat64Array); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -782,6 +845,7 @@ public void float64ArrayWrapper() { assertThat(v.isNull()).isFalse(); assertThat(v.getFloat64Array()).containsExactly(.1d, null, .3d).inOrder(); assertThat(v.toString()).isEqualTo("[0.1,NULL,0.3]"); + assertEquals("[0.1,NULL,0.3]", v.getAsString()); } @Test @@ -791,6 +855,7 @@ public void float64ArrayWrapperNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat64Array); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -809,6 +874,7 @@ public void numericArray() { .containsExactly(new BigDecimal("0.1"), null, new BigDecimal("0.3")) .inOrder(); assertThat(v.toString()).isEqualTo("[0.1,NULL,0.3]"); + assertEquals("[0.1,NULL,0.3]", v.getAsString()); } @Test @@ -823,6 +889,7 @@ public void pgNumericArray() { assertEquals(1.23D, float64Array.get(0), 0.001); assertNull(float64Array.get(1)); assertEquals(1.24D, float64Array.get(2), 0.001); + assertEquals("[1.23,NULL,1.24]", value.getAsString()); } @Test @@ -835,6 +902,7 @@ public void pgNumericArrayWithNaNs() { assertEquals(1.23D, float64Array.get(0), 0.001); assertNull(float64Array.get(1)); assertEquals(Double.NaN, float64Array.get(2), 0.001); + assertEquals("[1.23,NULL,NaN]", value.getAsString()); } @Test @@ -845,6 +913,7 @@ public void numericArrayNull() { IllegalStateException e = assertThrows(IllegalStateException.class, v::getNumericArray); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -862,6 +931,7 @@ public void pgNumericArrayNull() { final IllegalStateException e3 = assertThrows(IllegalStateException.class, value::getFloat64Array); assertTrue("exception should mention value is null", e3.getMessage().contains("null value")); + assertEquals("NULL", value.getAsString()); } @Test @@ -888,6 +958,7 @@ public void stringArray() { assertThat(v.isNull()).isFalse(); assertThat(v.getStringArray()).containsExactly("a", null, "c").inOrder(); assertThat(v.toString()).isEqualTo("[a,NULL,c]"); + assertEquals("[a,NULL,c]", v.getAsString()); } @Test @@ -897,6 +968,7 @@ public void stringArrayNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getStringArray); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -916,6 +988,7 @@ public void jsonArray() { assertArrayEquals(new String[] {one, two, three}, v.getJsonArray().toArray()); assertEquals("[{},NULL,{\"color\":\"red\",\"value\":\"#f00\"}]", v.toString()); assertArrayEquals(new String[] {one, two, three}, v.getStringArray().toArray()); + assertEquals("[{},NULL,{\"color\":\"red\",\"value\":\"#f00\"}]", v.getAsString()); } @Test @@ -925,17 +998,13 @@ public void jsonArrayNull() { assertEquals(NULL_STRING, v.toString()); assertThrowsWithMessage(v::getJsonArray, "null value"); assertThrowsWithMessage(v::getStringArray, "null value"); + assertEquals("NULL", v.getAsString()); } @Test public void jsonArrayTryGetBytesArray() { - Value value = Value.jsonArray(Arrays.asList("{}")); - try { - value.getBytesArray(); - fail("Expected exception"); - } catch (IllegalStateException e) { - assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); - } + Value value = Value.jsonArray(Collections.singletonList("{}")); + assertThrowsWithMessage(value::getBytesArray, "Expected: ARRAY actual: ARRAY"); } @Test @@ -954,6 +1023,7 @@ public void testPgJsonbArray() { assertArrayEquals(new String[] {one, two, three}, v.getPgJsonbArray().toArray()); assertEquals("[{},NULL,{\"color\":\"red\",\"value\":\"#f00\"}]", v.toString()); assertArrayEquals(new String[] {one, two, three}, v.getStringArray().toArray()); + assertEquals("[{},NULL,{\"color\":\"red\",\"value\":\"#f00\"}]", v.getAsString()); } @Test @@ -963,6 +1033,7 @@ public void testPgJsonbArrayNull() { assertEquals(NULL_STRING, v.toString()); assertThrowsWithMessage(v::getPgJsonbArray, "null value"); assertThrowsWithMessage(v::getStringArray, "null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -986,7 +1057,18 @@ public void bytesArray() { Value v = Value.bytesArray(Arrays.asList(a, null, c)); assertThat(v.isNull()).isFalse(); assertThat(v.getBytesArray()).containsExactly(a, null, c).inOrder(); - assertThat(v.toString()).isEqualTo(String.format("[%s,NULL,%s]", a, c)); + assertThat(v.toString()) + .isEqualTo( + String.format( + "[%s,NULL,%s]", + Base64.getEncoder().encodeToString("a".getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString("c".getBytes(StandardCharsets.UTF_8)))); + assertEquals( + String.format( + "[%s,NULL,%s]", + Base64.getEncoder().encodeToString("a".getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString("c".getBytes(StandardCharsets.UTF_8))), + v.getAsString()); } @Test @@ -996,6 +1078,7 @@ public void bytesArrayNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getBytesArray); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -1017,6 +1100,7 @@ public void timestampArray() { .containsExactly(Timestamp.parseTimestamp(t1), null, Timestamp.parseTimestamp(t2)) .inOrder(); assertThat(v.toString()).isEqualTo("[" + t1 + ",NULL," + t2 + "]"); + assertEquals(String.format("[%s,NULL,%s]", t1, t2), v.getAsString()); } @Test @@ -1026,6 +1110,7 @@ public void timestampArrayNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getTimestampArray); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -1039,6 +1124,7 @@ public void dateArray() { .containsExactly(Date.parseDate(d1), null, Date.parseDate(d2)) .inOrder(); assertThat(v.toString()).isEqualTo("[" + d1 + ",NULL," + d2 + "]"); + assertEquals(String.format("[%s,NULL,%s]", d1, d2), v.getAsString()); } @Test @@ -1048,6 +1134,7 @@ public void dateArrayNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getDateArray); assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -1058,6 +1145,7 @@ public void struct() { assertThat(v1.isNull()).isFalse(); assertThat(v1.getStruct()).isEqualTo(struct); assertThat(v1.toString()).isEqualTo("[v1, 30]"); + assertEquals("[v1, 30]", v1.getAsString()); Value v2 = Value.struct(struct.getType(), struct); assertThat(v2).isEqualTo(v1); @@ -1083,6 +1171,7 @@ public void nullStruct() { assertThat(v.toString()).isEqualTo(NULL_STRING); NullPointerException e = assertThrows(NullPointerException.class, () -> Value.struct(null)); assertThat(e.getMessage()).contains("Illegal call to create a NULL struct value."); + assertEquals("NULL", v.getAsString()); } @Test @@ -1095,6 +1184,7 @@ public void nullStructGetter() { assertThat(v.isNull()).isTrue(); IllegalStateException e = assertThrows(IllegalStateException.class, v::getStruct); assertThat(e.getMessage()).contains("Illegal call to getter of null value."); + assertEquals("NULL", v.getAsString()); } @Test @@ -1163,6 +1253,7 @@ public void structArray() { assertThat(v.getType().getArrayElementType()).isEqualTo(elementType); assertThat(v.getStructArray()).isEqualTo(arrayElements); assertThat(v.toString()).isEqualTo("[[v1, 1],NULL,NULL,[v3, 3]]"); + assertEquals("[[v1, 1],NULL,NULL,[v3, 3]]", v.getAsString()); } @Test @@ -1178,6 +1269,7 @@ public void structArrayNull() { assertThat(v.toString()).isEqualTo(NULL_STRING); IllegalStateException e = assertThrows(IllegalStateException.class, v::getStructArray); assertThat(e.getMessage()).contains("Illegal call to getter of null value"); + assertEquals("NULL", v.getAsString()); } @Test @@ -1792,6 +1884,48 @@ public void testEqualsHashCode() { tester.testEquals(); } + @Test + public void testGetAsString() { + assertEquals("true", Value.bool(true).getAsString()); + assertEquals("false", Value.bool(false).getAsString()); + + assertEquals("1", Value.int64(1L).getAsString()); + assertEquals(String.valueOf(Long.MAX_VALUE), Value.int64(Long.MAX_VALUE).getAsString()); + assertEquals(String.valueOf(Long.MIN_VALUE), Value.int64(Long.MIN_VALUE).getAsString()); + + assertEquals("3.14", Value.float64(3.14d).getAsString()); + assertEquals("NaN", Value.float64(Double.NaN).getAsString()); + assertEquals(String.valueOf(Double.MIN_VALUE), Value.float64(Double.MIN_VALUE).getAsString()); + assertEquals(String.valueOf(Double.MAX_VALUE), Value.float64(Double.MAX_VALUE).getAsString()); + + assertEquals("3.14", Value.numeric(new BigDecimal("3.14")).getAsString()); + assertEquals( + "123456789.123456789", Value.numeric(new BigDecimal("123456789.123456789")).getAsString()); + + assertEquals("3.14", Value.pgNumeric("3.14").getAsString()); + assertEquals("123456789.123456789", Value.pgNumeric("123456789.123456789").getAsString()); + assertEquals("NaN", Value.pgNumeric("NaN").getAsString()); + + assertEquals(Strings.repeat("foo", 36), Value.string(Strings.repeat("foo", 36)).getAsString()); + assertEquals(Strings.repeat("foo", 36), Value.json(Strings.repeat("foo", 36)).getAsString()); + assertEquals(Strings.repeat("foo", 36), Value.pgJsonb(Strings.repeat("foo", 36)).getAsString()); + + assertEquals( + "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()); + + Random random = new Random(); + byte[] bytes = new byte[random.nextInt(256)]; + assertEquals( + Base64.getEncoder().encodeToString(bytes), + Value.bytes(ByteArray.copyFrom(bytes)).getAsString()); + assertEquals( + Base64.getEncoder().encodeToString(bytes), + Value.internalBytes(new LazyByteArray(Base64.getEncoder().encodeToString(bytes))) + .getAsString()); + } + @Test public void serialization() { 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 c3ac655a40e..f8415fb00eb 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,7 +39,7 @@ * of Cloud Spanner filled with random data. */ public class RandomResultSetGenerator { - private static Type[] generateTypes(Dialect dialect) { + public static Type[] generateAllTypes(Dialect dialect) { return new Type[] { Type.newBuilder().setCode(TypeCode.BOOL).build(), Type.newBuilder().setCode(TypeCode.INT64).build(), @@ -109,7 +109,7 @@ private static Type[] generateTypes(Dialect dialect) { }; } - private static ResultSetMetadata generateMetadata(Type[] types) { + public static ResultSetMetadata generateAllTypesMetadata(Type[] types) { StructType.Builder rowTypeBuilder = StructType.newBuilder(); for (int col = 0; col < types.length; col++) { rowTypeBuilder.addFields(Field.newBuilder().setName("COL" + col).setType(types[col])).build(); @@ -132,8 +132,8 @@ public RandomResultSetGenerator(int rowCount) { public RandomResultSetGenerator(int rowCount, Dialect dialect) { this.rowCount = rowCount; this.dialect = dialect; - this.types = generateTypes(dialect); - this.metadata = generateMetadata(types); + this.types = generateAllTypes(dialect); + this.metadata = generateAllTypesMetadata(types); } public ResultSet generate() { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java index 2428e75c994..3179093139e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java @@ -21,7 +21,9 @@ import static com.google.cloud.spanner.Type.json; import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -42,6 +44,7 @@ import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.Value; @@ -52,6 +55,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -431,6 +435,55 @@ public void writeBytes() { assertThat(row.getBytes(0)).isEqualTo(data); } + @Test + public void writeBytesAsString() { + Random random = new Random(); + byte[] data = new byte[256]; + random.nextBytes(data); + String base64 = Base64.getEncoder().encodeToString(data); + write(baseInsert().set("BytesValue").to(base64).build()); + Struct row = readLastRow("BytesValue"); + assertFalse(row.isNull(0)); + assertArrayEquals(data, row.getBytes(0).toByteArray()); + assertEquals(base64, row.getValue(0).getAsString()); + } + + @Test + public void writeBytesAsStringUsingDml() { + Random random = new Random(); + byte[] data = new byte[256]; + random.nextBytes(data); + String base64 = Base64.getEncoder().encodeToString(data); + Long updateCount = + client + .readWriteTransaction() + .run( + transaction -> + transaction.executeUpdate( + Statement.newBuilder( + "insert into T (BytesValue, K) values (" + + queryParamString(1) + + ", " + + queryParamString(2) + + ")") + .bind("p1") + .to(Value.bytesFromBase64(base64)) + .bind("p2") + .to(lastKey = uniqueString()) + .build())); + assertNotNull(updateCount); + assertEquals(1L, updateCount.longValue()); + + Struct row = readLastRow("BytesValue"); + assertFalse(row.isNull(0)); + assertArrayEquals(data, row.getBytes(0).toByteArray()); + assertEquals(base64, row.getValue(0).getAsString()); + } + + String queryParamString(int index) { + return dialect.dialect == Dialect.GOOGLE_STANDARD_SQL ? "@p" + index : "$" + index; + } + @Test public void writeBytesRandom() { // Pseudo-random test for byte encoding. We explicitly set a random seed so that multiple