Skip to content

Commit b640454

Browse files
milaGGLehsannas
authored andcommitted
Add decimal128 to new types (#344)
* add decimal128 * add new tests * format * resolve comments * update comments * refactor compareNumbers * copy paste Quadruple code * resolve comments 1 * make Quadruple value in decimal128 private
1 parent cbba951 commit b640454

23 files changed

+2686
-262
lines changed

common/api-review/firestore-lite.api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ export function connectFirestoreEmulator(firestore: Firestore, host: string, por
136136
// @public
137137
export function count(): AggregateField<number>;
138138

139+
// @public
140+
export class Decimal128Value {
141+
constructor(value: string);
142+
isEqual(other: Decimal128Value): boolean;
143+
// (undocumented)
144+
readonly stringValue: string;
145+
}
146+
139147
// @public
140148
export function deleteDoc<AppModelType, DbModelType extends DocumentData>(reference: DocumentReference<AppModelType, DbModelType>): Promise<void>;
141149

common/api-review/firestore.api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ export function connectFirestoreEmulator(firestore: Firestore, host: string, por
142142
// @public
143143
export function count(): AggregateField<number>;
144144

145+
// @public
146+
export class Decimal128Value {
147+
constructor(value: string);
148+
isEqual(other: Decimal128Value): boolean;
149+
// (undocumented)
150+
readonly stringValue: string;
151+
}
152+
145153
// @public
146154
export function deleteAllPersistentCacheIndexes(indexManager: PersistentCacheIndexManager): void;
147155

packages/firestore/lite/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ export { VectorValue } from '../src/lite-api/vector_value';
143143

144144
export { Int32Value } from '../src/lite-api/int32_value';
145145

146+
export { Decimal128Value } from '../src/lite-api/decimal128_value';
147+
146148
export { RegexValue } from '../src/lite-api/regex_value';
147149

148150
export { BsonBinaryData } from '../src/lite-api/bson_binary_data';

packages/firestore/src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ export { VectorValue } from './lite-api/vector_value';
180180

181181
export { Int32Value } from './lite-api/int32_value';
182182

183+
export { Decimal128Value } from './lite-api/decimal128_value';
184+
183185
export { RegexValue } from './lite-api/regex_value';
184186

185187
export { BsonBinaryData } from './lite-api/bson_binary_data';

packages/firestore/src/index/firestore_index_value_writer.ts

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ import {
3131
MapRepresentation,
3232
RESERVED_REGEX_PATTERN_KEY,
3333
RESERVED_REGEX_OPTIONS_KEY,
34-
RESERVED_INT32_KEY
34+
RESERVED_INT32_KEY,
35+
RESERVED_DECIMAL128_KEY
3536
} from '../model/values';
36-
import { ArrayValue, MapValue, Value } from '../protos/firestore_proto_api';
37+
import {
38+
ArrayValue,
39+
MapValue,
40+
Value,
41+
Timestamp,
42+
LatLng
43+
} from '../protos/firestore_proto_api';
3744
import { fail } from '../util/assert';
3845
import { isNegativeZero } from '../util/types';
3946

@@ -106,26 +113,10 @@ export class FirestoreIndexValueWriter {
106113
this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
107114
encoder.writeNumber(normalizeNumber(indexValue.integerValue));
108115
} else if ('doubleValue' in indexValue) {
109-
const n = normalizeNumber(indexValue.doubleValue);
110-
if (isNaN(n)) {
111-
this.writeValueTypeLabel(encoder, INDEX_TYPE_NAN);
112-
} else {
113-
this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
114-
if (isNegativeZero(n)) {
115-
// -0.0, 0 and 0.0 are all considered the same
116-
encoder.writeNumber(0.0);
117-
} else {
118-
encoder.writeNumber(n);
119-
}
120-
}
116+
const doubleValue = normalizeNumber(indexValue.doubleValue);
117+
this.writeIndexDouble(doubleValue, encoder);
121118
} else if ('timestampValue' in indexValue) {
122-
let timestamp = indexValue.timestampValue!;
123-
this.writeValueTypeLabel(encoder, INDEX_TYPE_TIMESTAMP);
124-
if (typeof timestamp === 'string') {
125-
timestamp = normalizeTimestamp(timestamp);
126-
}
127-
encoder.writeString(`${timestamp.seconds || ''}`);
128-
encoder.writeNumber(timestamp.nanos || 0);
119+
this.writeIndexTimestamp(indexValue.timestampValue!, encoder);
129120
} else if ('stringValue' in indexValue) {
130121
this.writeIndexString(indexValue.stringValue!, encoder);
131122
this.writeTruncationMarker(encoder);
@@ -136,10 +127,7 @@ export class FirestoreIndexValueWriter {
136127
} else if ('referenceValue' in indexValue) {
137128
this.writeIndexEntityRef(indexValue.referenceValue!, encoder);
138129
} else if ('geoPointValue' in indexValue) {
139-
const geoPoint = indexValue.geoPointValue!;
140-
this.writeValueTypeLabel(encoder, INDEX_TYPE_GEOPOINT);
141-
encoder.writeNumber(geoPoint.latitude || 0);
142-
encoder.writeNumber(geoPoint.longitude || 0);
130+
this.writeIndexGeoPoint(indexValue.geoPointValue!, encoder);
143131
} else if ('mapValue' in indexValue) {
144132
const type = detectMapRepresentation(indexValue);
145133
if (type === MapRepresentation.INTERNAL_MAX) {
@@ -159,12 +147,14 @@ export class FirestoreIndexValueWriter {
159147
} else if (type === MapRepresentation.BSON_OBJECT_ID) {
160148
this.writeIndexBsonObjectId(indexValue.mapValue!, encoder);
161149
} else if (type === MapRepresentation.INT32) {
162-
this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
163-
encoder.writeNumber(
164-
normalizeNumber(
165-
indexValue.mapValue!.fields![RESERVED_INT32_KEY]!.integerValue!
166-
)
150+
this.writeIndexInt32(indexValue.mapValue!, encoder);
151+
} else if (type === MapRepresentation.DECIMAL128) {
152+
// Double and Decimal128 sort the same
153+
// Decimal128 is written as double with precision lost
154+
const parsedValue = parseFloat(
155+
indexValue.mapValue!.fields![RESERVED_DECIMAL128_KEY]!.stringValue!
167156
);
157+
this.writeIndexDouble(parsedValue, encoder);
168158
} else {
169159
this.writeIndexMap(indexValue.mapValue!, encoder);
170160
this.writeTruncationMarker(encoder);
@@ -192,6 +182,54 @@ export class FirestoreIndexValueWriter {
192182
encoder.writeString(stringIndexValue);
193183
}
194184

185+
private writeIndexDouble(
186+
double: number,
187+
encoder: DirectionalIndexByteEncoder
188+
): void {
189+
if (isNaN(double)) {
190+
this.writeValueTypeLabel(encoder, INDEX_TYPE_NAN);
191+
} else {
192+
this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
193+
if (isNegativeZero(double)) {
194+
// -0.0, 0 and 0.0 are all considered the same
195+
encoder.writeNumber(0.0);
196+
} else {
197+
encoder.writeNumber(double);
198+
}
199+
}
200+
}
201+
202+
private writeIndexInt32(
203+
mapValue: MapValue,
204+
encoder: DirectionalIndexByteEncoder
205+
): void {
206+
this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
207+
encoder.writeNumber(
208+
normalizeNumber(mapValue.fields![RESERVED_INT32_KEY]!.integerValue!)
209+
);
210+
}
211+
212+
private writeIndexTimestamp(
213+
timestamp: Timestamp,
214+
encoder: DirectionalIndexByteEncoder
215+
): void {
216+
this.writeValueTypeLabel(encoder, INDEX_TYPE_TIMESTAMP);
217+
if (typeof timestamp === 'string') {
218+
timestamp = normalizeTimestamp(timestamp);
219+
}
220+
encoder.writeString(`${timestamp.seconds || ''}`);
221+
encoder.writeNumber(timestamp.nanos || 0);
222+
}
223+
224+
private writeIndexGeoPoint(
225+
geoPoint: LatLng,
226+
encoder: DirectionalIndexByteEncoder
227+
): void {
228+
this.writeValueTypeLabel(encoder, INDEX_TYPE_GEOPOINT);
229+
encoder.writeNumber(geoPoint.latitude || 0);
230+
encoder.writeNumber(geoPoint.longitude || 0);
231+
}
232+
195233
private writeIndexMap(
196234
mapIndexValue: MapValue,
197235
encoder: DirectionalIndexByteEncoder
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { Quadruple } from '../util/quadruple';
19+
20+
/**
21+
* Represents a 128-bit decimal type in Firestore documents.
22+
*
23+
* @class Decimal128Value
24+
*/
25+
export class Decimal128Value {
26+
readonly stringValue: string;
27+
private value: Quadruple;
28+
29+
constructor(value: string) {
30+
this.stringValue = value;
31+
this.value = Quadruple.fromString(value);
32+
}
33+
34+
/**
35+
* Returns true if this `Decimal128Value` is equal to the provided one.
36+
*
37+
* @param other - The `Decimal128Value` to compare against.
38+
* @return 'true' if this `Decimal128Value` is equal to the provided one.
39+
*/
40+
isEqual(other: Decimal128Value): boolean {
41+
// Firestore considers +0 and -0 to be equal.
42+
if (this.value.isZero() && other.value.isZero()) {
43+
return true;
44+
}
45+
return this.value.compareTo(other.value) === 0;
46+
}
47+
}

packages/firestore/src/lite-api/user_data_reader.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ import {
5555
RESERVED_BSON_TIMESTAMP_INCREMENT_KEY,
5656
RESERVED_BSON_BINARY_KEY,
5757
RESERVED_MIN_KEY,
58-
RESERVED_MAX_KEY
58+
RESERVED_MAX_KEY,
59+
RESERVED_DECIMAL128_KEY
5960
} from '../model/values';
6061
import { newSerializer } from '../platform/serializer';
6162
import {
@@ -80,6 +81,7 @@ import { BsonObjectId } from './bson_object_Id';
8081
import { BsonTimestamp } from './bson_timestamp';
8182
import { Bytes } from './bytes';
8283
import { Firestore } from './database';
84+
import { Decimal128Value } from './decimal128_value';
8385
import { FieldPath } from './field_path';
8486
import { FieldValue } from './field_value';
8587
import { GeoPoint } from './geo_point';
@@ -936,6 +938,8 @@ function parseScalarValue(
936938
return parseBsonObjectId(value);
937939
} else if (value instanceof Int32Value) {
938940
return parseInt32Value(value);
941+
} else if (value instanceof Decimal128Value) {
942+
return parseDecimal128Value(value);
939943
} else if (value instanceof BsonTimestamp) {
940944
return parseBsonTimestamp(value);
941945
} else if (value instanceof BsonBinaryData) {
@@ -1045,6 +1049,17 @@ export function parseInt32Value(value: Int32Value): ProtoValue {
10451049
return { mapValue };
10461050
}
10471051

1052+
export function parseDecimal128Value(value: Decimal128Value): ProtoValue {
1053+
const mapValue: ProtoMapValue = {
1054+
fields: {
1055+
[RESERVED_DECIMAL128_KEY]: {
1056+
stringValue: value.stringValue
1057+
}
1058+
}
1059+
};
1060+
return { mapValue };
1061+
}
1062+
10481063
export function parseBsonTimestamp(value: BsonTimestamp): ProtoValue {
10491064
const mapValue: ProtoMapValue = {
10501065
fields: {
@@ -1105,6 +1120,7 @@ function looksLikeJsonObject(input: unknown): boolean {
11051120
!(input instanceof MinKey) &&
11061121
!(input instanceof MaxKey) &&
11071122
!(input instanceof Int32Value) &&
1123+
!(input instanceof Decimal128Value) &&
11081124
!(input instanceof RegexValue) &&
11091125
!(input instanceof BsonObjectId) &&
11101126
!(input instanceof BsonTimestamp) &&

packages/firestore/src/lite-api/user_data_writer.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ import {
4141
RESERVED_BSON_TIMESTAMP_KEY,
4242
RESERVED_BSON_TIMESTAMP_SECONDS_KEY,
4343
typeOrder,
44-
VECTOR_MAP_VECTORS_KEY
44+
VECTOR_MAP_VECTORS_KEY,
45+
RESERVED_DECIMAL128_KEY,
46+
isInt32Value,
47+
isDecimal128Value
4548
} from '../model/values';
4649
import {
4750
ApiClientObjectMap,
@@ -61,6 +64,7 @@ import { forEach } from '../util/obj';
6164
import { BsonBinaryData } from './bson_binary_data';
6265
import { BsonObjectId } from './bson_object_Id';
6366
import { BsonTimestamp } from './bson_timestamp';
67+
import { Decimal128Value } from './decimal128_value';
6468
import { GeoPoint } from './geo_point';
6569
import { Int32Value } from './int32_value';
6670
import { MaxKey } from './max_key';
@@ -89,7 +93,11 @@ export abstract class AbstractUserDataWriter {
8993
return value.booleanValue!;
9094
case TypeOrder.NumberValue:
9195
if ('mapValue' in value) {
92-
return this.convertToInt32Value(value.mapValue!);
96+
if (isInt32Value(value)) {
97+
return this.convertToInt32Value(value.mapValue!);
98+
} else if (isDecimal128Value(value)) {
99+
return this.convertToDecimal128Value(value.mapValue!);
100+
}
93101
}
94102
return normalizeNumber(value.integerValue || value.doubleValue);
95103
case TypeOrder.TimestampValue:
@@ -215,6 +223,12 @@ export abstract class AbstractUserDataWriter {
215223
return new Int32Value(value);
216224
}
217225

226+
private convertToDecimal128Value(mapValue: ProtoMapValue): Decimal128Value {
227+
const value =
228+
mapValue!.fields?.[RESERVED_DECIMAL128_KEY]?.stringValue ?? '';
229+
return new Decimal128Value(value);
230+
}
231+
218232
private convertGeoPoint(value: ProtoLatLng): GeoPoint {
219233
return new GeoPoint(
220234
normalizeNumber(value.latitude),

packages/firestore/src/model/transform_operation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { arrayEquals } from '../util/misc';
2323

2424
import { normalizeNumber } from './normalize';
2525
import { serverTimestamp } from './server_timestamps';
26-
import { isArray, isInteger, isNumber, valueEquals } from './values';
26+
import { isArray, isIntegerValue, isNumber, valueEquals } from './values';
2727

2828
/** Used to represent a field transform on a mutation. */
2929
export class TransformOperation {
@@ -205,7 +205,7 @@ export function applyNumericIncrementTransformOperationToLocalView(
205205
previousValue
206206
)!;
207207
const sum = asNumber(baseValue) + asNumber(transform.operand);
208-
if (isInteger(baseValue) && isInteger(transform.operand)) {
208+
if (isIntegerValue(baseValue) && isIntegerValue(transform.operand)) {
209209
return toInteger(sum);
210210
} else {
211211
return toDouble(transform.serializer, sum);

0 commit comments

Comments
 (0)