Skip to content

Commit ab27f69

Browse files
authored
Fix C++ FieldValue equality (#3043)
* Add FieldValue operator== * Normalize NaN values to a single canonical value This preserves existing Objective-C behavior. * Add EqualsTester * Port equality test from Android
1 parent 8b8ca76 commit ab27f69

File tree

8 files changed

+457
-2
lines changed

8 files changed

+457
-2
lines changed

Firestore/Example/Firestore.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,7 @@
10331033
BB92EB03E3F92485023F64ED /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
10341034
BD01F0E43E4E2A07B8B05099 /* Pods-Firestore_Tests_macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_macOS/Pods-Firestore_Tests_macOS.debug.xcconfig"; sourceTree = "<group>"; };
10351035
C8522DE226C467C54E6788D8 /* mutation_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = mutation_test.cc; sourceTree = "<group>"; };
1036+
CD422AF3E4515FB8E9BE67A0 /* equals_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = equals_tester.h; sourceTree = "<group>"; };
10361037
D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = delayed_constructor_test.cc; sourceTree = "<group>"; };
10371038
D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
10381039
D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = perf_spec_test.json; sourceTree = "<group>"; };
@@ -1228,6 +1229,7 @@
12281229
children = (
12291230
5467FB06203E6A44009C9584 /* app_testing.h */,
12301231
5467FB07203E6A44009C9584 /* app_testing.mm */,
1232+
CD422AF3E4515FB8E9BE67A0 /* equals_tester.h */,
12311233
54A0352820A3B3BD003E0143 /* testutil.cc */,
12321234
54A0352920A3B3BD003E0143 /* testutil.h */,
12331235
BA6E5B9D53CCF301F58A62D7 /* xcgmock.h */,

Firestore/core/src/firebase/firestore/model/field_value.cc

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
3030
#include "Firestore/core/src/firebase/firestore/util/hashing.h"
3131
#include "Firestore/core/src/firebase/firestore/util/to_string.h"
32+
#include "absl/algorithm/container.h"
33+
#include "absl/base/casts.h"
3234
#include "absl/memory/memory.h"
3335
#include "absl/strings/escaping.h"
3436

@@ -42,6 +44,7 @@ using BaseValue = FieldValue::BaseValue;
4244
using Type = FieldValue::Type;
4345

4446
using util::Compare;
47+
using util::CompareContainer;
4548
using util::ComparisonResult;
4649

4750
template <typename T>
@@ -59,6 +62,13 @@ class NullValue : public FieldValue::BaseValue {
5962
return util::ToString(nullptr);
6063
}
6164

65+
bool Equals(const BaseValue& other) const override {
66+
if (type() != other.type()) return false;
67+
68+
// NullValue is the only instance of itself
69+
return true;
70+
}
71+
6272
ComparisonResult CompareTo(const BaseValue& other) const override {
6373
ComparisonResult cmp = CompareTypes(other);
6474
if (!util::Same(cmp)) return cmp;
@@ -95,11 +105,19 @@ class SimpleFieldValue : public FieldValue::BaseValue {
95105
return util::ToString(value_);
96106
}
97107

108+
bool Equals(const BaseValue& other) const override {
109+
if (type() != other.type()) return false;
110+
111+
auto& other_value = Cast<SimpleFieldValue>(other);
112+
return value_ == other_value.value();
113+
}
114+
98115
ComparisonResult CompareTo(const BaseValue& other) const override {
99116
ComparisonResult cmp = CompareTypes(other);
100117
if (!util::Same(cmp)) return cmp;
101118

102-
return Compare(value_, Cast<SimpleFieldValue>(other).value());
119+
auto& other_value = Cast<SimpleFieldValue>(other);
120+
return Compare(value_, other_value.value());
103121
}
104122

105123
size_t Hash() const override {
@@ -139,6 +157,17 @@ int64_t Integer(const BaseValue& rep) {
139157
class DoubleValue : public NumberValue<Type::Double, double> {
140158
public:
141159
using NumberValue<Type::Double, double>::NumberValue;
160+
161+
bool Equals(const BaseValue& other) const override {
162+
if (type() != other.type()) return false;
163+
164+
auto& other_value = Cast<DoubleValue>(other);
165+
return util::DoubleBitwiseEquals(value(), other_value.value());
166+
}
167+
168+
size_t Hash() const override {
169+
return util::DoubleBitwiseHash(value());
170+
}
142171
};
143172

144173
double Double(const BaseValue& rep) {
@@ -173,6 +202,12 @@ ComparisonResult NumberValue<type_enum, ValueType>::CompareTo(
173202
}
174203
}
175204

205+
// TODO(wilhuff): Use SimpleFieldValue as a base once we migrate to absl::Hash.
206+
//
207+
// This can't extend SimpleFieldValue because `util::Hash` is undefined for
208+
// Timestamp (and you can't override a compile-time error in a base class out
209+
// of existence). absl::Hash allows us to implement hashing in a way that
210+
// requires no public declaration of conformance.
176211
class TimestampValue : public BaseValue {
177212
public:
178213
explicit TimestampValue(Timestamp value) : value_(value) {
@@ -186,6 +221,13 @@ class TimestampValue : public BaseValue {
186221
return util::ToString(value_);
187222
}
188223

224+
bool Equals(const BaseValue& other) const override {
225+
if (type() != other.type()) return false;
226+
227+
auto& other_value = Cast<TimestampValue>(other);
228+
return value_ == other_value.value_;
229+
}
230+
189231
ComparisonResult CompareTo(const BaseValue& other) const override {
190232
ComparisonResult cmp = CompareTypes(other);
191233
if (!util::Same(cmp)) return cmp;
@@ -230,6 +272,13 @@ class ServerTimestampValue : public FieldValue::BaseValue {
230272
return absl::StrCat("ServerTimestamp(local_write_time=", time, ")");
231273
}
232274

275+
bool Equals(const BaseValue& other) const override {
276+
if (type() != other.type()) return false;
277+
278+
auto& other_value = Cast<ServerTimestampValue>(other);
279+
return local_write_time_ == other_value.local_write_time_;
280+
}
281+
233282
ComparisonResult CompareTo(const BaseValue& other) const override {
234283
ComparisonResult cmp = CompareTypes(other);
235284
if (!util::Same(cmp)) return cmp;
@@ -291,6 +340,13 @@ class ReferenceValue : public FieldValue::BaseValue {
291340
return Type::Reference;
292341
}
293342

343+
bool Equals(const BaseValue& other) const override {
344+
if (type() != other.type()) return false;
345+
346+
auto& other_value = Cast<ReferenceValue>(other);
347+
return database_id_ == other_value.database_id_ && key_ == other_value.key_;
348+
}
349+
294350
ComparisonResult CompareTo(const BaseValue& other) const override {
295351
ComparisonResult cmp = CompareTypes(other);
296352
if (!util::Same(cmp)) return cmp;
@@ -328,6 +384,13 @@ class GeoPointValue : public BaseValue {
328384
return util::ToString(value_);
329385
}
330386

387+
bool Equals(const BaseValue& other) const override {
388+
if (type() != other.type()) return false;
389+
390+
auto& other_value = Cast<GeoPointValue>(other);
391+
return value_ == other_value.value_;
392+
}
393+
331394
ComparisonResult CompareTo(const BaseValue& other) const override {
332395
ComparisonResult cmp = CompareTypes(other);
333396
if (!util::Same(cmp)) return cmp;
@@ -357,6 +420,13 @@ class ArrayContents : public FieldValue::BaseValue {
357420
return Type::Array;
358421
}
359422

423+
bool Equals(const BaseValue& other) const override {
424+
if (type() != other.type()) return false;
425+
426+
auto& other_value = Cast<ArrayContents>(other);
427+
return absl::c_equal(value_, other_value.value_);
428+
}
429+
360430
ComparisonResult CompareTo(const BaseValue& other) const override {
361431
ComparisonResult cmp = CompareTypes(other);
362432
if (!util::Same(cmp)) return cmp;
@@ -390,6 +460,13 @@ class MapContents : public FieldValue::BaseValue {
390460
return Type::Object;
391461
}
392462

463+
bool Equals(const BaseValue& other) const override {
464+
if (type() != other.type()) return false;
465+
466+
auto& other_value = Cast<MapContents>(other);
467+
return absl::c_equal(value_, other_value.value_);
468+
}
469+
393470
ComparisonResult CompareTo(const BaseValue& other) const override {
394471
ComparisonResult cmp = CompareTypes(other);
395472
if (!util::Same(cmp)) return cmp;
@@ -576,7 +653,25 @@ FieldValue FieldValue::FromInteger(int64_t value) {
576653
return FieldValue(std::make_shared<IntegerValue>(value));
577654
}
578655

656+
// We use a canonical NaN bit pattern that's common for both Objective-C and
657+
// Java. Specifically:
658+
//
659+
// - sign: 0
660+
// - exponent: 11 bits, all 1
661+
// - significand: 52 bits, MSB=1, rest=0
662+
//
663+
// This matches the Firestore backend which uses Double.doubleToLongBits from
664+
// the JDK (which is defined to normalize all NaNs to this value). This also
665+
// happens to be a common value for NAN in C++, but C++ does not require this
666+
// specific NaN value to be used, so we normalize.
667+
const uint64_t kCanonicalNanBits = 0x7ff8000000000000ULL;
668+
579669
FieldValue FieldValue::FromDouble(double value) {
670+
static double canonical_nan = absl::bit_cast<double>(kCanonicalNanBits);
671+
if (std::isnan(value)) {
672+
value = canonical_nan;
673+
}
674+
580675
return FieldValue(std::make_shared<DoubleValue>(value));
581676
}
582677

@@ -636,6 +731,10 @@ FieldValue FieldValue::FromMap(FieldValue::Map&& value) {
636731
return FieldValue(std::make_shared<MapContents>(std::move(value)));
637732
}
638733

734+
bool operator==(const FieldValue& lhs, const FieldValue& rhs) {
735+
return lhs.rep_->Equals(*rhs.rep_);
736+
}
737+
639738
std::ostream& operator<<(std::ostream& os, const FieldValue& value) {
640739
return os << value.ToString();
641740
}

Firestore/core/src/firebase/firestore/model/field_value.h

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
3232
#include "Firestore/core/src/firebase/firestore/model/field_path.h"
3333
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
34+
#include "absl/base/attributes.h"
3435
#include "absl/types/optional.h"
3536

3637
#if __OBJC__
@@ -41,12 +42,14 @@ namespace firebase {
4142
namespace firestore {
4243
namespace model {
4344

45+
class ObjectValue;
46+
4447
/**
4548
* tagged-union class representing an immutable data value as stored in
4649
* Firestore. FieldValue represents all the different kinds of values
4750
* that can be stored in fields in a document.
4851
*/
49-
class FieldValue : public util::Comparable<FieldValue> {
52+
class FieldValue {
5053
public:
5154
using Array = std::vector<FieldValue>;
5255
using Map = immutable::SortedMap<std::string, FieldValue>;
@@ -77,6 +80,8 @@ class FieldValue : public util::Comparable<FieldValue> {
7780

7881
FieldValue();
7982

83+
FieldValue(ObjectValue object); // NOLINT(runtime/explicit)
84+
8085
#if __OBJC__
8186
FSTFieldValue* Wrap() &&;
8287
#endif // __OBJC__
@@ -153,6 +158,27 @@ class FieldValue : public util::Comparable<FieldValue> {
153158
return rep_->CompareTo(*rhs.rep_);
154159
}
155160

161+
/**
162+
* Checks if the two values are equal, returning false if the value is
163+
* perceptibly different in any regard.
164+
*
165+
* Comparison for FieldValues is defined by whether or not values should
166+
* match for the purposes of querying. Comparison therefore makes the broadest
167+
* possible allowance, looking only for logical equality. This means that e.g.
168+
* -0.0, +0.0 and 0 (floating point and integer zeros) are all considered the
169+
* same value for comparison purposes.
170+
*
171+
* Equality for FieldValues is defined by whether or not a user could
172+
* perceive a change to the value. That is, a change from integer zero to
173+
* a double zero can be perceived and so these values are unequal despite
174+
* comparing same.
175+
*
176+
* This makes FieldValue one of the special cases where equality is
177+
* inconsistent with comparison. There are cases where CompareTo will return
178+
* Same but operator== will return false.
179+
*/
180+
friend bool operator==(const FieldValue& lhs, const FieldValue& rhs);
181+
156182
std::string ToString() const {
157183
return rep_->ToString();
158184
}
@@ -168,6 +194,8 @@ class FieldValue : public util::Comparable<FieldValue> {
168194

169195
virtual std::string ToString() const = 0;
170196

197+
virtual bool Equals(const BaseValue& other) const = 0;
198+
171199
virtual util::ComparisonResult CompareTo(const BaseValue& other) const = 0;
172200

173201
virtual size_t Hash() const = 0;
@@ -254,6 +282,32 @@ class ObjectValue : public util::Comparable<ObjectValue> {
254282
FieldValue fv_;
255283
};
256284

285+
// Pretend you can automatically upcast from ObjectValue to FieldValue.
286+
inline FieldValue::FieldValue(ObjectValue object)
287+
: FieldValue(object.AsFieldValue()) {
288+
}
289+
290+
inline bool operator!=(const FieldValue& lhs, const FieldValue& rhs) {
291+
// See operator== for why this isn't using util::Same().
292+
return !(lhs == rhs);
293+
}
294+
295+
inline bool operator<(const FieldValue& lhs, const FieldValue& rhs) {
296+
return util::Ascending(lhs.CompareTo(rhs));
297+
}
298+
inline bool operator>(const FieldValue& lhs, const FieldValue& rhs) {
299+
return util::Descending(lhs.CompareTo(rhs));
300+
}
301+
inline bool operator<=(const FieldValue& lhs, const FieldValue& rhs) {
302+
return !(rhs < lhs);
303+
}
304+
inline bool operator>=(const FieldValue& lhs, const FieldValue& rhs) {
305+
return !(lhs < rhs);
306+
}
307+
308+
// A bit pattern for our canonical NaN value. Exposed here for testing.
309+
ABSL_CONST_INIT extern const uint64_t kCanonicalNanBits;
310+
257311
} // namespace model
258312
} // namespace firestore
259313
} // namespace firebase

Firestore/core/test/firebase/firestore/model/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ cc_test(
2929
DEPENDS
3030
firebase_firestore_model
3131
firebase_firestore_testutil
32+
GMock::GMock
3233
)
3334

3435
cc_binary(

0 commit comments

Comments
 (0)