Skip to content
This repository was archived by the owner on Aug 30, 2026. It is now read-only.

Commit 9f26785

Browse files
authored
fix: improve numeric range checks (#424)
* fix: improve numeric range checks * fix: skip numeric ITs on emulator
1 parent 0093f7a commit 9f26785

4 files changed

Lines changed: 204 additions & 6 deletions

File tree

google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,37 @@ public static Value float64(double v) {
123123
}
124124

125125
/**
126-
* Returns a {@code NUMERIC} value.
126+
* Returns a {@code NUMERIC} value. The valid value range for the whole component of the {@link
127+
* BigDecimal} is from -9,999,999,999,999,999,999,999,999 to +9,999,999,999,999,999,999,999,999
128+
* (both inclusive), i.e. the max length of the whole component is 29 digits. The max length of
129+
* the fractional part is 9 digits. Trailing zeros in the fractional part are not considered and
130+
* will be lost, as Cloud Spanner does not preserve the precision of a numeric value.
131+
*
132+
* <p>If you set a numeric value of a record to for example 0.10, Cloud Spanner will return this
133+
* value as 0.1 in subsequent queries. Use {@link BigDecimal#stripTrailingZeros()} to compare
134+
* inserted values with retrieved values if your application might insert numeric values with
135+
* trailing zeros.
127136
*
128137
* @param v the value, which may be null
129138
*/
130139
public static Value numeric(@Nullable BigDecimal v) {
140+
if (v != null) {
141+
// Cloud Spanner does not preserve the precision, so 0.1 is considered equal to 0.10.
142+
BigDecimal test = v.stripTrailingZeros();
143+
if (test.scale() > 9) {
144+
throw SpannerExceptionFactory.newSpannerException(
145+
ErrorCode.OUT_OF_RANGE,
146+
String.format(
147+
"Max scale for a numeric is 9. The requested numeric has scale %d", test.scale()));
148+
}
149+
if (test.precision() - test.scale() > 29) {
150+
throw SpannerExceptionFactory.newSpannerException(
151+
ErrorCode.OUT_OF_RANGE,
152+
String.format(
153+
"Max precision for the whole component of a numeric is 29. The requested numeric has a whole component with precision %d",
154+
test.precision() - test.scale()));
155+
}
156+
}
131157
return new NumericImpl(v == null, v);
132158
}
133159

google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.cloud.Timestamp;
2626
import com.google.cloud.spanner.spi.v1.SpannerRpc;
2727
import com.google.common.base.Function;
28+
import com.google.common.base.Strings;
2829
import com.google.common.collect.ImmutableList;
2930
import com.google.common.collect.ImmutableMap;
3031
import com.google.protobuf.ByteString;
@@ -692,17 +693,26 @@ public void getBigDecimal() {
692693
consumer.onPartialResultSet(
693694
PartialResultSet.newBuilder()
694695
.setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.numeric()))))
695-
.addValues(Value.numeric(BigDecimal.valueOf(Double.MIN_VALUE)).toProto())
696-
.addValues(Value.numeric(BigDecimal.valueOf(Double.MAX_VALUE)).toProto())
696+
.addValues(
697+
Value.numeric(
698+
new BigDecimal(
699+
"-" + Strings.repeat("9", 29) + "." + Strings.repeat("9", 9)))
700+
.toProto())
701+
.addValues(
702+
Value.numeric(
703+
new BigDecimal(Strings.repeat("9", 29) + "." + Strings.repeat("9", 9)))
704+
.toProto())
697705
.addValues(Value.numeric(BigDecimal.ZERO).toProto())
698706
.addValues(Value.numeric(new BigDecimal("1.23456")).toProto())
699707
.build());
700708
consumer.onCompleted();
701709

702710
assertThat(resultSet.next()).isTrue();
703-
assertThat(resultSet.getBigDecimal(0).doubleValue()).isWithin(0.0).of(Double.MIN_VALUE);
711+
assertThat(resultSet.getBigDecimal(0).toPlainString())
712+
.isEqualTo("-99999999999999999999999999999.999999999");
704713
assertThat(resultSet.next()).isTrue();
705-
assertThat(resultSet.getBigDecimal(0).doubleValue()).isWithin(0.0).of(Double.MAX_VALUE);
714+
assertThat(resultSet.getBigDecimal(0).toPlainString())
715+
.isEqualTo("99999999999999999999999999999.999999999");
706716
assertThat(resultSet.next()).isTrue();
707717
assertThat(resultSet.getBigDecimal(0)).isEqualTo(BigDecimal.ZERO);
708718
assertThat(resultSet.next()).isTrue();

