From 19b79764294e938ad85d02b7c0662db6ec3afeda Mon Sep 17 00:00:00 2001 From: Aravind Pedapudi Date: Wed, 28 Feb 2024 15:53:14 +0530 Subject: [PATCH] feat: support float32 type (#2894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: float32 changes with unit and integration tests * Update formatting and clirr * Update the hashCode logic to account for NaN equality * Prevent FLOAT32 integration tests from running on emulator and production * Fix integration tests for FLOAT32 * Update float32UntypedParameters test to work with PG dialect too * Split the parameters test in ITQueryTest into supported + currently-unsupported tests. * Split the Mutation.isNaN method to make it more readable * test: added some additional tests * Update to resolve comments on PR#2894. Major change: Ensures that the new methods in interfaces do not break for older clients. Minor changes: remove double cast; remove dependency on Truth assertions; remove unnecessary logic in Mutations::isNaN * Un-ignore the skipped FLOAT32 tests as the backend fixes have been deployed * Un-ignore the float32 tests in ITQueryTest --------- Co-authored-by: Owl Bot Co-authored-by: Knut Olav Løite --- .../clirr-ignored-differences.xml | 46 +- .../cloud/spanner/AbstractResultSet.java | 76 +++- .../cloud/spanner/AbstractStructReader.java | 51 +++ .../cloud/spanner/ForwardingStructReader.java | 36 ++ .../com/google/cloud/spanner/GrpcStruct.java | 38 +- .../com/google/cloud/spanner/Mutation.java | 18 +- .../com/google/cloud/spanner/ResultSets.java | 30 ++ .../java/com/google/cloud/spanner/Struct.java | 20 + .../google/cloud/spanner/StructReader.java | 54 +++ .../java/com/google/cloud/spanner/Type.java | 17 +- .../java/com/google/cloud/spanner/Value.java | 198 ++++++++- .../com/google/cloud/spanner/ValueBinder.java | 25 ++ .../connection/DirectExecuteResultSet.java | 36 ++ .../ReplaceableForwardingResultSet.java | 36 ++ .../AbstractStructReaderTypesTest.java | 36 ++ .../cloud/spanner/GrpcResultSetTest.java | 39 ++ .../cloud/spanner/MockSpannerServiceImpl.java | 4 + .../google/cloud/spanner/MutationTest.java | 56 ++- .../spanner/RandomResultSetGenerator.java | 8 + .../google/cloud/spanner/ReadAsyncTest.java | 5 +- .../cloud/spanner/ReadFormatTestRunner.java | 6 + .../google/cloud/spanner/ResultSetsTest.java | 28 ++ .../com/google/cloud/spanner/TypeTest.java | 20 + .../google/cloud/spanner/ValueBinderTest.java | 16 + .../com/google/cloud/spanner/ValueTest.java | 151 +++++++ .../connection/ChecksumResultSetTest.java | 14 + .../cloud/spanner/it/ITAsyncExamplesTest.java | 5 +- .../cloud/spanner/it/ITFloat32Test.java | 415 ++++++++++++++++++ .../google/cloud/spanner/it/ITQueryTest.java | 83 ++++ 29 files changed, 1544 insertions(+), 23 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITFloat32Test.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index fbbc0153f8..eaf7637b0b 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -506,6 +506,48 @@ com.google.cloud.spanner.connection.StatementResult execute(com.google.cloud.spanner.Statement, java.util.Set) + + + 7012 + com/google/cloud/spanner/StructReader + float getFloat(int) + + + 7012 + com/google/cloud/spanner/StructReader + float getFloat(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + float[] getFloatArray(int) + + + 7012 + com/google/cloud/spanner/StructReader + float[] getFloatArray(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getFloatList(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getFloatList(java.lang.String) + + + 7013 + com/google/cloud/spanner/Value + float getFloat32() + + + 7013 + com/google/cloud/spanner/Value + java.util.List getFloat32Array() + + 7012 @@ -569,7 +611,7 @@ void setSpan(io.opencensus.trace.Span) void setSpan(com.google.cloud.spanner.ISpan) - + 7012 @@ -580,5 +622,5 @@ 7012 com/google/cloud/spanner/connection/Connection void setDirectedRead(com.google.spanner.v1.DirectedReadOptions) - + 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 6cce03e72c..2cf93fb92e 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 @@ -173,16 +173,44 @@ static double valueProtoToFloat64(com.google.protobuf.Value proto) { return proto.getNumberValue(); } + static float valueProtoToFloat32(com.google.protobuf.Value proto) { + if (proto.getKindCase() == KindCase.STRING_VALUE) { + switch (proto.getStringValue()) { + case "-Infinity": + return Float.NEGATIVE_INFINITY; + case "Infinity": + return Float.POSITIVE_INFINITY; + case "NaN": + return Float.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.float32() + + " 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 (float) proto.getNumberValue(); + } + 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. + * Memory-optimized base class for {@code ARRAY}, {@code ARRAY} and {@code + * ARRAY} types. All 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. */ abstract static class PrimitiveArray extends AbstractList { private final A data; @@ -264,6 +292,31 @@ Long get(long[] array, int i) { } } + static class Float32Array extends PrimitiveArray { + Float32Array(ListValue protoList) { + super(protoList); + } + + Float32Array(float[] data, BitSet nulls) { + super(data, nulls, data.length); + } + + @Override + float[] newArray(int size) { + return new float[size]; + } + + @Override + void setProto(float[] array, int i, com.google.protobuf.Value protoValue) { + array[i] = valueProtoToFloat32(protoValue); + } + + @Override + Float get(float[] array, int i) { + return array[i]; + } + } + static class Float64Array extends PrimitiveArray { Float64Array(ListValue protoList) { super(protoList); @@ -306,6 +359,11 @@ protected long getLongInternal(int columnIndex) { return currRow().getLongInternal(columnIndex); } + @Override + protected float getFloatInternal(int columnIndex) { + return currRow().getFloatInternal(columnIndex); + } + @Override protected double getDoubleInternal(int columnIndex) { return currRow().getDoubleInternal(columnIndex); @@ -382,6 +440,16 @@ protected List getLongListInternal(int columnIndex) { return currRow().getLongListInternal(columnIndex); } + @Override + protected float[] getFloatArrayInternal(int columnIndex) { + return currRow().getFloatArrayInternal(columnIndex); + } + + @Override + protected List getFloatListInternal(int columnIndex) { + return currRow().getFloatListInternal(columnIndex); + } + @Override protected double[] getDoubleArrayInternal(int columnIndex) { return currRow().getDoubleArrayInternal(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 ef6f63d52e..a11c573233 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 @@ -43,6 +43,10 @@ public abstract class AbstractStructReader implements StructReader { protected abstract long getLongInternal(int columnIndex); + protected float getFloatInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + protected abstract double getDoubleInternal(int columnIndex); protected abstract BigDecimal getBigDecimalInternal(int columnIndex); @@ -94,6 +98,14 @@ protected Value getValueInternal(int columnIndex) { protected abstract List getLongListInternal(int columnIndex); + protected float[] getFloatArrayInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + + protected List getFloatListInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + protected abstract double[] getDoubleArrayInternal(int columnIndex); protected abstract List getDoubleListInternal(int columnIndex); @@ -164,6 +176,19 @@ public long getLong(String columnName) { return getLongInternal(columnIndex); } + @Override + public float getFloat(int columnIndex) { + checkNonNullOfType(columnIndex, Type.float32(), columnIndex); + return getFloatInternal(columnIndex); + } + + @Override + public float getFloat(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.float32(), columnName); + return getFloatInternal(columnIndex); + } + @Override public double getDouble(int columnIndex) { checkNonNullOfType(columnIndex, Type.float64(), columnIndex); @@ -368,6 +393,32 @@ public List getLongList(String columnName) { return getLongListInternal(columnIndex); } + @Override + public float[] getFloatArray(int columnIndex) { + checkNonNullOfType(columnIndex, Type.array(Type.float32()), columnIndex); + return getFloatArrayInternal(columnIndex); + } + + @Override + public float[] getFloatArray(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.array(Type.float32()), columnName); + return getFloatArrayInternal(columnIndex); + } + + @Override + public List getFloatList(int columnIndex) { + checkNonNullOfType(columnIndex, Type.array(Type.float32()), columnIndex); + return getFloatListInternal(columnIndex); + } + + @Override + public List getFloatList(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.array(Type.float32()), columnName); + return getFloatListInternal(columnIndex); + } + @Override public double[] getDoubleArray(int columnIndex) { checkNonNullOfType(columnIndex, Type.array(Type.float64()), 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 97c39c00a8..b3e37ffcdd 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 @@ -125,6 +125,18 @@ public long getLong(String columnName) { return delegate.get().getLong(columnName); } + @Override + public float getFloat(int columnIndex) { + checkValidState(); + return delegate.get().getFloat(columnIndex); + } + + @Override + public float getFloat(String columnName) { + checkValidState(); + return delegate.get().getFloat(columnName); + } + @Override public double getDouble(int columnIndex) { checkValidState(); @@ -267,6 +279,30 @@ public List getLongList(String columnName) { return delegate.get().getLongList(columnName); } + @Override + public float[] getFloatArray(int columnIndex) { + checkValidState(); + return delegate.get().getFloatArray(columnIndex); + } + + @Override + public float[] getFloatArray(String columnName) { + checkValidState(); + return delegate.get().getFloatArray(columnName); + } + + @Override + public List getFloatList(int columnIndex) { + checkValidState(); + return delegate.get().getFloatList(columnIndex); + } + + @Override + public List getFloatList(String columnName) { + checkValidState(); + return delegate.get().getFloatList(columnName); + } + @Override public double[] getDoubleArray(int columnIndex) { checkValidState(); 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 index 6be649ae7c..a6769acfad 100644 --- 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 @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.cloud.spanner.AbstractResultSet.throwNotNull; +import static com.google.cloud.spanner.AbstractResultSet.valueProtoToFloat32; import static com.google.cloud.spanner.AbstractResultSet.valueProtoToFloat64; import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; import static com.google.common.base.Preconditions.checkArgument; @@ -24,6 +25,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AbstractResultSet.Float32Array; import com.google.cloud.spanner.AbstractResultSet.Float64Array; import com.google.cloud.spanner.AbstractResultSet.Int64Array; import com.google.cloud.spanner.AbstractResultSet.LazyByteArray; @@ -83,6 +85,9 @@ private Object writeReplace() { case FLOAT64: builder.set(fieldName).to((Double) value); break; + case FLOAT32: + builder.set(fieldName).to((Float) value); + break; case NUMERIC: builder.set(fieldName).to((BigDecimal) value); break; @@ -135,6 +140,9 @@ private Object writeReplace() { case FLOAT64: builder.set(fieldName).toFloat64Array((Iterable) value); break; + case FLOAT32: + builder.set(fieldName).toFloat32Array((Iterable) value); + break; case NUMERIC: builder.set(fieldName).toNumericArray((Iterable) value); break; @@ -259,6 +267,8 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot return Long.parseLong(proto.getStringValue()); case FLOAT64: return valueProtoToFloat64(proto); + case FLOAT32: + return valueProtoToFloat32(proto); case NUMERIC: checkType(fieldType, proto, KindCase.STRING_VALUE); return new BigDecimal(proto.getStringValue()); @@ -310,11 +320,13 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) { switch (elementType.getCode()) { case INT64: case ENUM: - // For int64/float64/enum types, use custom containers. These avoid wrapper object - // creation for non-null arrays. + // For int64/float64/float32/enum types, use custom containers. + // These avoid wrapper object creation for non-null arrays. return new Int64Array(listValue); case FLOAT64: return new Float64Array(listValue); + case FLOAT32: + return new Float32Array(listValue); case BOOL: case NUMERIC: case PG_NUMERIC: @@ -418,6 +430,12 @@ protected double getDoubleInternal(int columnIndex) { return (Double) rowData.get(columnIndex); } + @Override + protected float getFloatInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Float) rowData.get(columnIndex); + } + @Override protected BigDecimal getBigDecimalInternal(int columnIndex) { ensureDecoded(columnIndex); @@ -537,6 +555,8 @@ protected Value getValueInternal(int columnIndex) { return Value.pgNumeric(isNull ? null : getStringInternal(columnIndex)); case FLOAT64: return Value.float64(isNull ? null : getDoubleInternal(columnIndex)); + case FLOAT32: + return Value.float32(isNull ? null : getFloatInternal(columnIndex)); case STRING: return Value.string(isNull ? null : getStringInternal(columnIndex)); case JSON: @@ -570,6 +590,8 @@ protected Value getValueInternal(int columnIndex) { return Value.pgNumericArray(isNull ? null : getStringListInternal(columnIndex)); case FLOAT64: return Value.float64Array(isNull ? null : getDoubleListInternal(columnIndex)); + case FLOAT32: + return Value.float32Array(isNull ? null : getFloatListInternal(columnIndex)); case STRING: return Value.stringArray(isNull ? null : getStringListInternal(columnIndex)); case JSON: @@ -652,6 +674,18 @@ protected Float64Array getDoubleListInternal(int columnIndex) { return (Float64Array) rowData.get(columnIndex); } + @Override + protected float[] getFloatArrayInternal(int columnIndex) { + ensureDecoded(columnIndex); + return getFloatListInternal(columnIndex).toPrimitiveArray(columnIndex); + } + + @Override + protected Float32Array getFloatListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Float32Array) rowData.get(columnIndex); + } + @Override @SuppressWarnings("unchecked") // We know ARRAY produces a List. protected List getBigDecimalListInternal(int columnIndex) { 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 73995a20df..6c869c549f 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 @@ -364,6 +364,8 @@ public int hashCode() { * mutation equality to check for modifications before committing. We noticed that when NaNs where * used the template would always indicate a modification was present, when it turned out not to * be the case. For more information see b/206339664. + * + *

Similar change is being done while calculating `Value.hashCode()`. */ private boolean areValuesEqual(List values, List otherValues) { if (values == null && otherValues == null) { @@ -385,9 +387,19 @@ private boolean areValuesEqual(List values, List otherValues) { } private boolean isNaN(Value value) { - return !value.isNull() - && value.getType().equals(Type.float64()) - && Double.isNaN(value.getFloat64()); + return !value.isNull() && (isFloat64NaN(value) || isFloat32NaN(value)); + } + + // Checks if the Float64 value is either a "Double" or a "Float" NaN. + // Refer the comment above `areValuesEqual` for more details. + private boolean isFloat64NaN(Value value) { + return value.getType().equals(Type.float64()) && Double.isNaN(value.getFloat64()); + } + + // Checks if the Float32 value is either a "Double" or a "Float" NaN. + // Refer the comment above `areValuesEqual` for more details. + private boolean isFloat32NaN(Value value) { + return value.getType().equals(Type.float32()) && Float.isNaN(value.getFloat32()); } static void toProto(Iterable mutations, List out) { 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 a6cc7c729e..3d12cf5ad2 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 @@ -236,6 +236,16 @@ public long getLong(String columnName) { return getCurrentRowAsStruct().getLong(columnName); } + @Override + public float getFloat(int columnIndex) { + return getCurrentRowAsStruct().getFloat(columnIndex); + } + + @Override + public float getFloat(String columnName) { + return getCurrentRowAsStruct().getFloat(columnName); + } + @Override public double getDouble(int columnIndex) { return getCurrentRowAsStruct().getDouble(columnIndex); @@ -388,6 +398,26 @@ public List getLongList(String columnName) { return getCurrentRowAsStruct().getLongList(columnName); } + @Override + public float[] getFloatArray(int columnIndex) { + return getCurrentRowAsStruct().getFloatArray(columnIndex); + } + + @Override + public float[] getFloatArray(String columnName) { + return getCurrentRowAsStruct().getFloatArray(columnName); + } + + @Override + public List getFloatList(int columnIndex) { + return getCurrentRowAsStruct().getFloatList(columnIndex); + } + + @Override + public List getFloatList(String columnName) { + return getCurrentRowAsStruct().getFloatList(columnName); + } + @Override public double[] getDoubleArray(int columnIndex) { return getCurrentRowAsStruct().getDoubleArray(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 40c30148d0..0e65fa7f1b 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 @@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.primitives.Booleans; import com.google.common.primitives.Doubles; +import com.google.common.primitives.Floats; import com.google.common.primitives.Longs; import com.google.protobuf.AbstractMessage; import com.google.protobuf.ProtocolMessageEnum; @@ -180,6 +181,11 @@ protected long getLongInternal(int columnIndex) { return values.get(columnIndex).getInt64(); } + @Override + protected float getFloatInternal(int columnIndex) { + return values.get(columnIndex).getFloat32(); + } + @Override protected double getDoubleInternal(int columnIndex) { return values.get(columnIndex).getFloat64(); @@ -261,6 +267,16 @@ protected List getLongListInternal(int columnIndex) { return values.get(columnIndex).getInt64Array(); } + @Override + protected float[] getFloatArrayInternal(int columnIndex) { + return Floats.toArray(getFloatListInternal(columnIndex)); + } + + @Override + protected List getFloatListInternal(int columnIndex) { + return values.get(columnIndex).getFloat32Array(); + } + @Override protected double[] getDoubleArrayInternal(int columnIndex) { return Doubles.toArray(getDoubleListInternal(columnIndex)); @@ -382,6 +398,8 @@ private Object getAsObject(int columnIndex) { case INT64: case ENUM: return getLongInternal(columnIndex); + case FLOAT32: + return getFloatInternal(columnIndex); case FLOAT64: return getDoubleInternal(columnIndex); case NUMERIC: @@ -410,6 +428,8 @@ private Object getAsObject(int columnIndex) { case INT64: case ENUM: return getLongListInternal(columnIndex); + case FLOAT32: + return getFloatListInternal(columnIndex); case FLOAT64: return getDoubleListInternal(columnIndex); case NUMERIC: 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 fd8cb77f39..f9967db045 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 @@ -123,6 +123,22 @@ public interface StructReader { */ long getLong(String columnName); + /** + * @param columnIndex index of the column + * @return the value of a non-{@code NULL} column with type {@link Type#float32()}. + */ + default float getFloat(int columnIndex) { + throw new UnsupportedOperationException("method should be overwritten"); + } + + /** + * @param columnName name of the column + * @return the value of a non-{@code NULL} column with type {@link Type#float32()}. + */ + default float getFloat(String columnName) { + throw new UnsupportedOperationException("method should be overwritten"); + } + /** * @param columnIndex index of the column * @return the value of a non-{@code NULL} column with type {@link Type#float64()}. @@ -361,6 +377,44 @@ default Value getValue(String columnName) { */ List getLongList(String columnName); + /** + * @param columnIndex index of the column + * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float32())}. + * @throws NullPointerException if any element of the array value is {@code NULL}. If the array + * may contain {@code NULL} values, use {@link #getFloatList(int)} instead. + */ + default float[] getFloatArray(int columnIndex) { + throw new UnsupportedOperationException("method should be overwritten"); + } + + /** + * @param columnName name of the column + * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float32())}. + * @throws NullPointerException if any element of the array value is {@code NULL}. If the array + * may contain {@code NULL} values, use {@link #getFloatList(String)} instead. + */ + default float[] getFloatArray(String columnName) { + throw new UnsupportedOperationException("method should be overwritten"); + } + + /** + * @param columnIndex index of the column + * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float32())} The + * list returned by this method is lazily constructed. Create a copy of it if you intend to + * access each element in the list multiple times. + */ + default List getFloatList(int columnIndex) { + throw new UnsupportedOperationException("method should be overwritten"); + } + + /** + * @param columnName name of the column + * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float32())} The + * list returned by this method is lazily constructed. Create a copy of it if you intend to + * access each element in the list multiple times. + */ + List getFloatList(String columnName); + /** * @param columnIndex index of the column * @return the value of a non-{@code NULL} column with type {@code Type.array(Type.float64())}. 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 348db5d04a..5d871227f5 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 @@ -48,6 +48,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_FLOAT32 = new Type(Code.FLOAT32, 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_PG_NUMERIC = new Type(Code.PG_NUMERIC, null, null); @@ -59,6 +60,7 @@ public final class Type implements Serializable { private static final Type TYPE_DATE = new Type(Code.DATE, null, null); 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_FLOAT32 = new Type(Code.ARRAY, TYPE_FLOAT32, 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_PG_NUMERIC = new Type(Code.ARRAY, TYPE_PG_NUMERIC, null); @@ -89,9 +91,17 @@ public static Type int64() { return TYPE_INT64; } + /** + * Returns the descriptor for the {@code FLOAT32} type: a floating point type with the same value + * domain as a Java {@code float}. + */ + public static Type float32() { + return TYPE_FLOAT32; + } + /** * Returns the descriptor for the {@code FLOAT64} type: a floating point type with the same value - * domain as a Java {code double}. + * domain as a Java {@code double}. */ public static Type float64() { return TYPE_FLOAT64; @@ -174,6 +184,8 @@ public static Type array(Type elementType) { return TYPE_ARRAY_BOOL; case INT64: return TYPE_ARRAY_INT64; + case FLOAT32: + return TYPE_ARRAY_FLOAT32; case FLOAT64: return TYPE_ARRAY_FLOAT64; case NUMERIC: @@ -264,6 +276,7 @@ public enum Code { NUMERIC(TypeCode.NUMERIC, "unknown"), PG_NUMERIC(TypeCode.NUMERIC, "numeric", TypeAnnotationCode.PG_NUMERIC), FLOAT64(TypeCode.FLOAT64, "double precision"), + FLOAT32(TypeCode.FLOAT32, "real"), STRING(TypeCode.STRING, "character varying"), JSON(TypeCode.JSON, "unknown"), PG_JSONB(TypeCode.JSON, "jsonb", TypeAnnotationCode.PG_JSONB), @@ -565,6 +578,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) { return bool(); case INT64: return int64(); + case FLOAT32: + return float32(); case FLOAT64: return float64(); case NUMERIC: 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 3f0155e4a5..e4db5ff146 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 @@ -149,6 +149,20 @@ public static Value int64(long v) { return new Int64Impl(false, v); } + /** + * Returns a {@code FLOAT32} value. + * + * @param v the value, which may be null + */ + public static Value float32(@Nullable Float v) { + return new Float32Impl(v == null, v == null ? 0 : v); + } + + /** Returns a {@code FLOAT32} value. */ + public static Value float32(float v) { + return new Float32Impl(false, v); + } + /** * Returns a {@code FLOAT64} value. * @@ -454,6 +468,40 @@ public static Value int64Array(@Nullable Iterable v) { return int64ArrayFactory.create(v); } + /** + * Returns an {@code ARRAY} value. + * + * @param v the source of element values, which may be null to produce a value for which {@code + * isNull()} is {@code true} + */ + public static Value float32Array(@Nullable float[] v) { + return float32Array(v, 0, v == null ? 0 : v.length); + } + + /** + * Returns an {@code ARRAY} value that takes its elements from a region of an array. + * + * @param v the source of element values, which may be null to produce a value for which {@code + * isNull()} is {@code true} + * @param pos the start position of {@code v} to copy values from. Ignored if {@code v} is {@code + * null}. + * @param length the number of values to copy from {@code v}. Ignored if {@code v} is {@code + * null}. + */ + public static Value float32Array(@Nullable float[] v, int pos, int length) { + return float32ArrayFactory.create(v, pos, length); + } + + /** + * 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 float32Array(@Nullable Iterable v) { + return float32ArrayFactory.create(v); + } + /** * Returns an {@code ARRAY} value. * @@ -729,6 +777,13 @@ private Value() {} */ public abstract long getInt64(); + /** + * Returns the value of a {@code FLOAT32}-typed instance. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public abstract float getFloat32(); + /** * Returns the value of a {@code FLOAT64}-typed instance. * @@ -835,6 +890,14 @@ public T getProtoEnum( */ public abstract List getInt64Array(); + /** + * 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 getFloat32Array(); + /** * 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. @@ -1052,6 +1115,23 @@ Value newValue(boolean isNull, BitSet nulls, long[] values) { return new Int64ArrayImpl(isNull, nulls, values); } }; + private static final PrimitiveArrayValueFactory float32ArrayFactory = + new PrimitiveArrayValueFactory() { + @Override + float[] newArray(int size) { + return new float[size]; + } + + @Override + void set(float[] arr, int i, Float value) { + arr[i] = value; + } + + @Override + Value newValue(boolean isNull, BitSet nulls, float[] values) { + return new Float32ArrayImpl(isNull, nulls, values); + } + }; private static final PrimitiveArrayValueFactory float64ArrayFactory = new PrimitiveArrayValueFactory() { @Override @@ -1122,6 +1202,11 @@ public long getInt64() { throw defaultGetter(Type.int64()); } + @Override + public float getFloat32() { + throw defaultGetter(Type.float32()); + } + @Override public double getFloat64() { throw defaultGetter(Type.float64()); @@ -1181,6 +1266,11 @@ public List getInt64Array() { throw defaultGetter(Type.array(Type.int64())); } + @Override + public List getFloat32Array() { + throw defaultGetter(Type.array(Type.float32())); + } + @Override public List getFloat64Array() { throw defaultGetter(Type.array(Type.float64())); @@ -1285,9 +1375,29 @@ public final boolean equals(Object o) { @Override public final int hashCode() { - int result = Objects.hash(getType(), isNull); + Type typeToHash = getType(); + int valueHash = isNull ? 0 : valueHash(); + + /** + * We are relaxing equality values here, making sure that Double.NaNs and Float.NaNs are equal + * to each other. This is because our Cloud Spanner Import / Export template in Apache Beam + * uses the mutation equality to check for modifications before committing. We noticed that + * when NaNs where used the template would always indicate a modification was present, when it + * turned out not to be the case. + * + *

With FLOAT32 being introduced, we want to ensure the backward compatibility of the NaN + * equality checks that existed for FLOAT64. We're promoting the type to FLOAT64 while + * calculating the type hash when the value is a NaN. We're doing a similar type promotion + * while calculating valueHash of Float32 type. Note that this is not applicable for composite + * types containing FLOAT32. + */ + if (type.getCode() == Type.Code.FLOAT32 && !isNull && Float.isNaN(getFloat32())) { + typeToHash = Type.float64(); + } + + int result = Objects.hash(typeToHash, isNull); if (!isNull) { - result = 31 * result + valueHash(); + result = 31 * result + valueHash; } return result; } @@ -1492,6 +1602,46 @@ int valueHash() { } } + private static class Float32Impl extends AbstractValue { + private final float value; + + private Float32Impl(boolean isNull, float value) { + super(isNull, Type.float32()); + this.value = value; + } + + @Override + public float getFloat32() { + checkNotNull(); + return value; + } + + @Override + com.google.protobuf.Value valueToProto() { + return com.google.protobuf.Value.newBuilder().setNumberValue(value).build(); + } + + @Override + void valueToString(StringBuilder b) { + b.append(value); + } + + @Override + boolean valueEquals(Value v) { + return ((Float32Impl) v).value == value; + } + + @Override + int valueHash() { + // For backward compatibility of NaN equality checks with Float64 NaNs. + // Refer the comment in `Value.hashCode()` for more details. + if (!isNull() && Float.isNaN(value)) { + return Double.valueOf(Double.NaN).hashCode(); + } + return Float.valueOf(value).hashCode(); + } + } + private static class Float64Impl extends AbstractValue { private final double value; @@ -2106,6 +2256,46 @@ int arrayHash() { } } + private static class Float32ArrayImpl extends PrimitiveArrayImpl { + private final float[] values; + + private Float32ArrayImpl(boolean isNull, BitSet nulls, float[] values) { + super(isNull, Type.float32(), nulls); + this.values = values; + } + + @Override + public List getFloat32Array() { + return getArray(); + } + + @Override + boolean valueEquals(Value v) { + Float32ArrayImpl that = (Float32ArrayImpl) v; + return Arrays.equals(values, that.values); + } + + @Override + int size() { + return values.length; + } + + @Override + Float getValue(int i) { + return values[i]; + } + + @Override + com.google.protobuf.Value getValueAsProto(int i) { + return com.google.protobuf.Value.newBuilder().setNumberValue(values[i]).build(); + } + + @Override + int arrayHash() { + return Arrays.hashCode(values); + } + } + private static class Float64ArrayImpl extends PrimitiveArrayImpl { private final double[] values; @@ -2588,6 +2778,8 @@ private Value getValue(int fieldIndex) { return Value.pgJsonb(value.getPgJsonb(fieldIndex)); case BYTES: return Value.bytes(value.getBytes(fieldIndex)); + case FLOAT32: + return Value.float32(value.getFloat(fieldIndex)); case FLOAT64: return Value.float64(value.getDouble(fieldIndex)); case NUMERIC: @@ -2622,6 +2814,8 @@ private Value getValue(int fieldIndex) { case BYTES: case PROTO: return Value.bytesArray(value.getBytesList(fieldIndex)); + case FLOAT32: + return Value.float32Array(value.getFloatList(fieldIndex)); case FLOAT64: return Value.float64Array(value.getDoubleList(fieldIndex)); case NUMERIC: 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 9915e12175..d675686ffe 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 @@ -81,6 +81,16 @@ public R to(@Nullable Long value) { return handle(Value.int64(value)); } + /** Binds to {@code Value.float32(value)} */ + public R to(float value) { + return handle(Value.float32(value)); + } + + /** Binds to {@code Value.float32(value)} */ + public R to(@Nullable Float value) { + return handle(Value.float32(value)); + } + /** Binds to {@code Value.float64(value)} */ public R to(double value) { return handle(Value.float64(value)); @@ -198,6 +208,21 @@ public R toInt64Array(@Nullable Iterable values) { return handle(Value.int64Array(values)); } + /** Binds to {@code Value.float32Array(values)} */ + public R toFloat32Array(@Nullable float[] values) { + return handle(Value.float32Array(values)); + } + + /** Binds to {@code Value.float32Array(values, pos, length)} */ + public R toFloat32Array(@Nullable float[] values, int pos, int length) { + return handle(Value.float32Array(values, pos, length)); + } + + /** Binds to {@code Value.float32Array(values)} */ + public R toFloat32Array(@Nullable Iterable values) { + return handle(Value.float32Array(values)); + } + /** Binds to {@code Value.float64Array(values)} */ public R toFloat64Array(@Nullable double[] values) { return handle(Value.float64Array(values)); 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 1b15ec5082..b5e4060ddd 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 @@ -180,6 +180,12 @@ public long getLong(String columnName) { return delegate.getLong(columnName); } + @Override + public float getFloat(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getFloat(columnIndex); + } + @Override public double getDouble(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); @@ -198,6 +204,12 @@ public BigDecimal getBigDecimal(int columnIndex) { return delegate.getBigDecimal(columnIndex); } + @Override + public float getFloat(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getFloat(columnName); + } + @Override public double getDouble(String columnName) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); @@ -336,6 +348,30 @@ public List getLongList(String columnName) { return delegate.getLongList(columnName); } + @Override + public float[] getFloatArray(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getFloatArray(columnIndex); + } + + @Override + public float[] getFloatArray(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getFloatArray(columnName); + } + + @Override + public List getFloatList(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getFloatList(columnIndex); + } + + @Override + public List getFloatList(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getFloatList(columnName); + } + @Override public double[] getDoubleArray(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 a8de14e512..bd7c794a0f 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 @@ -189,6 +189,18 @@ public long getLong(String columnName) { return delegate.getLong(columnName); } + @Override + public float getFloat(int columnIndex) { + checkClosed(); + return delegate.getFloat(columnIndex); + } + + @Override + public float getFloat(String columnName) { + checkClosed(); + return delegate.getFloat(columnName); + } + @Override public double getDouble(int columnIndex) { checkClosed(); @@ -345,6 +357,30 @@ public List getLongList(String columnName) { return delegate.getLongList(columnName); } + @Override + public float[] getFloatArray(int columnIndex) { + checkClosed(); + return delegate.getFloatArray(columnIndex); + } + + @Override + public float[] getFloatArray(String columnName) { + checkClosed(); + return delegate.getFloatArray(columnName); + } + + @Override + public List getFloatList(int columnIndex) { + checkClosed(); + return delegate.getFloatList(columnIndex); + } + + @Override + public List getFloatList(String columnName) { + checkClosed(); + return delegate.getFloatList(columnName); + } + @Override public double[] getDoubleArray(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 4fc3c67ceb..16dd51a36a 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 @@ -58,6 +58,11 @@ protected long getLongInternal(int columnIndex) { return 0; } + @Override + protected float getFloatInternal(int columnIndex) { + return 0f; + } + @Override protected double getDoubleInternal(int columnIndex) { return 0; @@ -134,6 +139,16 @@ protected List getLongListInternal(int columnIndex) { return null; } + @Override + protected float[] getFloatArrayInternal(int columnIndex) { + return null; + } + + @Override + protected List getFloatListInternal(int columnIndex) { + return null; + } + @Override protected double[] getDoubleArrayInternal(int columnIndex) { return null; @@ -222,6 +237,13 @@ public static Collection parameters() { Collections.singletonList("getValue") }, {Type.int64(), "getLongInternal", 123L, "getLong", Collections.singletonList("getValue")}, + { + Type.float32(), + "getFloatInternal", + 2.0f, + "getFloat", + Collections.singletonList("getValue") + }, { Type.float64(), "getDoubleInternal", @@ -306,6 +328,20 @@ public static Collection parameters() { "getLongList", Arrays.asList("getLongArray", "getValue") }, + { + Type.array(Type.float32()), + "getFloatArrayInternal", + new float[] {1.0f, 2.0f}, + "getFloatArray", + Arrays.asList("getFloatList", "getValue") + }, + { + Type.array(Type.float32()), + "getFloatListInternal", + Arrays.asList(2.0f, 4.0f), + "getFloatList", + Arrays.asList("getFloatArray", "getValue") + }, { Type.array(Type.float64()), "getDoubleArrayInternal", 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 cb73618d99..5e6a4ffc2c 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 @@ -537,6 +537,8 @@ public void serialization() { Value.int64(null), Value.float64(1.0), Value.float64(null), + Value.float32(1.0f), + Value.float32(null), Value.bytes(ByteArray.fromBase64("abcd")), Value.bytesFromBase64( Base64.getEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8))), @@ -554,6 +556,8 @@ public void serialization() { Value.int64Array((long[]) null), Value.float64Array(new double[] {1.1, 2.2, 3.3}), Value.float64Array((double[]) null), + Value.float32Array(new float[] {1.1f, 2.2f, 3.3f}), + Value.float32Array((float[]) null), Value.bytesArray(Arrays.asList(ByteArray.fromBase64("abcd"), null)), Value.bytesArrayFromBase64( Arrays.asList( @@ -655,6 +659,22 @@ public void getDouble() { assertThat(resultSet.getDouble(0)).isWithin(0.0).of(Double.MAX_VALUE); } + @Test + public void getFloat() { + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.float32())))) + .addValues(Value.float32(Float.MIN_VALUE).toProto()) + .addValues(Value.float32(Float.MAX_VALUE).toProto()) + .build()); + consumer.onCompleted(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getFloat(0)).isWithin(0.0f).of(Float.MIN_VALUE); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getFloat(0)).isWithin(0.0f).of(Float.MAX_VALUE); + } + @Test public void getBigDecimal() { consumer.onPartialResultSet( @@ -877,6 +897,25 @@ public void getDoubleArray() { .inOrder(); } + @Test + public void getFloatArray() { + float[] floatArray = {Float.MAX_VALUE, Float.MIN_VALUE, 111, 333, 444, 0, -1, -2234}; + + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata(Type.struct(Type.StructField.of("f", Type.array(Type.float32()))))) + .addValues(Value.float32Array(floatArray).toProto()) + .build()); + consumer.onCompleted(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getFloatArray(0)) + .usingTolerance(0.0) + .containsExactly(floatArray) + .inOrder(); + } + @Test public void getBigDecimalList() { List bigDecimalsList = new ArrayList<>(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index f27aa405aa..3b0b7812d2 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -1284,6 +1284,8 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).toDateArray(null); break; + case FLOAT32: + builder.bind(fieldName).toFloat32Array((Iterable) null); case FLOAT64: builder.bind(fieldName).toFloat64Array((Iterable) null); break; @@ -1327,6 +1329,8 @@ private Statement buildStatement( case DATE: builder.bind(fieldName).to((Date) null); break; + case FLOAT32: + builder.bind(fieldName).to((Float) null); case FLOAT64: builder.bind(fieldName).to((Double) null); break; 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 f38b5e47b8..0cfb57d4a2 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 @@ -211,12 +211,37 @@ public void equalsAndHashCode() { Mutation.delete("T1", KeySet.singleKey(Key.of("k"))), Mutation.delete("T1", Key.of("k"))); // Test NaNs + // Refer the comment in `Value.hashCode()` for more details on NaN equality. tester.addEqualityGroup( Mutation.newInsertBuilder("T1").set("C").to(Double.NaN).build(), Mutation.newInsertBuilder("T1").set("C").to(Value.float64(Double.NaN)).build(), Mutation.newInsertBuilder("T1").set("C").to(Float.NaN).build(), - Mutation.newInsertBuilder("T1").set("C").to(Value.float64(Float.NaN)).build()); + Mutation.newInsertBuilder("T1").set("C").to(Value.float64(Float.NaN)).build(), + Mutation.newInsertBuilder("T1").set("C").to(Value.float32(Float.NaN)).build()); + // Test NaN arrays + tester.addEqualityGroup( + Mutation.newInsertBuilder("T1").set("C").toFloat32Array(new float[] {Float.NaN}).build(), + Mutation.newInsertBuilder("T1") + .set("C") + .toFloat32Array(new float[] {Float.NaN}, 0, 1) + .build(), + Mutation.newInsertBuilder("T1") + .set("C") + .toFloat32Array(Collections.singletonList(Float.NaN)) + .build(), + Mutation.newInsertBuilder("T1") + .set("C") + .to(Value.float32Array(new float[] {Float.NaN})) + .build(), + Mutation.newInsertBuilder("T1") + .set("C") + .to(Value.float32Array(new float[] {Float.NaN}, 0, 1)) + .build(), + Mutation.newInsertBuilder("T1") + .set("C") + .to(Value.float32Array(Collections.singletonList(Float.NaN))) + .build()); tester.addEqualityGroup( Mutation.newInsertBuilder("T1").set("C").toFloat64Array(new double[] {Double.NaN}).build(), Mutation.newInsertBuilder("T1").set("C").toFloat64Array(new double[] {Float.NaN}).build(), @@ -270,6 +295,11 @@ public void equalsAndHashCode() { .set("C") .toFloat64Array(Arrays.asList(null, (double) Float.NaN)) .build()); + tester.addEqualityGroup( + Mutation.newInsertBuilder("T1") + .set("C") + .toFloat32Array(Arrays.asList(null, Float.NaN)) + .build()); tester.testEquals(); } @@ -523,11 +553,17 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) { .to((Long) null) .set("intValue") .to(Value.int64(1L)) - .set("float") + .set("float32") + .to(42.1f) + .set("float32Null") + .to((Float) null) + .set("float32Value") + .to(Value.float32(10f)) + .set("float64") .to(42.1) - .set("floatNull") + .set("float64Null") .to((Double) null) - .set("floatValue") + .set("float64Value") .to(Value.float64(10D)) .set("string") .to("str") @@ -583,11 +619,17 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) { .toInt64Array((long[]) null) .set("intArrValue") .to(Value.int64Array(ImmutableList.of(1L, 2L))) - .set("floatArr") + .set("float32Arr") + .toFloat32Array(new float[] {1.1f, 2.2f, 3.3f}) + .set("float32ArrNull") + .toFloat32Array((float[]) null) + .set("float32ArrValue") + .to(Value.float32Array(ImmutableList.of(10.1F, 10.2F, 10.3F))) + .set("float64Arr") .toFloat64Array(new double[] {1.1, 2.2, 3.3}) - .set("floatArrNull") + .set("float64ArrNull") .toFloat64Array((double[]) null) - .set("floatArrValue") + .set("float64ArrValue") .to(Value.float64Array(ImmutableList.of(10.1D, 10.2D, 10.3D))) .set("stringArr") .toStringArray(ImmutableList.of("one", "two")) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java index b5bbb9dd49..6cf1d0900a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java @@ -37,6 +37,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.FLOAT32).build(), Type.newBuilder().setCode(TypeCode.STRING).build(), Type.newBuilder().setCode(TypeCode.BYTES).build(), Type.newBuilder().setCode(TypeCode.DATE).build(), @@ -53,6 +54,10 @@ public class RandomResultSetGenerator { .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT64)) .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT32)) + .build(), Type.newBuilder() .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING)) @@ -138,6 +143,9 @@ private void setRandomValue(Value.Builder builder, Type type) { case FLOAT64: builder.setNumberValue(random.nextDouble()); break; + case FLOAT32: + builder.setNumberValue(random.nextFloat()); + break; case INT64: builder.setStringValue(String.valueOf(random.nextLong())); break; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java index bb8d130914..6801c82b66 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java @@ -298,9 +298,12 @@ public void readOnlyTransaction() throws Exception { values2 = rs.toListAsync(input -> input.getString("Value"), executor); } } + + ApiFuture>> allValuesAsList = + ApiFutures.allAsList(Arrays.asList(values1, values2)); ApiFuture> allValues = ApiFutures.transform( - ApiFutures.allAsList(Arrays.asList(values1, values2)), + allValuesAsList, input -> Iterables.mergeSorted( input, 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 a72c9872fa..8d97d9d894 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 @@ -170,6 +170,9 @@ private void assertRow(Struct actualRow, JSONArray expectedRow) throws Exception case INT64: assertThat(actualRow.getLong(i)).isEqualTo(expectedRow.getLong(i)); break; + case FLOAT32: + assertThat(actualRow.getFloat(i)).isEqualTo(expectedRow.getFloat(i)); + break; case FLOAT64: assertThat(actualRow.getDouble(i)).isEqualTo(expectedRow.getDouble(i)); break; @@ -208,6 +211,9 @@ private List getRawList(Struct actualRow, int index, Type elementType) { case INT64: rawList = actualRow.getLongList(index); break; + case FLOAT32: + rawList = actualRow.getFloatList(index); + break; case FLOAT64: rawList = actualRow.getDoubleList(index); break; 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 8e1f257594..454bd3c70a 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 @@ -31,6 +31,7 @@ import com.google.cloud.spanner.SingerProto.Genre; import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.common.primitives.Doubles; +import com.google.common.primitives.Floats; import com.google.common.primitives.Longs; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.AbstractMessage; @@ -53,6 +54,7 @@ public class ResultSetsTest { @Test public void resultSetIteration() { double doubleVal = 1.2; + float floatVal = 6.626f; BigDecimal bigDecimalVal = BigDecimal.valueOf(123, 2); String stringVal = "stringVal"; String jsonVal = "{\"color\":\"red\",\"value\":\"#f00\"}"; @@ -71,6 +73,7 @@ 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}; + float[] floatArray = {Float.MIN_VALUE, Float.MAX_VALUE, 0, 1, -1, 1.2341f}; BigDecimal[] bigDecimalArray = { BigDecimal.valueOf(1, Integer.MAX_VALUE), BigDecimal.valueOf(1, Integer.MIN_VALUE), @@ -102,6 +105,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("floatVal", Type.float32()), Type.StructField.of("bigDecimalVal", Type.numeric()), Type.StructField.of("stringVal", Type.string()), Type.StructField.of("jsonVal", Type.json()), @@ -116,6 +120,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("floatArray", Type.array(Type.float32())), Type.StructField.of("bigDecimalArray", Type.array(Type.numeric())), Type.StructField.of("byteArray", Type.array(Type.bytes())), Type.StructField.of("timestampArray", Type.array(Type.timestamp())), @@ -138,6 +143,8 @@ public void resultSetIteration() { .to(Value.bool(true)) .set("doubleVal") .to(Value.float64(doubleVal)) + .set("floatVal") + .to(Value.float32(floatVal)) .set("bigDecimalVal") .to(Value.numeric(bigDecimalVal)) .set("stringVal") @@ -162,6 +169,8 @@ public void resultSetIteration() { .to(Value.int64Array(longArray)) .set("doubleArray") .to(Value.float64Array(doubleArray)) + .set("floatArray") + .to(Value.float32Array(floatArray)) .set("bigDecimalArray") .to(Value.numericArray(Arrays.asList(bigDecimalArray))) .set("byteArray") @@ -195,6 +204,8 @@ public void resultSetIteration() { .to(Value.bool(null)) .set("doubleVal") .to(Value.float64(doubleVal)) + .set("floatVal") + .to(Value.float32(floatVal)) .set("bigDecimalVal") .to(Value.numeric(bigDecimalVal)) .set("stringVal") @@ -219,6 +230,8 @@ public void resultSetIteration() { .to(Value.int64Array(longArray)) .set("doubleArray") .to(Value.float64Array(doubleArray)) + .set("floatArray") + .to(Value.float32Array(floatArray)) .set("bigDecimalArray") .to(Value.numericArray(Arrays.asList(bigDecimalArray))) .set("byteArray") @@ -274,6 +287,10 @@ public void resultSetIteration() { assertThat(rs.getValue("doubleVal").getFloat64()).isWithin(0.0).of(doubleVal); assertThat(rs.getDouble(columnIndex)).isWithin(0.0).of(doubleVal); assertThat(rs.getValue(columnIndex++).getFloat64()).isWithin(0.0).of(doubleVal); + assertThat(rs.getFloat(columnIndex)).isWithin(0.0f).of(floatVal); + assertThat(rs.getValue(columnIndex++).getFloat32()).isWithin(0.0f).of(floatVal); + assertThat(rs.getFloat("floatVal")).isWithin(0.0f).of(floatVal); + assertThat(rs.getValue("floatVal").getFloat32()).isWithin(0.0f).of(floatVal); assertThat(rs.getBigDecimal("bigDecimalVal")).isEqualTo(new BigDecimal("1.23")); assertThat(rs.getValue("bigDecimalVal")).isEqualTo(Value.numeric(new BigDecimal("1.23"))); assertThat(rs.getBigDecimal(columnIndex)).isEqualTo(new BigDecimal("1.23")); @@ -338,6 +355,17 @@ public void resultSetIteration() { assertThat(rs.getValue("doubleArray")).isEqualTo(Value.float64Array(doubleArray)); assertThat(rs.getDoubleList(columnIndex++)).isEqualTo(Doubles.asList(doubleArray)); assertThat(rs.getDoubleList("doubleArray")).isEqualTo(Doubles.asList(doubleArray)); + + assertThat(rs.getFloatArray(columnIndex)).usingTolerance(0.0f).containsAtLeast(floatArray); + assertThat(rs.getValue(columnIndex)).isEqualTo(Value.float32Array(floatArray)); + assertThat(rs.getFloatArray("floatArray")) + .usingTolerance(0.0f) + .containsExactly(floatArray) + .inOrder(); + assertThat(rs.getValue("floatArray")).isEqualTo(Value.float32Array(floatArray)); + assertThat(rs.getFloatList(columnIndex++)).isEqualTo(Floats.asList(floatArray)); + assertThat(rs.getFloatList("floatArray")).isEqualTo(Floats.asList(floatArray)); + assertThat(rs.getBigDecimalList(columnIndex)).isEqualTo(Arrays.asList(bigDecimalArray)); assertThat(rs.getValue(columnIndex++)) .isEqualTo(Value.numericArray(Arrays.asList(bigDecimalArray))); 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 11b708ed48..6eedc058c5 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 @@ -110,6 +110,16 @@ Type newType() { }.test(); } + @Test + public void float32() { + new ScalarTypeTester(Type.Code.FLOAT32, TypeCode.FLOAT32) { + @Override + Type newType() { + return Type.float32(); + } + }.test(); + } + @Test public void float64() { new ScalarTypeTester(Type.Code.FLOAT64, TypeCode.FLOAT64) { @@ -307,6 +317,16 @@ Type newElementType() { }.test(); } + @Test + public void float32Array() { + new ArrayTypeTester(Type.Code.FLOAT32, TypeCode.FLOAT32, true) { + @Override + Type newElementType() { + return Type.float32(); + } + }.test(); + } + @Test public void float64Array() { new ArrayTypeTester(Type.Code.FLOAT64, TypeCode.FLOAT64, 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 d50814e84d..204880bf7d 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 @@ -265,6 +265,14 @@ public static Long defaultLongWrapper() { return 1234L; } + public static float defaultFloatPrimitive() { + return 1.0f; + } + + public static Float defaultFloatWrapper() { + return 1.0f; + } + public static double defaultDoublePrimitive() { return 1.0; } @@ -329,6 +337,14 @@ public static Iterable defaultLongIterable() { return Arrays.asList(1L, 2L); } + public static float[] defaultFloatArray() { + return new float[] {1.0f, 2.0f}; + } + + public static Iterable defaultFloatIterable() { + return Arrays.asList(1.0f, 2.0f); + } + public static double[] defaultDoubleArray() { return new double[] {1.0, 2.0}; } 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 5176013cf3..d692fc44e7 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 @@ -185,6 +185,37 @@ public void int64WrapperNull() { assertEquals("NULL", v.getAsString()); } + @Test + public void float32() { + Value v = Value.float32(1.23f); + assertThat(v.getType()).isEqualTo(Type.float32()); + assertThat(v.isNull()).isFalse(); + assertThat(v.getFloat32()).isWithin(0.0001f).of(1.23f); + assertThat(v.toString()).isEqualTo("1.23"); + assertEquals("1.23", v.getAsString()); + } + + @Test + public void float32Wrapper() { + Value v = Value.float32(Float.valueOf(1.23f)); + assertThat(v.getType()).isEqualTo(Type.float32()); + assertThat(v.isNull()).isFalse(); + assertThat(v.getFloat32()).isWithin(0.0001f).of(1.23f); + assertThat(v.toString()).isEqualTo("1.23"); + assertEquals("1.23", v.getAsString()); + } + + @Test + public void float32WrapperNull() { + Value v = Value.float32(null); + assertThat(v.getType()).isEqualTo(Type.float32()); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat32); + assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); + } + @Test public void float64() { Value v = Value.float64(1.23); @@ -863,6 +894,60 @@ public void int64ArrayNullTryGetBool() { assertThat(e.getMessage()).contains("Expected: BOOL actual: ARRAY"); } + @Test + public void float32Array() { + Value v = Value.float32Array(new float[] {.1f, .2f}); + assertThat(v.isNull()).isFalse(); + assertThat(v.getFloat32Array()).containsExactly(.1f, .2f).inOrder(); + assertThat(v.toString()).isEqualTo("[0.1,0.2]"); + assertEquals("[0.1,0.2]", v.getAsString()); + } + + @Test + public void float32ArrayRange() { + Value v = Value.float32Array(new float[] {.1f, .2f, .3f, .4f, .5f}, 1, 3); + assertThat(v.isNull()).isFalse(); + assertThat(v.getFloat32Array()).containsExactly(.2f, .3f, .4f).inOrder(); + assertThat(v.toString()).isEqualTo("[0.2,0.3,0.4]"); + assertEquals("[0.2,0.3,0.4]", v.getAsString()); + } + + @Test + public void float32ArrayNull() { + Value v = Value.float32Array((float[]) null); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat32Array); + assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); + } + + @Test + public void float32ArrayWrapper() { + Value v = Value.float32Array(Arrays.asList(.1f, null, .3f)); + assertThat(v.isNull()).isFalse(); + assertThat(v.getFloat32Array()).containsExactly(.1f, null, .3f).inOrder(); + assertThat(v.toString()).isEqualTo("[0.1,NULL,0.3]"); + assertEquals("[0.1,NULL,0.3]", v.getAsString()); + } + + @Test + public void float32ArrayWrapperNull() { + Value v = Value.float32Array((Iterable) null); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = assertThrows(IllegalStateException.class, v::getFloat32Array); + assertThat(e.getMessage()).contains("null value"); + assertEquals("NULL", v.getAsString()); + } + + @Test + public void float32ArrayTryGetFloat64Array() { + Value value = Value.float32Array(Collections.singletonList(.1f)); + IllegalStateException e = assertThrows(IllegalStateException.class, value::getFloat64Array); + assertThat(e.getMessage()).contains("Expected: ARRAY actual: ARRAY"); + } + @Test public void float64Array() { Value v = Value.float64Array(new double[] {.1, .2}); @@ -1426,6 +1511,13 @@ public void testValueToProto() { com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), Value.int64(null).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder().setNumberValue(3.14f).build(), + Value.float32(3.14f).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), + Value.float32(null).toProto()); + assertEquals( com.google.protobuf.Value.newBuilder().setNumberValue(3.14d).build(), Value.float64(3.14d).toProto()); @@ -1512,6 +1604,18 @@ public void testValueToProto() { .build()))) .build(), Value.int64Array(Arrays.asList(1L, null)).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addAllValues( + Arrays.asList( + com.google.protobuf.Value.newBuilder().setNumberValue(3.14f).build(), + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()))) + .build(), + Value.float32Array(Arrays.asList(3.14f, null)).toProto()); assertEquals( com.google.protobuf.Value.newBuilder() .setListValue( @@ -1667,6 +1771,29 @@ public void testValueToProto() { .build(), Value.struct(Struct.newBuilder().add(Value.int64Array(Arrays.asList(1L, null))).build()) .toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addAllValues( + Arrays.asList( + com.google.protobuf.Value.newBuilder() + .setNumberValue(3.14f) + .build(), + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build())) + .build()) + .build()) + .build()) + .build(), + Value.struct( + Struct.newBuilder().add(Value.float32Array(Arrays.asList(3.14f, null))).build()) + .toProto()); assertEquals( com.google.protobuf.Value.newBuilder() .setListValue( @@ -1872,6 +1999,10 @@ public void testEqualsHashCode() { tester.addEqualityGroup(Value.int64(456)); tester.addEqualityGroup(Value.int64(null)); + tester.addEqualityGroup(Value.float32(1.23f), Value.float32(Float.valueOf(1.23f))); + tester.addEqualityGroup(Value.float32(4.56f)); + tester.addEqualityGroup(Value.float32(null)); + tester.addEqualityGroup(Value.float64(1.23), Value.float64(Double.valueOf(1.23))); tester.addEqualityGroup(Value.float64(4.56)); tester.addEqualityGroup(Value.float64(null)); @@ -1938,6 +2069,14 @@ public void testEqualsHashCode() { tester.addEqualityGroup(Value.int64Array(Collections.singletonList(3L))); tester.addEqualityGroup(Value.int64Array((Iterable) null)); + tester.addEqualityGroup( + Value.float32Array(Arrays.asList(.1f, .2f)), + Value.float32Array(new float[] {.1f, .2f}), + Value.float32Array(new float[] {.0f, .1f, .2f, .3f}, 1, 2), + Value.float32Array(plainIterable(.1f, .2f))); + tester.addEqualityGroup(Value.float32Array(Collections.singletonList(.3f))); + tester.addEqualityGroup(Value.float32Array((Iterable) null)); + tester.addEqualityGroup( Value.float64Array(Arrays.asList(.1, .2)), Value.float64Array(new double[] {.1, .2}), @@ -2009,6 +2148,11 @@ public void testGetAsString() { assertEquals(String.valueOf(Long.MAX_VALUE), Value.int64(Long.MAX_VALUE).getAsString()); assertEquals(String.valueOf(Long.MIN_VALUE), Value.int64(Long.MIN_VALUE).getAsString()); + assertEquals("3.14", Value.float32(3.14f).getAsString()); + assertEquals("NaN", Value.float32(Float.NaN).getAsString()); + assertEquals(String.valueOf(Float.MIN_VALUE), Value.float32(Float.MIN_VALUE).getAsString()); + assertEquals(String.valueOf(Float.MAX_VALUE), Value.float32(Float.MAX_VALUE).getAsString()); + assertEquals("3.14", Value.float64(3.14d).getAsString()); assertEquals("NaN", Value.float64(Double.NaN).getAsString()); assertEquals(String.valueOf(Double.MIN_VALUE), Value.float64(Double.MIN_VALUE).getAsString()); @@ -2052,6 +2196,9 @@ public void serialization() { reserializeAndAssert(Value.int64(123)); reserializeAndAssert(Value.int64(null)); + reserializeAndAssert(Value.float32(1.23f)); + reserializeAndAssert(Value.float32(null)); + reserializeAndAssert(Value.float64(1.23)); reserializeAndAssert(Value.float64(null)); @@ -2089,6 +2236,10 @@ public void serialization() { reserializeAndAssert(Value.int64Array(new long[] {1L, 2L})); reserializeAndAssert(Value.int64Array((Iterable) null)); + reserializeAndAssert(Value.float32Array(new float[] {.1f, .2f})); + reserializeAndAssert(Value.float32Array(BrokenSerializationList.of(.1f, .2f, .3f))); + reserializeAndAssert(Value.float32Array((Iterable) null)); + reserializeAndAssert(Value.float64Array(new double[] {.1, .2})); reserializeAndAssert(Value.float64Array(BrokenSerializationList.of(.1, .2, .3))); reserializeAndAssert(Value.float64Array((Iterable) null)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java index 4e8fb0cfcb..759f058aa0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java @@ -57,6 +57,8 @@ public class ChecksumResultSetTest { .to(2 * 2) .set("doubleVal") .to(Value.float64(3.14d * 2d)) + .set("floatVal") + .to(Value.float32(3.14f * 3f)) .set("bigDecimalVal") .to(Value.numeric(BigDecimal.valueOf(123 * 2, 2))) .set("pgNumericVal") @@ -83,6 +85,8 @@ public class ChecksumResultSetTest { .to(Value.int64Array(Arrays.asList(2L, null, 1L, 0L))) .set("doubleArray") .to(Value.float64Array(Arrays.asList(3.14d, null, 6.6626d, 10.1d))) + .set("floatArray") + .to(Value.float32Array(Arrays.asList(2.71f, null, 6.6626f, 10.1f))) .set("bigDecimalArray") .to(Value.numericArray(Arrays.asList(BigDecimal.TEN, null, BigDecimal.ONE))) .set("pgNumericArray") @@ -128,6 +132,7 @@ public void testRetry() { Type.StructField.of("boolVal", Type.bool()), Type.StructField.of("longVal", Type.int64()), Type.StructField.of("doubleVal", Type.float64()), + Type.StructField.of("floatVal", Type.float32()), Type.StructField.of("bigDecimalVal", Type.numeric()), Type.StructField.of("pgNumericVal", Type.pgNumeric()), Type.StructField.of("stringVal", Type.string()), @@ -143,6 +148,7 @@ public void testRetry() { 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("floatArray", Type.array(Type.float32())), Type.StructField.of("bigDecimalArray", Type.array(Type.numeric())), Type.StructField.of("pgNumericArray", Type.array(Type.pgNumeric())), Type.StructField.of("byteArray", Type.array(Type.bytes())), @@ -164,6 +170,8 @@ public void testRetry() { .to(2) .set("doubleVal") .to(Value.float64(3.14d)) + .set("floatVal") + .to(Value.float32(2.71f)) .set("bigDecimalVal") .to(Value.numeric(BigDecimal.valueOf(123, 2))) .set("pgNumericVal") @@ -190,6 +198,8 @@ public void testRetry() { .to(Value.int64Array(Arrays.asList(1L, null, 2L))) .set("doubleArray") .to(Value.float64Array(Arrays.asList(3.14d, null, 6.6626d))) + .set("floatArray") + .to(Value.float32Array(Arrays.asList(2.71f, null, 6.6626f))) .set("bigDecimalArray") .to(Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN))) .set("pgNumericArray") @@ -238,6 +248,8 @@ public void testRetry() { .to((Long) null) .set("doubleVal") .to((Double) null) + .set("floatVal") + .to((Float) null) .set("bigDecimalVal") .to((BigDecimal) null) .set("pgNumericVal") @@ -264,6 +276,8 @@ public void testRetry() { .toInt64Array((Iterable) null) .set("doubleArray") .toFloat64Array((Iterable) null) + .set("floatArray") + .toFloat32Array((Iterable) null) .set("bigDecimalArray") .toNumericArray(null) .set("pgNumericArray") diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java index 5a22c64009..87f26da904 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java @@ -344,9 +344,12 @@ public void readOnlyTransaction() throws Exception { values2 = rs.toListAsync(input -> input.getString("StringValue"), executor); } } + + ApiFuture>> allAsListValues = + ApiFutures.allAsList(Arrays.asList(values1, values2)); ApiFuture> allValues = ApiFutures.transform( - ApiFutures.allAsList(Arrays.asList(values1, values2)), + allAsListValues, input -> Iterables.mergeSorted( input, Comparator.comparing(o -> Integer.valueOf(o.substring(1)))), diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITFloat32Test.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITFloat32Test.java new file mode 100644 index 0000000000..de2640b86c --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITFloat32Test.java @@ -0,0 +1,415 @@ +/* + * Copyright 2024 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.it; + +import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static com.google.common.base.Strings.isNullOrEmpty; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TimestampBound; +import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.connection.ConnectionOptions; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@Category(ParallelIntegrationTest.class) +@RunWith(Parameterized.class) +public class ITFloat32Test { + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + + @Parameterized.Parameters(name = "Dialect = {0}") + public static List data() { + return Arrays.asList( + new DialectTestParameter(Dialect.GOOGLE_STANDARD_SQL), + new DialectTestParameter(Dialect.POSTGRESQL)); + } + + @Parameterized.Parameter() public DialectTestParameter dialect; + + private static DatabaseClient googleStandardSQLClient; + private static DatabaseClient postgreSQLClient; + + private static final String[] GOOGLE_STANDARD_SQL_SCHEMA = + new String[] { + "CREATE TABLE T (" + + " Key STRING(MAX) NOT NULL," + + " Float32Value FLOAT32," + + " Float32ArrayValue ARRAY," + + ") PRIMARY KEY (Key)" + }; + + private static final String[] POSTGRESQL_SCHEMA = + new String[] { + "CREATE TABLE T (" + + " Key VARCHAR PRIMARY KEY," + + " Float32Value REAL," + + " Float32ArrayValue REAL[]" + + ")" + }; + + private static DatabaseClient client; + + private static boolean isUsingCloudDevel() { + String jobType = System.getenv("JOB_TYPE"); + + // Assumes that the jobType contains the string "cloud-devel" to signal that + // the environment is cloud-devel. + return !isNullOrEmpty(jobType) && jobType.contains("cloud-devel"); + } + + @BeforeClass + public static void setUpDatabase() + throws ExecutionException, InterruptedException, TimeoutException { + assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel()); + assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator()); + + Database googleStandardSQLDatabase = + env.getTestHelper().createTestDatabase(GOOGLE_STANDARD_SQL_SCHEMA); + + googleStandardSQLClient = env.getTestHelper().getDatabaseClient(googleStandardSQLDatabase); + + Database postgreSQLDatabase = + env.getTestHelper() + .createTestDatabase(Dialect.POSTGRESQL, Arrays.asList(POSTGRESQL_SCHEMA)); + postgreSQLClient = env.getTestHelper().getDatabaseClient(postgreSQLDatabase); + } + + @Before + public void before() { + client = + dialect.dialect == Dialect.GOOGLE_STANDARD_SQL ? googleStandardSQLClient : postgreSQLClient; + } + + @AfterClass + public static void tearDown() throws Exception { + ConnectionOptions.closeSpanner(); + } + + /** Sequence used to generate unique keys. */ + private static int seq; + + private static String uniqueString() { + return String.format("k%04d", seq++); + } + + private String lastKey; + + private Timestamp write(Mutation m) { + return client.write(Collections.singletonList(m)); + } + + private Mutation.WriteBuilder baseInsert() { + return Mutation.newInsertOrUpdateBuilder("T").set("Key").to(lastKey = uniqueString()); + } + + private Struct readRow(String table, String key, String... columns) { + return client + .singleUse(TimestampBound.strong()) + .readRow(table, Key.of(key), Arrays.asList(columns)); + } + + private Struct readLastRow(String... columns) { + return readRow("T", lastKey, columns); + } + + @Test + public void writeFloat32() { + write(baseInsert().set("Float32Value").to(2.0f).build()); + Struct row = readLastRow("Float32Value"); + assertFalse(row.isNull(0)); + assertEquals(2.0f, row.getFloat(0), 0.0f); + } + + @Test + public void writeFloat32NonNumbers() { + + write(baseInsert().set("Float32Value").to(Float.NEGATIVE_INFINITY).build()); + Struct row = readLastRow("Float32Value"); + assertFalse(row.isNull(0)); + assertEquals(Float.NEGATIVE_INFINITY, row.getFloat(0), 0.0f); + + write(baseInsert().set("Float32Value").to(Float.POSITIVE_INFINITY).build()); + row = readLastRow("Float32Value"); + assertFalse(row.isNull(0)); + assertEquals(Float.POSITIVE_INFINITY, row.getFloat(0), 0.0); + + write(baseInsert().set("Float32Value").to(Float.NaN).build()); + row = readLastRow("Float32Value"); + assertFalse(row.isNull(0)); + assertTrue(Float.isNaN(row.getFloat(0))); + } + + @Test + public void writeFloat32Null() { + write(baseInsert().set("Float32Value").to((Float) null).build()); + Struct row = readLastRow("Float32Value"); + assertTrue(row.isNull(0)); + } + + @Test + public void writeFloat32ArrayNull() { + write(baseInsert().set("Float32ArrayValue").toFloat32Array((float[]) null).build()); + Struct row = readLastRow("Float32ArrayValue"); + assertTrue(row.isNull(0)); + } + + @Test + public void writeFloat32ArrayEmpty() { + write(baseInsert().set("Float32ArrayValue").toFloat32Array(new float[] {}).build()); + Struct row = readLastRow("Float32ArrayValue"); + assertFalse(row.isNull(0)); + assertTrue(row.getFloatList(0).isEmpty()); + } + + @Test + public void writeFloat32Array() { + write( + baseInsert() + .set("Float32ArrayValue") + .toFloat32Array(Arrays.asList(null, 1.0f, 2.0f)) + .build()); + Struct row = readLastRow("Float32ArrayValue"); + assertFalse(row.isNull(0)); + assertEquals(row.getFloatList(0), Arrays.asList(null, 1.0f, 2.0f)); + assertThrows(NullPointerException.class, () -> row.getFloatArray(0)); + } + + @Test + public void writeFloat32ArrayNoNulls() { + write(baseInsert().set("Float32ArrayValue").toFloat32Array(Arrays.asList(1.0f, 2.0f)).build()); + Struct row = readLastRow("Float32ArrayValue"); + assertFalse(row.isNull(0)); + assertEquals(2, row.getFloatArray(0).length); + assertEquals(1.0f, row.getFloatArray(0)[0], 0.0f); + assertEquals(2.0f, row.getFloatArray(0)[1], 0.0f); + } + + private String getInsertStatementWithLiterals() { + String statement = "INSERT INTO T (Key, Float32Value, Float32ArrayValue) VALUES "; + + if (dialect.dialect == Dialect.POSTGRESQL) { + statement += + "('dml1', 3.14::float8, array[1.1]::float4[]), " + + "('dml2', '3.14'::float4, array[3.14::float4, 3.14::float8]::float4[]), " + + "('dml3', 'nan'::real, array['inf'::real, (3.14::float8)::float4, 1.2, '-inf']::float4[]), " + + "('dml4', 1.175494e-38::real, array[1.175494e-38, 3.4028234e38, -3.4028234e38]::real[]), " + + "('dml5', null, null)"; + } else { + statement += + "('dml1', 3.14, [CAST(1.1 AS FLOAT32)]), " + + "('dml2', CAST('3.14' AS FLOAT32), array[CAST(3.14 AS FLOAT32), 3.14]), " + + "('dml3', CAST('nan' AS FLOAT32), array[CAST('inf' AS FLOAT32), CAST(CAST(3.14 AS FLOAT64) AS FLOAT32), 1.2, CAST('-inf' AS FLOAT32)]), " + + "('dml4', 1.175494e-38, [CAST(1.175494e-38 AS FLOAT32), 3.4028234e38, -3.4028234e38]), " + + "('dml5', null, null)"; + } + return statement; + } + + @Test + public void float32Literals() { + client + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate(Statement.of(getInsertStatementWithLiterals())); + return null; + }); + + verifyContents("dml"); + } + + private String getInsertStatementWithParameters() { + String pgStatement = + "INSERT INTO T (Key, Float32Value, Float32ArrayValue) VALUES " + + "('param1', $1, $2), " + + "('param2', $3, $4), " + + "('param3', $5, $6), " + + "('param4', $7, $8), " + + "('param5', $9, $10)"; + + return (dialect.dialect == Dialect.POSTGRESQL) ? pgStatement : pgStatement.replace("$", "@p"); + } + + @Test + public void float32Parameter() { + client + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.newBuilder(getInsertStatementWithParameters()) + .bind("p1") + .to(Value.float32(3.14f)) + .bind("p2") + .to(Value.float32Array(Arrays.asList(1.1f))) + .bind("p3") + .to(Value.float32(3.14f)) + .bind("p4") + .to(Value.float32Array(new float[] {3.14f, 3.14f})) + .bind("p5") + .to(Value.float32(Float.NaN)) + .bind("p6") + .to( + Value.float32Array( + Arrays.asList( + Float.POSITIVE_INFINITY, 3.14f, 1.2f, Float.NEGATIVE_INFINITY))) + .bind("p7") + .to(Value.float32(Float.MIN_NORMAL)) + .bind("p8") + .to( + Value.float32Array( + Arrays.asList( + Float.MIN_NORMAL, Float.MAX_VALUE, -1 * Float.MAX_VALUE))) + .bind("p9") + .to(Value.float32(null)) + .bind("p10") + .to(Value.float32Array((float[]) null)) + .build()); + return null; + }); + + verifyContents("param"); + } + + private String getInsertStatementForUntypedParameters() { + if (dialect.dialect == Dialect.POSTGRESQL) { + return "INSERT INTO T (key, float32value, float32arrayvalue) VALUES " + + "('untyped1', ($1)::float4, ($2)::float4[])"; + } + return "INSERT INTO T (Key, Float32Value, Float32ArrayValue) VALUES " + + "('untyped1', CAST(@p1 AS FLOAT32), CAST(@p2 AS ARRAY))"; + } + + @Test + public void float32UntypedParameter() { + client + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.newBuilder(getInsertStatementForUntypedParameters()) + .bind("p1") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder() + .setNumberValue((double) 3.14f) + .build())) + .bind("p2") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder() + .setListValue( + com.google.protobuf.ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setNumberValue((double) Float.MIN_NORMAL))) + .build())) + .build()); + return null; + }); + + Struct row = readRow("T", "untyped1", "Float32Value", "Float32ArrayValue"); + // Float32Value + assertFalse(row.isNull(0)); + assertEquals(3.14f, row.getFloat(0), 0.00001f); + // Float32ArrayValue + assertFalse(row.isNull(1)); + assertEquals(1, row.getFloatList(1).size()); + assertEquals(Float.MIN_NORMAL, row.getFloatList(1).get(0), 0.00001f); + } + + private void verifyContents(String keyPrefix) { + try (ResultSet resultSet = + client + .singleUse() + .executeQuery( + Statement.of( + "SELECT Key AS key, Float32Value AS float32value, Float32ArrayValue AS float32arrayvalue FROM T WHERE Key LIKE '{keyPrefix}%' ORDER BY key" + .replace("{keyPrefix}", keyPrefix)))) { + + assertTrue(resultSet.next()); + + assertEquals(3.14f, resultSet.getFloat("float32value"), 0.00001f); + assertEquals(Value.float32(3.14f), resultSet.getValue("float32value")); + + assertArrayEquals(new float[] {1.1f}, resultSet.getFloatArray("float32arrayvalue"), 0.00001f); + + assertTrue(resultSet.next()); + + assertEquals(3.14f, resultSet.getFloat("float32value"), 0.00001f); + assertEquals(Arrays.asList(3.14f, 3.14f), resultSet.getFloatList("float32arrayvalue")); + assertEquals( + Value.float32Array(new float[] {3.14f, 3.14f}), resultSet.getValue("float32arrayvalue")); + + assertTrue(resultSet.next()); + assertTrue(Float.isNaN(resultSet.getFloat("float32value"))); + assertTrue(Float.isNaN(resultSet.getValue("float32value").getFloat32())); + assertEquals( + Arrays.asList(Float.POSITIVE_INFINITY, 3.14f, 1.2f, Float.NEGATIVE_INFINITY), + resultSet.getFloatList("float32arrayvalue")); + assertEquals( + Value.float32Array( + Arrays.asList(Float.POSITIVE_INFINITY, 3.14f, 1.2f, Float.NEGATIVE_INFINITY)), + resultSet.getValue("float32arrayvalue")); + + assertTrue(resultSet.next()); + assertEquals(Float.MIN_NORMAL, resultSet.getFloat("float32value"), 0.00001f); + assertEquals(Float.MIN_NORMAL, resultSet.getValue("float32value").getFloat32(), 0.00001f); + assertEquals(3, resultSet.getFloatList("float32arrayvalue").size()); + assertEquals(Float.MIN_NORMAL, resultSet.getFloatList("float32arrayvalue").get(0), 0.00001); + assertEquals(Float.MAX_VALUE, resultSet.getFloatList("float32arrayvalue").get(1), 0.00001f); + assertEquals( + -1 * Float.MAX_VALUE, resultSet.getFloatList("float32arrayvalue").get(2), 0.00001f); + assertEquals(3, resultSet.getValue("float32arrayvalue").getFloat32Array().size()); + + assertTrue(resultSet.next()); + assertTrue(resultSet.isNull("float32value")); + assertTrue(resultSet.isNull("float32arrayvalue")); + + assertFalse(resultSet.next()); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java index a691fbf78b..170ce75e69 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner.it; import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.truth.Truth.assertThat; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; @@ -254,6 +255,36 @@ public void bindInt64Null() { assertThat(row.isNull(0)).isTrue(); } + // TODO: Remove once FLOAT32 is supported in production. + private static boolean isUsingCloudDevel() { + String jobType = System.getenv("JOB_TYPE"); + + // Assumes that the jobType contains the string "cloud-devel" to signal that + // the environment is cloud-devel. + return !isNullOrEmpty(jobType) && jobType.contains("cloud-devel"); + } + + @Test + public void bindFloat32() { + assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator()); + assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel()); + + Struct row = + execute(Statement.newBuilder(selectValueQuery).bind("p1").to(2.0f), Type.float32()); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getFloat(0)).isWithin(0.0f).of(2.0f); + } + + @Test + public void bindFloat32Null() { + assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator()); + assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel()); + + Struct row = + execute(Statement.newBuilder(selectValueQuery).bind("p1").to((Float) null), Type.float32()); + assertThat(row.isNull(0)).isTrue(); + } + @Test public void bindFloat64() { Struct row = execute(Statement.newBuilder(selectValueQuery).bind("p1").to(2.0), Type.float64()); @@ -497,6 +528,58 @@ public void bindInt64ArrayNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void bindFloat32Array() { + assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator()); + assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel()); + + Struct row = + execute( + Statement.newBuilder(selectValueQuery) + .bind("p1") + .toFloat32Array( + asList( + null, + 1.0f, + 2.0f, + Float.NEGATIVE_INFINITY, + Float.POSITIVE_INFINITY, + Float.NaN)), + Type.array(Type.float32())); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getFloatList(0)) + .containsExactly( + null, 1.0f, 2.0f, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NaN) + .inOrder(); + } + + @Test + public void bindFloat32ArrayEmpty() { + assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator()); + assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel()); + + Struct row = + execute( + Statement.newBuilder(selectValueQuery) + .bind("p1") + .toFloat32Array(Collections.emptyList()), + Type.array(Type.float32())); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getFloatList(0)).containsExactly(); + } + + @Test + public void bindFloat32ArrayNull() { + assumeFalse("Emulator does not support FLOAT32 yet", isUsingEmulator()); + assumeTrue("FLOAT32 is currently only supported in cloud-devel", isUsingCloudDevel()); + + Struct row = + execute( + Statement.newBuilder(selectValueQuery).bind("p1").toFloat32Array((float[]) null), + Type.array(Type.float32())); + assertThat(row.isNull(0)).isTrue(); + } + @Test public void bindFloat64Array() { Struct row =