diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 13462a986c..ca6ae666a2 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -256,11 +256,53 @@ com/google/cloud/spanner/spi/v1/SpannerRpc com.google.api.gax.retrying.RetrySettings getPartitionedDmlRetrySettings() - + 7012 com/google/cloud/spanner/spi/v1/SpannerRpc com.google.api.gax.rpc.ServerStream executeStreamingPartitionedDml(com.google.spanner.v1.ExecuteSqlRequest, java.util.Map, org.threeten.bp.Duration) + + + + 7013 + com/google/cloud/spanner/AbstractStructReader + java.math.BigDecimal getBigDecimalInternal(int) + + + 7013 + com/google/cloud/spanner/AbstractStructReader + java.util.List getBigDecimalListInternal(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.math.BigDecimal getBigDecimal(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.math.BigDecimal getBigDecimal(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getBigDecimalList(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getBigDecimalList(java.lang.String) + + + 7013 + com/google/cloud/spanner/Value + java.math.BigDecimal getNumeric() + + + 7013 + com/google/cloud/spanner/Value + java.util.List getNumericArray() + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 1dfea318e0..11f36f4a92 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -52,6 +52,7 @@ import io.opencensus.trace.Tracing; import java.io.IOException; import java.io.Serializable; +import java.math.BigDecimal; import java.util.AbstractList; import java.util.ArrayList; import java.util.BitSet; @@ -358,6 +359,9 @@ private Object writeReplace() { case FLOAT64: builder.set(fieldName).to((Double) value); break; + case NUMERIC: + builder.set(fieldName).to((BigDecimal) value); + break; case STRING: builder.set(fieldName).to((String) value); break; @@ -381,6 +385,9 @@ private Object writeReplace() { case FLOAT64: builder.set(fieldName).toFloat64Array((Iterable) value); break; + case NUMERIC: + builder.set(fieldName).toNumericArray((Iterable) value); + break; case STRING: builder.set(fieldName).toStringArray((Iterable) value); break; @@ -457,6 +464,8 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot return Long.parseLong(proto.getStringValue()); case FLOAT64: return valueProtoToFloat64(proto); + case NUMERIC: + return new BigDecimal(proto.getStringValue()); case STRING: checkType(fieldType, proto, KindCase.STRING_VALUE); return proto.getStringValue(); @@ -513,6 +522,18 @@ public Boolean apply(com.google.protobuf.Value input) { return new Int64Array(listValue); case FLOAT64: return new Float64Array(listValue); + case NUMERIC: + { + // 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 + : new BigDecimal(value.getStringValue())); + } + return list; + } case STRING: return Lists.transform( listValue.getValuesList(), @@ -620,6 +641,11 @@ protected double getDoubleInternal(int columnIndex) { return (Double) rowData.get(columnIndex); } + @Override + protected BigDecimal getBigDecimalInternal(int columnIndex) { + return (BigDecimal) rowData.get(columnIndex); + } + @Override protected String getStringInternal(int columnIndex) { return (String) rowData.get(columnIndex); @@ -685,6 +711,12 @@ protected Float64Array getDoubleListInternal(int columnIndex) { return (Float64Array) rowData.get(columnIndex); } + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getBigDecimalListInternal(int columnIndex) { + return (List) rowData.get(columnIndex); + } + @Override @SuppressWarnings("unchecked") // We know ARRAY produces a List. protected List getStringListInternal(int columnIndex) { @@ -1176,6 +1208,11 @@ protected double getDoubleInternal(int columnIndex) { return currRow().getDoubleInternal(columnIndex); } + @Override + protected BigDecimal getBigDecimalInternal(int columnIndex) { + return currRow().getBigDecimalInternal(columnIndex); + } + @Override protected String getStringInternal(int columnIndex) { return currRow().getStringInternal(columnIndex); @@ -1226,6 +1263,11 @@ protected List getDoubleListInternal(int columnIndex) { return currRow().getDoubleListInternal(columnIndex); } + @Override + protected List getBigDecimalListInternal(int columnIndex) { + return currRow().getBigDecimalListInternal(columnIndex); + } + @Override protected List getStringListInternal(int columnIndex) { return currRow().getStringListInternal(columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java index d943562c73..75e683e2e1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java @@ -21,6 +21,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import java.math.BigDecimal; import java.util.List; /** @@ -38,6 +39,8 @@ public abstract class AbstractStructReader implements StructReader { protected abstract double getDoubleInternal(int columnIndex); + protected abstract BigDecimal getBigDecimalInternal(int columnIndex); + protected abstract String getStringInternal(int columnIndex); protected abstract ByteArray getBytesInternal(int columnIndex); @@ -58,6 +61,8 @@ public abstract class AbstractStructReader implements StructReader { protected abstract List getDoubleListInternal(int columnIndex); + protected abstract List getBigDecimalListInternal(int columnIndex); + protected abstract List getStringListInternal(int columnIndex); protected abstract List getBytesListInternal(int columnIndex); @@ -127,6 +132,19 @@ public double getDouble(String columnName) { return getDoubleInternal(columnIndex); } + @Override + public BigDecimal getBigDecimal(int columnIndex) { + checkNonNullOfType(columnIndex, Type.numeric(), columnIndex); + return getBigDecimalInternal(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.numeric(), columnName); + return getBigDecimalInternal(columnIndex); + } + @Override public String getString(int columnIndex) { checkNonNullOfType(columnIndex, Type.string(), columnIndex); @@ -257,6 +275,19 @@ public List getDoubleList(String columnName) { return getDoubleListInternal(columnIndex); } + @Override + public List getBigDecimalList(int columnIndex) { + checkNonNullOfType(columnIndex, Type.array(Type.numeric()), columnIndex); + return getBigDecimalListInternal(columnIndex); + } + + @Override + public List getBigDecimalList(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.array(Type.numeric()), columnName); + return getBigDecimalListInternal(columnIndex); + } + @Override public List getStringList(int columnIndex) { checkNonNullOfType(columnIndex, Type.array(Type.string()), columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java index 67e546ad5a..f7e49daafd 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java @@ -22,6 +22,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; +import java.math.BigDecimal; import java.util.List; /** Forwarding implements of StructReader */ @@ -133,6 +134,16 @@ public double getDouble(String columnName) { return delegate.get().getDouble(columnName); } + @Override + public BigDecimal getBigDecimal(int columnIndex) { + return delegate.get().getBigDecimal(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(String columnName) { + return delegate.get().getBigDecimal(columnName); + } + @Override public String getString(int columnIndex) { checkValidState(); @@ -253,6 +264,16 @@ public List getDoubleList(String columnName) { return delegate.get().getDoubleList(columnName); } + @Override + public List getBigDecimalList(int columnIndex) { + return delegate.get().getBigDecimalList(columnIndex); + } + + @Override + public List getBigDecimalList(String columnName) { + return delegate.get().getBigDecimalList(columnName); + } + @Override public List getStringList(int columnIndex) { checkValidState(); 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 bbe07f54f9..a8fc088b87 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 @@ -26,6 +26,7 @@ import com.google.protobuf.NullValue; import com.google.protobuf.Value; import java.io.Serializable; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -131,6 +132,11 @@ public Builder append(@Nullable Double value) { buffer.add(value); return this; } + /** Appends a {@code NUMERIC} value to the key. */ + public Builder append(@Nullable BigDecimal value) { + buffer.add(value); + return this; + } /** Appends a {@code STRING} value to the key. */ public Builder append(@Nullable String value) { buffer.add(value); @@ -172,6 +178,8 @@ public Builder appendObject(@Nullable Object value) { append((Float) value); } else if (value instanceof Double) { append((Double) value); + } else if (value instanceof BigDecimal) { + append((BigDecimal) value); } else if (value instanceof String) { append((String) value); } else if (value instanceof ByteArray) { @@ -215,6 +223,7 @@ public int size() { *
  • {@code BOOL} is represented by {@code Boolean} *
  • {@code INT64} is represented by {@code Long} *
  • {@code FLOAT64} is represented by {@code Double} + *
  • {@code NUMERIC} is represented by {@code BigDecimal} *
  • {@code STRING} is represented by {@code String} *
  • {@code BYTES} is represented by {@link ByteArray} *
  • {@code TIMESTAMP} is represented by {@link Timestamp} @@ -276,6 +285,8 @@ ListValue toProto() { builder.addValuesBuilder().setStringValue(part.toString()); } else if (part instanceof Double) { builder.addValuesBuilder().setNumberValue((Double) part); + } else if (part instanceof BigDecimal) { + builder.addValuesBuilder().setStringValue(part.toString()); } else if (part instanceof String) { builder.addValuesBuilder().setStringValue((String) part); } else if (part instanceof ByteArray) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java index 278b15d967..ee9e715a25 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java @@ -27,6 +27,7 @@ import com.google.common.collect.Lists; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.spanner.v1.ResultSetStats; +import java.math.BigDecimal; import java.util.List; /** Utility methods for working with {@link com.google.cloud.spanner.ResultSet}. */ @@ -186,6 +187,16 @@ public double getDouble(String columnName) { return getCurrentRowAsStruct().getDouble(columnName); } + @Override + public BigDecimal getBigDecimal(int columnIndex) { + return getCurrentRowAsStruct().getBigDecimal(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(String columnName) { + return getCurrentRowAsStruct().getBigDecimal(columnName); + } + @Override public String getString(int columnIndex) { return getCurrentRowAsStruct().getString(columnIndex); @@ -286,6 +297,16 @@ public List getDoubleList(String columnName) { return getCurrentRowAsStruct().getDoubleList(columnName); } + @Override + public List getBigDecimalList(int columnIndex) { + return getCurrentRowAsStruct().getBigDecimalList(columnIndex); + } + + @Override + public List getBigDecimalList(String columnName) { + return getCurrentRowAsStruct().getBigDecimalList(columnName); + } + @Override public List getStringList(int columnIndex) { return getCurrentRowAsStruct().getStringList(columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java index 0e916f009e..7982618948 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java @@ -29,6 +29,7 @@ import com.google.common.primitives.Doubles; import com.google.common.primitives.Longs; import java.io.Serializable; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -181,6 +182,11 @@ protected double getDoubleInternal(int columnIndex) { return values.get(columnIndex).getFloat64(); } + @Override + protected BigDecimal getBigDecimalInternal(int columnIndex) { + return values.get(columnIndex).getNumeric(); + } + @Override protected String getStringInternal(int columnIndex) { return values.get(columnIndex).getString(); @@ -236,6 +242,11 @@ protected List getDoubleListInternal(int columnIndex) { return values.get(columnIndex).getFloat64Array(); } + @Override + protected List getBigDecimalListInternal(int columnIndex) { + return values.get(columnIndex).getNumericArray(); + } + @Override protected List getStringListInternal(int columnIndex) { return values.get(columnIndex).getStringArray(); @@ -321,6 +332,8 @@ private Object getAsObject(int columnIndex) { return getLongInternal(columnIndex); case FLOAT64: return getDoubleInternal(columnIndex); + case NUMERIC: + return getBigDecimalInternal(columnIndex); case STRING: return getStringInternal(columnIndex); case BYTES: @@ -339,6 +352,8 @@ private Object getAsObject(int columnIndex) { return getLongListInternal(columnIndex); case FLOAT64: return getDoubleListInternal(columnIndex); + case NUMERIC: + return getBigDecimalListInternal(columnIndex); case STRING: return getStringListInternal(columnIndex); case BYTES: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java index c8b3f02c01..33cffeed35 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java @@ -19,6 +19,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import java.math.BigDecimal; import java.util.List; /** @@ -101,6 +102,12 @@ public interface StructReader { /** Returns the value of a non-{@code NULL} column with type {@link Type#float64()}. */ double getDouble(String columnName); + /** Returns the value of a non-{@code NULL} column with type {@link Type#numeric()}. */ + BigDecimal getBigDecimal(int columnIndex); + + /** Returns the value of a non-{@code NULL} column with type {@link Type#numeric()}. */ + BigDecimal getBigDecimal(String columnName); + /** Returns the value of a non-{@code NULL} column with type {@link Type#string()}. */ String getString(int columnIndex); @@ -195,6 +202,16 @@ public interface StructReader { */ List getDoubleList(String columnName); + /** + * Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.numeric())}. + */ + List getBigDecimalList(int columnIndex); + + /** + * Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.numeric())}. + */ + List getBigDecimalList(String columnName); + /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.string())}. */ List getStringList(int columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java index 2698b131e8..5bd986b80c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java @@ -44,6 +44,7 @@ public final class Type implements Serializable { private static final Type TYPE_BOOL = new Type(Code.BOOL, null, null); private static final Type TYPE_INT64 = new Type(Code.INT64, null, null); private static final Type TYPE_FLOAT64 = new Type(Code.FLOAT64, null, null); + private static final Type TYPE_NUMERIC = new Type(Code.NUMERIC, null, null); private static final Type TYPE_STRING = new Type(Code.STRING, null, null); private static final Type TYPE_BYTES = new Type(Code.BYTES, null, null); private static final Type TYPE_TIMESTAMP = new Type(Code.TIMESTAMP, null, null); @@ -51,6 +52,7 @@ public final class Type implements Serializable { private static final Type TYPE_ARRAY_BOOL = new Type(Code.ARRAY, TYPE_BOOL, null); private static final Type TYPE_ARRAY_INT64 = new Type(Code.ARRAY, TYPE_INT64, null); private static final Type TYPE_ARRAY_FLOAT64 = new Type(Code.ARRAY, TYPE_FLOAT64, null); + private static final Type TYPE_ARRAY_NUMERIC = new Type(Code.ARRAY, TYPE_NUMERIC, null); private static final Type TYPE_ARRAY_STRING = new Type(Code.ARRAY, TYPE_STRING, null); private static final Type TYPE_ARRAY_BYTES = new Type(Code.ARRAY, TYPE_BYTES, null); private static final Type TYPE_ARRAY_TIMESTAMP = new Type(Code.ARRAY, TYPE_TIMESTAMP, null); @@ -80,6 +82,11 @@ public static Type float64() { return TYPE_FLOAT64; } + /** Returns the descriptor for the {@code NUMERIC} type. */ + public static Type numeric() { + return TYPE_NUMERIC; + } + /** * Returns the descriptor for the {@code STRING} type: a variable-length Unicode character string. */ @@ -118,6 +125,8 @@ public static Type array(Type elementType) { return TYPE_ARRAY_INT64; case FLOAT64: return TYPE_ARRAY_FLOAT64; + case NUMERIC: + return TYPE_ARRAY_NUMERIC; case STRING: return TYPE_ARRAY_STRING; case BYTES: @@ -170,6 +179,7 @@ private Type( public enum Code { BOOL(TypeCode.BOOL), INT64(TypeCode.INT64), + NUMERIC(TypeCode.NUMERIC), FLOAT64(TypeCode.FLOAT64), STRING(TypeCode.STRING), BYTES(TypeCode.BYTES), @@ -380,6 +390,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) { return int64(); case FLOAT64: return float64(); + case NUMERIC: + return numeric(); case STRING: return string(); case BYTES: 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 c32b5fde81..fa0224ff95 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 @@ -27,6 +27,7 @@ import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; import java.io.Serializable; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; @@ -121,6 +122,15 @@ public static Value float64(double v) { return new Float64Impl(false, v); } + /** + * Returns a {@code NUMERIC} value. + * + * @param v the value, which may be null + */ + public static Value numeric(@Nullable BigDecimal v) { + return new NumericImpl(v == null, v); + } + /** * Returns a {@code STRING} value. * @@ -283,6 +293,16 @@ public static Value float64Array(@Nullable Iterable v) { return float64ArrayFactory.create(v); } + /** + * Returns an {@code ARRAY} value. + * + * @param v the source of element values. This may be {@code null} to produce a value for which + * {@code isNull()} is {@code true}. Individual elements may also be {@code null}. + */ + public static Value numericArray(@Nullable Iterable v) { + return new NumericArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); + } + /** * Returns an {@code ARRAY} value. * @@ -381,6 +401,13 @@ private Value() {} */ public abstract double getFloat64(); + /** + * Returns the value of a {@code NUMERIC}-typed instance. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public abstract BigDecimal getNumeric(); + /** * Returns the value of a {@code STRING}-typed instance. * @@ -444,6 +471,14 @@ private Value() {} */ public abstract List getFloat64Array(); + /** + * Returns the value of an {@code ARRAY}-typed instance. While the returned list itself + * will never be {@code null}, elements of that list may be null. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public abstract List getNumericArray(); + /** * 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. @@ -649,6 +684,11 @@ public double getFloat64() { throw defaultGetter(Type.float64()); } + @Override + public BigDecimal getNumeric() { + throw defaultGetter(Type.numeric()); + } + @Override public String getString() { throw defaultGetter(Type.string()); @@ -693,6 +733,11 @@ public List getFloat64Array() { throw defaultGetter(Type.array(Type.float64())); } + @Override + public List getNumericArray() { + throw defaultGetter(Type.array(Type.numeric())); + } + @Override public List getStringArray() { throw defaultGetter(Type.array(Type.string())); @@ -943,6 +988,25 @@ int valueHash() { } } + private static class DateImpl extends AbstractObjectValue { + + private DateImpl(boolean isNull, Date value) { + super(isNull, Type.date(), value); + } + + @Override + public Date getDate() { + checkType(Type.date()); + checkNotNull(); + return value; + } + + @Override + void valueToString(StringBuilder b) { + b.append(value); + } + } + private static class StringImpl extends AbstractObjectValue { private StringImpl(boolean isNull, @Nullable String value) { @@ -1052,15 +1116,15 @@ int valueHash() { } } - private static class DateImpl extends AbstractObjectValue { + private static class NumericImpl extends AbstractObjectValue { - private DateImpl(boolean isNull, Date value) { - super(isNull, Type.date(), value); + private NumericImpl(boolean isNull, BigDecimal value) { + super(isNull, Type.numeric(), value); } @Override - public Date getDate() { - checkType(Type.date()); + public BigDecimal getNumeric() { + checkType(Type.numeric()); checkNotNull(); return value; } @@ -1381,6 +1445,25 @@ void appendElement(StringBuilder b, Date element) { } } + private static class NumericArrayImpl extends AbstractArrayValue { + + private NumericArrayImpl(boolean isNull, @Nullable List values) { + super(isNull, Type.numeric(), values); + } + + @Override + public List getNumericArray() { + checkType(getType()); + checkNotNull(); + return value; + } + + @Override + void appendElement(StringBuilder b, BigDecimal element) { + b.append(element); + } + } + private static class StructImpl extends AbstractObjectValue { // Constructor for non-NULL struct values. @@ -1428,6 +1511,8 @@ private Value getValue(int fieldIndex) { return Value.bytes(value.getBytes(fieldIndex)); case FLOAT64: return Value.float64(value.getDouble(fieldIndex)); + case NUMERIC: + return Value.numeric(value.getBigDecimal(fieldIndex)); case DATE: return Value.date(value.getDate(fieldIndex)); case TIMESTAMP: @@ -1448,6 +1533,8 @@ private Value getValue(int fieldIndex) { return Value.bytesArray(value.getBytesList(fieldIndex)); case FLOAT64: return Value.float64Array(value.getDoubleArray(fieldIndex)); + case NUMERIC: + return Value.numericArray(value.getBigDecimalList(fieldIndex)); case DATE: return Value.dateArray(value.getDateList(fieldIndex)); case TIMESTAMP: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java index 5b663211f5..d6c5de4825 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java @@ -19,6 +19,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import java.math.BigDecimal; import javax.annotation.Nullable; /** @@ -84,6 +85,11 @@ public R to(@Nullable Double value) { return handle(Value.float64(value)); } + /** Binds to {@code Value.numeric(value)} */ + public R to(BigDecimal value) { + return handle(Value.numeric(value)); + } + /** Binds to {@code Value.string(value)} */ public R to(@Nullable String value) { return handle(Value.string(value)); @@ -162,6 +168,11 @@ public R toFloat64Array(@Nullable Iterable values) { return handle(Value.float64Array(values)); } + /** Binds to {@code Value.numericArray(values)} */ + public R toNumericArray(@Nullable Iterable values) { + return handle(Value.numericArray(values)); + } + /** Binds to {@code Value.stringArray(values)} */ public R toStringArray(@Nullable Iterable values) { return handle(Value.stringArray(values)); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java index d8e0e85844..649d6c51fd 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java @@ -36,6 +36,7 @@ import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.google.common.hash.PrimitiveSink; +import java.math.BigDecimal; import java.util.Objects; import java.util.concurrent.Callable; @@ -232,6 +233,9 @@ public void funnel(Struct row, PrimitiveSink into) { case FLOAT64: funnelValue(type, row.getDouble(i), into); break; + case NUMERIC: + funnelValue(type, row.getBigDecimal(i), into); + break; case INT64: funnelValue(type, row.getLong(i), into); break; @@ -278,6 +282,12 @@ private void funnelArray( funnelValue(Code.FLOAT64, value, into); } break; + case NUMERIC: + into.putInt(row.getBigDecimalList(columnIndex).size()); + for (BigDecimal value : row.getBigDecimalList(columnIndex)) { + funnelValue(Code.NUMERIC, value, into); + } + break; case INT64: into.putInt(row.getLongList(columnIndex).size()); for (Long value : row.getLongList(columnIndex)) { @@ -331,6 +341,11 @@ private void funnelValue(Code type, T value, PrimitiveSink into) { case FLOAT64: into.putDouble((Double) value); break; + case NUMERIC: + String stringRepresentation = ((BigDecimal) value).toString(); + into.putInt(stringRepresentation.length()); + into.putUnencodedChars(stringRepresentation); + break; case INT64: into.putLong((Long) value); break; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java index 9c24b8c470..69782517d9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java @@ -25,6 +25,7 @@ import com.google.cloud.spanner.Type; import com.google.common.base.Preconditions; import com.google.spanner.v1.ResultSetStats; +import java.math.BigDecimal; import java.util.List; /** @@ -164,6 +165,18 @@ public double getDouble(int columnIndex) { return delegate.getDouble(columnIndex); } + @Override + public BigDecimal getBigDecimal(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getBigDecimal(columnName); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getBigDecimal(columnIndex); + } + @Override public double getDouble(String columnName) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); @@ -290,6 +303,18 @@ public List getDoubleList(String columnName) { return delegate.getDoubleList(columnName); } + @Override + public List getBigDecimalList(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getBigDecimalList(columnIndex); + } + + @Override + public List getBigDecimalList(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getBigDecimalList(columnName); + } + @Override public List getStringList(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java index 25ae1d3074..2bd4f947cc 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java @@ -27,6 +27,7 @@ import com.google.cloud.spanner.Type; import com.google.common.base.Preconditions; import com.google.spanner.v1.ResultSetStats; +import java.math.BigDecimal; import java.util.List; /** @@ -170,6 +171,18 @@ public double getDouble(String columnName) { return delegate.getDouble(columnName); } + @Override + public BigDecimal getBigDecimal(int columnIndex) { + checkClosed(); + return delegate.getBigDecimal(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(String columnName) { + checkClosed(); + return delegate.getBigDecimal(columnName); + } + @Override public String getString(int columnIndex) { checkClosed(); @@ -290,6 +303,18 @@ public List getDoubleList(String columnName) { return delegate.getDoubleList(columnName); } + @Override + public List getBigDecimalList(int columnIndex) { + checkClosed(); + return delegate.getBigDecimalList(columnIndex); + } + + @Override + public List getBigDecimalList(String columnName) { + checkClosed(); + return delegate.getBigDecimalList(columnName); + } + @Override public List getStringList(int columnIndex) { checkClosed(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java index bde868fe55..49154980d4 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java @@ -29,6 +29,7 @@ import com.google.common.base.Throwables; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.math.BigDecimal; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -58,6 +59,11 @@ protected double getDoubleInternal(int columnIndex) { return 0; } + @Override + protected BigDecimal getBigDecimalInternal(int columnIndex) { + return null; + } + @Override protected String getStringInternal(int columnIndex) { return null; @@ -108,6 +114,11 @@ protected List getDoubleListInternal(int columnIndex) { return null; } + @Override + protected List getBigDecimalListInternal(int columnIndex) { + return null; + } + @Override protected List getStringListInternal(int columnIndex) { return null; @@ -154,6 +165,13 @@ public static Collection parameters() { {Type.bool(), "getBooleanInternal", false, "getBoolean", null}, {Type.int64(), "getLongInternal", 123L, "getLong", null}, {Type.float64(), "getDoubleInternal", 2.0, "getDouble", null}, + { + Type.numeric(), + "getBigDecimalInternal", + BigDecimal.valueOf(21, 1), + "getBigDecimal", + null + }, {Type.string(), "getStringInternal", "a", "getString", null}, {Type.bytes(), "getBytesInternal", ByteArray.copyFrom(new byte[] {0}), "getBytes", null}, { @@ -206,6 +224,13 @@ public static Collection parameters() { "getDoubleList", Arrays.asList("getDoubleArray") }, + { + Type.array(Type.numeric()), + "getBigDecimalListInternal", + Arrays.asList(BigDecimal.valueOf(21, 1), BigDecimal.valueOf(41, 1)), + "getBigDecimalList", + null + }, { Type.array(Type.string()), "getStringListInternal", diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index 2ee73e75d3..4952e179ad 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -34,6 +34,7 @@ import com.google.spanner.v1.ResultSetMetadata; import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.Transaction; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -686,6 +687,28 @@ public void getDouble() { assertThat(resultSet.getDouble(0)).isWithin(0.0).of(Double.MAX_VALUE); } + @Test + public void getBigDecimal() { + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.numeric())))) + .addValues(Value.numeric(BigDecimal.valueOf(Double.MIN_VALUE)).toProto()) + .addValues(Value.numeric(BigDecimal.valueOf(Double.MAX_VALUE)).toProto()) + .addValues(Value.numeric(BigDecimal.ZERO).toProto()) + .addValues(Value.numeric(new BigDecimal("1.23456")).toProto()) + .build()); + consumer.onCompleted(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getBigDecimal(0).doubleValue()).isWithin(0.0).of(Double.MIN_VALUE); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getBigDecimal(0).doubleValue()).isWithin(0.0).of(Double.MAX_VALUE); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getBigDecimal(0)).isEqualTo(BigDecimal.ZERO); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getBigDecimal(0)).isEqualTo(BigDecimal.valueOf(123456, 5)); + } + @Test public void getLong() { consumer.onPartialResultSet( @@ -777,6 +800,26 @@ public void getDoubleArray() { .inOrder(); } + @Test + public void getBigDecimalList() { + List bigDecimalsList = new ArrayList(); + bigDecimalsList.add(BigDecimal.valueOf(Double.MIN_VALUE)); + bigDecimalsList.add(BigDecimal.valueOf(Double.MAX_VALUE)); + bigDecimalsList.add(BigDecimal.ZERO); + bigDecimalsList.add(new BigDecimal("1.23456")); + + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata(Type.struct(Type.StructField.of("f", Type.array(Type.numeric()))))) + .addValues(Value.numericArray(bigDecimalsList).toProto()) + .build()); + consumer.onCompleted(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getBigDecimalList(0)).isEqualTo(bigDecimalsList); + } + @Test public void getTimestampList() { List timestampList = new ArrayList<>(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java index a135e30888..e88d3b951c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java @@ -25,6 +25,7 @@ import com.google.common.testing.EqualsTester; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; +import java.math.BigDecimal; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -47,6 +48,7 @@ public void of() { assertThat(k.getParts()).containsExactly("a", null, "c").inOrder(); // All supported Java types: note coercion to canonical types. + String numeric = "3.141592"; String timestamp = "2015-09-15T00:00:00Z"; String date = "2015-09-15"; k = @@ -57,11 +59,12 @@ public void of() { 64L, 2.0f, 4.0d, + new BigDecimal(numeric), "x", ByteArray.copyFrom("y"), Timestamp.parseTimestamp(timestamp), Date.parseDate(date)); - assertThat(k.size()).isEqualTo(10); + assertThat(k.size()).isEqualTo(11); assertThat(k.getParts()) .containsExactly( null, @@ -70,6 +73,7 @@ public void of() { 64L, 2.0d, 4.0d, + BigDecimal.valueOf(3141592, 6), "x", ByteArray.copyFrom("y"), Timestamp.parseTimestamp(timestamp), @@ -84,6 +88,7 @@ public void of() { @Test public void builder() { + String numeric = "3.141592"; String timestamp = "2015-09-15T00:00:00Z"; String date = "2015-09-15"; Key k = @@ -94,12 +99,13 @@ public void builder() { .append(64L) .append(2.0f) .append(4.0d) + .append(new BigDecimal(numeric)) .append("x") .append(ByteArray.copyFrom("y")) .append(Timestamp.parseTimestamp(timestamp)) .append(Date.parseDate(date)) .build(); - assertThat(k.size()).isEqualTo(10); + assertThat(k.size()).isEqualTo(11); assertThat(k.getParts()) .containsExactly( null, @@ -108,6 +114,7 @@ public void builder() { 64L, 2.0d, 4.0d, + BigDecimal.valueOf(3141592, 6), "x", ByteArray.copyFrom("y"), Timestamp.parseTimestamp(timestamp), @@ -129,6 +136,7 @@ public void testToString() { assertThat(Key.of(true).toString()).isEqualTo("[true]"); assertThat(Key.of(32).toString()).isEqualTo("[32]"); assertThat(Key.of(2.0).toString()).isEqualTo("[2.0]"); + assertThat(Key.of(new BigDecimal("3.14")).toString()).isEqualTo("[3.14]"); assertThat(Key.of("xyz").toString()).isEqualTo("[xyz]"); ByteArray b = ByteArray.copyFrom("xyz"); assertThat(Key.of(b).toString()).isEqualTo("[" + b.toString() + "]"); @@ -152,6 +160,7 @@ public void equalsAndHashCode() { Key.newBuilder().append((Boolean) null).build(), Key.newBuilder().append((Long) null).build(), Key.newBuilder().append((Double) null).build(), + Key.newBuilder().append((BigDecimal) null).build(), Key.newBuilder().append((String) null).build(), Key.newBuilder().append((ByteArray) null).build(), Key.newBuilder().append((Timestamp) null).build(), @@ -165,6 +174,10 @@ public void equalsAndHashCode() { tester.addEqualityGroup(Key.of(1, 2)); tester.addEqualityGroup(Key.of(1.0f), Key.of(1.0d), Key.newBuilder().append(1.0).build()); tester.addEqualityGroup(Key.of(2.0f), Key.of(2.0d), Key.newBuilder().append(2.0).build()); + tester.addEqualityGroup( + Key.of(new BigDecimal("3.141592")), + Key.of(BigDecimal.valueOf(3141592, 6)), + Key.newBuilder().append(new BigDecimal("3141592e-6")).build()); tester.addEqualityGroup(Key.of("a"), Key.newBuilder().append("a").build()); tester.addEqualityGroup(Key.of("a", "b", "c")); tester.addEqualityGroup( @@ -185,6 +198,7 @@ public void serialization() { reserializeAndAssert(Key.of(true)); reserializeAndAssert(Key.of(32)); reserializeAndAssert(Key.of(2.0)); + reserializeAndAssert(Key.of(new BigDecimal("3.141592"))); reserializeAndAssert(Key.of("xyz")); reserializeAndAssert(Key.of(ByteArray.copyFrom("xyz"))); reserializeAndAssert(Key.of(Timestamp.parseTimestamp("2015-09-15T00:00:00Z"))); @@ -204,6 +218,7 @@ public void toProto() { .append(64L) .append(2.0f) .append(4.0d) + .append(new BigDecimal("6.62607004e-34")) .append("x") .append(ByteArray.copyFrom("y")) .append(Timestamp.parseTimestamp(timestamp)) @@ -216,6 +231,7 @@ public void toProto() { builder.addValuesBuilder().setStringValue("64"); builder.addValuesBuilder().setNumberValue(2.0f); builder.addValuesBuilder().setNumberValue(4.0d); + builder.addValuesBuilder().setStringValue("6.62607004E-34"); builder.addValuesBuilder().setStringValue("x"); builder.addValuesBuilder().setStringValue("eQ=="); builder.addValuesBuilder().setStringValue(timestamp); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java index bf7c626499..475d8325a9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java @@ -24,6 +24,7 @@ import com.google.protobuf.util.JsonFormat; import com.google.spanner.v1.PartialResultSet; import com.google.spanner.v1.Transaction; +import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -159,6 +160,9 @@ private void assertRow(Struct actualRow, JSONArray expectedRow) throws Exception case FLOAT64: assertThat(actualRow.getDouble(i)).isEqualTo(expectedRow.getDouble(i)); break; + case NUMERIC: + assertThat(actualRow.getBigDecimal(i)).isEqualTo(expectedRow.getBigDecimal(i)); + break; case BYTES: assertThat(actualRow.getBytes(i)) .isEqualTo(ByteArray.fromBase64(expectedRow.getString(i))); @@ -191,6 +195,9 @@ private List getRawList(Struct actualRow, int index, Type elementType) throws case FLOAT64: rawList = actualRow.getDoubleList(index); break; + case NUMERIC: + rawList = actualRow.getBigDecimalList(index); + break; case STRUCT: rawList = actualRow.getStructList(index); break; @@ -220,6 +227,8 @@ private void assertArray(List actualValues, JSONArray expectedList) throws Ex } else { assertThat((Double) actualValue).isEqualTo(expectedList.getDouble(i)); } + } else if (actualValue instanceof BigDecimal) { + assertThat((BigDecimal) actualValue).isEqualTo(expectedList.getBigDecimal(i)); } else if (actualValue instanceof ByteArray) { assertThat((ByteArray) actualValue) .isEqualTo(ByteArray.fromBase64(expectedList.getString(i))); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java index 93b6d9f604..bd3c0c9c52 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java @@ -26,6 +26,7 @@ import com.google.common.primitives.Booleans; import com.google.common.primitives.Doubles; import com.google.common.primitives.Longs; +import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import org.junit.Test; @@ -39,6 +40,7 @@ public class ResultSetsTest { @Test public void resultSetIteration() { double doubleVal = 1.2; + BigDecimal bigDecimalVal = BigDecimal.valueOf(123, 2); String stringVal = "stringVal"; String byteVal = "101"; long usecs = 32343; @@ -48,6 +50,13 @@ public void resultSetIteration() { boolean[] boolArray = {true, false, true, true, false}; long[] longArray = {Long.MAX_VALUE, Long.MIN_VALUE, 0, 1, -1}; double[] doubleArray = {Double.MIN_VALUE, Double.MAX_VALUE, 0, 1, -1, 1.2341}; + BigDecimal[] bigDecimalArray = { + BigDecimal.valueOf(1, Integer.MAX_VALUE), + BigDecimal.valueOf(1, Integer.MIN_VALUE), + BigDecimal.ZERO, + BigDecimal.TEN, + BigDecimal.valueOf(3141592, 6) + }; ByteArray[] byteArray = { ByteArray.copyFrom("123"), ByteArray.copyFrom("456"), ByteArray.copyFrom("789") }; @@ -67,6 +76,7 @@ public void resultSetIteration() { Type.StructField.of("f2", Type.int64()), Type.StructField.of("f3", Type.bool()), Type.StructField.of("doubleVal", Type.float64()), + Type.StructField.of("bigDecimalVal", Type.numeric()), Type.StructField.of("stringVal", Type.string()), Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), @@ -74,6 +84,7 @@ public void resultSetIteration() { Type.StructField.of("boolArray", Type.array(Type.bool())), Type.StructField.of("longArray", Type.array(Type.int64())), Type.StructField.of("doubleArray", Type.array(Type.float64())), + Type.StructField.of("bigDecimalArray", Type.array(Type.numeric())), Type.StructField.of("byteArray", Type.array(Type.bytes())), Type.StructField.of("timestampArray", Type.array(Type.timestamp())), Type.StructField.of("dateArray", Type.array(Type.date())), @@ -88,6 +99,8 @@ public void resultSetIteration() { .to(Value.bool(true)) .set("doubleVal") .to(Value.float64(doubleVal)) + .set("bigDecimalVal") + .to(Value.numeric(bigDecimalVal)) .set("stringVal") .to(stringVal) .set("byteVal") @@ -102,6 +115,8 @@ public void resultSetIteration() { .to(Value.int64Array(longArray)) .set("doubleArray") .to(Value.float64Array(doubleArray)) + .set("bigDecimalArray") + .to(Value.numericArray(Arrays.asList(bigDecimalArray))) .set("byteArray") .to(Value.bytesArray(Arrays.asList(byteArray))) .set("timestampArray") @@ -121,6 +136,8 @@ public void resultSetIteration() { .to(Value.bool(null)) .set("doubleVal") .to(Value.float64(doubleVal)) + .set("bigDecimalVal") + .to(Value.numeric(bigDecimalVal)) .set("stringVal") .to(stringVal) .set("byteVal") @@ -135,6 +152,8 @@ public void resultSetIteration() { .to(Value.int64Array(longArray)) .set("doubleArray") .to(Value.float64Array(doubleArray)) + .set("bigDecimalArray") + .to(Value.numericArray(Arrays.asList(bigDecimalArray))) .set("byteArray") .to(Value.bytesArray(Arrays.asList(byteArray))) .set("timestampArray") @@ -173,36 +192,40 @@ public void resultSetIteration() { assertThat(rs.getBoolean("f3")).isTrue(); assertThat(rs.getDouble("doubleVal")).isWithin(0.0).of(doubleVal); assertThat(rs.getDouble(3)).isWithin(0.0).of(doubleVal); - assertThat(rs.getString(4)).isEqualTo(stringVal); + assertThat(rs.getBigDecimal("bigDecimalVal")).isEqualTo(new BigDecimal("1.23")); + assertThat(rs.getBigDecimal(4)).isEqualTo(new BigDecimal("1.23")); + assertThat(rs.getString(5)).isEqualTo(stringVal); assertThat(rs.getString("stringVal")).isEqualTo(stringVal); - assertThat(rs.getBytes(5)).isEqualTo(ByteArray.copyFrom(byteVal)); + assertThat(rs.getBytes(6)).isEqualTo(ByteArray.copyFrom(byteVal)); assertThat(rs.getBytes("byteVal")).isEqualTo(ByteArray.copyFrom(byteVal)); - assertThat(rs.getTimestamp(6)).isEqualTo(Timestamp.ofTimeMicroseconds(usecs)); + assertThat(rs.getTimestamp(7)).isEqualTo(Timestamp.ofTimeMicroseconds(usecs)); assertThat(rs.getTimestamp("timestamp")).isEqualTo(Timestamp.ofTimeMicroseconds(usecs)); - assertThat(rs.getDate(7)).isEqualTo(Date.fromYearMonthDay(year, month, day)); + assertThat(rs.getDate(8)).isEqualTo(Date.fromYearMonthDay(year, month, day)); assertThat(rs.getDate("date")).isEqualTo(Date.fromYearMonthDay(year, month, day)); - assertThat(rs.getBooleanArray(8)).isEqualTo(boolArray); + assertThat(rs.getBooleanArray(9)).isEqualTo(boolArray); assertThat(rs.getBooleanArray("boolArray")).isEqualTo(boolArray); - assertThat(rs.getBooleanList(8)).isEqualTo(Booleans.asList(boolArray)); + assertThat(rs.getBooleanList(9)).isEqualTo(Booleans.asList(boolArray)); assertThat(rs.getBooleanList("boolArray")).isEqualTo(Booleans.asList(boolArray)); - assertThat(rs.getLongArray(9)).isEqualTo(longArray); + assertThat(rs.getLongArray(10)).isEqualTo(longArray); assertThat(rs.getLongArray("longArray")).isEqualTo(longArray); - assertThat(rs.getLongList(9)).isEqualTo(Longs.asList(longArray)); + assertThat(rs.getLongList(10)).isEqualTo(Longs.asList(longArray)); assertThat(rs.getLongList("longArray")).isEqualTo(Longs.asList(longArray)); - assertThat(rs.getDoubleArray(10)).usingTolerance(0.0).containsAtLeast(doubleArray); + assertThat(rs.getDoubleArray(11)).usingTolerance(0.0).containsAtLeast(doubleArray); assertThat(rs.getDoubleArray("doubleArray")) .usingTolerance(0.0) .containsExactly(doubleArray) .inOrder(); - assertThat(rs.getDoubleList(10)).isEqualTo(Doubles.asList(doubleArray)); + assertThat(rs.getDoubleList(11)).isEqualTo(Doubles.asList(doubleArray)); assertThat(rs.getDoubleList("doubleArray")).isEqualTo(Doubles.asList(doubleArray)); - assertThat(rs.getBytesList(11)).isEqualTo(Arrays.asList(byteArray)); + assertThat(rs.getBigDecimalList(12)).isEqualTo(Arrays.asList(bigDecimalArray)); + assertThat(rs.getBigDecimalList("bigDecimalArray")).isEqualTo(Arrays.asList(bigDecimalArray)); + assertThat(rs.getBytesList(13)).isEqualTo(Arrays.asList(byteArray)); assertThat(rs.getBytesList("byteArray")).isEqualTo(Arrays.asList(byteArray)); - assertThat(rs.getTimestampList(12)).isEqualTo(Arrays.asList(timestampArray)); + assertThat(rs.getTimestampList(14)).isEqualTo(Arrays.asList(timestampArray)); assertThat(rs.getTimestampList("timestampArray")).isEqualTo(Arrays.asList(timestampArray)); - assertThat(rs.getDateList(13)).isEqualTo(Arrays.asList(dateArray)); + assertThat(rs.getDateList(15)).isEqualTo(Arrays.asList(dateArray)); assertThat(rs.getDateList("dateArray")).isEqualTo(Arrays.asList(dateArray)); - assertThat(rs.getStringList(14)).isEqualTo(Arrays.asList(stringArray)); + assertThat(rs.getStringList(16)).isEqualTo(Arrays.asList(stringArray)); assertThat(rs.getStringList("stringArray")).isEqualTo(Arrays.asList(stringArray)); assertThat(rs.next()).isTrue(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SelectRandomBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SelectRandomBenchmark.java new file mode 100644 index 0000000000..2235e21438 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SelectRandomBenchmark.java @@ -0,0 +1,150 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.cloud.NoCredentials; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningScheduledExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.AuxCounters; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Benchmarks for common session pool scenarios. The simulated execution times are based on + * reasonable estimates and are primarily intended to keep the benchmarks comparable with each other + * before and after changes have been made to the pool. The benchmarks are bound to the Maven + * profile `benchmark` and can be executed like this: + * mvn clean test -DskipTests -Pbenchmark -Dbenchmark.name=SelectRandomBenchmark + * + */ +@BenchmarkMode(Mode.AverageTime) +@Fork(value = 1, warmups = 0) +@Measurement(batchSize = 1, iterations = 1, timeUnit = TimeUnit.MILLISECONDS) +@Warmup(batchSize = 0, iterations = 0) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class SelectRandomBenchmark { + private static final String TEST_PROJECT = "my-project"; + private static final String TEST_INSTANCE = "my-instance"; + private static final String TEST_DATABASE = "my-database"; + private static final int RND_WAIT_TIME_BETWEEN_REQUESTS = 10; + private static final Random RND = new Random(); + + @State(Scope.Thread) + @AuxCounters(org.openjdk.jmh.annotations.AuxCounters.Type.EVENTS) + public static class BenchmarkState { + private StandardBenchmarkMockServer mockServer; + private Spanner spanner; + private DatabaseClientImpl client; + + @Param({"100"}) + int minSessions; + + @Param({"400"}) + int maxSessions; + + @Setup(Level.Invocation) + public void setup() throws Exception { + mockServer = new StandardBenchmarkMockServer(); + TransportChannelProvider channelProvider = mockServer.start(); + + SpannerOptions options = + SpannerOptions.newBuilder() + .setProjectId(TEST_PROJECT) + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption( + SessionPoolOptions.newBuilder() + .setMinSessions(minSessions) + .setMaxSessions(maxSessions) + .build()) + .build(); + + spanner = options.getService(); + client = + (DatabaseClientImpl) + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + // Wait until the session pool has initialized. + while (client.pool.getNumberOfSessionsInPool() + < spanner.getOptions().getSessionPoolOptions().getMinSessions()) { + Thread.sleep(1L); + } + } + + @TearDown(Level.Invocation) + public void teardown() throws Exception { + spanner.close(); + mockServer.shutdown(); + } + } + + /** Measures the time needed to execute a burst of read requests. */ + @Benchmark + public void burstRead(final BenchmarkState server) throws Exception { + int totalQueries = server.maxSessions * 8; + int parallelThreads = server.maxSessions * 2; + final DatabaseClient client = + server.spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + SessionPool pool = ((DatabaseClientImpl) client).pool; + assertThat(pool.totalSessions()).isEqualTo(server.minSessions); + + ListeningScheduledExecutorService service = + MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); + List> futures = new ArrayList<>(totalQueries); + for (int i = 0; i < totalQueries; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Void call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + try (ResultSet rs = + client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT_RANDOM)) { + while (rs.next()) { + // Get the entire current row and convert to String. + rs.getCurrentRowAsStruct().toString(); + } + return null; + } + } + })); + } + Futures.allAsList(futures).get(); + service.shutdown(); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StandardBenchmarkMockServer.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StandardBenchmarkMockServer.java index 7262fa163b..bb733d3f52 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StandardBenchmarkMockServer.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StandardBenchmarkMockServer.java @@ -20,6 +20,7 @@ import com.google.api.gax.rpc.TransportChannelProvider; import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.connection.RandomResultSetGenerator; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.protobuf.AbstractMessage; @@ -76,6 +77,9 @@ class StandardBenchmarkMockServer { .build()) .setMetadata(SELECT1_METADATA) .build(); + static final Statement SELECT_RANDOM = Statement.of("SELECT * FROM RANDOM_TABLE"); + private static final com.google.spanner.v1.ResultSet SELECT_RANDOM_RESULTSET = + new RandomResultSetGenerator(100).generate(); private MockSpannerServiceImpl mockSpanner; private Server server; private LocalChannelProvider channelProvider; @@ -85,6 +89,7 @@ TransportChannelProvider start() throws IOException { mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); mockSpanner.putStatementResult(StatementResult.query(SELECT1, SELECT1_RESULTSET)); + mockSpanner.putStatementResult(StatementResult.query(SELECT_RANDOM, SELECT_RANDOM_RESULTSET)); mockSpanner.putStatementResult( StatementResult.exception( INVALID_UPDATE_STATEMENT, diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java index db83c1ba66..e9f9009fe8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java @@ -94,6 +94,16 @@ Type newType() { }.test(); } + @Test + public void numeric() { + new ScalarTypeTester(Type.Code.NUMERIC, TypeCode.NUMERIC) { + @Override + Type newType() { + return Type.numeric(); + } + }.test(); + } + @Test public void string() { new ScalarTypeTester(Type.Code.STRING, TypeCode.STRING) { @@ -203,6 +213,16 @@ Type newElementType() { }.test(); } + @Test + public void numericArray() { + new ArrayTypeTester(Type.Code.NUMERIC, TypeCode.NUMERIC, true) { + @Override + Type newElementType() { + return Type.numeric(); + } + }.test(); + } + @Test public void stringArray() { new ArrayTypeTester(Type.Code.STRING, TypeCode.STRING, true) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java index 2fe9f0205d..4681d80970 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java @@ -25,6 +25,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.math.BigDecimal; import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; @@ -184,6 +185,10 @@ public static Double defaultDoubleWrapper() { return 1.0; } + public static BigDecimal defaultBigDecimal() { + return BigDecimal.valueOf(123, 2); + } + public static String defaultString() { return "x"; } @@ -224,6 +229,10 @@ public static Iterable defaultDoubleIterable() { return Arrays.asList(1.0, 2.0); } + public static Iterable defaultBigDecimalIterable() { + return Arrays.asList(BigDecimal.valueOf(123, 2), BigDecimal.valueOf(456, 2)); + } + public static Iterable defaultStringIterable() { return Arrays.asList("a", "b"); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index a7b99d313e..0288f177af 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -29,6 +29,7 @@ import com.google.common.collect.Lists; import com.google.common.testing.EqualsTester; import java.io.Serializable; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -187,6 +188,98 @@ public void float64WrapperNull() { } } + @Test + public void numeric() { + Value v = Value.numeric(new BigDecimal("1.23")); + assertThat(v.getType()).isEqualTo(Type.numeric()); + assertThat(v.isNull()).isFalse(); + assertThat(v.getNumeric()).isEqualTo(BigDecimal.valueOf(123, 2)); + assertThat(v.toString()).isEqualTo("1.23"); + } + + @Test + public void testNumericFormats() { + // The following is copied from the Numeric proto documentation. + // Encoded as `string`, in decimal format or scientific notation format. + //
    Decimal format: + //
    `[+-]Digits[.[Digits]]` or + //
    `[+-][Digits].Digits` + // + // Scientific notation: + //
    `[+-]Digits[.[Digits]][ExponentIndicator[+-]Digits]` or + //
    `[+-][Digits].Digits[ExponentIndicator[+-]Digits]` + //
    (ExponentIndicator is `"e"` or `"E"`) + + // The following is copied from the BigDecimal#toString() documentation. + //
  • There is a one-to-one mapping between the distinguishable + // {@code BigDecimal} values and the result of this conversion. + // That is, every distinguishable {@code BigDecimal} value + // (unscaled value and scale) has a unique string representation + // as a result of using {@code toString}. If that string + // representation is converted back to a {@code BigDecimal} using + // the {@link #BigDecimal(String)} constructor, then the original + // value will be recovered. + // + //
  • The string produced for a given number is always the same; + // it is not affected by locale. This means that it can be used + // as a canonical string representation for exchanging decimal + // data, or as a key for a Hashtable, etc. Locale-sensitive + // number formatting and parsing is handled by the {@link + // java.text.NumberFormat} class and its subclasses. + + // Test that BigDecimal supports all formats that are supported by Cloud Spanner. + assertThat(new BigDecimal("1").toString()).isEqualTo("1"); + assertThat(new BigDecimal("01").toString()).isEqualTo("1"); + assertThat(new BigDecimal("1.").toString()).isEqualTo("1"); + assertThat(new BigDecimal("+1").toString()).isEqualTo("1"); + assertThat(new BigDecimal("+1.").toString()).isEqualTo("1"); + assertThat(new BigDecimal("-1").toString()).isEqualTo("-1"); + assertThat(new BigDecimal("-1.").toString()).isEqualTo("-1"); + + assertThat(new BigDecimal("0.1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("00.1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal(".1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("+0.1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("+.1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("-0.1").toString()).isEqualTo("-0.1"); + assertThat(new BigDecimal("-.1").toString()).isEqualTo("-0.1"); + + assertThat(new BigDecimal("1E+1").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("1e+1").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("1E1").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("1e1").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("01E+1").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("01e+1").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("01E1").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("01e1").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("1E+01").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("1e+01").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("1E01").toString()).isEqualTo("1E+1"); + assertThat(new BigDecimal("1e01").toString()).isEqualTo("1E+1"); + + assertThat(new BigDecimal("1E-1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("1e-1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("01E-1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("01e-1").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("1E-01").toString()).isEqualTo("0.1"); + assertThat(new BigDecimal("1e-01").toString()).isEqualTo("0.1"); + } + + @Test + public void numericNull() { + Value v = Value.numeric(null); + assertThat(v.getType()).isEqualTo(Type.numeric()); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + + try { + v.getNumeric(); + fail("missing expected IllegalStateException"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("null value"); + } + } + @Test public void string() { Value v = Value.string("abc"); @@ -534,6 +627,43 @@ public void float64ArrayTryGetInt64Array() { } } + @Test + public void numericArray() { + Value v = + Value.numericArray(Arrays.asList(BigDecimal.valueOf(1, 1), null, BigDecimal.valueOf(3, 1))); + assertThat(v.isNull()).isFalse(); + assertThat(v.getNumericArray()) + .containsExactly(new BigDecimal("0.1"), null, new BigDecimal("0.3")) + .inOrder(); + assertThat(v.toString()).isEqualTo("[0.1,NULL,0.3]"); + } + + @Test + public void numericArrayNull() { + Value v = Value.numericArray((Iterable) null); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + + try { + v.getNumericArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); + } + } + + @Test + public void numericArrayTryGetInt64Array() { + Value value = Value.numericArray(Arrays.asList(BigDecimal.valueOf(1, 1))); + + try { + value.getInt64Array(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); + } + } + @Test public void stringArray() { Value v = Value.stringArray(Arrays.asList("a", null, "c")); @@ -829,6 +959,11 @@ public void testEqualsHashCode() { tester.addEqualityGroup(Value.float64(4.56)); tester.addEqualityGroup(Value.float64(null)); + tester.addEqualityGroup( + Value.numeric(BigDecimal.valueOf(123, 2)), Value.numeric(new BigDecimal("1.23"))); + tester.addEqualityGroup(Value.numeric(BigDecimal.valueOf(456, 2))); + tester.addEqualityGroup(Value.numeric(null)); + tester.addEqualityGroup(Value.string("abc"), Value.string("abc")); tester.addEqualityGroup(Value.string("def")); tester.addEqualityGroup(Value.string(null)); @@ -884,6 +1019,11 @@ public void testEqualsHashCode() { tester.addEqualityGroup(Value.float64Array(Arrays.asList(.3))); tester.addEqualityGroup(Value.float64Array((Iterable) null)); + tester.addEqualityGroup( + Value.numericArray(Arrays.asList(BigDecimal.valueOf(1, 1), BigDecimal.valueOf(2, 1)))); + tester.addEqualityGroup(Value.numericArray(Arrays.asList(BigDecimal.valueOf(3, 1)))); + tester.addEqualityGroup(Value.numericArray((Iterable) null)); + tester.addEqualityGroup( Value.stringArray(Arrays.asList("a", "b")), Value.stringArray(Arrays.asList("a", "b"))); tester.addEqualityGroup(Value.stringArray(Arrays.asList("c"))); @@ -933,6 +1073,9 @@ public void serialization() { reserializeAndAssert(Value.float64(1.23)); reserializeAndAssert(Value.float64(null)); + reserializeAndAssert(Value.numeric(BigDecimal.valueOf(123, 2))); + reserializeAndAssert(Value.numeric(null)); + reserializeAndAssert(Value.string("abc")); reserializeAndAssert(Value.string(null)); @@ -961,6 +1104,15 @@ public void serialization() { reserializeAndAssert(Value.float64Array(BrokenSerializationList.of(.1, .2, .3))); reserializeAndAssert(Value.float64Array((Iterable) null)); + reserializeAndAssert( + Value.numericArray( + Arrays.asList(BigDecimal.valueOf(1, 1), null, BigDecimal.valueOf(2, 1)))); + reserializeAndAssert( + Value.numericArray( + BrokenSerializationList.of( + BigDecimal.valueOf(1, 1), BigDecimal.valueOf(2, 1), BigDecimal.valueOf(3, 1)))); + reserializeAndAssert(Value.numericArray((Iterable) null)); + reserializeAndAssert(Value.timestamp(null)); reserializeAndAssert(Value.timestamp(Value.COMMIT_TIMESTAMP)); reserializeAndAssert(Value.timestamp(Timestamp.now())); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java index 237c6af718..74bfb2e9a6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java @@ -194,6 +194,19 @@ public void testValidMethodCall() throws IllegalArgumentException { subject.getDoubleList("test2"); verify(delegate).getDoubleList("test2"); + subject.getBigDecimal(0); + verify(delegate).getBigDecimal(0); + subject.getBigDecimal("test0"); + verify(delegate).getBigDecimal("test0"); + subject.getBigDecimalList(1); + verify(delegate).getBigDecimalList(1); + subject.getBigDecimalList("test1"); + verify(delegate).getBigDecimalList("test1"); + subject.getBigDecimalList(2); + verify(delegate).getBigDecimalList(2); + subject.getBigDecimalList("test2"); + verify(delegate).getBigDecimalList("test2"); + subject.getLong(0); verify(delegate).getLong(0); subject.getLong("test0"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java index 76f85da3b5..9bb6932219 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java @@ -29,6 +29,7 @@ import com.google.spanner.v1.StructType.Field; import com.google.spanner.v1.Type; import com.google.spanner.v1.TypeCode; +import java.math.BigDecimal; import java.util.Random; /** @@ -41,6 +42,7 @@ public class RandomResultSetGenerator { Type.newBuilder().setCode(TypeCode.BOOL).build(), Type.newBuilder().setCode(TypeCode.INT64).build(), Type.newBuilder().setCode(TypeCode.FLOAT64).build(), + Type.newBuilder().setCode(TypeCode.NUMERIC).build(), Type.newBuilder().setCode(TypeCode.STRING).build(), Type.newBuilder().setCode(TypeCode.BYTES).build(), Type.newBuilder().setCode(TypeCode.DATE).build(), @@ -57,6 +59,10 @@ public class RandomResultSetGenerator { .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT64)) .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.NUMERIC)) + .build(), Type.newBuilder() .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING)) @@ -142,6 +148,9 @@ private void setRandomValue(Value.Builder builder, Type type) { case FLOAT64: builder.setNumberValue(random.nextDouble()); break; + case NUMERIC: + builder.setStringValue(BigDecimal.valueOf(random.nextDouble()).toString()); + break; case INT64: builder.setStringValue(String.valueOf(random.nextLong())); break; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java index e0cd8db9a6..1a332ab438 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java @@ -48,6 +48,7 @@ import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.cloud.spanner.connection.StatementParser.StatementType; import com.google.spanner.v1.ResultSetStats; +import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; import org.junit.Test; @@ -483,38 +484,113 @@ public void testChecksumResultSet() { when(parsedStatement.getStatement()).thenReturn(statement); ResultSet delegate1 = ResultSets.forRows( - Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), + Type.struct( + StructField.of("ID", Type.int64()), + StructField.of("NAME", Type.string()), + StructField.of("AMOUNT", Type.numeric())), Arrays.asList( - Struct.newBuilder().set("ID").to(1l).set("NAME").to("TEST 1").build(), - Struct.newBuilder().set("ID").to(2l).set("NAME").to("TEST 2").build())); + Struct.newBuilder() + .set("ID") + .to(1l) + .set("NAME") + .to("TEST 1") + .set("AMOUNT") + .to(BigDecimal.valueOf(550, 2)) + .build(), + Struct.newBuilder() + .set("ID") + .to(2l) + .set("NAME") + .to("TEST 2") + .set("AMOUNT") + .to(BigDecimal.valueOf(750, 2)) + .build())); ChecksumResultSet rs1 = transaction.createChecksumResultSet(delegate1, parsedStatement, AnalyzeMode.NONE); ResultSet delegate2 = ResultSets.forRows( - Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), + Type.struct( + StructField.of("ID", Type.int64()), + StructField.of("NAME", Type.string()), + StructField.of("AMOUNT", Type.numeric())), Arrays.asList( - Struct.newBuilder().set("ID").to(1l).set("NAME").to("TEST 1").build(), - Struct.newBuilder().set("ID").to(2l).set("NAME").to("TEST 2").build())); + Struct.newBuilder() + .set("ID") + .to(1l) + .set("NAME") + .to("TEST 1") + .set("AMOUNT") + .to(new BigDecimal("5.50")) + .build(), + Struct.newBuilder() + .set("ID") + .to(2l) + .set("NAME") + .to("TEST 2") + .set("AMOUNT") + .to(new BigDecimal("7.50")) + .build())); ChecksumResultSet rs2 = transaction.createChecksumResultSet(delegate2, parsedStatement, AnalyzeMode.NONE); // rs1 and rs2 are equal, rs3 contains the same rows, but in a different order ResultSet delegate3 = ResultSets.forRows( - Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), + Type.struct( + StructField.of("ID", Type.int64()), + StructField.of("NAME", Type.string()), + StructField.of("AMOUNT", Type.numeric())), Arrays.asList( - Struct.newBuilder().set("ID").to(2l).set("NAME").to("TEST 2").build(), - Struct.newBuilder().set("ID").to(1l).set("NAME").to("TEST 1").build())); + Struct.newBuilder() + .set("ID") + .to(2l) + .set("NAME") + .to("TEST 2") + .set("AMOUNT") + .to(new BigDecimal("7.50")) + .build(), + Struct.newBuilder() + .set("ID") + .to(1l) + .set("NAME") + .to("TEST 1") + .set("AMOUNT") + .to(new BigDecimal("5.50")) + .build())); ChecksumResultSet rs3 = transaction.createChecksumResultSet(delegate3, parsedStatement, AnalyzeMode.NONE); // rs4 contains the same rows as rs1 and rs2, but also an additional row ResultSet delegate4 = ResultSets.forRows( - Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), + Type.struct( + StructField.of("ID", Type.int64()), + StructField.of("NAME", Type.string()), + StructField.of("AMOUNT", Type.numeric())), Arrays.asList( - Struct.newBuilder().set("ID").to(1l).set("NAME").to("TEST 1").build(), - Struct.newBuilder().set("ID").to(2l).set("NAME").to("TEST 2").build(), - Struct.newBuilder().set("ID").to(3l).set("NAME").to("TEST 3").build())); + Struct.newBuilder() + .set("ID") + .to(1l) + .set("NAME") + .to("TEST 1") + .set("AMOUNT") + .to(new BigDecimal("5.50")) + .build(), + Struct.newBuilder() + .set("ID") + .to(2l) + .set("NAME") + .to("TEST 2") + .set("AMOUNT") + .to(new BigDecimal("7.50")) + .build(), + Struct.newBuilder() + .set("ID") + .to(3l) + .set("NAME") + .to("TEST 3") + .set("AMOUNT") + .to(new BigDecimal("9.99")) + .build())); ChecksumResultSet rs4 = transaction.createChecksumResultSet(delegate4, parsedStatement, AnalyzeMode.NONE); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java index a30b15e6c2..51c8999add 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java @@ -248,6 +248,19 @@ public void testValidMethodCall() throws IllegalArgumentException { subject.getDoubleList("test2"); verify(delegate).getDoubleList("test2"); + subject.getBigDecimal(0); + verify(delegate).getBigDecimal(0); + subject.getBigDecimal("test0"); + verify(delegate).getBigDecimal("test0"); + subject.getBigDecimalList(1); + verify(delegate).getBigDecimalList(1); + subject.getBigDecimalList("test1"); + verify(delegate).getBigDecimalList("test1"); + subject.getBigDecimalList(2); + verify(delegate).getBigDecimalList(2); + subject.getBigDecimalList("test2"); + verify(delegate).getBigDecimalList("test2"); + subject.getLong(0); verify(delegate).getLong(0); subject.getLong("test0"); diff --git a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/read_tests.json b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/read_tests.json index 9b44b40778..8f30964036 100644 --- a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/read_tests.json +++ b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/read_tests.json @@ -5,6 +5,7 @@ "abc", "100", 1.1, + "3.141592", "YWJj", [ "abc", @@ -18,7 +19,7 @@ ["ghi"] ] ]]}, - "chunks": ["{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": \"BOOL\"\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": \"STRING\"\n }\n }, {\n \"name\": \"f3\",\n \"type\": {\n \"code\": \"INT64\"\n }\n }, {\n \"name\": \"f4\",\n \"type\": {\n \"code\": \"FLOAT64\"\n }\n }, {\n \"name\": \"f5\",\n \"type\": {\n \"code\": \"BYTES\"\n }\n }, {\n \"name\": \"f6\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"STRING\"\n }\n }\n }, {\n \"name\": \"f7\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"STRUCT\",\n \"structType\": {\n \"fields\": [{\n \"name\": \"f71\",\n \"type\": {\n \"code\": \"STRING\"\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [true, \"abc\", \"100\", 1.1, \"YWJj\", [\"abc\", \"def\", null, \"ghi\"], [[\"abc\"], [\"def\"], [\"ghi\"]]]\n}"], + "chunks": ["{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": \"BOOL\"\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": \"STRING\"\n }\n }, {\n \"name\": \"f3\",\n \"type\": {\n \"code\": \"INT64\"\n }\n }, {\n \"name\": \"f4\",\n \"type\": {\n \"code\": \"FLOAT64\"\n }\n }, {\n \"name\": \"f5\",\n \"type\": {\n \"code\": \"NUMERIC\"\n }\n }, {\n \"name\": \"f6\",\n \"type\": {\n \"code\": \"BYTES\"\n }\n }, {\n \"name\": \"f7\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"STRING\"\n }\n }\n }, {\n \"name\": \"f8\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"STRUCT\",\n \"structType\": {\n \"fields\": [{\n \"name\": \"f81\",\n \"type\": {\n \"code\": \"STRING\"\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [true, \"abc\", \"100\", 1.1, \"3.141592\", \"YWJj\", [\"abc\", \"def\", null, \"ghi\"], [[\"abc\"], [\"def\"], [\"ghi\"]]]\n}"], "name": "Basic Test" }, { @@ -115,6 +116,21 @@ ], "name": "FLOAT64 Array Chunking Test" }, + { + "result": {"value": [[[ + "1.1", + "2.3", + "4.0", + null, + 5.5 + ]]]}, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"NUMERIC\"\n }\n }\n }]\n }\n },\n \"values\": [[\"1.1\", \"2.\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"3\", \"4.0\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"\", null, \"5.5\"]]\n}" + ], + "name": "NUMERIC Array Chunking Test" + }, { "result": {"value": [[[ [