google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.cloud.Date;
2626
import com.google.cloud.Timestamp;
2727
import com.google.cloud.spanner.Type.StructField;
28+
import com.google.common.base.Strings;
2829
import com.google.common.collect.ForwardingList;
2930
import com.google.common.collect.Lists;
3031
import com.google.common.testing.EqualsTester;
@@ -265,6 +266,86 @@ public void testNumericFormats() {
265266
assertThat(new BigDecimal("1e-01").toString()).isEqualTo("0.1");
266267
}
267268

269+
@Test
270+
public void numericPrecisionAndScale() {
271+
for (long s : new long[] {1L, -1L}) {
272+
BigDecimal sign = new BigDecimal(s);
273+
assertThat(Value.numeric(new BigDecimal(Strings.repeat("9", 29)).multiply(sign)).toString())
274+
.isEqualTo((s == -1L ? "-" : "") + Strings.repeat("9", 29));
275+
try {
276+
Value.numeric(new BigDecimal(Strings.repeat("9", 30)).multiply(sign));
277+
fail("Missing expected exception");
278+
} catch (SpannerException e) {
279+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.OUT_OF_RANGE);
280+
}
281+
try {
282+
Value.numeric(new BigDecimal("1" + Strings.repeat("0", 29)).multiply(sign));
283+
fail("Missing expected exception");
284+
} catch (SpannerException e) {
285+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.OUT_OF_RANGE);
286+
}
287+
288+
assertThat(
289+
Value.numeric(new BigDecimal("0." + Strings.repeat("9", 9)).multiply(sign))
290+
.toString())
291+
.isEqualTo((s == -1L ? "-" : "") + "0." + Strings.repeat("9", 9));
292+
assertThat(
293+
Value.numeric(new BigDecimal("0.1" + Strings.repeat("0", 8)).multiply(sign))
294+
.toString())
295+
.isEqualTo((s == -1L ? "-" : "") + "0.1" + Strings.repeat("0", 8));
296+
// Cloud Spanner does not store precision and considers 0.1 to be equal to 0.10.
297+
// 0.100000000000000000000000000 is therefore also a valid value, as it will be capped to 0.1.
298+
assertThat(
299+
Value.numeric(new BigDecimal("0.1" + Strings.repeat("0", 20)).multiply(sign))
300+
.toString())
301+
.isEqualTo((s == -1L ? "-" : "") + "0.1" + Strings.repeat("0", 20));
302+
try {
303+
Value.numeric(new BigDecimal("0." + Strings.repeat("9", 10)).multiply(sign));
304+
fail("Missing expected exception");
305+
} catch (SpannerException e) {
306+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.OUT_OF_RANGE);
307+
}
308+
309+
assertThat(
310+
Value.numeric(
311+
new BigDecimal(Strings.repeat("9", 29) + "." + Strings.repeat("9", 9))
312+
.multiply(sign))
313+
.toString())
314+
.isEqualTo(
315+
(s == -1L ? "-" : "") + Strings.repeat("9", 29) + "." + Strings.repeat("9", 9));
316+
317+
try {
318+
Value.numeric(
319+
new BigDecimal(Strings.repeat("9", 30) + "." + Strings.repeat("9", 9)).multiply(sign));
320+
fail("Missing expected exception");
321+
} catch (SpannerException e) {
322+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.OUT_OF_RANGE);
323+
}
324+
try {
325+
Value.numeric(
326+
new BigDecimal("1" + Strings.repeat("0", 29) + "." + Strings.repeat("9", 9))
327+
.multiply(sign));
328+
fail("Missing expected exception");
329+
} catch (SpannerException e) {
330+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.OUT_OF_RANGE);
331+
}
332+
333+
try {
334+
Value.numeric(
335+
new BigDecimal(Strings.repeat("9", 29) + "." + Strings.repeat("9", 10)).multiply(sign));
336+
fail("Missing expected exception");
337+
} catch (SpannerException e) {
338+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.OUT_OF_RANGE);
339+
}
340+
try {
341+
Value.numeric(new BigDecimal("1." + Strings.repeat("9", 10)).multiply(sign));
342+
fail("Missing expected exception");
343+
} catch (SpannerException e) {
344+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.OUT_OF_RANGE);
345+
}
346+
}
347+
}
348+
268349
@Test
269350
public void numericNull() {
270351
Value v = Value.numeric(null);

google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package com.google.cloud.spanner.it;
1818

19-
import static com.google.cloud.spanner.Type.StructField;
2019
import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
2120
import static com.google.common.truth.Truth.assertThat;
2221
import static java.util.Arrays.asList;
@@ -39,10 +38,13 @@
3938
import com.google.cloud.spanner.Struct;
4039
import com.google.cloud.spanner.TimestampBound;
4140
import com.google.cloud.spanner.Type;
41+
import com.google.cloud.spanner.Type.StructField;
4242
import com.google.cloud.spanner.Value;
43+
import com.google.cloud.spanner.testing.EmulatorSpannerHelper;
4344
import com.google.common.base.Joiner;
4445
import com.google.common.collect.Iterables;
4546
import com.google.spanner.v1.ResultSetStats;
47+
import java.math.BigDecimal;
4648
import java.util.ArrayList;
4749
import java.util.Arrays;
4850
import java.util.List;
@@ -269,6 +271,34 @@ public void bindDateNull() {
269271
assertThat(row.isNull(0)).isTrue();
270272
}
271273

274+
@Test
275+
public void bindNumeric() {
276+
assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator());
277+
BigDecimal b = new BigDecimal("1.1");
278+
Struct row = execute(Statement.newBuilder("SELECT @v").bind("v").to(b), Type.numeric());
279+
assertThat(row.isNull(0)).isFalse();
280+
assertThat(row.getBigDecimal(0)).isEqualTo(b);
281+
}
282+
283+
@Test
284+
public void bindNumericNull() {
285+
assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator());
286+
Struct row =
287+
execute(Statement.newBuilder("SELECT @v").bind("v").to((BigDecimal) null), Type.numeric());
288+
assertThat(row.isNull(0)).isTrue();
289+
}
290+
291+
@Test
292+
public void bindNumeric_doesNotPreservePrecision() {
293+
assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator());
294+
BigDecimal b = new BigDecimal("1.10");
295+
Struct row = execute(Statement.newBuilder("SELECT @v").bind("v").to(b), Type.numeric());
296+
assertThat(row.isNull(0)).isFalse();
297+
// Cloud Spanner does not store precision, and will therefore return 1.10 as 1.1.
298+
assertThat(row.getBigDecimal(0)).isNotEqualTo(b);
299+
assertThat(row.getBigDecimal(0)).isEqualTo(b.stripTrailingZeros());
300+
}
301+
272302
@Test
273303
public void bindBoolArray() {
274304
Struct row =
@@ -494,6 +524,57 @@ public void bindDateArrayNull() {
494524
assertThat(row.isNull(0)).isTrue();
495525
}
496526

527+
@Test
528+
public void bindNumericArray() {
529+
assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator());
530+
BigDecimal b1 = new BigDecimal("3.14");
531+
BigDecimal b2 = new BigDecimal("6.626");
532+
533+
Struct row =
534+
execute(
535+
Statement.newBuilder("SELECT @v").bind("v").toNumericArray(asList(b1, b2, null)),
536+
Type.array(Type.numeric()));
537+
assertThat(row.isNull(0)).isFalse();
538+
assertThat(row.getBigDecimalList(0)).containsExactly(b1, b2, null).inOrder();
539+
}
540+
541+
@Test
542+
public void bindNumericArrayEmpty() {
543+
assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator());
544+
Struct row =
545+
execute(
546+
Statement.newBuilder("SELECT @v").bind("v").toNumericArray(Arrays.<BigDecimal>asList()),
547+
Type.array(Type.numeric()));
548+
assertThat(row.isNull(0)).isFalse();
549+
assertThat(row.getBigDecimalList(0)).containsExactly();
550+
}
551+
552+
@Test
553+
public void bindNumericArrayNull() {
554+
assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator());
555+
Struct row =
556+
execute(
557+
Statement.newBuilder("SELECT @v").bind("v").toNumericArray(null),
558+
Type.array(Type.numeric()));
559+
assertThat(row.isNull(0)).isTrue();
560+
}
561+
562+
@Test
563+
public void bindNumericArray_doesNotPreservePrecision() {
564+
assumeFalse("Emulator does not yet support NUMERIC", EmulatorSpannerHelper.isUsingEmulator());
565+
BigDecimal b1 = new BigDecimal("3.14");
566+
BigDecimal b2 = new BigDecimal("6.626070");
567+
568+
Struct row =
569+
execute(
570+
Statement.newBuilder("SELECT @v").bind("v").toNumericArray(asList(b1, b2, null)),
571+
Type.array(Type.numeric()));
572+
assertThat(row.isNull(0)).isFalse();
573+
assertThat(row.getBigDecimalList(0))
574+
.containsExactly(b1.stripTrailingZeros(), b2.stripTrailingZeros(), null)
575+
.inOrder();
576+
}
577+
497578
@Test
498579
public void unsupportedSelectStructValue() {
499580
assumeFalse("The emulator accepts this query", isUsingEmulator());

0 commit comments

Comments
 (0)