Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloud Spanner: Make Spanner value classes serializable #2040

Merged
merged 17 commits into from
Jun 6, 2017
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import io.grpc.Context;
import io.grpc.ManagedChannel;
import java.io.IOException;
import java.io.Serializable;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.BitSet;
Expand Down Expand Up @@ -1511,6 +1512,8 @@ public Transaction call() throws Exception {
* @param <R> the type of row data
*/
private static class BaseStruct<R> extends Struct {
private static final long serialVersionUID = -1850257565078946587L;

protected final Type type;
protected final List<Object> rowData;

Expand Down Expand Up @@ -1838,6 +1841,8 @@ private SpannerException yieldError(SpannerException e) {
}

private static class GrpcStruct extends BaseStruct<List<Object>> {
private static final long serialVersionUID = -2067943506736181881L;

GrpcStruct(Type type, List<Object> rowData) {
super(type, rowData);
}
Expand Down Expand Up @@ -1898,29 +1903,15 @@ 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<com.google.protobuf.Value, Boolean>() {
@Override
public Boolean apply(com.google.protobuf.Value input) {
return input.getKindCase() == KindCase.NULL_VALUE ? null : input.getBoolValue();
}
});
return new BoolArray(listValue);
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<com.google.protobuf.Value, String>() {
@Override
public String apply(com.google.protobuf.Value input) {
return input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue();
}
});
return new StringArray(listValue);
case BYTES:
{
// Materialize list: element conversion is expensive and should happen only once.
Expand Down Expand Up @@ -2418,7 +2409,8 @@ private static NullPointerException throwNotNull(int columnIndex) {
* {@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<T, A> extends AbstractList<T> {
private abstract static class PrimitiveArray<T, A> extends AbstractList<T> implements Serializable {
private static final long serialVersionUID = -3635424493234309483L;
private final A data;
private final BitSet nulls;
private final int size;
Expand Down Expand Up @@ -2474,6 +2466,8 @@ A toPrimitiveArray(int columnIndex) {
}

private static class Int64Array extends PrimitiveArray<Long, long[]> {

This comment was marked as spam.

private static final long serialVersionUID = -1119776393736089987L;

Int64Array(ListValue protoList) {
super(protoList);
}
Expand All @@ -2499,6 +2493,8 @@ Long get(long[] array, int i) {
}

private static class Float64Array extends PrimitiveArray<Double, double[]> {
private static final long serialVersionUID = 615840120676713454L;

Float64Array(ListValue protoList) {
super(protoList);
}
Expand All @@ -2522,4 +2518,44 @@ Double get(double[] array, int i) {
return array[i];
}
}

private static class StringArray extends AbstractList<String> implements Serializable {
private static final long serialVersionUID = 695127243179520960L;
private final ListValue listValue;

private StringArray(ListValue listValue) {
this.listValue = listValue;
}

@Override
public String get(int index) {
com.google.protobuf.Value value = listValue.getValues(index);
return value.getKindCase() == KindCase.NULL_VALUE ? null : value.getStringValue();
}

@Override
public int size() {
return listValue.getValuesCount();
}
}

private static class BoolArray extends AbstractList<Boolean> implements Serializable {
private static final long serialVersionUID = -2850504708084921083L;
private final ListValue listValue;

private BoolArray(ListValue listValue) {
this.listValue = listValue;
}

@Override
public Boolean get(int index) {
com.google.protobuf.Value value = listValue.getValues(index);
return value.getKindCase() == KindCase.NULL_VALUE ? null : value.getBoolValue();
}

@Override
public int size() {
return listValue.getValuesCount();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.common.collect.ImmutableMap;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -50,7 +52,9 @@
*
* <p>{@code Statement} instances are immutable.
*/
public final class Statement {
public final class Statement implements Serializable {
private static final long serialVersionUID = -1967958247625065259L;

private final ImmutableMap<String, Value> parameters;

This comment was marked as spam.

This comment was marked as spam.

private final String sql;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
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;
Expand All @@ -41,7 +43,9 @@
* <p>{@code Struct} instances are immutable.
*/
@Immutable
public abstract class Struct extends AbstractStructReader {
public abstract class Struct extends AbstractStructReader implements Serializable {

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

private static final long serialVersionUID = 1698871272663044565L;

// Only implementations within the package are allowed.
Struct() {}

Expand Down Expand Up @@ -121,6 +125,8 @@ private void checkBindingInProgress(boolean expectInProgress) {

/** Default implementation for test value structs produced by {@link Builder}. */
private static class ValueListStruct extends Struct {
private static final long serialVersionUID = 1712981659934867577L;

private final Type type;
private final List<Value> values;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.google.protobuf.Duration;
import com.google.protobuf.util.Durations;
import com.google.spanner.v1.TransactionOptions;

import java.io.Serializable;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -115,10 +117,11 @@
* @see Session#singleUseReadOnlyTransaction(TimestampBound)
* @see Session#readOnlyTransaction(TimestampBound)
*/
public final class TimestampBound {
public final class TimestampBound implements Serializable {
private static final TimestampBound STRONG_BOUND = new TimestampBound(Mode.STRONG, null, null);
private static final TransactionOptions.ReadOnly STRONG_PROTO =
TransactionOptions.ReadOnly.newBuilder().setStrong(true).build();
private static final long serialVersionUID = 9194565742651275731L;

private final Mode mode;
private final Timestamp timestamp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ static Code fromProtoCode(TypeCode protoCode) {
}

/** Describes an individual field in a {@code STRUCT type}. */
public static final class StructField {
public static final class StructField implements Serializable {
private static final long serialVersionUID = 8640511292704408210L;

private final String name;
private final Type type;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
package com.google.cloud.spanner;

import static com.google.cloud.spanner.SpannerMatchers.isSpannerException;
import static com.google.common.testing.SerializableTester.reserializeAndAssert;
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.cloud.spanner.spi.v1.SpannerRpc;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ByteString;
import com.google.protobuf.NullValue;
Expand Down Expand Up @@ -588,4 +592,50 @@ private static ResultSetMetadata makeMetadata(Type rowType) {
com.google.spanner.v1.Type typeProto = rowType.toProto();
return ResultSetMetadata.newBuilder().setRowType(typeProto.getStructType()).build();
}

@Test
public void serialization() throws Exception {
verifySerialization(Value.string("a"),
Value.string(null),
Value.bool(true),
Value.bool(null),
Value.int64(1),
Value.int64(null),
Value.float64(1.0),
Value.float64(null),
Value.bytes(ByteArray.fromBase64("abcd")),
Value.bytes(null),
Value.timestamp(Timestamp.ofTimeSecondsAndNanos(1, 2)),
Value.timestamp(null),
Value.date(Date.fromYearMonthDay(2017, 04, 17)),
Value.date(null),
Value.stringArray(ImmutableList.of("one", "two")),
Value.stringArray(null),
Value.boolArray(new boolean[]{true, false}),
Value.boolArray((boolean[]) null),
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)
);
}

private void verifySerialization(Value... values) {
resultSet = new SpannerImpl.GrpcResultSet(stream, new NoOpListener(), QueryMode.NORMAL);
PartialResultSet.Builder builder = PartialResultSet.newBuilder();
List<Type.StructField> types = new ArrayList<>();
for (Value value : values) {
types.add(Type.StructField.of("f", value.getType()));
builder.addValues(value.toProto());
}
consumer.onPartialResultSet(
builder.setMetadata(makeMetadata(Type.struct(types)))
.build());
consumer.onCompleted();
assertThat(resultSet.next()).isTrue();
reserializeAndAssert(resultSet.getCurrentRowAsStruct());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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.collect.ImmutableMap;
Expand All @@ -38,6 +39,7 @@ public void basic() {
assertThat(stmt.getSql()).isEqualTo(sql);
assertThat(stmt.getParameters()).isEmpty();
assertThat(stmt.toString()).isEqualTo(sql);
reserializeAndAssert(stmt);

This comment was marked as spam.

This comment was marked as spam.

}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.cloud.spanner;

import static com.google.cloud.spanner.TimestampBound.Mode;
import static com.google.common.testing.SerializableTester.reserializeAndAssert;
import static com.google.common.truth.Truth.assertThat;

import com.google.cloud.Timestamp;
Expand All @@ -37,6 +38,15 @@ public class TimestampBoundTest {
private static final String TEST_TIME_ISO = "2015-10-12T15:14:54Z";
@Rule public ExpectedException expectedException = ExpectedException.none();

@Test
public void serialization() throws Exception {
reserializeAndAssert(TimestampBound.strong());
reserializeAndAssert(TimestampBound.ofExactStaleness(10, TimeUnit.NANOSECONDS));
reserializeAndAssert(TimestampBound.ofMaxStaleness(100, TimeUnit.DAYS));
reserializeAndAssert(TimestampBound.ofMinReadTimestamp(Timestamp.now()));
reserializeAndAssert(TimestampBound.ofReadTimestamp(Timestamp.now()));
}

@Test
public void strong() {
TimestampBound bound = TimestampBound.strong();
Expand Down