diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java new file mode 100644 index 000000000000..085ae9fe83ea --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java @@ -0,0 +1,548 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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 com.google.cloud.ByteArray; +import com.google.cloud.Date; +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ListValue; +import com.google.protobuf.Value; + +import java.util.*; + +import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; +import static com.google.common.base.Preconditions.checkArgument; + +/** + * A gRPC/proto-based struct. + */ +class GrpcStruct extends Struct { + private final Type type; + private final List rowData; + + GrpcStruct(Type type, List rowData) { + this.type = type; + this.rowData = rowData; + } + + static com.google.cloud.spanner.Value valueFromTypedProto(Value proto) { + if (proto.getKindCase() != Value.KindCase.LIST_VALUE && proto.getListValue().getValuesCount() + != 2) { + throw new AssertionError("Expected a list of two elements: type and value"); + } + Type type; + try { + type = Type.fromProto( + com.google.spanner.v1.Type.parseFrom( + proto.getListValue().getValues(0).getStringValueBytes())); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError("Expected serialized type."); + } + Object value = decodeValue(type, proto.getListValue().getValues(1)); + switch (type.getCode()) { + case BOOL: + return com.google.cloud.spanner.Value.bool((Boolean) value); + case INT64: + return com.google.cloud.spanner.Value.int64((Long) value); + case FLOAT64: + return com.google.cloud.spanner.Value.float64((Double) value); + case STRING: + return com.google.cloud.spanner.Value.string((String) value); + case BYTES: + return com.google.cloud.spanner.Value.bytes((ByteArray) value); + case TIMESTAMP: + return com.google.cloud.spanner.Value.timestamp( + (com.google.cloud.Timestamp) value); + case DATE: + return com.google.cloud.spanner.Value.date( + (Date) value); + case ARRAY: + Type elementType = type.getArrayElementType(); + switch (elementType.getCode()) { + case BOOL: + return com.google.cloud.spanner.Value.boolArray((Iterable) value); + case INT64: + return com.google.cloud.spanner.Value.int64Array((Iterable) value); + case FLOAT64: + return com.google.cloud.spanner.Value.float64Array((Iterable) value); + case STRING: + return com.google.cloud.spanner.Value.stringArray((Iterable) value); + case BYTES: + return com.google.cloud.spanner.Value.bytesArray((Iterable) value); + case TIMESTAMP: + return com.google.cloud.spanner.Value.timestampArray( + (Iterable) value); + case DATE: + return com.google.cloud.spanner.Value.dateArray( + (Iterable) value); + + case STRUCT: + List fields = elementType.getStructFields(); + return com.google.cloud.spanner.Value.structArray( + fields, (Iterable) value); + default: + throw new AssertionError( + "Unhandled type code: " + elementType.getCode()); + } + case STRUCT: // Not a legal top-level field type. + default: + throw new AssertionError("Unhandled type code: " + type.getCode()); + } + } + + static Object fromTypedProto(Value proto) { + if (proto.getKindCase() != Value.KindCase.LIST_VALUE && proto.getListValue().getValuesCount() + != 2) { + throw new AssertionError("Expected a list of two elements: type and value"); + } + Type type; + try { + type = Type.fromProto( + com.google.spanner.v1.Type.parseFrom( + proto.getListValue().getValues(0).getStringValueBytes())); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError("Expected serialized type."); + } + return decodeValue(type, proto.getListValue().getValues(1)); + } + + private static Object decodeValue(Type fieldType, Value proto) { + if (proto.getKindCase() == Value.KindCase.NULL_VALUE) { + return null; + } + switch (fieldType.getCode()) { + case BOOL: + checkType(fieldType, proto, Value.KindCase.BOOL_VALUE); + return proto.getBoolValue(); + case INT64: + checkType(fieldType, proto, Value.KindCase.STRING_VALUE); + return Long.parseLong(proto.getStringValue()); + case FLOAT64: + return valueProtoToFloat64(proto); + case STRING: + checkType(fieldType, proto, Value.KindCase.STRING_VALUE); + return proto.getStringValue(); + case BYTES: + checkType(fieldType, proto, Value.KindCase.STRING_VALUE); + return ByteArray.fromBase64(proto.getStringValue()); + case TIMESTAMP: + checkType(fieldType, proto, Value.KindCase.STRING_VALUE); + return com.google.cloud.Timestamp.parseTimestamp(proto.getStringValue()); + case DATE: + checkType(fieldType, proto, Value.KindCase.STRING_VALUE); + return Date.parseDate(proto.getStringValue()); + case ARRAY: + checkType(fieldType, proto, Value.KindCase.LIST_VALUE); + ListValue listValue = proto.getListValue(); + return decodeArrayValue(fieldType.getArrayElementType(), listValue); + case STRUCT: // Not a legal top-level field type. + default: + throw new AssertionError("Unhandled type code: " + fieldType.getCode()); + } + } + + private static Object decodeArrayValue(Type elementType, ListValue listValue) { + switch (elementType.getCode()) { + case BOOL: + // Use a view: element conversion is virtually free. + return Lists.transform( + listValue.getValuesList(), + new Function() { + @Override + public Boolean apply(Value input) { + return input.getKindCase() == Value.KindCase.NULL_VALUE + ? null + : input.getBoolValue(); + } + }); + case INT64: + // For int64/float64 types, use custom containers. These avoid wrapper object + // creation for non-null arrays. + return new Int64Array(listValue); + case FLOAT64: + return new Float64Array(listValue); + case STRING: + return Lists.transform( + listValue.getValuesList(), + new Function() { + @Override + public String apply(Value input) { + return input.getKindCase() == Value.KindCase.NULL_VALUE + ? null + : input.getStringValue(); + } + }); + case BYTES: + { + // Materialize list: element conversion is expensive and should happen only once. + ArrayList list = new ArrayList<>(listValue.getValuesCount()); + for (Value value : listValue.getValuesList()) { + list.add( + value.getKindCase() == Value.KindCase.NULL_VALUE + ? null + : ByteArray.fromBase64(value.getStringValue())); + } + return list; + } + case TIMESTAMP: + { + // Materialize list: element conversion is expensive and should happen only once. + ArrayList list = new ArrayList<>(listValue.getValuesCount()); + for (Value value : listValue.getValuesList()) { + list.add( + value.getKindCase() == Value.KindCase.NULL_VALUE + ? null + : com.google.cloud.Timestamp.parseTimestamp(value.getStringValue())); + } + return list; + } + case DATE: + { + // Materialize list: element conversion is expensive and should happen only once. + ArrayList list = new ArrayList<>(listValue.getValuesCount()); + for (Value value : listValue.getValuesList()) { + list.add( + value.getKindCase() == Value.KindCase.NULL_VALUE + ? null + : Date.parseDate(value.getStringValue())); + } + return list; + } + + case STRUCT: + { + ArrayList list = new ArrayList<>(listValue.getValuesCount()); + for (Value value : listValue.getValuesList()) { + if (value.getKindCase() == Value.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)); + } + } + return list; + } + default: + throw new AssertionError("Unhandled type code: " + elementType.getCode()); + } + } + + private static void checkType(Type fieldType, Value proto, Value.KindCase expected) { + if (proto.getKindCase() != expected) { + throw newSpannerException( + ErrorCode.INTERNAL, + "Invalid value for column type " + + fieldType + + " expected " + + expected + + " but was " + + proto.getKindCase()); + } + } + + private static double valueProtoToFloat64(Value proto) { + if (proto.getKindCase() == Value.KindCase.STRING_VALUE) { + switch (proto.getStringValue()) { + case "-Infinity": + return Double.NEGATIVE_INFINITY; + case "Infinity": + return Double.POSITIVE_INFINITY; + case "NaN": + return Double.NaN; + default: + // Fall-through to handling below to produce an error. + } + } + if (proto.getKindCase() != Value.KindCase.NUMBER_VALUE) { + throw newSpannerException( + ErrorCode.INTERNAL, + "Invalid value for column type " + + Type.float64() + + " expected NUMBER_VALUE or STRING_VALUE with value one of" + + " \"Infinity\", \"-Infinity\", or \"NaN\" but was " + + proto.getKindCase() + + (proto.getKindCase() == Value.KindCase.STRING_VALUE + ? " with value \"" + proto.getStringValue() + "\"" + : "")); + } + return proto.getNumberValue(); + } + + private static NullPointerException throwNotNull(int columnIndex) { + throw new NullPointerException( + "Cannot call array getter for column " + columnIndex + " with null elements"); + } + + boolean consumeRow(Iterator iterator) { + rowData.clear(); + if (!iterator.hasNext()) { + return false; + } + for (Type.StructField fieldType : getType().getStructFields()) { + if (!iterator.hasNext()) { + throw newSpannerException( + ErrorCode.INTERNAL, + "Invalid value stream: end of stream reached before row is complete"); + } + Value value = iterator.next(); + rowData.add(decodeValue(fieldType.getType(), value)); + } + return true; + } + + Struct immutableCopy() { + return new GrpcStruct(type, new ArrayList<>(rowData)); + } + + @Override + public Type getType() { + return type; + } + + @Override + public boolean isNull(int columnIndex) { + return rowData.get(columnIndex) == null; + } + + @Override + protected boolean getBooleanInternal(int columnIndex) { + return (Boolean) rowData.get(columnIndex); + } + + @Override + protected long getLongInternal(int columnIndex) { + return (Long) rowData.get(columnIndex); + } + + @Override + protected double getDoubleInternal(int columnIndex) { + return (Double) rowData.get(columnIndex); + } + + @Override + protected String getStringInternal(int columnIndex) { + return (String) rowData.get(columnIndex); + } + + @Override + protected ByteArray getBytesInternal(int columnIndex) { + return (ByteArray) rowData.get(columnIndex); + } + + @Override + protected com.google.cloud.Timestamp getTimestampInternal(int columnIndex) { + return (com.google.cloud.Timestamp) rowData.get(columnIndex); + } + + @Override + protected Date getDateInternal(int columnIndex) { + return (Date) rowData.get(columnIndex); + } + + @Override + protected boolean[] getBooleanArrayInternal(int columnIndex) { + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + List values = (List) rowData.get(columnIndex); + boolean[] r = new boolean[values.size()]; + for (int i = 0; i < values.size(); ++i) { + if (values.get(i) == null) { + throw throwNotNull(columnIndex); + } + r[i] = values.get(i); + } + return r; + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getBooleanListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + protected long[] getLongArrayInternal(int columnIndex) { + return getLongListInternal(columnIndex).toPrimitiveArray(columnIndex); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces an Int64Array. + protected Int64Array getLongListInternal(int columnIndex) { + return (Int64Array) rowData.get(columnIndex); + } + + @Override + protected double[] getDoubleArrayInternal(int columnIndex) { + return getDoubleListInternal(columnIndex).toPrimitiveArray(columnIndex); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a Float64Array. + protected Float64Array getDoubleListInternal(int columnIndex) { + return (Float64Array) rowData.get(columnIndex); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getStringListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getBytesListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getTimestampListInternal(int columnIndex) { + return Collections.unmodifiableList( + (List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getDateListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY> produces a List. + protected List getStructListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + /** + * Memory-optimized base class for {@code ARRAY} and {@code ARRAY} types. Both of + * these involve conversions from the type yielded by JSON parsing, which are {@code String} and + * {@code BigDecimal} respectively. Rather than construct new wrapper objects for each array + * element, we use primitive arrays and a {@code BitSet} to track nulls. + */ + private abstract static class PrimitiveArray extends AbstractList { + private final A data; + private final BitSet nulls; + private final int size; + + PrimitiveArray(ListValue protoList) { + this.size = protoList.getValuesCount(); + A data = newArray(size); + BitSet nulls = new BitSet(size); + for (int i = 0; i < protoList.getValuesCount(); ++i) { + if (protoList.getValues(i).getKindCase() == Value.KindCase.NULL_VALUE) { + nulls.set(i); + } else { + setProto(data, i, protoList.getValues(i)); + } + } + this.data = data; + this.nulls = nulls; + } + + PrimitiveArray(A data, BitSet nulls, int size) { + this.data = data; + this.nulls = nulls; + this.size = size; + } + + abstract A newArray(int size); + + abstract void setProto(A array, int i, Value protoValue); + + abstract T get(A array, int i); + + @Override + public T get(int index) { + if (index < 0 || index >= size) { + throw new ArrayIndexOutOfBoundsException("index=" + index + " size=" + size); + } + return nulls.get(index) ? null : get(data, index); + } + + @Override + public int size() { + return size; + } + + A toPrimitiveArray(int columnIndex) { + if (nulls.length() > 0) { + throw throwNotNull(columnIndex); + } + A r = newArray(size); + System.arraycopy(data, 0, r, 0, size); + return r; + } + } + + private static class Int64Array extends PrimitiveArray { + Int64Array(ListValue protoList) { + super(protoList); + } + + Int64Array(long[] data, BitSet nulls) { + super(data, nulls, data.length); + } + + @Override + long[] newArray(int size) { + return new long[size]; + } + + @Override + void setProto(long[] array, int i, Value protoValue) { + array[i] = Long.parseLong(protoValue.getStringValue()); + } + + @Override + Long get(long[] array, int i) { + return array[i]; + } + } + + private static class Float64Array extends PrimitiveArray { + Float64Array(ListValue protoList) { + super(protoList); + } + + Float64Array(double[] data, BitSet nulls) { + super(data, nulls, data.length); + } + + @Override + double[] newArray(int size) { + return new double[size]; + } + + @Override + void setProto(double[] array, int i, Value protoValue) { + array[i] = valueProtoToFloat64(protoValue); + } + + @Override + Double get(double[] array, int i) { + return array[i]; + } + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java index 1f89e4c3f06e..f6f3c983f433 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java @@ -16,19 +16,22 @@ package com.google.cloud.spanner; -import static com.google.common.base.Preconditions.checkNotNull; - import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.common.base.Function; import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; import com.google.protobuf.Value; + +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; /** * Represents a row key in a Cloud Spanner table or index. A key is a tuple of values constrained to @@ -225,6 +228,7 @@ public Iterable getParts() { return parts; } + /** Returns a builder initialized with the value of this key. */ public Builder toBuilder() { return new Builder(this); @@ -263,6 +267,47 @@ public int hashCode() { return parts.hashCode(); } +/** + * Returns the parts in this key represented as Cloud Spanner + * {@link com.google.cloud.spanner.Value}. + */ + Iterable toValues() { + return Iterables.transform(parts, new Function() { + @Nullable + @Override + public com.google.cloud.spanner.Value apply(@Nullable Object value) { + if (value == null) { + return com.google.cloud.spanner.Value.bool(null); + } else if (value instanceof Boolean) { + return com.google.cloud.spanner.Value.bool((Boolean) value); + } else if (value instanceof Integer) { + return com.google.cloud.spanner.Value.int64((Integer) value); + } else if (value instanceof Long) { + return com.google.cloud.spanner.Value.int64((Long) value); + } else if (value instanceof Float) { + return com.google.cloud.spanner.Value.float64((Float) value); + } else if (value instanceof Double) { + return com.google.cloud.spanner.Value.float64((Double) value); + } else if (value instanceof String) { + return com.google.cloud.spanner.Value.string((String) value); + } else if (value instanceof ByteArray) { + return com.google.cloud.spanner.Value.bytes((ByteArray) value); + } else if (value instanceof Timestamp) { + return com.google.cloud.spanner.Value.timestamp((Timestamp) value); + } else if (value instanceof Date) { + return com.google.cloud.spanner.Value.date((Date) value); + } else { + throw new IllegalArgumentException( + "Unsupported type [" + + value.getClass().getCanonicalName() + + "] for argument: " + + value); + } + + } + }); + } + ListValue toProto() { ListValue.Builder builder = ListValue.newBuilder(); for (Object part : parts) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Mutation.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Mutation.java index 5bbed50f0a0d..bcc080de63c9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Mutation.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Mutation.java @@ -16,17 +16,20 @@ package com.google.cloud.spanner; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.protobuf.ListValue; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; + import javax.annotation.Nullable; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.util.*; + +import static com.google.cloud.spanner.GrpcStruct.fromTypedProto; +import static com.google.cloud.spanner.GrpcStruct.valueFromTypedProto; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; /** * Represents an individual table modification to be applied to Cloud Spanner. @@ -47,7 +50,7 @@ * *

{@code Mutation} instances are immutable. */ -public final class Mutation { +public final class Mutation implements Serializable { /** Enumerates the types of mutation that can be applied. */ public enum Op { /** @@ -79,6 +82,8 @@ public enum Op { DELETE, } + private static final long serialVersionUID = 1L; + private final String table; private final Op operation; private final ImmutableList columns; @@ -399,4 +404,172 @@ static void toProto(Iterable mutations, List() { + @Override + public com.google.protobuf.Value apply(Value input) { + return input.toTypedProto(); + } + })).build(); + } + + private static Key keyFromListValues(ListValue value) { + Key.Builder builder = Key.newBuilder(); + for (com.google.protobuf.Value part : value.getValuesList()) { + builder.appendObject(fromTypedProto(part)); + } + return builder.build(); + } + + private static KeyRange keyRangeFromProto(com.google.spanner.v1.KeyRange proto) { + KeyRange.Builder builder = KeyRange.newBuilder(); + if (proto.getEndKeyTypeCase() == com.google.spanner.v1.KeyRange.EndKeyTypeCase.END_OPEN) { + builder.setEnd(keyFromListValues(proto.getEndOpen())); + builder.setEndType(KeyRange.Endpoint.OPEN); + } else if (proto.getEndKeyTypeCase() == com.google.spanner.v1.KeyRange.EndKeyTypeCase + .END_CLOSED) { + builder.setEnd(keyFromListValues(proto.getEndClosed())); + builder.setEndType(KeyRange.Endpoint.CLOSED); + } + if (proto.getStartKeyTypeCase() == com.google.spanner.v1.KeyRange.StartKeyTypeCase.START_OPEN) { + builder.setStart(keyFromListValues(proto.getStartOpen())); + builder.setStartType(KeyRange.Endpoint.OPEN); + } else if (proto.getStartKeyTypeCase() == com.google.spanner.v1.KeyRange.StartKeyTypeCase.START_CLOSED) { + builder.setStart(keyFromListValues(proto.getStartClosed())); + builder.setStartType(KeyRange.Endpoint.CLOSED); + } + return builder.build(); + } + + /** + * Is called right after the serialization, and substitutes itself with the {@link Mutation} + * object. + * + * @return an instance of {@link Mutation}. + */ + private Object readResolve() throws ObjectStreamException { + com.google.spanner.v1.Mutation.Write write; + Mutation.WriteBuilder builder; + switch (proto.getOperationCase()) { + case INSERT: + builder = Mutation.newInsertBuilder(proto.getInsert().getTable()); + write = proto.getInsert(); + break; + case UPDATE: + builder = Mutation.newUpdateBuilder(proto.getUpdate().getTable()); + write = proto.getUpdate(); + break; + case INSERT_OR_UPDATE: + builder = Mutation.newInsertOrUpdateBuilder(proto.getInsertOrUpdate().getTable()); + write = proto.getInsertOrUpdate(); + break; + case REPLACE: + builder = Mutation.newReplaceBuilder(proto.getReplace().getTable()); + write = proto.getReplace(); + break; + case DELETE: + KeySet.Builder keySetBuilder = KeySet.newBuilder(); + com.google.spanner.v1.KeySet keySetProto = proto.getDelete().getKeySet(); + if (keySetProto.getAll()) { + keySetBuilder.setAll(); + } else { + for(ListValue key : keySetProto.getKeysList()) { + keySetBuilder.addKey(keyFromListValues(key)); + } + for(com.google.spanner.v1.KeyRange range : keySetProto.getRangesList()) { + keySetBuilder.addRange(keyRangeFromProto(range)); + } + } + return Mutation.delete(proto.getDelete().getTable(), keySetBuilder.build()); + default: + throw new AssertionError("Impossible: " + proto.getOperationCase()); + } + + for (int i = 0; i < write.getColumnsCount(); i++) { + com.google.protobuf.Value value = write.getValues(0).getValues(i); + builder.set(write.getColumns(i)).handle(valueFromTypedProto(value)); + } + + return builder.build(); + } + + } + + + /** + * Delegates serialization to the {@link SerializationProxy} class. + * + * @return an instance of {@link SerializationProxy} that is safe to serialize. + */ + private Object writeReplace() { + return new SerializationProxy(this); + } + } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java index 2e9f8a0a5e29..b2a2e8d13b7d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -1503,142 +1503,6 @@ public Transaction call() throws Exception { } } - /** - * Base class for gRPC/proto-based structs. - * - * @param the type of row data - */ - private static class BaseStruct extends Struct { - protected final Type type; - protected final List rowData; - - private BaseStruct(Type type, List rowData) { - this.type = type; - this.rowData = rowData; - } - - Struct immutableCopy() { - return new BaseStruct(type, new ArrayList<>(rowData)); - } - - @Override - public Type getType() { - return type; - } - - @Override - public boolean isNull(int columnIndex) { - return rowData.get(columnIndex) == null; - } - - @Override - protected boolean getBooleanInternal(int columnIndex) { - return (Boolean) rowData.get(columnIndex); - } - - @Override - protected long getLongInternal(int columnIndex) { - return (Long) rowData.get(columnIndex); - } - - @Override - protected double getDoubleInternal(int columnIndex) { - return (Double) rowData.get(columnIndex); - } - - @Override - protected String getStringInternal(int columnIndex) { - return (String) rowData.get(columnIndex); - } - - @Override - protected ByteArray getBytesInternal(int columnIndex) { - return (ByteArray) rowData.get(columnIndex); - } - - @Override - protected Timestamp getTimestampInternal(int columnIndex) { - return (Timestamp) rowData.get(columnIndex); - } - - @Override - protected Date getDateInternal(int columnIndex) { - return (Date) rowData.get(columnIndex); - } - - @Override - protected boolean[] getBooleanArrayInternal(int columnIndex) { - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - List values = (List) rowData.get(columnIndex); - boolean[] r = new boolean[values.size()]; - for (int i = 0; i < values.size(); ++i) { - if (values.get(i) == null) { - throw throwNotNull(columnIndex); - } - r[i] = values.get(i); - } - return r; - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getBooleanListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - protected long[] getLongArrayInternal(int columnIndex) { - return getLongListInternal(columnIndex).toPrimitiveArray(columnIndex); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces an Int64Array. - protected Int64Array getLongListInternal(int columnIndex) { - return (Int64Array) rowData.get(columnIndex); - } - - @Override - protected double[] getDoubleArrayInternal(int columnIndex) { - return getDoubleListInternal(columnIndex).toPrimitiveArray(columnIndex); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a Float64Array. - protected Float64Array getDoubleListInternal(int columnIndex) { - return (Float64Array) rowData.get(columnIndex); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getStringListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getBytesListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getTimestampListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getDateListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY> produces a List. - protected List getStructListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - } - @VisibleForTesting abstract static class AbstractResultSet extends AbstractStructReader implements ResultSet { interface Listener { @@ -1655,7 +1519,7 @@ interface Listener { void onDone(); } - protected abstract BaseStruct currRow(); + protected abstract GrpcStruct currRow(); @Override public Struct getCurrentRowAsStruct() { @@ -1776,7 +1640,7 @@ static class GrpcResultSet extends AbstractResultSet> { } @Override - protected BaseStruct> currRow() { + protected GrpcStruct currRow() { checkState(!closed, "ResultSet is closed"); checkState(currRow != null, "next() call required"); return currRow; @@ -1835,168 +1699,6 @@ private SpannerException yieldError(SpannerException e) { } } - private static class GrpcStruct extends BaseStruct> { - GrpcStruct(Type type, List rowData) { - super(type, rowData); - } - - boolean consumeRow(Iterator iterator) { - rowData.clear(); - if (!iterator.hasNext()) { - return false; - } - for (Type.StructField fieldType : getType().getStructFields()) { - if (!iterator.hasNext()) { - throw newSpannerException( - ErrorCode.INTERNAL, - "Invalid value stream: end of stream reached before row is complete"); - } - com.google.protobuf.Value value = iterator.next(); - rowData.add(decodeValue(fieldType.getType(), value)); - } - return true; - } - - private static Object decodeValue(Type fieldType, com.google.protobuf.Value proto) { - if (proto.getKindCase() == KindCase.NULL_VALUE) { - return null; - } - switch (fieldType.getCode()) { - case BOOL: - checkType(fieldType, proto, KindCase.BOOL_VALUE); - return proto.getBoolValue(); - case INT64: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return Long.parseLong(proto.getStringValue()); - case FLOAT64: - return valueProtoToFloat64(proto); - case STRING: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return proto.getStringValue(); - case BYTES: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return ByteArray.fromBase64(proto.getStringValue()); - case TIMESTAMP: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return Timestamp.parseTimestamp(proto.getStringValue()); - case DATE: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return Date.parseDate(proto.getStringValue()); - case ARRAY: - checkType(fieldType, proto, KindCase.LIST_VALUE); - ListValue listValue = proto.getListValue(); - return decodeArrayValue(fieldType.getArrayElementType(), listValue); - case STRUCT: // Not a legal top-level field type. - default: - throw new AssertionError("Unhandled type code: " + fieldType.getCode()); - } - } - - private static Object decodeArrayValue(Type elementType, ListValue listValue) { - switch (elementType.getCode()) { - case BOOL: - // Use a view: element conversion is virtually free. - return Lists.transform( - listValue.getValuesList(), - new Function() { - @Override - public Boolean apply(com.google.protobuf.Value input) { - return input.getKindCase() == KindCase.NULL_VALUE ? null : input.getBoolValue(); - } - }); - case INT64: - // For int64/float64 types, use custom containers. These avoid wrapper object - // creation for non-null arrays. - return new Int64Array(listValue); - case FLOAT64: - return new Float64Array(listValue); - case STRING: - return Lists.transform( - listValue.getValuesList(), - new Function() { - @Override - public String apply(com.google.protobuf.Value input) { - return input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue(); - } - }); - case BYTES: - { - // Materialize list: element conversion is expensive and should happen only once. - ArrayList list = new ArrayList<>(listValue.getValuesCount()); - for (com.google.protobuf.Value value : listValue.getValuesList()) { - list.add( - value.getKindCase() == KindCase.NULL_VALUE - ? null - : ByteArray.fromBase64(value.getStringValue())); - } - return list; - } - case TIMESTAMP: - { - // Materialize list: element conversion is expensive and should happen only once. - ArrayList list = new ArrayList<>(listValue.getValuesCount()); - for (com.google.protobuf.Value value : listValue.getValuesList()) { - list.add( - value.getKindCase() == KindCase.NULL_VALUE - ? null - : Timestamp.parseTimestamp(value.getStringValue())); - } - return list; - } - case DATE: - { - // Materialize list: element conversion is expensive and should happen only once. - ArrayList list = new ArrayList<>(listValue.getValuesCount()); - for (com.google.protobuf.Value value : listValue.getValuesList()) { - list.add( - value.getKindCase() == KindCase.NULL_VALUE - ? null - : Date.parseDate(value.getStringValue())); - } - return list; - } - - case STRUCT: - { - ArrayList list = new ArrayList<>(listValue.getValuesCount()); - for (com.google.protobuf.Value value : listValue.getValuesList()) { - 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)); - } - } - return list; - } - default: - throw new AssertionError("Unhandled type code: " + elementType.getCode()); - } - } - - private static void checkType( - Type fieldType, com.google.protobuf.Value proto, KindCase expected) { - if (proto.getKindCase() != expected) { - throw newSpannerException( - ErrorCode.INTERNAL, - "Invalid value for column type " - + fieldType - + " expected " - + expected - + " but was " - + proto.getKindCase()); - } - } - } - @VisibleForTesting interface CloseableIterator extends Iterator { @@ -2377,147 +2079,7 @@ private boolean isMergeable(KindCase kind) { } } - private static double valueProtoToFloat64(com.google.protobuf.Value proto) { - if (proto.getKindCase() == KindCase.STRING_VALUE) { - switch (proto.getStringValue()) { - case "-Infinity": - return Double.NEGATIVE_INFINITY; - case "Infinity": - return Double.POSITIVE_INFINITY; - case "NaN": - return Double.NaN; - default: - // Fall-through to handling below to produce an error. - } - } - if (proto.getKindCase() != KindCase.NUMBER_VALUE) { - throw newSpannerException( - ErrorCode.INTERNAL, - "Invalid value for column type " - + Type.float64() - + " expected NUMBER_VALUE or STRING_VALUE with value one of" - + " \"Infinity\", \"-Infinity\", or \"NaN\" but was " - + proto.getKindCase() - + (proto.getKindCase() == KindCase.STRING_VALUE - ? " with value \"" + proto.getStringValue() + "\"" - : "")); - } - return proto.getNumberValue(); - } - private static NullPointerException throwNotNull(int columnIndex) { - throw new NullPointerException( - "Cannot call array getter for column " + columnIndex + " with null elements"); - } - /** - * Memory-optimized base class for {@code ARRAY} and {@code ARRAY} types. Both of - * these involve conversions from the type yielded by JSON parsing, which are {@code String} and - * {@code BigDecimal} respectively. Rather than construct new wrapper objects for each array - * element, we use primitive arrays and a {@code BitSet} to track nulls. - */ - private abstract static class PrimitiveArray extends AbstractList { - private final A data; - private final BitSet nulls; - private final int size; - - PrimitiveArray(ListValue protoList) { - this.size = protoList.getValuesCount(); - A data = newArray(size); - BitSet nulls = new BitSet(size); - for (int i = 0; i < protoList.getValuesCount(); ++i) { - if (protoList.getValues(i).getKindCase() == KindCase.NULL_VALUE) { - nulls.set(i); - } else { - setProto(data, i, protoList.getValues(i)); - } - } - this.data = data; - this.nulls = nulls; - } - - PrimitiveArray(A data, BitSet nulls, int size) { - this.data = data; - this.nulls = nulls; - this.size = size; - } - - abstract A newArray(int size); - - abstract void setProto(A array, int i, com.google.protobuf.Value protoValue); - - abstract T get(A array, int i); - - @Override - public T get(int index) { - if (index < 0 || index >= size) { - throw new ArrayIndexOutOfBoundsException("index=" + index + " size=" + size); - } - return nulls.get(index) ? null : get(data, index); - } - - @Override - public int size() { - return size; - } - - A toPrimitiveArray(int columnIndex) { - if (nulls.length() > 0) { - throw throwNotNull(columnIndex); - } - A r = newArray(size); - System.arraycopy(data, 0, r, 0, size); - return r; - } - } - - private static class Int64Array extends PrimitiveArray { - Int64Array(ListValue protoList) { - super(protoList); - } - - Int64Array(long[] data, BitSet nulls) { - super(data, nulls, data.length); - } - @Override - long[] newArray(int size) { - return new long[size]; - } - - @Override - void setProto(long[] array, int i, com.google.protobuf.Value protoValue) { - array[i] = Long.parseLong(protoValue.getStringValue()); - } - - @Override - Long get(long[] array, int i) { - return array[i]; - } - } - - private static class Float64Array extends PrimitiveArray { - Float64Array(ListValue protoList) { - super(protoList); - } - - Float64Array(double[] data, BitSet nulls) { - super(data, nulls, data.length); - } - - @Override - double[] newArray(int size) { - return new double[size]; - } - - @Override - void setProto(double[] array, int i, com.google.protobuf.Value protoValue) { - array[i] = valueProtoToFloat64(protoValue); - } - - @Override - Double get(double[] array, int i) { - return array[i]; - } - } } 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 3d49d599ad41..16fe4bfda11b 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 @@ -25,15 +25,10 @@ import com.google.common.collect.Lists; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.BitSet; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; + import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import java.util.*; /** * Represents a value to be consumed by the Cloud Spanner API. A value can be {@code NULL} or @@ -442,6 +437,20 @@ static Value structArray(Iterable fieldTypes, @Nullable Iterab abstract com.google.protobuf.Value toProto(); + /** + * Returns a com.google.protobuf.Value that preserves type information, so it is possible ot + * recover all the original value from it. + * + * @return com.google.protobuf.Value of a list [serialized type proto value, value] + */ + com.google.protobuf.Value toTypedProto() { + com.google.protobuf.Value.Builder valueBuilder = com.google.protobuf.Value.newBuilder(); + ListValue.Builder listBuilder = valueBuilder.getListValueBuilder(); + listBuilder.addValuesBuilder().setStringValueBytes(getType().toProto().toByteString()).build(); + listBuilder.addValues(toProto()); + return valueBuilder.build(); + } + private static List immutableCopyOf(Iterable v) { ArrayList copy = new ArrayList<>(); Iterables.addAll(copy, v); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java index fcfea6191b2d..25c0319da7fb 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java @@ -16,13 +16,12 @@ package com.google.cloud.spanner; -import static com.google.common.truth.Truth.assertThat; - +import com.google.cloud.ByteArray; +import com.google.cloud.Date; +import com.google.cloud.Timestamp; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.testing.EqualsTester; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; import org.junit.Rule; @@ -31,6 +30,13 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.google.common.testing.SerializableTester.reserializeAndAssert; +import static com.google.common.truth.Truth.assertThat; + /** Unit tests for {@link com.google.cloud.spanner.Mutation}. */ @RunWith(JUnit4.class) public class MutationTest { @@ -399,6 +405,85 @@ public void toProtoCoalescingDeleteChanges() { "insert { table: 'T2', columns: 'C', values { values { string_value: 'V1' } } }")); } + @Test + public void javaSerialization() throws Exception { + reserializeAndAssert(appendAllTypes(Mutation.newInsertBuilder("test")).build()); + reserializeAndAssert(appendAllTypes(Mutation.newUpdateBuilder("test")).build()); + reserializeAndAssert(appendAllTypes(Mutation.newReplaceBuilder("test")).build()); + reserializeAndAssert(appendAllTypes(Mutation.newInsertOrUpdateBuilder("test")).build()); + + reserializeAndAssert(Mutation.delete("test", Key.of("one", 2, null, true, 2.3, ByteArray + .fromBase64("abcd"), Timestamp.ofTimeSecondsAndNanos(1, 2), Date.fromYearMonthDay + (2017, 04, 17) + ))); + reserializeAndAssert(Mutation.delete("test", KeySet.all())); + reserializeAndAssert(Mutation.delete("test", KeySet.newBuilder().addRange(KeyRange + .closedClosed(Key.of("one", 2, null), Key.of("two", 3, null))).build())); + reserializeAndAssert(Mutation.delete("test", KeySet.newBuilder().addRange(KeyRange + .closedOpen(Key.of("one", 2, null), Key.of("two", 3, null))).build())); + reserializeAndAssert(Mutation.delete("test", KeySet.newBuilder().addRange(KeyRange + .openClosed(Key.of("one", 2, null), Key.of("two", 3, null))).build())); + reserializeAndAssert(Mutation.delete("test", KeySet.newBuilder().addRange(KeyRange + .openOpen(Key.of("one", 2, null), Key.of("two", 3, null))).build())); + } + + private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) { + return builder + .set("bool") + .to(true) + .set("boolNull") + .to((Boolean) null) + .set("int") + .to(42) + .set("intNull") + .to((Long) null) + .set("float") + .to(42.1) + .set("floatNull") + .to((Double) null) + .set("string") + .to("str") + .set("stringNull") + .to((String) null) + .set("boolArr") + .toBoolArray(new boolean[] {true, false}) + .set("boolArrNull") + .toBoolArray((boolean[]) null) + .set("intArr") + .toInt64Array(new long[] {1, 2, 3}) + .set("intArrNull") + .toInt64Array((long[]) null) + .set("floatArr") + .toFloat64Array(new double[] {1.1, 2.2, 3.3}) + .set("floatArrNull") + .toFloat64Array((double[]) null) + .set("nullStr") + .to((String) null) + .set("timestamp") + .to(Timestamp.MAX_VALUE) + .set("timestampNull") + .to((Timestamp) null) + .set("date") + .to(Date.fromYearMonthDay(2017, 04, 17)) + .set("dateNull") + .to((Date) null) + .set("stringArr") + .toStringArray(ImmutableList.of("one", "two")) + .set("stringArrNull") + .toStringArray(null) + .set("timestampArr") + .toTimestampArray(ImmutableList.of(Timestamp.MAX_VALUE, Timestamp.MAX_VALUE)) + .set("timestampArrNull") + .toTimestampArray(null) + .set("dateArr") + .toDateArray( + ImmutableList.of( + Date.fromYearMonthDay(2017, 04, 17), Date.fromYearMonthDay(2017, 04, 18))) + .set("dateArrNull") + .toDateArray(null); + } + + static Matcher matchesProto(String expected) { return SpannerMatchers.matchesProto(com.google.spanner.v1.Mutation.class, expected); }