From df23ce760e349e0ca42c2a94861fbac50a7fb6a6 Mon Sep 17 00:00:00 2001 From: Aditya Rastogi Date: Fri, 27 Apr 2018 17:23:26 -0700 Subject: [PATCH] Add support for struct-typed parameters. --- .../cloud/spanner/AbstractStructReader.java | 11 +- .../com/google/cloud/spanner/ResultSets.java | 8 + .../com/google/cloud/spanner/SpannerImpl.java | 52 +++- .../java/com/google/cloud/spanner/Struct.java | 102 +++++-- .../java/com/google/cloud/spanner/Value.java | 256 ++++++++++++---- .../com/google/cloud/spanner/ValueBinder.java | 23 ++ .../cloud/spanner/GrpcResultSetTest.java | 106 +++---- .../google/cloud/spanner/ResultSetsTest.java | 94 +++++- .../com/google/cloud/spanner/StructTest.java | 168 +++++++++- .../google/cloud/spanner/ValueBinderTest.java | 56 +++- .../com/google/cloud/spanner/ValueTest.java | 207 ++++++++++--- .../google/cloud/spanner/it/ITQueryTest.java | 288 ++++++++++++++++-- 12 files changed, 1133 insertions(+), 238 deletions(-) diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java index 94e9cd77cb6c..3caae8df28a4 100644 --- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java +++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java @@ -331,6 +331,12 @@ public int getColumnIndex(String columnName) { return getType().getFieldIndex(columnName); } + protected void checkNonNull(int columnIndex, Object columnNameForError) { + if (isNull(columnIndex)) { + throw new NullPointerException("Column " + columnNameForError + " contains NULL value"); + } + } + private void checkNonNullOfType(int columnIndex, Type expectedType, Object columnNameForError) { Type actualType = getColumnType(columnIndex); checkState( @@ -353,9 +359,4 @@ private void checkNonNullArrayOfStruct(int columnIndex, Object columnNameForErro checkNonNull(columnIndex, columnNameForError); } - private void checkNonNull(int columnIndex, Object columnNameForError) { - if (isNull(columnIndex)) { - throw new NullPointerException("Column " + columnNameForError + " contains NULL value"); - } - } } diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java index 999b383cb984..29c3e52c6ac5 100644 --- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java +++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java @@ -19,6 +19,8 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Type.Code; +import com.google.cloud.spanner.Type.StructField; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.spanner.v1.ResultSetStats; @@ -49,6 +51,12 @@ private static class PrePopulatedResultSet implements ResultSet { Preconditions.checkNotNull(rows); Preconditions.checkNotNull(type); Preconditions.checkArgument(type.getCode() == Type.Code.STRUCT); + for (StructField field : type.getStructFields()) { + if (field.getType().getCode() == Code.STRUCT) { + throw new UnsupportedOperationException( + "STRUCT-typed columns are not supported inside ResultSets."); + } + } this.type = type; this.rows = rows instanceof List ? (List) rows : Lists.newArrayList(rows); for (Struct row : rows) { diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java index 9960c2218da5..850bf2898f57 100644 --- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -1973,14 +1973,22 @@ private Object writeReplace() { builder.set(fieldName).toDateArray((Iterable) value); break; case STRUCT: - builder.add(fieldName, fieldType.getArrayElementType().getStructFields(), (Iterable) value); + builder + .set(fieldName) + .toStructArray(fieldType.getArrayElementType(), (Iterable) value); break; default: throw new AssertionError( "Unhandled array type code: " + fieldType.getArrayElementType()); } break; - case STRUCT: // Not a legal top-level field type. + case STRUCT: + if (value == null) { + builder.set(fieldName).to(fieldType, null); + } else { + builder.set(fieldName).to((Struct) value); + } + break; default: throw new AssertionError("Unhandled type code: " + fieldType.getCode()); } @@ -1994,6 +2002,11 @@ private Object writeReplace() { this.rowData = rowData; } + @Override + public String toString() { + return this.rowData.toString(); + } + boolean consumeRow(Iterator iterator) { rowData.clear(); if (!iterator.hasNext()) { @@ -2040,12 +2053,28 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot checkType(fieldType, proto, KindCase.LIST_VALUE); ListValue listValue = proto.getListValue(); return decodeArrayValue(fieldType.getArrayElementType(), listValue); - case STRUCT: // Not a legal top-level field type. + case STRUCT: + checkType(fieldType, proto, KindCase.LIST_VALUE); + ListValue structValue = proto.getListValue(); + return decodeStructValue(fieldType, structValue); default: throw new AssertionError("Unhandled type code: " + fieldType.getCode()); } } + private static Struct decodeStructValue(Type structType, ListValue structValue) { + List fieldTypes = structType.getStructFields(); + checkArgument( + structValue.getValuesCount() == fieldTypes.size(), + "Size mismatch between type descriptor and actual values."); + List fields = new ArrayList<>(fieldTypes.size()); + List fieldValues = structValue.getValuesList(); + for (int i = 0; i < fieldTypes.size(); ++i) { + fields.add(decodeValue(fieldTypes.get(i).getType(), fieldValues.get(i))); + } + return new GrpcStruct(structType, fields); + } + private static Object decodeArrayValue(Type elementType, ListValue listValue) { switch (elementType.getCode()) { case BOOL: @@ -2117,16 +2146,8 @@ public String apply(com.google.protobuf.Value input) { if (value.getKindCase() == KindCase.NULL_VALUE) { list.add(null); } else { - List fieldTypes = elementType.getStructFields(); - List fields = new ArrayList<>(fieldTypes.size()); - ListValue structValues = value.getListValue(); - checkArgument( - structValues.getValuesCount() == fieldTypes.size(), - "Size mismatch between type descriptor and actual values."); - for (int i = 0; i < fieldTypes.size(); ++i) { - fields.add(decodeValue(fieldTypes.get(i).getType(), structValues.getValues(i))); - } - list.add(new GrpcStruct(elementType, fields)); + ListValue structValue = value.getListValue(); + list.add(decodeStructValue(elementType, structValue)); } } return list; @@ -2199,6 +2220,11 @@ protected Date getDateInternal(int columnIndex) { return (Date) rowData.get(columnIndex); } + @Override + protected Struct getStructInternal(int columnIndex) { + return (Struct) rowData.get(columnIndex); + } + @Override protected boolean[] getBooleanArrayInternal(int columnIndex) { @SuppressWarnings("unchecked") // We know ARRAY produces a List. diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java index ae7f1282002a..0e916f009e60 100644 --- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java +++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java @@ -22,41 +22,53 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Type.Code; +import com.google.cloud.spanner.Type.StructField; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Booleans; import com.google.common.primitives.Doubles; import com.google.common.primitives.Longs; - import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** - * Represents a value of {@link Type.Code#STRUCT}. Such values are a tuple of named and typed - * columns, where individual columns may be null. Individual rows from a read or query operation can - * be considered as structs; {@link ResultSet#getCurrentRowAsStruct()} allows an immutable struct to - * be created from the row that the result set is currently positioned over. + * Represents a non-{@code NULL} value of {@link Type.Code#STRUCT}. Such values are a tuple of named + * and typed columns, where individual columns may be null. Individual rows from a read or query + * operation can be considered as structs; {@link ResultSet#getCurrentRowAsStruct()} allows an + * immutable struct to be created from the row that the result set is currently positioned over. * *

{@code Struct} instances are immutable. + * + *

This class does not support representing typed {@code NULL} {@code Struct} values. + * + *

However, struct values inside SQL queries are always typed and can be externally + * supplied to a query only in the form of struct/array-of-struct query parameter values for which + * typed {@code NULL} struct values can be specified in the following ways: + * + *

1. As a standalone {@code NULL} struct value or as a nested struct field value, constructed + * using {@link ValueBinder#to(Type, Struct)} or {@link Value#struct(Type, Struct)}. + * + *

2. As as a null {@code Struct} reference representing a {@code NULL} struct typed element + * value inside an array/list of '{@code Struct}' references, that is used to construct an + * array-of-struct value using {@link Value#structArray(Type, Iterable)} or {@link + * ValueBinder#toStructArray(Type, Iterable)}. In this case, the type of the {@code NULL} struct + * value is assumed to be the same as the explicitly specified struct element type of the + * array/list. */ @Immutable public abstract class Struct extends AbstractStructReader implements Serializable { // Only implementations within the package are allowed. Struct() {} - /** - * Returns a builder for creating a {@code Struct} instance. This is intended mainly for test - * purposes: the library implementation is typically responsible for creating {@code Struct} - * instances. - */ + /** Returns a builder for creating a non-{@code NULL} {@code Struct} instance. */ public static Builder newBuilder() { return new Builder(); } - /** Builder for {@code Struct} instances. */ + /** Builder for constructing non-{@code NULL} {@code Struct} instances. */ public static final class Builder { private final List types = new ArrayList<>(); private final List values = new ArrayList<>(); @@ -76,32 +88,25 @@ Builder handle(Value value) { }; } - /** Returns a binder to set the value of a new field in the struct named {@code fieldName}. */ + /** + * Returns a binder to set the value of a new field in the struct named {@code fieldName}. + * + * @param fieldName name of the field to set. Can be empty or the same as an existing field name + * in the {@code STRUCT} + */ public ValueBinder set(String fieldName) { checkBindingInProgress(false); currentField = checkNotNull(fieldName); return binder; } - /** Adds a new field named {@code fieldName} with the given value. */ - public Builder add(String fieldName, Value value) { + /** Adds a new unnamed field {@code fieldName} with the given value. */ + public Builder add(Value value) { checkBindingInProgress(false); - addInternal(fieldName, value); + addInternal("", value); return this; } - /** - * Adds a new field of type {@code ARRAY>} named {@code fieldName} with the - * given element values. {@code elements} may be null, as may any member of {@code elements}. - * All non-null members of {@code elements} must be of type {@code STRUCT} - */ - public Builder add( - String fieldName, - Iterable fieldTypes, - @Nullable Iterable elements) { - return add(fieldName, Value.structArray(fieldTypes, elements)); - } - public Struct build() { checkBindingInProgress(false); return new ValueListStruct(types, values); @@ -121,12 +126,42 @@ private void checkBindingInProgress(boolean expectInProgress) { } } - /** Default implementation for test value structs produced by {@link Builder}. */ + /** + * TODO(user) : Consider moving these methods to the StructReader interface once STRUCT-typed + * columns are supported in {@link ResultSet}. + */ + + /* Public methods for accessing struct-typed fields */ + public Struct getStruct(int columnIndex) { + checkNonNullStruct(columnIndex, columnIndex); + return getStructInternal(columnIndex); + } + + public Struct getStruct(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullStruct(columnIndex, columnName); + return getStructInternal(columnIndex); + } + + /* Sub-classes must implement this method */ + protected abstract Struct getStructInternal(int columnIndex); + + private void checkNonNullStruct(int columnIndex, Object columnNameForError) { + Type actualType = getColumnType(columnIndex); + checkState( + actualType.getCode() == Code.STRUCT, + "Column %s is not of correct type: expected STRUCT<...> but was %s", + columnNameForError, + actualType); + checkNonNull(columnIndex, columnNameForError); + } + + /** Default implementation for value structs produced by {@link Builder}. */ private static class ValueListStruct extends Struct { private final Type type; private final List values; - private ValueListStruct(List types, List values) { + private ValueListStruct(Iterable types, Iterable values) { this.type = Type.struct(types); this.values = ImmutableList.copyOf(values); } @@ -166,6 +201,11 @@ protected Date getDateInternal(int columnIndex) { return values.get(columnIndex).getDate(); } + @Override + protected Struct getStructInternal(int columnIndex) { + return values.get(columnIndex).getStruct(); + } + @Override protected boolean[] getBooleanArrayInternal(int columnIndex) { return Booleans.toArray(getBooleanListInternal(columnIndex)); @@ -289,6 +329,8 @@ private Object getAsObject(int columnIndex) { return getTimestampInternal(columnIndex); case DATE: return getDateInternal(columnIndex); + case STRUCT: + return getStructInternal(columnIndex); case ARRAY: switch (type.getArrayElementType().getCode()) { case BOOL: diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 4a9f33216d60..63b9a3a13501 100644 --- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -19,6 +19,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Type.Code; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; @@ -66,7 +67,7 @@ public abstract class Value implements Serializable { * corresponding to the commit time of the transaction, which has no relation to this placeholder. * * @see - * Transaction Semantics + * Transaction Semantics */ public static final Timestamp COMMIT_TIMESTAMP = Timestamp.ofTimeMicroseconds(0L); @@ -152,7 +153,32 @@ public static Value date(@Nullable Date v) { return new DateImpl(v == null, v); } - // TODO(user): Implement struct()/structArray(). + /** 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."); + return new StructImpl(v); + } + + /** + * Returns a {@code STRUCT} value of {@code Type} type. + * + * @param type the type of the {@code STRUCT} value + * @param v the struct {@code STRUCT} value. This may be {@code null} to produce a value for which + * {@code isNull()} is {@code true}. If non-{@code null}, {@link Struct#getType()} must match + * type. + */ + public static Value struct(Type type, @Nullable Struct v) { + if (v == null) { + Preconditions.checkArgument( + type.getCode() == Code.STRUCT, + "Illegal call to create a NULL struct with a non-struct type."); + return new StructImpl(type); + } else { + Preconditions.checkArgument( + type.equals(v.getType()), "Mismatch between struct value and type."); + return new StructImpl(v); + } + } /** * Returns an {@code ARRAY} value. @@ -299,6 +325,33 @@ public static Value dateArray(@Nullable Iterable v) { return new DateArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); } + /** + * Returns an {@code ARRAY>} value. + * + * @param elementType + * @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 structArray(Type elementType, @Nullable Iterable v) { + if (v == null) { + Preconditions.checkArgument( + elementType.getCode() == Code.STRUCT, + "Illegal call to create a NULL array-of-struct with a non-struct element type."); + return new StructArrayImpl(elementType, null); + } + List values = immutableCopyOf(v); + for (Struct value : values) { + if (value != null) { + Preconditions.checkArgument( + value.getType().equals(elementType), + "Members of v must have type %s (found %s)", + elementType, + value.getType()); + } + } + return new StructArrayImpl(elementType, values); + } + private Value() {} /** Returns the type of this value. This will return a type even if {@code isNull()} is true. */ @@ -360,6 +413,13 @@ private Value() {} */ public abstract Date getDate(); + /** + * Returns the value of a {@code STRUCT}-typed instance. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public abstract Struct getStruct(); + /** * 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. @@ -416,6 +476,14 @@ private Value() {} */ 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 + */ + abstract List getStructArray(); + @Override public String toString() { StringBuilder b = new StringBuilder(); @@ -425,43 +493,6 @@ public String toString() { // END OF PUBLIC API. - /** - * Returns an {@code ARRAY>} value. - * - *

This method is intentionally not in the public API for Value: {@code ARRAY>} - * values are not accepted by the backend. - * - * @param fieldTypes the types of the fields in the array elements. All non-null elements must - * conform to this type. - * @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}. - */ - static Value structArray(Iterable fieldTypes, @Nullable Iterable v) { - Type elementType = Type.struct(fieldTypes); - if (v == null) { - return new StructArrayImpl(elementType, null); - } - List values = immutableCopyOf(v); - for (Struct value : values) { - if (value != null && !value.getType().equals(elementType)) { - throw new IllegalArgumentException( - "Members of v must have type " + elementType + " (found " + value.getType() + ")"); - } - } - return new StructArrayImpl(elementType, values); - } - - /** - * 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. - * - *

This method is intentionally not in the public API for Value: {@code ARRAY>} - * values are not accepted by the backend. - * - * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type - */ - abstract List getStructArray(); - abstract void toString(StringBuilder b); abstract com.google.protobuf.Value toProto(); @@ -638,6 +669,15 @@ public Date getDate() { throw defaultGetter(Type.date()); } + @Override + public Struct getStruct() { + if (getType().getCode() != Type.Code.STRUCT) { + throw new IllegalStateException( + "Illegal call to getter of incorrect type. Expected: STRUCT<...> actual: " + getType()); + } + throw new AssertionError("Should have been overridden"); + } + @Override public List getBoolArray() { throw defaultGetter(Type.array(Type.bool())); @@ -674,7 +714,7 @@ public List getDateArray() { } @Override - List getStructArray() { + public List getStructArray() { if (getType().getCode() != Type.Code.ARRAY || getType().getArrayElementType().getCode() != Type.Code.STRUCT) { throw new IllegalStateException( @@ -941,9 +981,7 @@ public ByteArray getBytes() { @Override com.google.protobuf.Value valueToProto() { - return com.google.protobuf.Value.newBuilder() - .setStringValue(value.toBase64()) - .build(); + return com.google.protobuf.Value.newBuilder().setStringValue(value.toBase64()).build(); } @Override @@ -1343,43 +1381,155 @@ void appendElement(StringBuilder b, Date element) { } } - private static class StructArrayImpl extends AbstractValue { - private static final Joiner joiner = Joiner.on(LIST_SEPERATOR).useForNull(NULL_STRING); + private static class StructImpl extends AbstractObjectValue { + + // Constructor for non-NULL struct values. + private StructImpl(Struct value) { + super(false, value.getType(), value); + } - private final List values; + // Constructor for NULL struct values. + private StructImpl(Type structType) { + super(true, structType, null); + } + + @Override + public Struct getStruct() { + checkType(getType()); + checkNotNull(); + return value; + } + + @Override + void valueToString(StringBuilder b) { + b.append(value); + } + + @Override + int valueHash() { + return value.hashCode(); + } + + @Override + boolean valueEquals(Value v) { + return ((StructImpl) v).value.equals(value); + } + + private Value getValue(int fieldIndex) { + Type fieldType = value.getColumnType(fieldIndex); + switch (fieldType.getCode()) { + case BOOL: + return Value.bool(value.getBoolean(fieldIndex)); + case INT64: + return Value.int64(value.getLong(fieldIndex)); + case STRING: + return Value.string(value.getString(fieldIndex)); + case BYTES: + return Value.bytes(value.getBytes(fieldIndex)); + case FLOAT64: + return Value.float64(value.getDouble(fieldIndex)); + case DATE: + return Value.date(value.getDate(fieldIndex)); + case TIMESTAMP: + return Value.timestamp(value.getTimestamp(fieldIndex)); + case STRUCT: + return Value.struct(value.getStruct(fieldIndex)); + case ARRAY: + { + Type elementType = fieldType.getArrayElementType(); + switch (elementType.getCode()) { + case BOOL: + return Value.boolArray(value.getBooleanArray(fieldIndex)); + case INT64: + return Value.int64Array(value.getLongArray(fieldIndex)); + case STRING: + return Value.stringArray(value.getStringList(fieldIndex)); + case BYTES: + return Value.bytesArray(value.getBytesList(fieldIndex)); + case FLOAT64: + return Value.float64Array(value.getDoubleArray(fieldIndex)); + case DATE: + return Value.dateArray(value.getDateList(fieldIndex)); + case TIMESTAMP: + return Value.timestampArray(value.getTimestampList(fieldIndex)); + case STRUCT: + return Value.structArray(elementType, value.getStructList(fieldIndex)); + case ARRAY: + throw new UnsupportedOperationException( + "ARRAY field types are not " + + "supported inside STRUCT-typed values."); + default: + throw new IllegalArgumentException( + "Unrecognized array element type : " + fieldType); + } + } + default: + throw new IllegalArgumentException("Unrecognized field type : " + fieldType); + } + } + + @Override + com.google.protobuf.Value valueToProto() { + checkNotNull(); + ListValue.Builder struct = ListValue.newBuilder(); + for (int fieldIndex = 0; fieldIndex < value.getColumnCount(); ++fieldIndex) { + if (value.isNull(fieldIndex)) { + struct.addValues(NULL_PROTO); + } else { + struct.addValues(getValue(fieldIndex).toProto()); + } + } + return com.google.protobuf.Value.newBuilder().setListValue(struct).build(); + } + } + + private static class StructArrayImpl extends AbstractArrayValue { + private static final Joiner joiner = Joiner.on(LIST_SEPERATOR).useForNull(NULL_STRING); private StructArrayImpl(Type elementType, @Nullable List values) { - super(values == null, Type.array(elementType)); - this.values = values; + super(values == null, elementType, values); } @Override public List getStructArray() { checkType(getType()); checkNotNull(); - return values; + return value; } @Override com.google.protobuf.Value valueToProto() { - throw new UnsupportedOperationException("ARRAY> cannot be serialized to proto"); + ListValue.Builder list = ListValue.newBuilder(); + for (Struct element : value) { + if (element == null) { + list.addValues(NULL_PROTO); + } else { + list.addValues(Value.struct(element).toProto()); + } + } + return com.google.protobuf.Value.newBuilder().setListValue(list).build(); + } + + @Override + void appendElement(StringBuilder b, Struct element) { + b.append(element); } @Override void valueToString(StringBuilder b) { b.append(LIST_OPEN); - joiner.appendTo(b, values); + joiner.appendTo(b, value); b.append(LIST_CLOSE); } @Override boolean valueEquals(Value v) { - return ((StructArrayImpl) v).values.equals(values); + return ((StructArrayImpl) v).value.equals(value); } @Override int valueHash() { - return values.hashCode(); + return value.hashCode(); } } } diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java index cdf29ed9f685..5b663211f51d 100644 --- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java +++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java @@ -49,6 +49,11 @@ public abstract class ValueBinder { */ abstract R handle(Value value); + /** Binds a {@link Value} */ + public R to(Value value) { + return handle(value); + } + /** Binds to {@code Value.bool(value)} */ public R to(boolean value) { return handle(Value.bool(value)); @@ -99,6 +104,19 @@ public R to(@Nullable Date value) { return handle(Value.date(value)); } + /** Binds a non-{@code NULL} struct value to {@code Value.struct(value)} */ + public R to(Struct value) { + return handle(Value.struct(value)); + } + + /** + * Binds a nullable {@code Struct} reference with given {@code Type} to {@code + * Value.struct(type,value} + */ + public R to(Type type, @Nullable Struct value) { + return handle(Value.struct(type, value)); + } + /** Binds to {@code Value.boolArray(values)} */ public R toBoolArray(@Nullable boolean[] values) { return handle(Value.boolArray(values)); @@ -163,4 +181,9 @@ public R toTimestampArray(@Nullable Iterable values) { public R toDateArray(@Nullable Iterable values) { return handle(Value.dateArray(values)); } + + /** Binds to {@code Value.structArray(fieldTypes, values)} */ + public R toStructArray(Type elementType, @Nullable Iterable values) { + return handle(Value.structArray(elementType, values)); + } } diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index b73f8e153f47..f84e564b7b92 100644 --- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -18,7 +18,6 @@ import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.common.testing.SerializableTester.reserialize; -import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; import com.google.cloud.ByteArray; @@ -29,7 +28,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.protobuf.ByteString; -import com.google.protobuf.NullValue; import com.google.spanner.v1.ExecuteSqlRequest.QueryMode; import com.google.spanner.v1.PartialResultSet; import com.google.spanner.v1.QueryPlan; @@ -398,7 +396,8 @@ public void multiResponseChunkingStructArray() { Type.StructField.of("a", Type.string()), Type.StructField.of("b", Type.int64())); List beforeValue = Arrays.asList(s("before", 10)); List chunkedValue = - Arrays.asList(s("a", 1), s("b", 2), s("c", 3), null, s("e", 5), null, s("g", 7), s("h", 8)); + Arrays.asList( + s("a", 1), s("b", 2), s("c", 3), null, s(null, 5), null, s("g", 7), s("h", 8)); List afterValue = Arrays.asList(s("after", 20)); doArrayTest( beforeValue, @@ -408,7 +407,7 @@ public void multiResponseChunkingStructArray() { new Function, com.google.protobuf.Value>() { @Override public com.google.protobuf.Value apply(List input) { - return toProto(input); + return Value.structArray(elementType, input).toProto(); } }, new Function>() { @@ -419,29 +418,6 @@ public List apply(StructReader input) { }); } - private com.google.protobuf.Value toProto(List input) { - // Value itself doesn't support serialization to proto, as struct isn't ever sent to the - // backend. Implement simple serialization ourselves. - com.google.protobuf.Value.Builder proto = com.google.protobuf.Value.newBuilder(); - for (Struct element : input) { - com.google.protobuf.Value.Builder elementProto = - proto.getListValueBuilder().addValuesBuilder(); - if (element == null) { - elementProto.setNullValue(NullValue.NULL_VALUE); - } else { - elementProto - .getListValueBuilder() - .addValuesBuilder() - .setStringValue(element.getString(0)); - elementProto - .getListValueBuilder() - .addValuesBuilder() - .setStringValue(Long.toString(element.getLong(1))); - } - } - return proto.build(); - } - @Test public void profileResultInFinalResultSet() { Map statsMap = @@ -600,7 +576,13 @@ private static ResultSetMetadata makeMetadata(Type rowType) { @Test public void serialization() throws Exception { - verifySerialization(Value.string("a"), + Type structType = + Type.struct( + Arrays.asList( + Type.StructField.of("a", Type.string()), Type.StructField.of("b", Type.int64()))); + + verifySerialization( + Value.string("a"), Value.string(null), Value.bool(true), Value.bool(null), @@ -616,48 +598,59 @@ public void serialization() throws Exception { Value.date(null), Value.stringArray(ImmutableList.of("one", "two")), Value.stringArray(null), - Value.boolArray(new boolean[]{true, false}), + Value.boolArray(new boolean[] {true, false}), Value.boolArray((boolean[]) null), - Value.int64Array(new long[]{1, 2, 3}), + Value.int64Array(new long[] {1, 2, 3}), Value.int64Array((long[]) null), Value.timestampArray(ImmutableList.of(Timestamp.MAX_VALUE, Timestamp.MAX_VALUE)), Value.timestampArray(null), - Value.dateArray(ImmutableList.of( - Date.fromYearMonthDay(2017, 4, 17), Date.fromYearMonthDay(2017, 5, 18))), - Value.dateArray(null) - ); + Value.dateArray( + ImmutableList.of( + Date.fromYearMonthDay(2017, 4, 17), Date.fromYearMonthDay(2017, 5, 18))), + Value.dateArray(null), + Value.struct(s(null, 30)), + Value.struct(structType, null), + Value.structArray(structType, Arrays.asList(s("def", 10), null)), + Value.structArray(structType, Arrays.asList((Struct) null)), + Value.structArray(structType, null)); } @Test public void nestedStructSerialization() throws Exception { - Type.StructField[] structFields = { - Type.StructField.of("a", Type.string()), - Type.StructField.of("b", Type.int64()) - }; + Type structType = + Type.struct( + Arrays.asList( + Type.StructField.of("a", Type.string()), Type.StructField.of("b", Type.int64()))); Struct nestedStruct = s("1", 2L); - Value struct = Value.structArray(Arrays.asList(structFields), Arrays.asList(nestedStruct)); - verifySerialization(new Function() { - - @Override @Nullable public com.google.protobuf.Value apply(@Nullable Value input) { - return toProto(input.getStructArray()); - } - }, struct); + Value struct = Value.structArray(structType, Arrays.asList(nestedStruct)); + verifySerialization( + new Function() { + @Override + @Nullable + public com.google.protobuf.Value apply(@Nullable Value input) { + return input.toProto(); + } + }, + struct); } private void verifySerialization(Value... values) { - verifySerialization(new Function() { + verifySerialization( + new Function() { - @Override - @Nullable - public com.google.protobuf.Value apply(@Nullable Value input) { - return input.toProto(); - } - }, values); + @Override + @Nullable + public com.google.protobuf.Value apply(@Nullable Value input) { + return input.toProto(); + } + }, + values); } - private void verifySerialization( Function - protoFn, Value... values) { + + private void verifySerialization( + Function protoFn, Value... values) { resultSet = new SpannerImpl.GrpcResultSet(stream, new NoOpListener(), QueryMode.NORMAL); PartialResultSet.Builder builder = PartialResultSet.newBuilder(); List types = new ArrayList<>(); @@ -665,14 +658,11 @@ private void verifySerialization( Function types.add(Type.StructField.of("f", value.getType())); builder.addValues(protoFn.apply(value)); } - consumer.onPartialResultSet( - builder.setMetadata(makeMetadata(Type.struct(types))) - .build()); + consumer.onPartialResultSet(builder.setMetadata(makeMetadata(Type.struct(types))).build()); consumer.onCompleted(); assertThat(resultSet.next()).isTrue(); Struct row = resultSet.getCurrentRowAsStruct(); Struct copy = reserialize(row); assertThat(row).isEqualTo(copy); } - } diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java index 911b9769eb18..361ade38e09c 100644 --- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java +++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import java.util.Arrays; +import java.util.List; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -39,9 +40,23 @@ public void resultSetIteration() { Type.StructField.of("f2", Type.int64()), Type.StructField.of("f3", Type.bool())); Struct struct1 = - Struct.newBuilder().set("f1").to("x").set("f2").to(2).add("f3", Value.bool(true)).build(); + Struct.newBuilder() + .set("f1") + .to("x") + .set("f2") + .to(2) + .set("f3") + .to(Value.bool(true)) + .build(); Struct struct2 = - Struct.newBuilder().set("f1").to("y").set("f2").to(3).add("f3", Value.bool(null)).build(); + Struct.newBuilder() + .set("f1") + .to("y") + .set("f2") + .to(3) + .set("f3") + .to(Value.bool(null)) + .build(); ResultSet rs = ResultSets.forRows(type, Arrays.asList(struct1, struct2)); assertThat(rs.next()).isTrue(); @@ -68,6 +83,81 @@ public void resultSetIteration() { assertThat(rs.next()).isFalse(); } + @Test + public void resultSetIterationWithStructColumns() { + Type nestedStructType = Type.struct(Type.StructField.of("g1", Type.string())); + Type type = + Type.struct( + Type.StructField.of("f1", nestedStructType), Type.StructField.of("f2", Type.int64())); + + Struct value1 = Struct.newBuilder().set("g1").to("abc").build(); + + Struct struct1 = Struct.newBuilder().set("f1").to(value1).set("f2").to((Long) null).build(); + + expected.expect(UnsupportedOperationException.class); + expected.expectMessage("STRUCT-typed columns are not supported inside ResultSets."); + ResultSets.forRows(type, Arrays.asList(struct1)); + } + + @Test + public void resultSetIterationWithArrayStructColumns() { + Type nestedStructType = Type.struct(Type.StructField.of("g1", Type.string())); + Type type = + Type.struct( + Type.StructField.of("f1", Type.array(nestedStructType)), + Type.StructField.of("f2", Type.int64())); + + Struct value1 = Struct.newBuilder().set("g1").to("abc").build(); + + List arrayValue = Arrays.asList(value1, null); + + Struct struct1 = + Struct.newBuilder() + .set("f1") + .toStructArray(nestedStructType, arrayValue) + .set("f2") + .to((Long) null) + .build(); + + Struct struct2 = + Struct.newBuilder() + .set("f1") + .toStructArray(nestedStructType, null) + .set("f2") + .to(20) + .build(); + + ResultSet rs = ResultSets.forRows(type, Arrays.asList(struct1, struct2)); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getType()).isEqualTo(type); + assertThat(rs.getColumnCount()).isEqualTo(2); + + assertThat(rs.getColumnIndex("f1")).isEqualTo(0); + assertThat(rs.getColumnType("f1")).isEqualTo(Type.array(nestedStructType)); + assertThat(rs.getColumnType(0)).isEqualTo(Type.array(nestedStructType)); + + assertThat(rs.getColumnIndex("f2")).isEqualTo(1); + assertThat(rs.getColumnType("f2")).isEqualTo(Type.int64()); + assertThat(rs.getColumnType(1)).isEqualTo(Type.int64()); + + assertThat(rs.getCurrentRowAsStruct()).isEqualTo(struct1); + + assertThat(rs.getStructList(0)).isEqualTo(arrayValue); + assertThat(rs.getStructList("f1")).isEqualTo(arrayValue); + assertThat(rs.isNull(1)).isTrue(); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getCurrentRowAsStruct()).isEqualTo(struct2); + + assertThat(rs.isNull(0)).isTrue(); + assertThat(rs.isNull("f1")).isTrue(); + assertThat(rs.getLong(1)).isEqualTo(20); + assertThat(rs.getLong("f2")).isEqualTo(20); + + assertThat(rs.next()).isFalse(); + } + @Test public void closeResultSet() { ResultSet rs = diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StructTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StructTest.java index 34beea75135c..503723df0bf3 100644 --- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StructTest.java +++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StructTest.java @@ -16,23 +16,37 @@ package com.google.cloud.spanner; +import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; import com.google.common.testing.EqualsTester; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.Struct}. */ @RunWith(JUnit4.class) public class StructTest { + + @Rule public ExpectedException expectedException = ExpectedException.none(); + @Test public void builder() { // These tests are basic: AbstractStructReaderTypesTest already covers all type getters. Struct struct = - Struct.newBuilder().set("f1").to("x").set("f2").to(2).add("f3", Value.bool(null)).build(); + Struct.newBuilder() + .set("f1") + .to("x") + .set("f2") + .to(2) + .set("f3") + .to(Value.bool(null)) + .build(); assertThat(struct.getType()) .isEqualTo( Type.struct( @@ -49,7 +63,7 @@ public void builder() { @Test public void duplicateFields() { // Duplicate fields are allowed - some SQL queries produce this type of value. - Struct struct = Struct.newBuilder().set("").to("x").add("", Value.int64(2)).build(); + Struct struct = Struct.newBuilder().set("").to("x").set("").to(Value.int64(2)).build(); assertThat(struct.getType()) .isEqualTo( Type.struct( @@ -60,22 +74,67 @@ public void duplicateFields() { assertThat(struct.getLong(1)).isEqualTo(2); } + @Test + public void unnamedFields() { + Struct struct = Struct.newBuilder().add(Value.int64(2)).add(Value.int64(3)).build(); + assertThat(struct.getType()) + .isEqualTo( + Type.struct( + Type.StructField.of("", Type.int64()), Type.StructField.of("", Type.int64()))); + assertThat(struct.getLong(0)).isEqualTo(2); + assertThat(struct.getLong(1)).isEqualTo(3); + } + + @Test + public void structWithStructField() { + Struct nestedStruct = Struct.newBuilder().set("f2f1").to(10).build(); + Struct struct = + Struct.newBuilder() + .set("f1") + .to("v1") + .set("f2") + .to(nestedStruct) + .set("f3") + .to(nestedStruct.getType(), null) + .build(); + assertThat(struct.getType()) + .isEqualTo( + Type.struct( + Type.StructField.of("f1", Type.string()), + Type.StructField.of("f2", Type.struct(Type.StructField.of("f2f1", Type.int64()))), + Type.StructField.of("f3", Type.struct(Type.StructField.of("f2f1", Type.int64()))))); + assertThat(struct.isNull(0)).isFalse(); + assertThat(struct.isNull(1)).isFalse(); + assertThat(struct.isNull(2)).isTrue(); + assertThat(struct.getString(0)).isEqualTo("v1"); + assertThat(struct.getString("f1")).isEqualTo("v1"); + assertThat(struct.getStruct(1)).isEqualTo(nestedStruct); + assertThat(struct.getStruct("f2")).isEqualTo(nestedStruct); + } + @Test public void structWithArrayOfStructField() { - List fieldTypes = - Arrays.asList( - Type.StructField.of("ff1", Type.string()), Type.StructField.of("ff2", Type.int64())); + Type elementType = + Type.struct( + Arrays.asList( + Type.StructField.of("ff1", Type.string()), + Type.StructField.of("ff2", Type.int64()))); List arrayElements = Arrays.asList( Struct.newBuilder().set("ff1").to("v1").set("ff2").to(1).build(), Struct.newBuilder().set("ff1").to("v1").set("ff2").to(1).build()); Struct struct = - Struct.newBuilder().set("f1").to("x").add("f2", fieldTypes, arrayElements).build(); + Struct.newBuilder() + .set("f1") + .to("x") + .set("f2") + .toStructArray(elementType, arrayElements) + .build(); assertThat(struct.getType()) .isEqualTo( Type.struct( Type.StructField.of("f1", Type.string()), - Type.StructField.of("f2", Type.array(Type.struct(fieldTypes))))); + Type.StructField.of("f2", Type.array(elementType)))); assertThat(struct.isNull(0)).isFalse(); assertThat(struct.isNull(1)).isFalse(); assertThat(struct.getString(0)).isEqualTo("x"); @@ -85,15 +144,106 @@ public void structWithArrayOfStructField() { @Test public void equalsAndHashCode() { EqualsTester tester = new EqualsTester(); - tester.addEqualityGroup(Struct.newBuilder().build(), Struct.newBuilder().build()); tester.addEqualityGroup( Struct.newBuilder().set("x").to(1).build(), - Struct.newBuilder().add("x", Value.int64(1)).build()); + Struct.newBuilder().set("x").to(Value.int64(1)).build()); tester.addEqualityGroup(Struct.newBuilder().set("x").to((Long) null).build()); tester.addEqualityGroup(Struct.newBuilder().set("x").to((String) null).build()); tester.addEqualityGroup(Struct.newBuilder().set("x").to(1).set("y").to(2).build()); tester.addEqualityGroup(Struct.newBuilder().set("x").to(1).set("y").to("2").build()); tester.addEqualityGroup(Struct.newBuilder().set("y").to(2).set("x").to(1).build()); + + // Equality comparison with empty structs. + tester.addEqualityGroup(Struct.newBuilder().build(), Struct.newBuilder().build()); + + // Equality comparison with structs with struct-typed fields. + Struct nestedStruct = Struct.newBuilder().set("f").to(1).build(); + Struct structFieldStruct1 = + Struct.newBuilder() + .set("sf") + .to(nestedStruct) + .set("nullsf") + .to(nestedStruct.getType(), null) + .build(); + Struct structFieldStruct2 = + Struct.newBuilder() + .set("sf") + .to(Value.struct(nestedStruct)) + .set("nullsf") + .to(Value.struct(nestedStruct.getType(), null)) + .build(); + tester.addEqualityGroup(structFieldStruct1, structFieldStruct2); + + // Equality comparison with array-of-struct typed fields. + Struct arrayStructFieldStruct1 = + Struct.newBuilder() + .set("arraysf") + .toStructArray(nestedStruct.getType(), Arrays.asList(null, nestedStruct)) + .set("nullarraysf") + .toStructArray(nestedStruct.getType(), null) + .build(); + Struct arrayStructFieldStruct2 = + Struct.newBuilder() + .set("arraysf") + .to(Value.structArray(nestedStruct.getType(), Arrays.asList(null, nestedStruct))) + .set("nullarraysf") + .to(Value.structArray(nestedStruct.getType(), null)) + .build(); + tester.addEqualityGroup(arrayStructFieldStruct1, arrayStructFieldStruct2); + + // Equality comparison of structs with duplicate fields. + Struct duplicateFieldStruct1 = + Struct.newBuilder().set("f1").to(3).set("f1").to(nestedStruct).build(); + Struct duplicateFieldStruct2 = + Struct.newBuilder() + .set("f1") + .to(Value.int64(3)) + .set("f1") + .to(Value.struct(nestedStruct)) + .build(); + tester.addEqualityGroup(duplicateFieldStruct1, duplicateFieldStruct2); + + // Equality comparison of structs with unnamed fields. + Struct emptyFieldStruct1 = Struct.newBuilder().set("").to(3).set("").to(nestedStruct).build(); + Struct emptyFieldStruct2 = + Struct.newBuilder().add(Value.int64(3)).add(Value.struct(nestedStruct)).build(); + tester.addEqualityGroup(emptyFieldStruct1, emptyFieldStruct2); + tester.testEquals(); } + + @Test + public void serialization() { + // Simple struct. + Struct simpleStruct = Struct.newBuilder().set("x").to(1).build(); + reserializeAndAssert(simpleStruct); + simpleStruct = Struct.newBuilder().set("x").to((Long) null).build(); + reserializeAndAssert(simpleStruct); + + // Struct with struct field. + Struct structFieldStruct = Struct.newBuilder().set("f1").to(simpleStruct).build(); + reserializeAndAssert(structFieldStruct); + structFieldStruct = Struct.newBuilder().set("f1").to(simpleStruct.getType(), null).build(); + reserializeAndAssert(structFieldStruct); + + // Struct with array-of-struct field + Struct arrayStructFieldStruct = + Struct.newBuilder() + .set("f1") + .toStructArray(simpleStruct.getType(), new ArrayList()) + .build(); + reserializeAndAssert(arrayStructFieldStruct); + arrayStructFieldStruct = + Struct.newBuilder().set("f1").toStructArray(simpleStruct.getType(), null).build(); + reserializeAndAssert(arrayStructFieldStruct); + + // Struct with no field. + reserializeAndAssert(Struct.newBuilder().build()); + + // Struct with duplicate field names. + reserializeAndAssert(Struct.newBuilder().set("f1").to(3).set("f1").to(30).build()); + + // Struct with unnamed fields. + reserializeAndAssert(Struct.newBuilder().add(Value.int64(3)).add(Value.int64(30)).build()); + } } diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java index fcb0c3803d81..918209d5773b 100644 --- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java +++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java @@ -56,18 +56,72 @@ public void reflection() throws InvocationTargetException, IllegalAccessExceptio Method binderMethod = findBinderMethod(method); assertThat(binderMethod).named("Binder for " + method.toString()).isNotNull(); - if (binderMethod.getParameterTypes().length == 1) { + if (method.getName().toLowerCase().contains("struct")) { + // Struct / Array-of-struct binding methods. + Struct struct = Struct.newBuilder().set("f1").to("abc").build(); + Type structType = struct.getType(); + + if (binderMethod.getName().equals("toStructArray")) { + // Array of structs. + assertThat(binderMethod.getParameterTypes()).hasLength(2); + + Value expected = (Value) method.invoke(Value.class, structType, Arrays.asList(struct)); + assertThat(binderMethod.invoke(binder, structType, Arrays.asList(struct))) + .isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expected); + + // Test ValueBinder.to(value) + assertThat(binder.to(expected)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expected); + + // Null Array-of-structs + Value expectedNull = (Value) method.invoke(Value.class, structType, null); + assertThat(binderMethod.invoke(binder, structType, null)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expectedNull); + + assertThat(binder.to(expectedNull)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expectedNull); + } else if (binderMethod.getParameterTypes().length == 2) { + // NULL struct. + assertThat(binderMethod.getParameterTypes()[0]).isEqualTo(Type.class); + assertThat(binderMethod.getParameterTypes()[1]).isEqualTo(Struct.class); + + Value expectedNull = (Value) method.invoke(Value.class, structType, null); + assertThat(binderMethod.invoke(binder, structType, null)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expectedNull); + + assertThat(binder.to(expectedNull)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expectedNull); + } else { + // non-NULL struct. + assertThat(binderMethod.getParameterTypes()).hasLength(1); + assertThat(binderMethod.getParameterTypes()[0]).isEqualTo(Struct.class); + + Value expected = (Value) method.invoke(Value.class, struct); + assertThat(binderMethod.invoke(binder, struct)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expected); + + assertThat(binder.to(expected)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expected); + } + } else if (binderMethod.getParameterTypes().length == 1) { // Test unary null. if (!binderMethod.getParameterTypes()[0].isPrimitive()) { Value expected = (Value) method.invoke(Value.class, (Object) null); assertThat(binderMethod.invoke(binder, (Object) null)).isEqualTo(lastReturnValue); assertThat(lastValue).isEqualTo(expected); + + assertThat(binder.to(expected)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expected); } // Test unary non-null. Object defaultObject = DefaultValues.getDefault(method.getGenericParameterTypes()[0]); Value expected = (Value) method.invoke(Value.class, defaultObject); assertThat(binderMethod.invoke(binder, defaultObject)).isEqualTo(lastReturnValue); assertThat(lastValue).isEqualTo(expected); + + assertThat(binder.to(expected)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expected); } else { // Array slice method: depends on DefaultValues returning arrays of length 2. assertThat(binderMethod.getParameterTypes().length).isEqualTo(3); diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index d73cfc8df592..d5eee9388205 100644 --- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -22,10 +22,10 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Type.StructField; import com.google.common.collect.ForwardingList; import com.google.common.collect.Lists; import com.google.common.testing.EqualsTester; - import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; @@ -526,9 +526,7 @@ public void bytesArray() { ByteArray c = newByteArray("c"); Value v = Value.bytesArray(Arrays.asList(a, null, c)); assertThat(v.isNull()).isFalse(); - assertThat(v.getBytesArray()) - .containsExactly(a, null, c) - .inOrder(); + assertThat(v.getBytesArray()).containsExactly(a, null, c).inOrder(); assertThat(v.toString()).isEqualTo(String.format("[%s,NULL,%s]", a, c)); } @@ -602,22 +600,76 @@ public void dateArrayNull() { } @Test - public void structArray() { + public void struct() { + Struct struct = Struct.newBuilder().set("f1").to("v1").set("f2").to(30).build(); + Value v1 = Value.struct(struct); + assertThat(v1.getType()).isEqualTo(struct.getType()); + assertThat(v1.isNull()).isFalse(); + assertThat(v1.getStruct()).isEqualTo(struct); + assertThat(v1.toString()).isEqualTo("[v1, 30]"); + + Value v2 = Value.struct(struct.getType(), struct); + assertThat(v2).isEqualTo(v1); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Mismatch between struct value and type."); + Value.struct(Type.struct(Arrays.asList(StructField.of("f3", Type.string()))), struct); + } + + @Test + public void nullStruct() { List fieldTypes = Arrays.asList( - Type.StructField.of("ff1", Type.string()), Type.StructField.of("ff2", Type.int64())); + Type.StructField.of("f1", Type.string()), Type.StructField.of("f2", Type.int64())); + + Value v = Value.struct(Type.struct(fieldTypes), null); + assertThat(v.getType().getStructFields()).isEqualTo(fieldTypes); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Illegal call to create a NULL struct value."); + Value.struct(null); + } + + @Test + public void nullStructGetter() { + List fieldTypes = + Arrays.asList( + Type.StructField.of("f1", Type.string()), Type.StructField.of("f2", Type.int64())); + + Value v = Value.struct(Type.struct(fieldTypes), null); + assertThat(v.isNull()).isTrue(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Illegal call to getter of null value."); + v.getStruct(); + } + + @Test + public void structArrayField() { + Type elementType = + Type.struct( + Arrays.asList( + Type.StructField.of("ff1", Type.string()), + Type.StructField.of("ff2", Type.int64()))); List arrayElements = Arrays.asList( Struct.newBuilder().set("ff1").to("v1").set("ff2").to(1).build(), null, Struct.newBuilder().set("ff1").to("v3").set("ff2").to(3).build()); Struct struct = - Struct.newBuilder().set("f1").to("x").add("f2", fieldTypes, arrayElements).build(); + Struct.newBuilder() + .set("f1") + .to("x") + .set("f2") + .toStructArray(elementType, arrayElements) + .build(); assertThat(struct.getType()) .isEqualTo( Type.struct( Type.StructField.of("f1", Type.string()), - Type.StructField.of("f2", Type.array(Type.struct(fieldTypes))))); + Type.StructField.of("f2", Type.array(elementType)))); assertThat(struct.isNull(0)).isFalse(); assertThat(struct.isNull(1)).isFalse(); assertThat(struct.getString(0)).isEqualTo("x"); @@ -625,25 +677,67 @@ public void structArray() { } @Test - public void structArrayNull() { - List fieldTypes = - Arrays.asList( - Type.StructField.of("ff1", Type.string()), Type.StructField.of("ff2", Type.int64())); - Struct struct = Struct.newBuilder().set("f1").to("x").add("f2", fieldTypes, null).build(); + public void structArrayFieldNull() { + Type elementType = + Type.struct( + Arrays.asList( + Type.StructField.of("ff1", Type.string()), + Type.StructField.of("ff2", Type.int64()))); + Struct struct = + Struct.newBuilder().set("f1").to("x").set("f2").toStructArray(elementType, null).build(); assertThat(struct.getType()) .isEqualTo( Type.struct( Type.StructField.of("f1", Type.string()), - Type.StructField.of("f2", Type.array(Type.struct(fieldTypes))))); + Type.StructField.of("f2", Type.array(elementType)))); assertThat(struct.isNull(0)).isFalse(); assertThat(struct.isNull(1)).isTrue(); } @Test - public void structArrayInvalidType() { - List fieldTypes = + public void structArray() { + Type elementType = + Type.struct( + Arrays.asList( + Type.StructField.of("ff1", Type.string()), + Type.StructField.of("ff2", Type.int64()))); + List arrayElements = Arrays.asList( - Type.StructField.of("ff1", Type.string()), Type.StructField.of("ff2", Type.int64())); + Struct.newBuilder().set("ff1").to("v1").set("ff2").to(1).build(), + null, + null, + Struct.newBuilder().set("ff1").to("v3").set("ff2").to(3).build()); + Value v = Value.structArray(elementType, arrayElements); + assertThat(v.isNull()).isFalse(); + assertThat(v.getType().getArrayElementType()).isEqualTo(elementType); + assertThat(v.getStructArray()).isEqualTo(arrayElements); + assertThat(v.toString()).isEqualTo("[[v1, 1],NULL,NULL,[v3, 3]]"); + } + + @Test + public void structArrayNull() { + Type elementType = + Type.struct( + Arrays.asList( + Type.StructField.of("ff1", Type.string()), + Type.StructField.of("ff2", Type.int64()))); + Value v = Value.structArray(elementType, null); + assertThat(v.isNull()).isTrue(); + assertThat(v.getType().getArrayElementType()).isEqualTo(elementType); + assertThat(v.toString()).isEqualTo(NULL_STRING); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Illegal call to getter of null value"); + v.getStructArray(); + } + + @Test + public void structArrayInvalidType() { + Type elementType = + Type.struct( + Arrays.asList( + Type.StructField.of("ff1", Type.string()), + Type.StructField.of("ff2", Type.int64()))); // Second element has INT64 first field, not STRING. List arrayElements = Arrays.asList( @@ -652,7 +746,7 @@ public void structArrayInvalidType() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("must have type STRUCT"); - Struct.newBuilder().add("f1", fieldTypes, arrayElements); + Value.structArray(elementType, arrayElements); } @Test @@ -680,8 +774,8 @@ public void testEqualsHashCode() { tester.addEqualityGroup(Value.bytes(null)); tester.addEqualityGroup(Value.timestamp(null), Value.timestamp(null)); - tester.addEqualityGroup(Value.timestamp(Value.COMMIT_TIMESTAMP), - Value.timestamp(Value.COMMIT_TIMESTAMP)); + tester.addEqualityGroup( + Value.timestamp(Value.COMMIT_TIMESTAMP), Value.timestamp(Value.COMMIT_TIMESTAMP)); Timestamp now = Timestamp.now(); tester.addEqualityGroup(Value.timestamp(now), Value.timestamp(now)); tester.addEqualityGroup(Value.timestamp(Timestamp.ofTimeMicroseconds(0))); @@ -690,8 +784,17 @@ public void testEqualsHashCode() { tester.addEqualityGroup( Value.date(Date.fromYearMonthDay(2018, 2, 26)), Value.date(Date.fromYearMonthDay(2018, 2, 26))); - tester.addEqualityGroup( - Value.date(Date.fromYearMonthDay(2018, 2, 27))); + tester.addEqualityGroup(Value.date(Date.fromYearMonthDay(2018, 2, 27))); + + 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(); + tester.addEqualityGroup(Value.struct(structValue1), Value.struct(structValue2)); + + Type structType1 = structValue1.getType(); + Type structType2 = Type.struct(Arrays.asList(StructField.of("f1", Type.string()))); + tester.addEqualityGroup(Value.struct(structType1, null), Value.struct(structType1, null)); + tester.addEqualityGroup(Value.struct(structType2, null), Value.struct(structType2, null)); tester.addEqualityGroup( Value.boolArray(Arrays.asList(false, true)), @@ -728,17 +831,27 @@ public void testEqualsHashCode() { tester.addEqualityGroup(Value.bytesArray(Arrays.asList(newByteArray("c")))); tester.addEqualityGroup(Value.bytesArray(null)); - tester.addEqualityGroup(Value.timestampArray(Arrays.asList(null, now)), + tester.addEqualityGroup( + Value.timestampArray(Arrays.asList(null, now)), Value.timestampArray(Arrays.asList(null, now))); tester.addEqualityGroup(Value.timestampArray(null)); tester.addEqualityGroup( - Value.dateArray( - Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26))), - Value.dateArray( - Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26)))); + Value.dateArray(Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26))), + Value.dateArray(Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26)))); tester.addEqualityGroup(Value.dateArray(null)); + tester.addEqualityGroup( + Value.structArray(structType1, Arrays.asList(structValue1, null)), + Value.structArray(structType1, Arrays.asList(structValue2, null))); + tester.addEqualityGroup( + Value.structArray(structType1, Arrays.asList((Struct) null)), + Value.structArray(structType1, Arrays.asList((Struct) null))); + tester.addEqualityGroup( + Value.structArray(structType1, null), Value.structArray(structType1, null)); + tester.addEqualityGroup( + Value.structArray(structType1, new ArrayList()), + Value.structArray(structType1, new ArrayList())); tester.testEquals(); } @@ -762,15 +875,25 @@ public void serialization() { reserializeAndAssert(Value.bytes(newByteArray("abc"))); reserializeAndAssert(Value.bytes(null)); - reserializeAndAssert(Value.boolArray(new boolean[] { false, true })); + reserializeAndAssert( + Value.struct(Struct.newBuilder().set("f").to(3).set("f").to((Date) null).build())); + reserializeAndAssert( + Value.struct( + Type.struct( + Arrays.asList( + Type.StructField.of("a", Type.string()), + Type.StructField.of("b", Type.int64()))), + null)); + + reserializeAndAssert(Value.boolArray(new boolean[] {false, true})); reserializeAndAssert(Value.boolArray(BrokenSerializationList.of(true, false))); reserializeAndAssert(Value.boolArray((Iterable) null)); reserializeAndAssert(Value.int64Array(BrokenSerializationList.of(1L, 2L))); - reserializeAndAssert(Value.int64Array(new long[] { 1L, 2L })); + reserializeAndAssert(Value.int64Array(new long[] {1L, 2L})); reserializeAndAssert(Value.int64Array((Iterable) null)); - reserializeAndAssert(Value.float64Array(new double[] { .1, .2 })); + reserializeAndAssert(Value.float64Array(new double[] {.1, .2})); reserializeAndAssert(Value.float64Array(BrokenSerializationList.of(.1, .2, .3))); reserializeAndAssert(Value.float64Array((Iterable) null)); @@ -781,8 +904,7 @@ public void serialization() { reserializeAndAssert(Value.date(null)); reserializeAndAssert(Value.date(Date.fromYearMonthDay(2018, 2, 26))); - reserializeAndAssert(Value.dateArray(Arrays.asList(null, - Date.fromYearMonthDay(2018, 2, 26)))); + reserializeAndAssert(Value.dateArray(Arrays.asList(null, Date.fromYearMonthDay(2018, 2, 26)))); BrokenSerializationList of = BrokenSerializationList.of("a", "b"); reserializeAndAssert(Value.stringArray(of)); @@ -791,6 +913,11 @@ public void serialization() { reserializeAndAssert( Value.bytesArray(BrokenSerializationList.of(newByteArray("a"), newByteArray("b")))); reserializeAndAssert(Value.bytesArray(null)); + + Struct s1 = Struct.newBuilder().set("f1").to(1).build(); + Struct s2 = Struct.newBuilder().set("f1").to(2).build(); + reserializeAndAssert(Value.structArray(s1.getType(), BrokenSerializationList.of(s1, null, s2))); + reserializeAndAssert(Value.structArray(s1.getType(), null)); } @Test(expected = IllegalStateException.class) @@ -798,8 +925,9 @@ public void verifyBrokenSerialization() { reserializeAndAssert(BrokenSerializationList.of(1, 2, 3)); } - private static class BrokenSerializationList extends ForwardingList implements - Serializable { + private static class BrokenSerializationList extends ForwardingList + implements Serializable { + private static final long serialVersionUID = 1L; private final List delegate; public static BrokenSerializationList of(T... values) { @@ -810,18 +938,19 @@ private BrokenSerializationList(List delegate) { this.delegate = delegate; } - @Override protected List delegate() { + @Override + protected List delegate() { return delegate; } - private void readObject(java.io.ObjectInputStream unusedStream) + + private void readObject(@SuppressWarnings("unused") java.io.ObjectInputStream unusedStream) throws IOException, ClassNotFoundException { throw new IllegalStateException("Serialization disabled"); } - private void writeObject(java.io.ObjectOutputStream unusedStream) + + private void writeObject(@SuppressWarnings("unused") java.io.ObjectOutputStream unusedStream) throws IOException { throw new IllegalStateException("Serialization disabled"); } - - } } diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java index 58535c8e2c6e..64962469bd33 100644 --- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java +++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java @@ -19,6 +19,7 @@ import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.cloud.spanner.Type.StructField; import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.asList; import com.google.cloud.ByteArray; import com.google.cloud.Date; @@ -35,9 +36,11 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.Type; +import com.google.cloud.spanner.Value; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import com.google.spanner.v1.ResultSetStats; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.BeforeClass; @@ -104,11 +107,12 @@ public void arrayOfStruct() { // a manually constructed Struct. Struct expectedRow = Struct.newBuilder() - .add( - "", - Arrays.asList( - StructField.of("C1", Type.string()), StructField.of("C2", Type.int64())), - Arrays.asList( + .set("") + .toStructArray( + Type.struct( + asList( + StructField.of("C1", Type.string()), StructField.of("C2", Type.int64()))), + asList( Struct.newBuilder().set("C1").to("a").set("C2").to(1).build(), Struct.newBuilder().set("C1").to("b").set("C2").to(2).build())) .build(); @@ -265,9 +269,7 @@ public void bindDateNull() { public void bindBoolArray() { Struct row = execute( - Statement.newBuilder("SELECT @v") - .bind("v") - .toBoolArray(Arrays.asList(true, null, false)), + Statement.newBuilder("SELECT @v").bind("v").toBoolArray(asList(true, null, false)), Type.array(Type.bool())); assertThat(row.isNull(0)).isFalse(); assertThat(row.getBooleanList(0)).containsExactly(true, null, false).inOrder(); @@ -296,7 +298,7 @@ public void bindBoolArrayNull() { public void bindInt64Array() { Struct row = execute( - Statement.newBuilder("SELECT @v").bind("v").toInt64Array(Arrays.asList(null, 1L, 2L)), + Statement.newBuilder("SELECT @v").bind("v").toInt64Array(asList(null, 1L, 2L)), Type.array(Type.int64())); assertThat(row.isNull(0)).isFalse(); assertThat(row.getLongList(0)).containsExactly(null, 1L, 2L).inOrder(); @@ -328,7 +330,7 @@ public void bindFloat64Array() { Statement.newBuilder("SELECT @v") .bind("v") .toFloat64Array( - Arrays.asList( + asList( null, 1.0, 2.0, @@ -366,9 +368,7 @@ public void bindFloat64ArrayNull() { public void bindStringArray() { Struct row = execute( - Statement.newBuilder("SELECT @v") - .bind("v") - .toStringArray(Arrays.asList("a", "b", null)), + Statement.newBuilder("SELECT @v").bind("v").toStringArray(asList("a", "b", null)), Type.array(Type.string())); assertThat(row.isNull(0)).isFalse(); assertThat(row.getStringList(0)).containsExactly("a", "b", null).inOrder(); @@ -400,7 +400,7 @@ public void bindBytesArray() { ByteArray e3 = null; Struct row = execute( - Statement.newBuilder("SELECT @v").bind("v").toBytesArray(Arrays.asList(e1, e2, e3)), + Statement.newBuilder("SELECT @v").bind("v").toBytesArray(asList(e1, e2, e3)), Type.array(Type.bytes())); assertThat(row.isNull(0)).isFalse(); assertThat(row.getBytesList(0)).containsExactly(e1, e2, e3).inOrder(); @@ -432,9 +432,7 @@ public void bindTimestampArray() { Struct row = execute( - Statement.newBuilder("SELECT @v") - .bind("v") - .toTimestampArray(Arrays.asList(t1, t2, null)), + Statement.newBuilder("SELECT @v").bind("v").toTimestampArray(asList(t1, t2, null)), Type.array(Type.timestamp())); assertThat(row.isNull(0)).isFalse(); assertThat(row.getTimestampList(0)).containsExactly(t1, t2, null).inOrder(); @@ -468,7 +466,7 @@ public void bindDateArray() { Struct row = execute( - Statement.newBuilder("SELECT @v").bind("v").toDateArray(Arrays.asList(d1, d2, null)), + Statement.newBuilder("SELECT @v").bind("v").toDateArray(asList(d1, d2, null)), Type.array(Type.date())); assertThat(row.isNull(0)).isFalse(); assertThat(row.getDateList(0)).containsExactly(d1, d2, null).inOrder(); @@ -492,6 +490,228 @@ public void bindDateArrayNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void unsupportedSelectStructValue() { + Struct p = structValue(); + expectedException.expect(isSpannerException(ErrorCode.UNIMPLEMENTED)); + expectedException.expectMessage( + "Unsupported query shape: " + "A struct value cannot be returned as a column value."); + execute(Statement.newBuilder("SELECT @p").bind("p").to(p).build(), p.getType()); + } + + @Test + public void unsupportedSelectArrayStructValue() { + Struct p = structValue(); + expectedException.expect(isSpannerException(ErrorCode.UNIMPLEMENTED)); + expectedException.expectMessage( + "Unsupported query shape: " + + "This query can return a null-valued array of struct, " + + "which is not supported by Spanner."); + execute( + Statement.newBuilder("SELECT @p") + .bind("p") + .toStructArray(p.getType(), asList(p)) + .build(), + p.getType()); + } + + @Test + public void invalidAmbiguousFieldAccess() { + Struct p = Struct.newBuilder().set("f1").to(20).set("f1").to("abc").build(); + + expectedException.expect(isSpannerException(ErrorCode.INVALID_ARGUMENT)); + expectedException.expectMessage("Struct field name f1 is ambiguous"); + execute(Statement.newBuilder("SELECT @p.f1").bind("p").to(p).build(), Type.int64()); + } + + private Struct structValue() { + return Struct.newBuilder() + .set("f_int") + .to(10) + .set("f_bool") + .to(false) + .set("f_double") + .to(3.4) + .set("f_timestamp") + .to(Timestamp.ofTimeMicroseconds(20)) + .set("f_date") + .to(Date.fromYearMonthDay(1, 3, 1)) + .set("f_string") + .to("hello") + .set("f_bytes") + .to(ByteArray.copyFrom("bytes")) + .build(); + } + + @Test + public void bindStruct() { + Struct p = structValue(); + String query = + "SELECT " + + "@p.f_int," + + "@p.f_bool," + + "@p.f_double," + + "@p.f_timestamp," + + "@p.f_date," + + "@p.f_string," + + "@p.f_bytes"; + + Struct row = + executeWithRowResultType(Statement.newBuilder(query).bind("p").to(p).build(), p.getType()); + assertThat(row).isEqualTo(p); + } + + @Test + public void bindArrayOfStruct() { + Struct arrayElement = structValue(); + List p = asList(arrayElement, null); + + List rows = + resultRows( + Statement.newBuilder("SELECT * FROM UNNEST(@p)") + .bind("p") + .toStructArray(arrayElement.getType(), p) + .build(), + arrayElement.getType()); + + assertThat(rows).hasSize(p.size()); + assertThat(rows.get(0)).isEqualTo(p.get(0)); + + // Field accesses on a null struct element (because of SELECT *) return null values. + Struct structElementFromNull = rows.get(1); + // assertThat(structElementFromNull.isNull()).isFalse(); + for (int i = 0; i < arrayElement.getType().getStructFields().size(); ++i) { + assertThat(structElementFromNull.isNull(i)).isTrue(); + } + } + + @Test + public void bindStructNull() { + Struct row = + execute( + Statement.newBuilder("SELECT @p IS NULL") + .bind("p") + .to( + Type.struct( + asList( + Type.StructField.of("f1", Type.string()), + Type.StructField.of("f2", Type.float64()))), + null) + .build(), + Type.bool()); + assertThat(row.getBoolean(0)).isTrue(); + } + + @Test + public void bindArrayOfStructNull() { + Type elementType = + Type.struct( + asList( + Type.StructField.of("f1", Type.string()), + Type.StructField.of("f2", Type.float64()))); + + Struct row = + execute( + Statement.newBuilder("SELECT @p IS NULL") + .bind("p") + .toStructArray(elementType, null) + .build(), + Type.bool()); + assertThat(row.getBoolean(0)).isTrue(); + } + + @Test + public void bindEmptyStruct() { + Struct p = Struct.newBuilder().build(); + Struct row = + execute(Statement.newBuilder("SELECT @p IS NULL").bind("p").to(p).build(), Type.bool()); + assertThat(row.getBoolean(0)).isFalse(); + } + + @Test + public void bindStructWithUnnamedFields() { + Struct p = Struct.newBuilder().add(Value.int64(1337)).add(Value.int64(7331)).build(); + Struct row = + executeWithRowResultType( + Statement.newBuilder("SELECT * FROM UNNEST([@p])").bind("p").to(p).build(), + p.getType()); + assertThat(row.getLong(0)).isEqualTo(1337); + assertThat(row.getLong(1)).isEqualTo(7331); + } + + @Test + public void bindStructWithDuplicateFieldNames() { + Struct p = + Struct.newBuilder() + .set("f1") + .to(Value.int64(1337)) + .set("f1") + .to(Value.string("1337")) + .build(); + Struct row = + executeWithRowResultType( + Statement.newBuilder("SELECT * FROM UNNEST([@p])").bind("p").to(p).build(), + p.getType()); + assertThat(row.getLong(0)).isEqualTo(1337); + assertThat(row.getString(1)).isEqualTo("1337"); + } + + @Test + public void bindEmptyArrayOfStruct() { + Type elementType = Type.struct(asList(Type.StructField.of("f1", Type.date()))); + List p = asList(); + assertThat(p).isEmpty(); + + List rows = + resultRows( + Statement.newBuilder("SELECT * FROM UNNEST(@p)") + .bind("p") + .toStructArray(elementType, p) + .build(), + elementType); + assertThat(rows).isEmpty(); + } + + @Test + public void bindStructWithNullStructField() { + Type emptyStructType = Type.struct(new ArrayList()); + Struct p = Struct.newBuilder().set("f1").to(emptyStructType, null).build(); + + Struct row = + execute(Statement.newBuilder("SELECT @p.f1 IS NULL").bind("p").to(p).build(), Type.bool()); + assertThat(row.getBoolean(0)).isTrue(); + } + + @Test + public void bindStructWithStructField() { + Struct nestedStruct = Struct.newBuilder().set("ff1").to("abc").build(); + Struct p = Struct.newBuilder().set("f1").to(nestedStruct).build(); + + Struct row = + executeWithRowResultType( + Statement.newBuilder("SELECT @p.f1.ff1").bind("p").to(p).build(), + nestedStruct.getType()); + assertThat(row.getString(0)).isEqualTo("abc"); + } + + @Test + public void bindStructWithArrayOfStructField() { + Struct arrayElement1 = Struct.newBuilder().set("ff1").to("abc").build(); + Struct arrayElement2 = Struct.newBuilder().set("ff1").to("def").build(); + Struct p = + Struct.newBuilder() + .set("f1") + .toStructArray(arrayElement1.getType(), asList(arrayElement1, arrayElement2)) + .build(); + + List rows = + resultRows( + Statement.newBuilder("SELECT * FROM UNNEST(@p.f1)").bind("p").to(p).build(), + arrayElement1.getType()); + assertThat(rows.get(0).getString(0)).isEqualTo("abc"); + assertThat(rows.get(1).getString(0)).isEqualTo("def"); + } + @Test public void unboundParameter() { ResultSet resultSet = @@ -556,7 +776,7 @@ public void queryRealTable() { "CREATE TABLE T ( K STRING(MAX) NOT NULL, V STRING(MAX) ) PRIMARY KEY (K)"); DatabaseClient client = env.getTestHelper().getDatabaseClient(populatedDb); client.writeAtLeastOnce( - Arrays.asList( + asList( Mutation.newInsertBuilder("T").set("K").to("k1").set("V").to("v1").build(), Mutation.newInsertBuilder("T").set("K").to("k2").set("V").to("v2").build(), Mutation.newInsertBuilder("T").set("K").to("k3").set("V").to("v3").build(), @@ -613,21 +833,33 @@ public void analyzeProfile() { assertThat(receivedStats.hasQueryStats()).isTrue(); } - private Struct execute(Statement statement, Type expectedType) { + private List resultRows(Statement statement, Type expectedRowType) { + ArrayList results = new ArrayList<>(); ResultSet resultSet = statement.executeQuery(client.singleUse(TimestampBound.strong())); - return checkSingleValueOfType(resultSet, expectedType); - } - - private Struct execute(Statement.Builder builder, Type expectedType) { - return execute(builder.build(), expectedType); + while (resultSet.next()) { + Struct row = resultSet.getCurrentRowAsStruct(); + results.add(row); + } + assertThat(resultSet.getType()).isEqualTo(expectedRowType); + assertThat(resultSet.next()).isFalse(); + return results; } - private Struct checkSingleValueOfType(ResultSet resultSet, Type expectedType) { + private Struct executeWithRowResultType(Statement statement, Type expectedRowType) { + ResultSet resultSet = statement.executeQuery(client.singleUse(TimestampBound.strong())); assertThat(resultSet.next()).isTrue(); - assertThat(resultSet.getType().getStructFields()).hasSize(1); - assertThat(resultSet.getType().getStructFields().get(0).getType()).isEqualTo(expectedType); + assertThat(resultSet.getType()).isEqualTo(expectedRowType); Struct row = resultSet.getCurrentRowAsStruct(); assertThat(resultSet.next()).isFalse(); return row; } + + private Struct execute(Statement statement, Type expectedColumnType) { + Type rowType = Type.struct(StructField.of("", expectedColumnType)); + return executeWithRowResultType(statement, rowType); + } + + private Struct execute(Statement.Builder builder, Type expectedColumnType) { + return execute(builder.build(), expectedColumnType); + } }