Skip to content

Commit 7da95f8

Browse files
committed
WIP: decimal128 values. next: add quadruple + compare. next: add tests.
1 parent 6d5290b commit 7da95f8

File tree

9 files changed

+171
-25
lines changed

9 files changed

+171
-25
lines changed

Firestore/Example/Tests/API/FIRBsonTypesUnitTests.mm

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ - (void)testCreateAndReadAndCompareDecimal128Value {
9393
XCTAssertFalse([val1 isEqual:val5]);
9494
}
9595

96-
9796
- (void)testCreateAndReadAndCompareBsonObjectId {
9897
FIRBSONObjectId *val1 = [[FIRBSONObjectId alloc] initWithValue:@"abcd"];
9998
FIRBSONObjectId *val2 = [[FIRBSONObjectId alloc] initWithValue:@"abcd"];

Firestore/Source/API/FSTUserDataReader.mm

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,14 +454,15 @@ - (ParsedUpdateData)parsedUpdateData:(id)input {
454454
}
455455

456456
- (Message<google_firestore_v1_Value>)parseDecimal128Value:(FIRDecimal128Value *)decimal128
457-
context:(ParseContext &&)context {
457+
context:(ParseContext &&)context {
458458
__block Message<google_firestore_v1_Value> result;
459459
result->which_value_type = google_firestore_v1_Value_map_value_tag;
460460
result->map_value = {};
461461
result->map_value.fields_count = 1;
462462
result->map_value.fields = nanopb::MakeArray<google_firestore_v1_MapValue_FieldsEntry>(1);
463463
result->map_value.fields[0].key = nanopb::CopyBytesArray(model::kDecimal128TypeFieldValue);
464-
result->map_value.fields[0].value = *[self encodeStringValue:MakeString(decimal128.value)].release();
464+
result->map_value.fields[0].value =
465+
*[self encodeStringValue:MakeString(decimal128.value)].release();
465466

466467
return std::move(result);
467468
}

Firestore/Source/API/FSTUserDataWriter.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@
5959
using firebase::firestore::google_protobuf_Timestamp;
6060
using firebase::firestore::model::kRawBsonTimestampTypeIncrementFieldValue;
6161
using firebase::firestore::model::kRawBsonTimestampTypeSecondsFieldValue;
62-
using firebase::firestore::model::kRawInt32TypeFieldValue;
6362
using firebase::firestore::model::kRawDecimal128TypeFieldValue;
63+
using firebase::firestore::model::kRawInt32TypeFieldValue;
6464
using firebase::firestore::model::kRawRegexTypeOptionsFieldValue;
6565
using firebase::firestore::model::kRawRegexTypePatternFieldValue;
6666
using firebase::firestore::model::kRawVectorValueFieldKey;

Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct FirestorePassthroughTypes: StructureCodingPassthroughTypeResolver {
3737
t is MaxKey ||
3838
t is RegexValue ||
3939
t is Int32Value ||
40+
t is Decimal128Value ||
4041
t is BSONObjectId ||
4142
t is BSONTimestamp ||
4243
t is BSONBinaryData
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
#if SWIFT_PACKAGE
18+
@_exported import FirebaseFirestoreInternalWrapper
19+
#else
20+
@_exported import FirebaseFirestoreInternal
21+
#endif // SWIFT_PACKAGE
22+
23+
/**
24+
* A protocol describing the encodable properties of a Decimal128Value.
25+
*
26+
* Note: this protocol exists as a workaround for the Swift compiler: if the Decimal128Value class
27+
* was extended directly to conform to Codable, the methods implementing the protocol would be need
28+
* to be marked required but that can't be done in an extension. Declaring the extension on the
29+
* protocol sidesteps this issue.
30+
*/
31+
private protocol CodableDecimal128Value: Codable {
32+
var value: String { get }
33+
34+
init(_ value: String)
35+
}
36+
37+
/** The keys in a Decimal128Value. Must match the properties of Decimal128Value. */
38+
private enum Decimal128ValueKeys: String, CodingKey {
39+
case value
40+
}
41+
42+
/**
43+
* An extension of Decimal128Value that implements the behavior of the Codable protocol.
44+
*
45+
* Note: this is implemented manually here because the Swift compiler can't synthesize these methods
46+
* when declaring an extension to conform to Codable.
47+
*/
48+
extension CodableDecimal128Value {
49+
public init(from decoder: Decoder) throws {
50+
let container = try decoder.container(keyedBy: Decimal128ValueKeys.self)
51+
let value = try container.decode(String.self, forKey: .value)
52+
self.init(value)
53+
}
54+
55+
public func encode(to encoder: Encoder) throws {
56+
var container = encoder.container(keyedBy: Decimal128ValueKeys.self)
57+
try container.encode(value, forKey: .value)
58+
}
59+
}
60+
61+
/** Extends Decimal128Value to conform to Codable. */
62+
extension FirebaseFirestore.Decimal128Value: FirebaseFirestore.CodableDecimal128Value {}

Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
113113
var vector: VectorValue
114114
var regex: RegexValue
115115
var int32: Int32Value
116+
var decimal128: Decimal128Value
116117
var minKey: MinKey
117118
var maxKey: MaxKey
118119
var bsonOjectId: BSONObjectId
@@ -128,6 +129,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
128129
vector: FieldValue.vector([0.7, 0.6]),
129130
regex: RegexValue(pattern: "^foo", options: "i"),
130131
int32: Int32Value(1),
132+
decimal128: Decimal128Value("1.5"),
131133
minKey: MinKey.shared,
132134
maxKey: MaxKey.shared,
133135
bsonOjectId: BSONObjectId("507f191e810c19729de860ec"),
@@ -251,6 +253,10 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
251253
try assertCanWriteAndReadCodableValueWithAllFlavors(value: Int32Value(123))
252254
}
253255

256+
func testDecimal128Value() throws {
257+
try assertCanWriteAndReadCodableValueWithAllFlavors(value: Decimal128Value("1.2e3"))
258+
}
259+
254260
func testBsonObjectId() throws {
255261
try assertCanWriteAndReadCodableValueWithAllFlavors(
256262
value: BSONObjectId("507f191e810c19729de860ec")

Firestore/core/src/index/firestore_index_value_writer.cc

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,35 @@ void WriteIndexInt32Value(const google_firestore_v1_MapValue& map_index_value,
202202
encoder->WriteDouble(map_index_value.fields[0].value.integer_value);
203203
}
204204

205+
void WriteIndexDoubleValue(double number,
206+
DirectionalIndexByteEncoder* encoder) {
207+
if (std::isnan(number)) {
208+
WriteValueTypeLabel(encoder, IndexType::kNan);
209+
return;
210+
}
211+
212+
WriteValueTypeLabel(encoder, IndexType::kNumber);
213+
if (number == -0.0) {
214+
// -0.0, 0 and 0.0 are all considered the same
215+
encoder->WriteDouble(0.0);
216+
} else {
217+
encoder->WriteDouble(number);
218+
}
219+
}
220+
221+
void WriteIndexDecimal128Value(
222+
const google_firestore_v1_MapValue& map_index_value,
223+
DirectionalIndexByteEncoder* encoder) {
224+
// Note: We currently give up some precision and store the 128-bit decimal as
225+
// a 64-bit double for client-side indexing purposes. We could consider
226+
// improving this in the future.
227+
// Note: std::stod is able to parse 'NaN', '-NaN', 'Infinity' and '-Infinity',
228+
// with different string cases.
229+
const double number = std::stod(
230+
nanopb::MakeString(map_index_value.fields[0].value.string_value));
231+
WriteIndexDoubleValue(number, encoder);
232+
}
233+
205234
void WriteIndexValueAux(const google_firestore_v1_Value& index_value,
206235
DirectionalIndexByteEncoder* encoder) {
207236
switch (index_value.which_value_type) {
@@ -215,18 +244,7 @@ void WriteIndexValueAux(const google_firestore_v1_Value& index_value,
215244
break;
216245
}
217246
case google_firestore_v1_Value_double_value_tag: {
218-
double number = index_value.double_value;
219-
if (std::isnan(number)) {
220-
WriteValueTypeLabel(encoder, IndexType::kNan);
221-
break;
222-
}
223-
WriteValueTypeLabel(encoder, IndexType::kNumber);
224-
if (number == -0.0) {
225-
// -0.0, 0 and 0.0 are all considered the same
226-
encoder->WriteDouble(0.0);
227-
} else {
228-
encoder->WriteDouble(number);
229-
}
247+
WriteIndexDoubleValue(index_value.double_value, encoder);
230248
break;
231249
}
232250
case google_firestore_v1_Value_integer_value_tag: {
@@ -292,6 +310,9 @@ void WriteIndexValueAux(const google_firestore_v1_Value& index_value,
292310
} else if (model::IsBsonObjectId(index_value)) {
293311
WriteIndexBsonObjectId(index_value.map_value, encoder);
294312
break;
313+
} else if (model::IsDecimal128Value(index_value)) {
314+
WriteIndexDecimal128Value(index_value.map_value, encoder);
315+
break;
295316
} else if (model::IsInt32Value(index_value)) {
296317
WriteIndexInt32Value(index_value.map_value, encoder);
297318
break;

Firestore/core/src/model/value_util.cc

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ MapType DetectMapType(const google_firestore_v1_Value& value) {
153153
return MapType::kMaxKey;
154154
} else if (IsRegexValue(value)) {
155155
return MapType::kRegex;
156+
} else if (IsDecimal128Value(value)) {
157+
return MapType::kDecimal128;
156158
} else if (IsInt32Value(value)) {
157159
return MapType::kInt32;
158160
} else if (IsBsonObjectId(value)) {
@@ -211,6 +213,7 @@ TypeOrder GetTypeOrder(const google_firestore_v1_Value& value) {
211213
case MapType::kRegex:
212214
return TypeOrder::kRegex;
213215
case MapType::kInt32:
216+
case MapType::kDecimal128:
214217
return TypeOrder::kNumber;
215218
case MapType::kBsonObjectId:
216219
return TypeOrder::kBsonObjectId;
@@ -256,8 +259,20 @@ void SortFields(google_firestore_v1_Value& value) {
256259
}
257260
}
258261

262+
ComparisonResult Compare128BitNumbers(const google_firestore_v1_Value& left,
263+
const google_firestore_v1_Value& right) {
264+
// TODO: implement.
265+
(void)left;
266+
(void)right;
267+
return ComparisonResult::Same;
268+
}
269+
259270
ComparisonResult CompareNumbers(const google_firestore_v1_Value& left,
260271
const google_firestore_v1_Value& right) {
272+
if (IsDecimal128Value(left) || IsDecimal128Value(right)) {
273+
return Compare128BitNumbers(left, right);
274+
}
275+
261276
if (left.which_value_type == google_firestore_v1_Value_double_value_tag) {
262277
double left_double = left.double_value;
263278
if (right.which_value_type == google_firestore_v1_Value_double_value_tag) {
@@ -649,6 +664,10 @@ ComparisonResult UpperBoundCompare(const google_firestore_v1_Value& left,
649664

650665
bool NumberEquals(const google_firestore_v1_Value& left,
651666
const google_firestore_v1_Value& right) {
667+
if (IsDecimal128Value(left) || IsDecimal128Value(right)) {
668+
return Compare128BitNumbers(left, right) == util::ComparisonResult::Same;
669+
}
670+
652671
if (left.which_value_type == google_firestore_v1_Value_integer_value_tag &&
653672
right.which_value_type == google_firestore_v1_Value_integer_value_tag) {
654673
return left.integer_value == right.integer_value;
@@ -911,8 +930,9 @@ google_firestore_v1_Value GetLowerBound(
911930
return MinBsonBinaryData();
912931
} else if (IsRegexValue(value)) {
913932
return MinRegex();
914-
} else if (IsInt32Value(value)) {
915-
// int32Value is treated the same as integerValue and doubleValue.
933+
} else if (IsInt32Value(value) || IsDecimal128Value(value)) {
934+
// Int32Value and Decimal128Value are treated the same as integerValue
935+
// and doubleValue.
916936
return MinNumber();
917937
} else if (IsMinKeyValue(value)) {
918938
return MinKeyValue();
@@ -955,8 +975,9 @@ google_firestore_v1_Value GetUpperBound(
955975
return MinMap();
956976
} else if (IsMinKeyValue(value)) {
957977
return MinBoolean();
958-
} else if (IsInt32Value(value)) {
959-
// int32Value is treated the same as integerValue and doubleValue.
978+
} else if (IsInt32Value(value) || IsDecimal128Value(value)) {
979+
// Int32Value and Decimal128Value are treated the same as integerValue
980+
// and doubleValue.
960981
return MinTimestamp();
961982
} else if (IsBsonTimestamp(value)) {
962983
return MinString();
@@ -1377,11 +1398,40 @@ bool IsInt32Value(const google_firestore_v1_Value& value) {
13771398
return true;
13781399
}
13791400

1401+
bool IsDecimal128Value(const google_firestore_v1_Value& value) {
1402+
// A Decimal128Value is expected to be a map as follows:
1403+
// {
1404+
// "__decimal128__": 12345
1405+
// }
1406+
1407+
// Must be a map with 1 field.
1408+
if (value.which_value_type != google_firestore_v1_Value_map_value_tag ||
1409+
value.map_value.fields_count != 1) {
1410+
return false;
1411+
}
1412+
1413+
// Must have a "__decimal128__" key.
1414+
absl::optional<pb_size_t> field_index = IndexOfKey(
1415+
value.map_value, kRawDecimal128TypeFieldValue, kDecimal128TypeFieldValue);
1416+
if (!field_index.has_value()) {
1417+
return false;
1418+
}
1419+
1420+
// Must have a string value.
1421+
google_firestore_v1_Value& decimal_str = value.map_value.fields[0].value;
1422+
if (decimal_str.which_value_type !=
1423+
google_firestore_v1_Value_string_value_tag) {
1424+
return false;
1425+
}
1426+
1427+
return true;
1428+
}
1429+
13801430
bool IsBsonType(const google_firestore_v1_Value& value) {
1381-
MapType mapType = DetectMapType(value);
1431+
const MapType mapType = DetectMapType(value);
13821432
return mapType == MapType::kMinKey || mapType == MapType::kMaxKey ||
13831433
mapType == MapType::kRegex || mapType == MapType::kInt32 ||
1384-
mapType == MapType::kBsonObjectId ||
1434+
mapType == MapType::kDecimal128 || mapType == MapType::kBsonObjectId ||
13851435
mapType == MapType::kBsonTimestamp ||
13861436
mapType == MapType::kBsonBinaryData;
13871437
}

Firestore/core/src/model/value_util.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,10 @@ enum class MapType {
149149
kMaxKey = 5,
150150
kRegex = 6,
151151
kInt32 = 7,
152-
kBsonObjectId = 8,
153-
kBsonTimestamp = 9,
154-
kBsonBinaryData = 10
152+
kDecimal128 = 8,
153+
kBsonObjectId = 9,
154+
kBsonTimestamp = 10,
155+
kBsonBinaryData = 11
155156
};
156157

157158
/** Returns the Map type for the given value. */
@@ -284,6 +285,11 @@ bool IsRegexValue(const google_firestore_v1_Value& value);
284285
*/
285286
bool IsInt32Value(const google_firestore_v1_Value& value);
286287

288+
/**
289+
* Returns `true` if `value` represents a Decimal128Value.
290+
*/
291+
bool IsDecimal128Value(const google_firestore_v1_Value& value);
292+
287293
/**
288294
* Returns `true` if `value` represents a BsonObjectId.
289295
*/

0 commit comments

Comments
 (0)