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

Commit 29dda01

Browse files
committed
feat: Add Decimal128Value support (#6)
* feat: Add `Decimal128Value` support. * Address feedback. * Address feedback, and fix serialization. * Address feedback. * Add test for numerically equal numbers of different types. * Format. * Address feedback. * Format. * Update the Quadruple and QuadrupleBuilder with the latest updates from GOB. * Fix the documentation lint CI failure.
1 parent d24a591 commit 29dda01

14 files changed

Lines changed: 1537 additions & 51 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.firestore;
18+
19+
import com.google.firestore.v1.MapValue;
20+
import java.io.Serializable;
21+
import java.util.Objects;
22+
import javax.annotation.Nonnull;
23+
24+
/** Represents a 128-bit decimal type in Firestore documents. */
25+
public class Decimal128Value implements Serializable {
26+
private static final long serialVersionUID = 8091951856970036899L;
27+
28+
public final String stringValue;
29+
final Quadruple value;
30+
31+
public Decimal128Value(String val) {
32+
this.stringValue = val;
33+
this.value = Quadruple.fromString(val);
34+
}
35+
36+
MapValue toProto() {
37+
return UserDataConverter.encodeDecimal128Value(stringValue);
38+
}
39+
40+
/**
41+
* Returns true if this Decimal128Value is equal to the provided object.
42+
*
43+
* @param obj The object to compare against.
44+
* @return Whether this Decimal128Value is equal to the provided object.
45+
*/
46+
@Override
47+
public boolean equals(Object obj) {
48+
if (obj == null || getClass() != obj.getClass()) {
49+
return false;
50+
}
51+
52+
Quadruple lhs = this.value;
53+
Quadruple rhs = ((Decimal128Value) obj).value;
54+
55+
// Firestore considers +0 and -0 to be equal, but `Quadruple.compareTo()` does not.
56+
if (lhs.isZero() && rhs.isZero()) return true;
57+
58+
return this == obj || lhs.compareTo(rhs) == 0;
59+
}
60+
61+
@Override
62+
public int hashCode() {
63+
// Since +0 and -0 are considered equal, they should have the same hash code.
64+
Quadruple quadruple =
65+
(this.value.compareTo(Quadruple.NEGATIVE_ZERO) == 0) ? Quadruple.POSITIVE_ZERO : this.value;
66+
67+
return Objects.hash(quadruple);
68+
}
69+
70+
@Nonnull
71+
@Override
72+
public String toString() {
73+
return "Decimal128Value{value=" + this.stringValue + "}";
74+
}
75+
}

google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,18 @@ public Int32Value getInt32Value(@Nonnull String field) {
446446
return (Int32Value) get(field);
447447
}
448448

449+
/**
450+
* Returns the value of the field as a 128-bit decimal.
451+
*
452+
* @param field The path to the field.
453+
* @throws RuntimeException if the value is not a Decimal128Value.
454+
* @return The value of the field.
455+
*/
456+
@Nullable
457+
public Decimal128Value getDecimal128Value(@Nonnull String field) {
458+
return (Decimal128Value) get(field);
459+
}
460+
449461
/**
450462
* Returns the value of the field as a BsonObjectId.
451463
*

google-cloud-firestore/src/main/java/com/google/cloud/firestore/MapType.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ abstract class MapType {
3838
// For Int32 type
3939
static final String RESERVED_INT32_KEY = "__int__";
4040

41+
// For Decimal128 type.
42+
static final String RESERVED_DECIMAL128_KEY = "__decimal128__";
43+
4144
// For RequestTimestamp
4245
static final String RESERVED_BSON_TIMESTAMP_KEY = "__request_timestamp__";
4346
static final String RESERVED_BSON_TIMESTAMP_SECONDS_KEY = "seconds";

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Order.java

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ static TypeOrder fromMapValue(MapValue mapValue) {
9090
return TypeOrder.MAX_KEY;
9191
case REGEX:
9292
return TypeOrder.REGEX;
93+
case DECIMAL128:
9394
case INT32:
9495
return TypeOrder.NUMBER;
9596
case BSON_OBJECT_ID:
@@ -328,8 +329,7 @@ private long getIntegerValue(Value value) {
328329
if (value.hasIntegerValue()) {
329330
return value.getIntegerValue();
330331
}
331-
if (value.hasMapValue()
332-
&& value.getMapValue().getFieldsMap().containsKey(MapType.RESERVED_INT32_KEY)) {
332+
if (UserDataConverter.isInt32Value(value)) {
333333
return value.getMapValue().getFieldsMap().get(MapType.RESERVED_INT32_KEY).getIntegerValue();
334334
}
335335
throw new IllegalArgumentException("getIntegerValue was called on a non-integer value.");
@@ -343,6 +343,18 @@ private int compareNumbers(Value left, Value right) {
343343
return 1;
344344
}
345345

346+
// If either argument is Decimal128, we cast both to wider (128-bit) representation, and compare
347+
// Quadruple values.
348+
if (UserDataConverter.isDecimal128Value(left) || UserDataConverter.isDecimal128Value(right)) {
349+
Quadruple leftQuadruple = convertNumberToQuadruple(left);
350+
Quadruple rightQuadruple = convertNumberToQuadruple(right);
351+
352+
// Firestore considers +0 and -0 to be equal, but `Quadruple.compareTo()` does not.
353+
if (leftQuadruple.isZero() && rightQuadruple.isZero()) return 0;
354+
355+
return leftQuadruple.compareTo(rightQuadruple);
356+
}
357+
346358
if (left.getValueTypeCase() == ValueTypeCase.DOUBLE_VALUE) {
347359
if (right.getValueTypeCase() == ValueTypeCase.DOUBLE_VALUE) {
348360
// left and right are both doubles.
@@ -391,7 +403,44 @@ private int compareBsonBinary(Value left, Value right) {
391403
}
392404

393405
private boolean isNaN(Value value) {
394-
return value.hasDoubleValue() && Double.isNaN(value.getDoubleValue());
406+
if (value.hasDoubleValue() && Double.isNaN(value.getDoubleValue())) {
407+
return true;
408+
}
409+
410+
if (UserDataConverter.isDecimal128Value(value)) {
411+
return value
412+
.getMapValue()
413+
.getFieldsMap()
414+
.get(MapType.RESERVED_DECIMAL128_KEY)
415+
.getStringValue()
416+
.equals("NaN");
417+
}
418+
419+
return false;
420+
}
421+
422+
/**
423+
* Converts the given number value to a Quadruple. Throws an exception if the value is not a
424+
* number.
425+
*/
426+
private Quadruple convertNumberToQuadruple(Value value) {
427+
// Doubles
428+
if (value.hasDoubleValue()) {
429+
return Quadruple.fromDouble(value.getDoubleValue());
430+
}
431+
432+
// 32-bit and 64-bit integers.
433+
if (UserDataConverter.isIntegerValue(value)) {
434+
return Quadruple.fromLong(getIntegerValue(value));
435+
}
436+
437+
// Decimal128 numbers
438+
if (UserDataConverter.isDecimal128Value(value)) {
439+
return UserDataConverter.decodeDecimal128Value(value.getMapValue()).value;
440+
}
441+
442+
throw new IllegalArgumentException(
443+
"convertNumberToQuadruple was called on a non-numeric value.");
395444
}
396445

397446
private int compareDoubles(double left, double right) {

0 commit comments

Comments
 (0)