From 39d8252300015c26f1932cff42032613fdb36a09 Mon Sep 17 00:00:00 2001 From: Gil Date: Fri, 19 Jan 2018 12:27:11 -0800 Subject: Port comparison to C++ (#678) This reimplements our comparison functions as C++ Comparators and then provides compatibility shims for interoperating with existing Objective-C usage. A few specialized comparators aren't suitable for porting but only have a single usage (e.g. CompareBytes for comparing NSData * instances). In these cases I've moved them into the caller. * Use int32_t for typeof(ID) in FSTDocumentReference * Migrate callers of FSTComparison.h to Objective-C++ * Port comparison to C++ * Migrate usages of FSTComparison.h to C++ equivalents * Remove FSTComparison --- .../Example/Firestore.xcodeproj/project.pbxproj | 8 +- Firestore/Example/Tests/Util/FSTComparisonTests.m | 143 ---- Firestore/Source/API/FIRGeoPoint.m | 85 -- Firestore/Source/API/FIRGeoPoint.mm | 90 ++ Firestore/Source/Core/FSTTimestamp.m | 122 --- Firestore/Source/Core/FSTTimestamp.mm | 125 +++ Firestore/Source/Local/FSTDocumentReference.h | 4 +- Firestore/Source/Local/FSTDocumentReference.m | 83 -- Firestore/Source/Local/FSTDocumentReference.mm | 86 ++ Firestore/Source/Local/FSTMemoryMutationQueue.m | 441 ---------- Firestore/Source/Local/FSTMemoryMutationQueue.mm | 444 ++++++++++ Firestore/Source/Model/FSTFieldValue.m | 887 -------------------- Firestore/Source/Model/FSTFieldValue.mm | 931 +++++++++++++++++++++ Firestore/Source/Util/FSTComparison.h | 66 -- Firestore/Source/Util/FSTComparison.m | 175 ---- .../src/firebase/firestore/util/CMakeLists.txt | 3 + .../core/src/firebase/firestore/util/comparison.cc | 112 +++ .../core/src/firebase/firestore/util/comparison.h | 181 ++++ .../src/firebase/firestore/util/string_apple.h | 13 + .../test/firebase/firestore/util/CMakeLists.txt | 1 + .../firebase/firestore/util/comparison_test.cc | 210 +++++ .../third_party/abseil-cpp/absl/base/macros.h | 2 +- 22 files changed, 2203 insertions(+), 2009 deletions(-) delete mode 100644 Firestore/Example/Tests/Util/FSTComparisonTests.m delete mode 100644 Firestore/Source/API/FIRGeoPoint.m create mode 100644 Firestore/Source/API/FIRGeoPoint.mm delete mode 100644 Firestore/Source/Core/FSTTimestamp.m create mode 100644 Firestore/Source/Core/FSTTimestamp.mm delete mode 100644 Firestore/Source/Local/FSTDocumentReference.m create mode 100644 Firestore/Source/Local/FSTDocumentReference.mm delete mode 100644 Firestore/Source/Local/FSTMemoryMutationQueue.m create mode 100644 Firestore/Source/Local/FSTMemoryMutationQueue.mm delete mode 100644 Firestore/Source/Model/FSTFieldValue.m create mode 100644 Firestore/Source/Model/FSTFieldValue.mm delete mode 100644 Firestore/Source/Util/FSTComparison.h delete mode 100644 Firestore/Source/Util/FSTComparison.m create mode 100644 Firestore/core/src/firebase/firestore/util/comparison.cc create mode 100644 Firestore/core/src/firebase/firestore/util/comparison.h create mode 100644 Firestore/core/test/firebase/firestore/util/comparison_test.cc (limited to 'Firestore') diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 538758d..7e922b6 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 54764FAB1FAA0C320085E60A /* string_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54764FAA1FAA0C320085E60A /* string_util_test.cc */; }; 54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */; }; 548DB927200D590300E00ABC /* assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB926200D590300E00ABC /* assert_test.cc */; }; + 548DB929200D59F600E00ABC /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; 5491BC721FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */; }; 5491BC731FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */; }; 54C2294F1FECABAE007D065B /* log_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54C2294E1FECABAE007D065B /* log_test.cc */; }; @@ -148,7 +149,6 @@ DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1991F0D48AC0013853F /* FSTSpecTests.m */; }; DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */; }; DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1861F0D48AC0013853F /* FSTAssertTests.m */; }; - DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */; }; DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; }; F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; }; /* End PBXBuildFile section */ @@ -204,6 +204,7 @@ 54764FAA1FAA0C320085E60A /* string_util_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = string_util_test.cc; path = ../../Port/string_util_test.cc; sourceTree = ""; }; 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FSTGoogleTestTests.mm; path = GoogleTest/FSTGoogleTestTests.mm; sourceTree = ""; }; 548DB926200D590300E00ABC /* assert_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = assert_test.cc; path = ../../core/test/firebase/firestore/util/assert_test.cc; sourceTree = ""; }; + 548DB928200D59F600E00ABC /* comparison_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = comparison_test.cc; path = ../../core/test/firebase/firestore/util/comparison_test.cc; sourceTree = ""; }; 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTIntegrationTestCase.mm; sourceTree = ""; }; 54C2294E1FECABAE007D065B /* log_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = log_test.cc; path = ../../core/test/firebase/firestore/util/log_test.cc; sourceTree = ""; }; 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = collection_spec_test.json; sourceTree = ""; }; @@ -308,7 +309,6 @@ DE51B1821F0D48AC0013853F /* FSTPathTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPathTests.m; sourceTree = ""; }; DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRGeoPointTests.m; sourceTree = ""; }; DE51B1861F0D48AC0013853F /* FSTAssertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTAssertTests.m; sourceTree = ""; }; - DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTComparisonTests.m; sourceTree = ""; }; DE51B1881F0D48AC0013853F /* FSTHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTHelpers.h; sourceTree = ""; }; DE51B1891F0D48AC0013853F /* FSTHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTHelpers.m; sourceTree = ""; }; DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBSpecTests.m; sourceTree = ""; }; @@ -400,6 +400,7 @@ children = ( 548DB926200D590300E00ABC /* assert_test.cc */, 54740A521FC913E500713A1A /* autoid_test.cc */, + 548DB928200D59F600E00ABC /* comparison_test.cc */, 54C2294E1FECABAE007D065B /* log_test.cc */, 54740A531FC913E500713A1A /* secure_random_test.cc */, 5436F32320008FAD006E51E3 /* string_printf_test.cc */, @@ -646,7 +647,6 @@ 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */, 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */, DE51B1861F0D48AC0013853F /* FSTAssertTests.m */, - DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */, DE51B1881F0D48AC0013853F /* FSTHelpers.h */, DE51B1891F0D48AC0013853F /* FSTHelpers.m */, 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */, @@ -1210,7 +1210,6 @@ DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */, DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */, ABAEEF4F1FD5F8B100C966CB /* FIRQueryTests.m in Sources */, - DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */, ABF341051FE860CA00C48322 /* FSTAPIHelpers.m in Sources */, DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */, DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */, @@ -1276,6 +1275,7 @@ DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */, DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */, AB382F7E1FE03059007CA955 /* FIRFieldPathTests.m in Sources */, + 548DB929200D59F600E00ABC /* comparison_test.cc in Sources */, DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */, AB99452C1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m in Sources */, 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */, diff --git a/Firestore/Example/Tests/Util/FSTComparisonTests.m b/Firestore/Example/Tests/Util/FSTComparisonTests.m deleted file mode 100644 index 5632e64..0000000 --- a/Firestore/Example/Tests/Util/FSTComparisonTests.m +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Util/FSTComparison.h" - -#import - -union DoubleBits { - double d; - uint64_t bits; -}; - -#define ASSERT_BIT_EQUALS(expected, actual) \ - do { \ - union DoubleBits expectedBits = {.d = expected}; \ - union DoubleBits actualBits = {.d = expected}; \ - if (expectedBits.bits != actualBits.bits) { \ - XCTFail(@"Expected <%f> to compare equal to <%f> with bits <%llX> equal to <%llX>", actual, \ - expected, actualBits.bits, expectedBits.bits); \ - } \ - } while (0); - -#define ASSERT_ORDERED_SAME(doubleValue, longValue) \ - do { \ - NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ - if (result != NSOrderedSame) { \ - XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ - } \ - } while (0); - -#define ASSERT_ORDERED_DESCENDING(doubleValue, longValue) \ - do { \ - NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ - if (result != NSOrderedDescending) { \ - XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ - } \ - } while (0); - -#define ASSERT_ORDERED_ASCENDING(doubleValue, longValue) \ - do { \ - NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ - if (result != NSOrderedAscending) { \ - XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ - } \ - } while (0); - -@interface FSTComparisonTests : XCTestCase -@end - -@implementation FSTComparisonTests - -- (void)testMixedComparison { - // Infinities - ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MIN); - ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MAX); - ASSERT_ORDERED_ASCENDING(-INFINITY, 0LL); - - ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MIN); - ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MAX); - ASSERT_ORDERED_DESCENDING(INFINITY, 0LL); - - // NaN - ASSERT_ORDERED_ASCENDING(NAN, LLONG_MIN); - ASSERT_ORDERED_ASCENDING(NAN, LLONG_MAX); - ASSERT_ORDERED_ASCENDING(NAN, 0LL); - - // Large values (note DBL_MIN is positive and near zero). - ASSERT_ORDERED_ASCENDING(-DBL_MAX, LLONG_MIN); - - // Tests around LLONG_MIN - ASSERT_BIT_EQUALS((double)LLONG_MIN, -0x1.0p63); - ASSERT_ORDERED_SAME(-0x1.0p63, LLONG_MIN); - ASSERT_ORDERED_ASCENDING(-0x1.0p63, LLONG_MIN + 1); - - XCTAssertLessThan(-0x1.0000000000001p63, -0x1.0p63); - ASSERT_ORDERED_ASCENDING(-0x1.0000000000001p63, LLONG_MIN); - ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp62, LLONG_MIN); - - // Tests around LLONG_MAX - // Note LLONG_MAX cannot be exactly represented by a double, so the system rounds it to the - // nearest double, which is 2^63. This number, in turn is larger than the maximum representable - // as a long. - ASSERT_BIT_EQUALS(0x1.0p63, (double)LLONG_MAX); - ASSERT_ORDERED_DESCENDING(0x1.0p63, LLONG_MAX); - - // The largest value with an exactly long representation - XCTAssertEqual((long)0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL); - ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL); - - ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFB00LL); - ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFBFFLL); - ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC01LL); - ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFD00LL); - - ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp62, 0x7FFFFFFFFFFFFC00LL); - - // Tests around MAX_SAFE_INTEGER - ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFFLL); - ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFELL); - ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp52, 0x1FFFFFFFFFFFFFLL); - ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp52, 0x20000000000000LL); - - // Tests around MIN_SAFE_INTEGER - ASSERT_ORDERED_SAME(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFFLL); - ASSERT_ORDERED_ASCENDING(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFELL); - ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFEp52, -0x1FFFFFFFFFFFFFLL); - ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp52, -0x20000000000000LL); - - // Tests around zero. - ASSERT_ORDERED_SAME(-0.0, 0LL); - ASSERT_ORDERED_SAME(0.0, 0LL); - - // The smallest representable positive value should be greater than zero - ASSERT_ORDERED_DESCENDING(DBL_MIN, 0LL); - ASSERT_ORDERED_ASCENDING(-DBL_MIN, 0LL); - - // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal - // number: . - double minSubNormal = 0x1.0p-1074; - ASSERT_ORDERED_DESCENDING(minSubNormal, 0LL); - ASSERT_ORDERED_ASCENDING(-minSubNormal, 0LL); - - // Other sanity checks - ASSERT_ORDERED_ASCENDING(0.5, 1LL); - ASSERT_ORDERED_DESCENDING(0.5, 0LL); - ASSERT_ORDERED_ASCENDING(1.5, 2LL); - ASSERT_ORDERED_DESCENDING(1.5, 1LL); -} - -@end diff --git a/Firestore/Source/API/FIRGeoPoint.m b/Firestore/Source/API/FIRGeoPoint.m deleted file mode 100644 index 72e9e7d..0000000 --- a/Firestore/Source/API/FIRGeoPoint.m +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/API/FIRGeoPoint+Internal.h" - -#import "Firestore/Source/Util/FSTComparison.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FIRGeoPoint - -- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude { - if (self = [super init]) { - if (latitude < -90 || latitude > 90 || !isfinite(latitude)) { - FSTThrowInvalidArgument( - @"GeoPoint requires a latitude value in the range of [-90, 90], " - "but was %f", - latitude); - } - if (longitude < -180 || longitude > 180 || !isfinite(longitude)) { - FSTThrowInvalidArgument( - @"GeoPoint requires a longitude value in the range of [-180, 180], " - "but was %f", - longitude); - } - - _latitude = latitude; - _longitude = longitude; - } - return self; -} - -- (NSComparisonResult)compare:(FIRGeoPoint *)other { - NSComparisonResult result = FSTCompareDoubles(self.latitude, other.latitude); - if (result != NSOrderedSame) { - return result; - } else { - return FSTCompareDoubles(self.longitude, other.longitude); - } -} - -#pragma mark - NSObject methods - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.latitude, self.longitude]; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } - if (![other isKindOfClass:[FIRGeoPoint class]]) { - return NO; - } - FIRGeoPoint *otherGeoPoint = (FIRGeoPoint *)other; - return FSTDoubleBitwiseEquals(self.latitude, otherGeoPoint.latitude) && - FSTDoubleBitwiseEquals(self.longitude, otherGeoPoint.longitude); -} - -- (NSUInteger)hash { - return 31 * FSTDoubleBitwiseHash(self.latitude) + FSTDoubleBitwiseHash(self.longitude); -} - -/** Implements NSCopying without actually copying because geopoints are immutable. */ -- (id)copyWithZone:(NSZone *_Nullable)zone { - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRGeoPoint.mm b/Firestore/Source/API/FIRGeoPoint.mm new file mode 100644 index 0000000..8d89633 --- /dev/null +++ b/Firestore/Source/API/FIRGeoPoint.mm @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/Source/API/FIRGeoPoint+Internal.h" + +#import "Firestore/core/src/firebase/firestore/util/comparison.h" + +#import "Firestore/Source/Util/FSTUsageValidation.h" + +using firebase::firestore::util::DoubleBitwiseEquals; +using firebase::firestore::util::DoubleBitwiseHash; +using firebase::firestore::util::WrapCompare; + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRGeoPoint + +- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude { + if (self = [super init]) { + if (latitude < -90 || latitude > 90 || !isfinite(latitude)) { + FSTThrowInvalidArgument( + @"GeoPoint requires a latitude value in the range of [-90, 90], " + "but was %f", + latitude); + } + if (longitude < -180 || longitude > 180 || !isfinite(longitude)) { + FSTThrowInvalidArgument( + @"GeoPoint requires a longitude value in the range of [-180, 180], " + "but was %f", + longitude); + } + + _latitude = latitude; + _longitude = longitude; + } + return self; +} + +- (NSComparisonResult)compare:(FIRGeoPoint *)other { + NSComparisonResult result = WrapCompare(self.latitude, other.latitude); + if (result != NSOrderedSame) { + return result; + } else { + return WrapCompare(self.longitude, other.longitude); + } +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.latitude, self.longitude]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FIRGeoPoint class]]) { + return NO; + } + FIRGeoPoint *otherGeoPoint = (FIRGeoPoint *)other; + return DoubleBitwiseEquals(self.latitude, otherGeoPoint.latitude) && + DoubleBitwiseEquals(self.longitude, otherGeoPoint.longitude); +} + +- (NSUInteger)hash { + return 31 * DoubleBitwiseHash(self.latitude) + DoubleBitwiseHash(self.longitude); +} + +/** Implements NSCopying without actually copying because geopoints are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTimestamp.m b/Firestore/Source/Core/FSTTimestamp.m deleted file mode 100644 index 6d9e314..0000000 --- a/Firestore/Source/Core/FSTTimestamp.m +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Core/FSTTimestamp.h" - -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTComparison.h" - -NS_ASSUME_NONNULL_BEGIN - -static const int kNanosPerSecond = 1000000000; - -@implementation FSTTimestamp - -#pragma mark - Constructors - -+ (instancetype)timestamp { - return [FSTTimestamp timestampWithDate:[NSDate date]]; -} - -+ (instancetype)timestampWithDate:(NSDate *)date { - double secondsDouble; - double fraction = modf(date.timeIntervalSince1970, &secondsDouble); - // GCP Timestamps always have non-negative nanos. - if (fraction < 0) { - fraction += 1.0; - secondsDouble -= 1.0; - } - int64_t seconds = (int64_t)secondsDouble; - int32_t nanos = (int32_t)(fraction * kNanosPerSecond); - return [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos]; -} - -- (instancetype)initWithSeconds:(int64_t)seconds nanos:(int32_t)nanos { - self = [super init]; - if (self) { - FSTAssert(nanos >= 0, @"timestamp nanoseconds out of range: %d", nanos); - FSTAssert(nanos < 1e9, @"timestamp nanoseconds out of range: %d", nanos); - // Midnight at the beginning of 1/1/1 is the earliest timestamp Firestore supports. - FSTAssert(seconds >= -62135596800L, @"timestamp seconds out of range: %lld", seconds); - // This will break in the year 10,000. - FSTAssert(seconds < 253402300800L, @"timestamp seconds out of range: %lld", seconds); - - _seconds = seconds; - _nanos = nanos; - } - return self; -} - -#pragma mark - NSObject methods - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[FSTTimestamp class]]) { - return NO; - } - return [self isEqualToTimestamp:(FSTTimestamp *)object]; -} - -- (NSUInteger)hash { - return (NSUInteger)((self.seconds >> 32) ^ self.seconds ^ self.nanos); -} - -- (NSString *)description { - return [NSString - stringWithFormat:@"", self.seconds, self.nanos]; -} - -/** Implements NSCopying without actually copying because timestamps are immutable. */ -- (id)copyWithZone:(NSZone *_Nullable)zone { - return self; -} - -#pragma mark - Public methods - -- (NSDate *)approximateDateValue { - NSTimeInterval interval = (NSTimeInterval)self.seconds + ((NSTimeInterval)self.nanos) / 1e9; - return [NSDate dateWithTimeIntervalSince1970:interval]; -} - -- (BOOL)isEqualToTimestamp:(FSTTimestamp *)other { - return [self compare:other] == NSOrderedSame; -} - -- (NSString *)ISO8601String { - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss"; - formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; - NSDate *secondsDate = [NSDate dateWithTimeIntervalSince1970:self.seconds]; - NSString *secondsString = [formatter stringFromDate:secondsDate]; - FSTAssert(secondsString.length == 19, @"Invalid ISO string: %@", secondsString); - - NSString *nanosString = [NSString stringWithFormat:@"%09d", self.nanos]; - return [NSString stringWithFormat:@"%@.%@Z", secondsString, nanosString]; -} - -- (NSComparisonResult)compare:(FSTTimestamp *)other { - NSComparisonResult result = FSTCompareInt64s(self.seconds, other.seconds); - if (result != NSOrderedSame) { - return result; - } - return FSTCompareInt32s(self.nanos, other.nanos); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTimestamp.mm b/Firestore/Source/Core/FSTTimestamp.mm new file mode 100644 index 0000000..d2b492a --- /dev/null +++ b/Firestore/Source/Core/FSTTimestamp.mm @@ -0,0 +1,125 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/Source/Core/FSTTimestamp.h" + +#include "Firestore/core/src/firebase/firestore/util/comparison.h" + +#import "Firestore/Source/Util/FSTAssert.h" + +using firebase::firestore::util::WrapCompare; + +NS_ASSUME_NONNULL_BEGIN + +static const int kNanosPerSecond = 1000000000; + +@implementation FSTTimestamp + +#pragma mark - Constructors + ++ (instancetype)timestamp { + return [FSTTimestamp timestampWithDate:[NSDate date]]; +} + ++ (instancetype)timestampWithDate:(NSDate *)date { + double secondsDouble; + double fraction = modf(date.timeIntervalSince1970, &secondsDouble); + // GCP Timestamps always have non-negative nanos. + if (fraction < 0) { + fraction += 1.0; + secondsDouble -= 1.0; + } + int64_t seconds = (int64_t)secondsDouble; + int32_t nanos = (int32_t)(fraction * kNanosPerSecond); + return [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos]; +} + +- (instancetype)initWithSeconds:(int64_t)seconds nanos:(int32_t)nanos { + self = [super init]; + if (self) { + FSTAssert(nanos >= 0, @"timestamp nanoseconds out of range: %d", nanos); + FSTAssert(nanos < 1e9, @"timestamp nanoseconds out of range: %d", nanos); + // Midnight at the beginning of 1/1/1 is the earliest timestamp Firestore supports. + FSTAssert(seconds >= -62135596800L, @"timestamp seconds out of range: %lld", seconds); + // This will break in the year 10,000. + FSTAssert(seconds < 253402300800L, @"timestamp seconds out of range: %lld", seconds); + + _seconds = seconds; + _nanos = nanos; + } + return self; +} + +#pragma mark - NSObject methods + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTTimestamp class]]) { + return NO; + } + return [self isEqualToTimestamp:(FSTTimestamp *)object]; +} + +- (NSUInteger)hash { + return (NSUInteger)((self.seconds >> 32) ^ self.seconds ^ self.nanos); +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", self.seconds, self.nanos]; +} + +/** Implements NSCopying without actually copying because timestamps are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +#pragma mark - Public methods + +- (NSDate *)approximateDateValue { + NSTimeInterval interval = (NSTimeInterval)self.seconds + ((NSTimeInterval)self.nanos) / 1e9; + return [NSDate dateWithTimeIntervalSince1970:interval]; +} + +- (BOOL)isEqualToTimestamp:(FSTTimestamp *)other { + return [self compare:other] == NSOrderedSame; +} + +- (NSString *)ISO8601String { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss"; + formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; + NSDate *secondsDate = [NSDate dateWithTimeIntervalSince1970:self.seconds]; + NSString *secondsString = [formatter stringFromDate:secondsDate]; + FSTAssert(secondsString.length == 19, @"Invalid ISO string: %@", secondsString); + + NSString *nanosString = [NSString stringWithFormat:@"%09d", self.nanos]; + return [NSString stringWithFormat:@"%@.%@Z", secondsString, nanosString]; +} + +- (NSComparisonResult)compare:(FSTTimestamp *)other { + NSComparisonResult result = WrapCompare(self.seconds, other.seconds); + if (result != NSOrderedSame) { + return result; + } + return WrapCompare(self.nanos, other.nanos); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTDocumentReference.h b/Firestore/Source/Local/FSTDocumentReference.h index eff60e4..04b8416 100644 --- a/Firestore/Source/Local/FSTDocumentReference.h +++ b/Firestore/Source/Local/FSTDocumentReference.h @@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTDocumentReference : NSObject /** Initializes the document reference with the given key and ID. */ -- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int)ID NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int32_t)ID NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; @@ -43,7 +43,7 @@ NS_ASSUME_NONNULL_BEGIN * The targetID of a referring target or the batchID of a referring mutation batch. (Which this * is depends upon which FSTReferenceSet this reference is a part of.) */ -@property(nonatomic, assign, readonly) int ID; +@property(nonatomic, assign, readonly) int32_t ID; @end diff --git a/Firestore/Source/Local/FSTDocumentReference.m b/Firestore/Source/Local/FSTDocumentReference.m deleted file mode 100644 index 25a5935..0000000 --- a/Firestore/Source/Local/FSTDocumentReference.m +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Local/FSTDocumentReference.h" - -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Util/FSTComparison.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTDocumentReference - -- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int)ID { - self = [super init]; - if (self) { - _key = key; - _ID = ID; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - FSTDocumentReference *reference = (FSTDocumentReference *)other; - - return [self.key isEqualToKey:reference.key] && self.ID == reference.ID; -} - -- (NSUInteger)hash { - NSUInteger result = [self.key hash]; - result = result * 31u + self.ID; - return result; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.key, self.ID]; -} - -- (id)copyWithZone:(nullable NSZone *)zone { - // FSTDocumentReference is immutable - return self; -} - -@end - -#pragma mark Comparators - -/** Sorts document references by key then ID. */ -const NSComparator FSTDocumentReferenceComparatorByKey = - ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) { - NSComparisonResult result = FSTDocumentKeyComparator(left.key, right.key); - if (result != NSOrderedSame) { - return result; - } - return FSTCompareInts(left.ID, right.ID); - }; - -/** Sorts document references by ID then key. */ -const NSComparator FSTDocumentReferenceComparatorByID = - ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) { - NSComparisonResult result = FSTCompareInts(left.ID, right.ID); - if (result != NSOrderedSame) { - return result; - } - return FSTDocumentKeyComparator(left.key, right.key); - }; - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTDocumentReference.mm b/Firestore/Source/Local/FSTDocumentReference.mm new file mode 100644 index 0000000..4310baa --- /dev/null +++ b/Firestore/Source/Local/FSTDocumentReference.mm @@ -0,0 +1,86 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/Source/Local/FSTDocumentReference.h" + +#include "Firestore/core/src/firebase/firestore/util/comparison.h" + +#import "Firestore/Source/Model/FSTDocumentKey.h" + +using firebase::firestore::util::WrapCompare; + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTDocumentReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int32_t)ID { + self = [super init]; + if (self) { + _key = key; + _ID = ID; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + FSTDocumentReference *reference = (FSTDocumentReference *)other; + + return [self.key isEqualToKey:reference.key] && self.ID == reference.ID; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31u + self.ID; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.key, self.ID]; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + // FSTDocumentReference is immutable + return self; +} + +@end + +#pragma mark Comparators + +/** Sorts document references by key then ID. */ +const NSComparator FSTDocumentReferenceComparatorByKey = + ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) { + NSComparisonResult result = FSTDocumentKeyComparator(left.key, right.key); + if (result != NSOrderedSame) { + return result; + } + return WrapCompare(left.ID, right.ID); + }; + +/** Sorts document references by ID then key. */ +const NSComparator FSTDocumentReferenceComparatorByID = + ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) { + NSComparisonResult result = WrapCompare(left.ID, right.ID); + if (result != NSOrderedSame) { + return result; + } + return FSTDocumentKeyComparator(left.key, right.key); + }; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.m b/Firestore/Source/Local/FSTMemoryMutationQueue.m deleted file mode 100644 index b155264..0000000 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.m +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Local/FSTMemoryMutationQueue.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTDocumentReference.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTComparison.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTMemoryMutationQueue () - -/** - * A FIFO queue of all mutations to apply to the backend. Mutations are added to the end of the - * queue as they're written, and removed from the front of the queue as the mutations become - * visible or are rejected. - * - * When successfully applied, mutations must be acknowledged by the write stream and made visible - * on the watch stream. It's possible for the watch stream to fall behind in which case the batches - * at the head of the queue will be acknowledged but held until the watch stream sees the changes. - * - * If a batch is rejected while there are held write acknowledgements at the head of the queue - * the rejected batch is converted to a tombstone: its mutations are removed but the batch remains - * in the queue. This maintains a simple consecutive ordering of batches in the queue. - * - * Once the held write acknowledgements become visible they are removed from the head of the queue - * along with any tombstones that follow. - */ -@property(nonatomic, strong, readonly) NSMutableArray *queue; - -/** An ordered mapping between documents and the mutation batch IDs. */ -@property(nonatomic, strong) FSTImmutableSortedSet *batchesByDocumentKey; - -/** The next value to use when assigning sequential IDs to each mutation batch. */ -@property(nonatomic, assign) FSTBatchID nextBatchID; - -/** The highest acknowledged mutation in the queue. */ -@property(nonatomic, assign) FSTBatchID highestAcknowledgedBatchID; - -/** - * The last received stream token from the server, used to acknowledge which responses the client - * has processed. Stream tokens are opaque checkpoint markers whose only real value is their - * inclusion in the next request. - */ -@property(nonatomic, strong, nullable) NSData *lastStreamToken; - -@end - -@implementation FSTMemoryMutationQueue - -+ (instancetype)mutationQueue { - return [[FSTMemoryMutationQueue alloc] init]; -} - -- (instancetype)init { - if (self = [super init]) { - _queue = [NSMutableArray array]; - _batchesByDocumentKey = - [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; - - _nextBatchID = 1; - _highestAcknowledgedBatchID = kFSTBatchIDUnknown; - } - return self; -} - -#pragma mark - FSTMutationQueue implementation - -- (void)startWithGroup:(FSTWriteGroup *)group { - // Note: The queue may be shutdown / started multiple times, since we maintain the queue for the - // duration of the app session in case a user logs out / back in. To behave like the - // LevelDB-backed MutationQueue (and accommodate tests that expect as much), we reset nextBatchID - // and highestAcknowledgedBatchID if the queue is empty. - if (self.isEmpty) { - self.nextBatchID = 1; - self.highestAcknowledgedBatchID = kFSTBatchIDUnknown; - } - FSTAssert(self.highestAcknowledgedBatchID < self.nextBatchID, - @"highestAcknowledgedBatchID must be less than the nextBatchID"); -} - -- (void)shutdown { -} - -- (BOOL)isEmpty { - // If the queue has any entries at all, the first entry must not be a tombstone (otherwise it - // would have been removed already). - return self.queue.count == 0; -} - -- (FSTBatchID)highestAcknowledgedBatchID { - return _highestAcknowledgedBatchID; -} - -- (void)acknowledgeBatch:(FSTMutationBatch *)batch - streamToken:(nullable NSData *)streamToken - group:(__unused FSTWriteGroup *)group { - NSMutableArray *queue = self.queue; - - FSTBatchID batchID = batch.batchID; - FSTAssert(batchID > self.highestAcknowledgedBatchID, - @"Mutation batchIDs must be acknowledged in order"); - - NSInteger batchIndex = [self indexOfExistingBatchID:batchID action:@"acknowledged"]; - - // Verify that the batch in the queue is the one to be acknowledged. - FSTMutationBatch *check = queue[(NSUInteger)batchIndex]; - FSTAssert(batchID == check.batchID, @"Queue ordering failure: expected batch %d, got batch %d", - batchID, check.batchID); - FSTAssert(![check isTombstone], @"Can't acknowledge a previously removed batch"); - - self.highestAcknowledgedBatchID = batchID; - self.lastStreamToken = streamToken; -} - -- (void)setLastStreamToken:(nullable NSData *)streamToken group:(__unused FSTWriteGroup *)group { - self.lastStreamToken = streamToken; -} - -- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime - mutations:(NSArray *)mutations - group:(FSTWriteGroup *)group { - FSTAssert(mutations.count > 0, @"Mutation batches should not be empty"); - - FSTBatchID batchID = self.nextBatchID; - self.nextBatchID += 1; - - NSMutableArray *queue = self.queue; - if (queue.count > 0) { - FSTMutationBatch *prior = queue[queue.count - 1]; - FSTAssert(prior.batchID < batchID, @"Mutation batchIDs must be monotonically increasing order"); - } - - FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID - localWriteTime:localWriteTime - mutations:mutations]; - [queue addObject:batch]; - - // Track references by document key. - FSTImmutableSortedSet *references = self.batchesByDocumentKey; - for (FSTMutation *mutation in batch.mutations) { - references = [references - setByAddingObject:[[FSTDocumentReference alloc] initWithKey:mutation.key ID:batchID]]; - } - self.batchesByDocumentKey = references; - - return batch; -} - -- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID { - NSMutableArray *queue = self.queue; - - NSInteger index = [self indexOfBatchID:batchID]; - if (index < 0 || index >= queue.count) { - return nil; - } - - FSTMutationBatch *batch = queue[(NSUInteger)index]; - FSTAssert(batch.batchID == batchID, @"If found batch must match"); - return [batch isTombstone] ? nil : batch; -} - -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID { - NSMutableArray *queue = self.queue; - NSUInteger count = queue.count; - - // All batches with batchID <= self.highestAcknowledgedBatchID have been acknowledged so the - // first unacknowledged batch after batchID will have a batchID larger than both of these values. - batchID = MAX(batchID + 1, self.highestAcknowledgedBatchID); - - // The requested batchID may still be out of range so normalize it to the start of the queue. - NSInteger rawIndex = [self indexOfBatchID:batchID]; - NSUInteger index = rawIndex < 0 ? 0 : (NSUInteger)rawIndex; - - // Finally return the first non-tombstone batch. - for (; index < count; index++) { - FSTMutationBatch *batch = queue[index]; - if (![batch isTombstone]) { - return batch; - } - } - - return nil; -} - -- (NSArray *)allMutationBatches { - return [self allLiveMutationBatchesBeforeIndex:self.queue.count]; -} - -- (NSArray *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID { - NSMutableArray *queue = self.queue; - NSUInteger count = queue.count; - - NSInteger endIndex = [self indexOfBatchID:batchID]; - if (endIndex < 0) { - endIndex = 0; - } else if (endIndex >= count) { - endIndex = count; - } else { - // The endIndex is in the queue so increment to pull everything in the queue including it. - endIndex += 1; - } - - return [self allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex]; -} - -- (NSArray *)allMutationBatchesAffectingDocumentKey: - (FSTDocumentKey *)documentKey { - FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:documentKey ID:0]; - - NSMutableArray *result = [NSMutableArray array]; - FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) { - if (![documentKey isEqualToKey:reference.key]) { - *stop = YES; - return; - } - - FSTMutationBatch *batch = [self lookupMutationBatch:reference.ID]; - FSTAssert(batch, @"Batches in the index must exist in the main table"); - [result addObject:batch]; - }; - - [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block]; - return result; -} - -- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { - // Use the query path as a prefix for testing if a document matches the query. - FSTResourcePath *prefix = query.path; - int immediateChildrenPathLength = prefix.length + 1; - - // Construct a document reference for actually scanning the index. Unlike the prefix, the document - // key in this reference must have an even number of segments. The empty segment can be used as - // a suffix of the query path because it precedes all other segments in an ordered traversal. - FSTResourcePath *startPath = query.path; - if (![FSTDocumentKey isDocumentKey:startPath]) { - startPath = [startPath pathByAppendingSegment:@""]; - } - FSTDocumentReference *start = - [[FSTDocumentReference alloc] initWithKey:[FSTDocumentKey keyWithPath:startPath] ID:0]; - - // Find unique batchIDs referenced by all documents potentially matching the query. - __block FSTImmutableSortedSet *uniqueBatchIDs = - [FSTImmutableSortedSet setWithComparator:FSTNumberComparator]; - FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) { - FSTResourcePath *rowKeyPath = reference.key.path; - if (![prefix isPrefixOfPath:rowKeyPath]) { - *stop = YES; - return; - } - - // Rows with document keys more than one segment longer than the query path can't be matches. - // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx. - // TODO(mcg): we'll need a different scanner when we implement ancestor queries. - if (rowKeyPath.length != immediateChildrenPathLength) { - return; - } - - uniqueBatchIDs = [uniqueBatchIDs setByAddingObject:@(reference.ID)]; - }; - [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block]; - - // Construct an array of matching batches, sorted by batchID to ensure that multiple mutations - // affecting the same document key are applied in order. - NSMutableArray *result = [NSMutableArray array]; - [uniqueBatchIDs enumerateObjectsUsingBlock:^(NSNumber *batchID, BOOL *stop) { - FSTMutationBatch *batch = [self lookupMutationBatch:[batchID intValue]]; - if (batch) { - [result addObject:batch]; - } - }]; - - return result; -} - -- (void)removeMutationBatches:(NSArray *)batches group:(FSTWriteGroup *)group { - NSUInteger batchCount = batches.count; - FSTAssert(batchCount > 0, @"Should not remove mutations when none exist."); - - FSTBatchID firstBatchID = batches[0].batchID; - - NSMutableArray *queue = self.queue; - NSUInteger queueCount = queue.count; - - // Find the position of the first batch for removal. This need not be the first entry in the - // queue. - NSUInteger startIndex = [self indexOfExistingBatchID:firstBatchID action:@"removed"]; - FSTAssert(queue[startIndex].batchID == firstBatchID, @"Removed batches must exist in the queue"); - - // Check that removed batches are contiguous (while excluding tombstones). - NSUInteger batchIndex = 1; - NSUInteger queueIndex = startIndex + 1; - while (batchIndex < batchCount && queueIndex < queueCount) { - FSTMutationBatch *batch = queue[queueIndex]; - if ([batch isTombstone]) { - queueIndex++; - continue; - } - - FSTAssert(batch.batchID == batches[batchIndex].batchID, - @"Removed batches must be contiguous in the queue"); - batchIndex++; - queueIndex++; - } - - // Only actually remove batches if removing at the front of the queue. Previously rejected batches - // may have left tombstones in the queue, so expand the removal range to include any tombstones. - if (startIndex == 0) { - for (; queueIndex < queueCount; queueIndex++) { - FSTMutationBatch *batch = queue[queueIndex]; - if (![batch isTombstone]) { - break; - } - } - - NSUInteger length = queueIndex - startIndex; - [queue removeObjectsInRange:NSMakeRange(startIndex, length)]; - - } else { - // Mark tombstones - for (NSUInteger i = startIndex; i < queueIndex; i++) { - queue[i] = [queue[i] toTombstone]; - } - } - - // Remove entries from the index too. - id garbageCollector = self.garbageCollector; - FSTImmutableSortedSet *references = self.batchesByDocumentKey; - for (FSTMutationBatch *batch in batches) { - FSTBatchID batchID = batch.batchID; - for (FSTMutation *mutation in batch.mutations) { - FSTDocumentKey *key = mutation.key; - [garbageCollector addPotentialGarbageKey:key]; - - FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:batchID]; - references = [references setByRemovingObject:reference]; - } - } - self.batchesByDocumentKey = references; -} - -- (void)performConsistencyCheck { - if (self.queue.count == 0) { - FSTAssert([self.batchesByDocumentKey isEmpty], - @"Document leak -- detected dangling mutation references when queue is empty."); - } -} - -#pragma mark - FSTGarbageSource implementation - -- (BOOL)containsKey:(FSTDocumentKey *)key { - // Create a reference with a zero ID as the start position to find any document reference with - // this key. - FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0]; - - NSEnumerator *enumerator = - [self.batchesByDocumentKey objectEnumeratorFrom:reference]; - FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key; - return [firstKey isEqual:key]; -} - -#pragma mark - Helpers - -/** - * A private helper that collects all the mutation batches in the queue up to but not including - * the given endIndex. All tombstones in the queue are excluded. - */ -- (NSArray *)allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex { - NSMutableArray *result = [NSMutableArray arrayWithCapacity:endIndex]; - - NSUInteger index = 0; - for (FSTMutationBatch *batch in self.queue) { - if (index++ >= endIndex) break; - - if (![batch isTombstone]) { - [result addObject:batch]; - } - } - - return result; -} - -/** - * Finds the index of the given batchID in the mutation queue. This operation is O(1). - * - * @return The computed index of the batch with the given batchID, based on the state of the - * queue. Note this index can negative if the requested batchID has already been removed from - * the queue or past the end of the queue if the batchID is larger than the last added batch. - */ -- (NSInteger)indexOfBatchID:(FSTBatchID)batchID { - NSMutableArray *queue = self.queue; - NSUInteger count = queue.count; - if (count == 0) { - // As an index this is past the end of the queue - return 0; - } - - // Examine the front of the queue to figure out the difference between the batchID and indexes - // in the array. Note that since the queue is ordered by batchID, if the first batch has a larger - // batchID then the requested batchID doesn't exist in the queue. - FSTMutationBatch *firstBatch = queue[0]; - FSTBatchID firstBatchID = firstBatch.batchID; - return batchID - firstBatchID; -} - -/** - * Finds the index of the given batchID in the mutation queue and asserts that the resulting - * index is within the bounds of the queue. - * - * @param batchID The batchID to search for - * @param action A description of what the caller is doing, phrased in passive form (e.g. - * "acknowledged" in a routine that acknowledges batches). - */ -- (NSUInteger)indexOfExistingBatchID:(FSTBatchID)batchID action:(NSString *)action { - NSInteger index = [self indexOfBatchID:batchID]; - FSTAssert(index >= 0 && index < self.queue.count, @"Batches must exist to be %@", action); - return (NSUInteger)index; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.mm b/Firestore/Source/Local/FSTMemoryMutationQueue.mm new file mode 100644 index 0000000..702f614 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.mm @@ -0,0 +1,444 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/Source/Local/FSTMemoryMutationQueue.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Local/FSTDocumentReference.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +static const NSComparator NumberComparator = ^NSComparisonResult(NSNumber *left, NSNumber *right) { + return [left compare:right]; +}; + +@interface FSTMemoryMutationQueue () + +/** + * A FIFO queue of all mutations to apply to the backend. Mutations are added to the end of the + * queue as they're written, and removed from the front of the queue as the mutations become + * visible or are rejected. + * + * When successfully applied, mutations must be acknowledged by the write stream and made visible + * on the watch stream. It's possible for the watch stream to fall behind in which case the batches + * at the head of the queue will be acknowledged but held until the watch stream sees the changes. + * + * If a batch is rejected while there are held write acknowledgements at the head of the queue + * the rejected batch is converted to a tombstone: its mutations are removed but the batch remains + * in the queue. This maintains a simple consecutive ordering of batches in the queue. + * + * Once the held write acknowledgements become visible they are removed from the head of the queue + * along with any tombstones that follow. + */ +@property(nonatomic, strong, readonly) NSMutableArray *queue; + +/** An ordered mapping between documents and the mutation batch IDs. */ +@property(nonatomic, strong) FSTImmutableSortedSet *batchesByDocumentKey; + +/** The next value to use when assigning sequential IDs to each mutation batch. */ +@property(nonatomic, assign) FSTBatchID nextBatchID; + +/** The highest acknowledged mutation in the queue. */ +@property(nonatomic, assign) FSTBatchID highestAcknowledgedBatchID; + +/** + * The last received stream token from the server, used to acknowledge which responses the client + * has processed. Stream tokens are opaque checkpoint markers whose only real value is their + * inclusion in the next request. + */ +@property(nonatomic, strong, nullable) NSData *lastStreamToken; + +@end + +@implementation FSTMemoryMutationQueue + ++ (instancetype)mutationQueue { + return [[FSTMemoryMutationQueue alloc] init]; +} + +- (instancetype)init { + if (self = [super init]) { + _queue = [NSMutableArray array]; + _batchesByDocumentKey = + [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; + + _nextBatchID = 1; + _highestAcknowledgedBatchID = kFSTBatchIDUnknown; + } + return self; +} + +#pragma mark - FSTMutationQueue implementation + +- (void)startWithGroup:(FSTWriteGroup *)group { + // Note: The queue may be shutdown / started multiple times, since we maintain the queue for the + // duration of the app session in case a user logs out / back in. To behave like the + // LevelDB-backed MutationQueue (and accommodate tests that expect as much), we reset nextBatchID + // and highestAcknowledgedBatchID if the queue is empty. + if (self.isEmpty) { + self.nextBatchID = 1; + self.highestAcknowledgedBatchID = kFSTBatchIDUnknown; + } + FSTAssert(self.highestAcknowledgedBatchID < self.nextBatchID, + @"highestAcknowledgedBatchID must be less than the nextBatchID"); +} + +- (void)shutdown { +} + +- (BOOL)isEmpty { + // If the queue has any entries at all, the first entry must not be a tombstone (otherwise it + // would have been removed already). + return self.queue.count == 0; +} + +- (FSTBatchID)highestAcknowledgedBatchID { + return _highestAcknowledgedBatchID; +} + +- (void)acknowledgeBatch:(FSTMutationBatch *)batch + streamToken:(nullable NSData *)streamToken + group:(__unused FSTWriteGroup *)group { + NSMutableArray *queue = self.queue; + + FSTBatchID batchID = batch.batchID; + FSTAssert(batchID > self.highestAcknowledgedBatchID, + @"Mutation batchIDs must be acknowledged in order"); + + NSInteger batchIndex = [self indexOfExistingBatchID:batchID action:@"acknowledged"]; + + // Verify that the batch in the queue is the one to be acknowledged. + FSTMutationBatch *check = queue[(NSUInteger)batchIndex]; + FSTAssert(batchID == check.batchID, @"Queue ordering failure: expected batch %d, got batch %d", + batchID, check.batchID); + FSTAssert(![check isTombstone], @"Can't acknowledge a previously removed batch"); + + self.highestAcknowledgedBatchID = batchID; + self.lastStreamToken = streamToken; +} + +- (void)setLastStreamToken:(nullable NSData *)streamToken group:(__unused FSTWriteGroup *)group { + self.lastStreamToken = streamToken; +} + +- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray *)mutations + group:(FSTWriteGroup *)group { + FSTAssert(mutations.count > 0, @"Mutation batches should not be empty"); + + FSTBatchID batchID = self.nextBatchID; + self.nextBatchID += 1; + + NSMutableArray *queue = self.queue; + if (queue.count > 0) { + FSTMutationBatch *prior = queue[queue.count - 1]; + FSTAssert(prior.batchID < batchID, @"Mutation batchIDs must be monotonically increasing order"); + } + + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID + localWriteTime:localWriteTime + mutations:mutations]; + [queue addObject:batch]; + + // Track references by document key. + FSTImmutableSortedSet *references = self.batchesByDocumentKey; + for (FSTMutation *mutation in batch.mutations) { + references = [references + setByAddingObject:[[FSTDocumentReference alloc] initWithKey:mutation.key ID:batchID]]; + } + self.batchesByDocumentKey = references; + + return batch; +} + +- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID { + NSMutableArray *queue = self.queue; + + NSInteger index = [self indexOfBatchID:batchID]; + if (index < 0 || index >= queue.count) { + return nil; + } + + FSTMutationBatch *batch = queue[(NSUInteger)index]; + FSTAssert(batch.batchID == batchID, @"If found batch must match"); + return [batch isTombstone] ? nil : batch; +} + +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID { + NSMutableArray *queue = self.queue; + NSUInteger count = queue.count; + + // All batches with batchID <= self.highestAcknowledgedBatchID have been acknowledged so the + // first unacknowledged batch after batchID will have a batchID larger than both of these values. + batchID = MAX(batchID + 1, self.highestAcknowledgedBatchID); + + // The requested batchID may still be out of range so normalize it to the start of the queue. + NSInteger rawIndex = [self indexOfBatchID:batchID]; + NSUInteger index = rawIndex < 0 ? 0 : (NSUInteger)rawIndex; + + // Finally return the first non-tombstone batch. + for (; index < count; index++) { + FSTMutationBatch *batch = queue[index]; + if (![batch isTombstone]) { + return batch; + } + } + + return nil; +} + +- (NSArray *)allMutationBatches { + return [self allLiveMutationBatchesBeforeIndex:self.queue.count]; +} + +- (NSArray *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID { + NSMutableArray *queue = self.queue; + NSUInteger count = queue.count; + + NSInteger endIndex = [self indexOfBatchID:batchID]; + if (endIndex < 0) { + endIndex = 0; + } else if (endIndex >= count) { + endIndex = count; + } else { + // The endIndex is in the queue so increment to pull everything in the queue including it. + endIndex += 1; + } + + return [self allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex]; +} + +- (NSArray *)allMutationBatchesAffectingDocumentKey: + (FSTDocumentKey *)documentKey { + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:documentKey ID:0]; + + NSMutableArray *result = [NSMutableArray array]; + FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) { + if (![documentKey isEqualToKey:reference.key]) { + *stop = YES; + return; + } + + FSTMutationBatch *batch = [self lookupMutationBatch:reference.ID]; + FSTAssert(batch, @"Batches in the index must exist in the main table"); + [result addObject:batch]; + }; + + [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block]; + return result; +} + +- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { + // Use the query path as a prefix for testing if a document matches the query. + FSTResourcePath *prefix = query.path; + int immediateChildrenPathLength = prefix.length + 1; + + // Construct a document reference for actually scanning the index. Unlike the prefix, the document + // key in this reference must have an even number of segments. The empty segment can be used as + // a suffix of the query path because it precedes all other segments in an ordered traversal. + FSTResourcePath *startPath = query.path; + if (![FSTDocumentKey isDocumentKey:startPath]) { + startPath = [startPath pathByAppendingSegment:@""]; + } + FSTDocumentReference *start = + [[FSTDocumentReference alloc] initWithKey:[FSTDocumentKey keyWithPath:startPath] ID:0]; + + // Find unique batchIDs referenced by all documents potentially matching the query. + __block FSTImmutableSortedSet *uniqueBatchIDs = + [FSTImmutableSortedSet setWithComparator:NumberComparator]; + FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) { + FSTResourcePath *rowKeyPath = reference.key.path; + if (![prefix isPrefixOfPath:rowKeyPath]) { + *stop = YES; + return; + } + + // Rows with document keys more than one segment longer than the query path can't be matches. + // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx. + // TODO(mcg): we'll need a different scanner when we implement ancestor queries. + if (rowKeyPath.length != immediateChildrenPathLength) { + return; + } + + uniqueBatchIDs = [uniqueBatchIDs setByAddingObject:@(reference.ID)]; + }; + [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block]; + + // Construct an array of matching batches, sorted by batchID to ensure that multiple mutations + // affecting the same document key are applied in order. + NSMutableArray *result = [NSMutableArray array]; + [uniqueBatchIDs enumerateObjectsUsingBlock:^(NSNumber *batchID, BOOL *stop) { + FSTMutationBatch *batch = [self lookupMutationBatch:[batchID intValue]]; + if (batch) { + [result addObject:batch]; + } + }]; + + return result; +} + +- (void)removeMutationBatches:(NSArray *)batches group:(FSTWriteGroup *)group { + NSUInteger batchCount = batches.count; + FSTAssert(batchCount > 0, @"Should not remove mutations when none exist."); + + FSTBatchID firstBatchID = batches[0].batchID; + + NSMutableArray *queue = self.queue; + NSUInteger queueCount = queue.count; + + // Find the position of the first batch for removal. This need not be the first entry in the + // queue. + NSUInteger startIndex = [self indexOfExistingBatchID:firstBatchID action:@"removed"]; + FSTAssert(queue[startIndex].batchID == firstBatchID, @"Removed batches must exist in the queue"); + + // Check that removed batches are contiguous (while excluding tombstones). + NSUInteger batchIndex = 1; + NSUInteger queueIndex = startIndex + 1; + while (batchIndex < batchCount && queueIndex < queueCount) { + FSTMutationBatch *batch = queue[queueIndex]; + if ([batch isTombstone]) { + queueIndex++; + continue; + } + + FSTAssert(batch.batchID == batches[batchIndex].batchID, + @"Removed batches must be contiguous in the queue"); + batchIndex++; + queueIndex++; + } + + // Only actually remove batches if removing at the front of the queue. Previously rejected batches + // may have left tombstones in the queue, so expand the removal range to include any tombstones. + if (startIndex == 0) { + for (; queueIndex < queueCount; queueIndex++) { + FSTMutationBatch *batch = queue[queueIndex]; + if (![batch isTombstone]) { + break; + } + } + + NSUInteger length = queueIndex - startIndex; + [queue removeObjectsInRange:NSMakeRange(startIndex, length)]; + + } else { + // Mark tombstones + for (NSUInteger i = startIndex; i < queueIndex; i++) { + queue[i] = [queue[i] toTombstone]; + } + } + + // Remove entries from the index too. + id garbageCollector = self.garbageCollector; + FSTImmutableSortedSet *references = self.batchesByDocumentKey; + for (FSTMutationBatch *batch in batches) { + FSTBatchID batchID = batch.batchID; + for (FSTMutation *mutation in batch.mutations) { + FSTDocumentKey *key = mutation.key; + [garbageCollector addPotentialGarbageKey:key]; + + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:batchID]; + references = [references setByRemovingObject:reference]; + } + } + self.batchesByDocumentKey = references; +} + +- (void)performConsistencyCheck { + if (self.queue.count == 0) { + FSTAssert([self.batchesByDocumentKey isEmpty], + @"Document leak -- detected dangling mutation references when queue is empty."); + } +} + +#pragma mark - FSTGarbageSource implementation + +- (BOOL)containsKey:(FSTDocumentKey *)key { + // Create a reference with a zero ID as the start position to find any document reference with + // this key. + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0]; + + NSEnumerator *enumerator = + [self.batchesByDocumentKey objectEnumeratorFrom:reference]; + FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key; + return [firstKey isEqual:key]; +} + +#pragma mark - Helpers + +/** + * A private helper that collects all the mutation batches in the queue up to but not including + * the given endIndex. All tombstones in the queue are excluded. + */ +- (NSArray *)allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:endIndex]; + + NSUInteger index = 0; + for (FSTMutationBatch *batch in self.queue) { + if (index++ >= endIndex) break; + + if (![batch isTombstone]) { + [result addObject:batch]; + } + } + + return result; +} + +/** + * Finds the index of the given batchID in the mutation queue. This operation is O(1). + * + * @return The computed index of the batch with the given batchID, based on the state of the + * queue. Note this index can negative if the requested batchID has already been removed from + * the queue or past the end of the queue if the batchID is larger than the last added batch. + */ +- (NSInteger)indexOfBatchID:(FSTBatchID)batchID { + NSMutableArray *queue = self.queue; + NSUInteger count = queue.count; + if (count == 0) { + // As an index this is past the end of the queue + return 0; + } + + // Examine the front of the queue to figure out the difference between the batchID and indexes + // in the array. Note that since the queue is ordered by batchID, if the first batch has a larger + // batchID then the requested batchID doesn't exist in the queue. + FSTMutationBatch *firstBatch = queue[0]; + FSTBatchID firstBatchID = firstBatch.batchID; + return batchID - firstBatchID; +} + +/** + * Finds the index of the given batchID in the mutation queue and asserts that the resulting + * index is within the bounds of the queue. + * + * @param batchID The batchID to search for + * @param action A description of what the caller is doing, phrased in passive form (e.g. + * "acknowledged" in a routine that acknowledges batches). + */ +- (NSUInteger)indexOfExistingBatchID:(FSTBatchID)batchID action:(NSString *)action { + NSInteger index = [self indexOfBatchID:batchID]; + FSTAssert(index >= 0 && index < self.queue.count, @"Batches must exist to be %@", action); + return (NSUInteger)index; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTFieldValue.m b/Firestore/Source/Model/FSTFieldValue.m deleted file mode 100644 index a6326a7..0000000 --- a/Firestore/Source/Model/FSTFieldValue.m +++ /dev/null @@ -1,887 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Model/FSTFieldValue.h" - -#import "Firestore/Source/API/FIRGeoPoint+Internal.h" -#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTClasses.h" -#import "Firestore/Source/Util/FSTComparison.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTFieldValueOptions - -@implementation FSTFieldValueOptions - -+ (instancetype)optionsForSnapshotOptions:(FIRSnapshotOptions *)options { - if (options.serverTimestampBehavior == FSTServerTimestampBehaviorNone) { - static FSTFieldValueOptions *defaultInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - defaultInstance = [[FSTFieldValueOptions alloc] - initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; - }); - return defaultInstance; - } else { - return [[FSTFieldValueOptions alloc] - initWithServerTimestampBehavior:options.serverTimestampBehavior]; - } -} - -- (instancetype)initWithServerTimestampBehavior: - (FSTServerTimestampBehavior)serverTimestampBehavior { - self = [super init]; - - if (self) { - _serverTimestampBehavior = serverTimestampBehavior; - } - return self; -} - -@end - -#pragma mark - FSTFieldValue - -@interface FSTFieldValue () -- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other; -@end - -@implementation FSTFieldValue - -- (FSTTypeOrder)typeOrder { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (id)value { - return [self valueWithOptions:[FSTFieldValueOptions - optionsForSnapshotOptions:[FIRSnapshotOptions defaultOptions]]]; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (BOOL)isEqual:(id)other { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (NSUInteger)hash { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - @throw FSTAbstractMethodException(); // NOLINT -} - -- (NSString *)description { - return [[self value] description]; -} - -- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other { - if (self.typeOrder > other.typeOrder) { - return NSOrderedDescending; - } else { - FSTAssert(self.typeOrder < other.typeOrder, - @"defaultCompare should not be used for values of same type."); - return NSOrderedAscending; - } -} - -@end - -#pragma mark - FSTNullValue - -@implementation FSTNullValue - -+ (instancetype)nullValue { - static FSTNullValue *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTNullValue alloc] init]; - }); - return sharedInstance; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderNull; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - return [NSNull null]; -} - -- (BOOL)isEqual:(id)other { - return [other isKindOfClass:[self class]]; -} - -- (NSUInteger)hash { - return 47; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[self class]]) { - return NSOrderedSame; - } else { - return [self defaultCompare:other]; - } -} - -@end - -#pragma mark - FSTBooleanValue - -@interface FSTBooleanValue () -@property(nonatomic, assign, readonly) BOOL internalValue; -@end - -@implementation FSTBooleanValue - -+ (instancetype)trueValue { - static FSTBooleanValue *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTBooleanValue alloc] initWithValue:YES]; - }); - return sharedInstance; -} - -+ (instancetype)falseValue { - static FSTBooleanValue *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTBooleanValue alloc] initWithValue:NO]; - }); - return sharedInstance; -} - -+ (instancetype)booleanValue:(BOOL)value { - return value ? [FSTBooleanValue trueValue] : [FSTBooleanValue falseValue]; -} - -- (id)initWithValue:(BOOL)value { - self = [super init]; - if (self) { - _internalValue = value; - } - return self; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderBoolean; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - return self.internalValue ? @YES : @NO; -} - -- (BOOL)isEqual:(id)other { - // Since we create shared instances for true / false, we can use reference equality. - return self == other; -} - -- (NSUInteger)hash { - return self.internalValue ? 1231 : 1237; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTBooleanValue class]]) { - return FSTCompareBools(self.internalValue, ((FSTBooleanValue *)other).internalValue); - } else { - return [self defaultCompare:other]; - } -} - -@end - -#pragma mark - FSTNumberValue - -@implementation FSTNumberValue - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderNumber; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if (![other isKindOfClass:[FSTNumberValue class]]) { - return [self defaultCompare:other]; - } else { - if ([self isKindOfClass:[FSTDoubleValue class]]) { - double thisDouble = ((FSTDoubleValue *)self).internalValue; - if ([other isKindOfClass:[FSTDoubleValue class]]) { - return FSTCompareDoubles(thisDouble, ((FSTDoubleValue *)other).internalValue); - } else { - FSTAssert([other isKindOfClass:[FSTIntegerValue class]], @"Unknown number value: %@", - other); - return FSTCompareMixed(thisDouble, ((FSTIntegerValue *)other).internalValue); - } - } else { - int64_t thisInt = ((FSTIntegerValue *)self).internalValue; - if ([other isKindOfClass:[FSTIntegerValue class]]) { - return FSTCompareInt64s(thisInt, ((FSTIntegerValue *)other).internalValue); - } else { - FSTAssert([other isKindOfClass:[FSTDoubleValue class]], @"Unknown number value: %@", other); - return -1 * FSTCompareMixed(((FSTDoubleValue *)other).internalValue, thisInt); - } - } - } -} - -@end - -#pragma mark - FSTIntegerValue - -@interface FSTIntegerValue () -@property(nonatomic, assign, readonly) int64_t internalValue; -@end - -@implementation FSTIntegerValue - -+ (instancetype)integerValue:(int64_t)value { - return [[FSTIntegerValue alloc] initWithValue:value]; -} - -- (id)initWithValue:(int64_t)value { - self = [super init]; - if (self) { - _internalValue = value; - } - return self; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - return @(self.internalValue); -} - -- (BOOL)isEqual:(id)other { - // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them - // equal via isEqual: - return [other isKindOfClass:[FSTIntegerValue class]] && - self.internalValue == ((FSTIntegerValue *)other).internalValue; -} - -- (NSUInteger)hash { - return (((NSUInteger)self.internalValue) ^ (NSUInteger)(self.internalValue >> 32)); -} - -// NOTE: compare: is implemented in NumberValue. - -@end - -#pragma mark - FSTDoubleValue - -@interface FSTDoubleValue () -@property(nonatomic, assign, readonly) double internalValue; -@end - -@implementation FSTDoubleValue - -+ (instancetype)doubleValue:(double)value { - // Normalize NaNs to match the behavior on the backend (which uses Double.doubletoLongBits()). - if (isnan(value)) { - return [FSTDoubleValue nanValue]; - } - return [[FSTDoubleValue alloc] initWithValue:value]; -} - -+ (instancetype)nanValue { - static FSTDoubleValue *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTDoubleValue alloc] initWithValue:NAN]; - }); - return sharedInstance; -} - -- (id)initWithValue:(double)value { - self = [super init]; - if (self) { - _internalValue = value; - } - return self; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - return @(self.internalValue); -} - -- (BOOL)isEqual:(id)other { - // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them - // equal via isEqual: - - // NOTE: isEqual: should compare NaN equal to itself and -0.0 not equal to 0.0. - - return [other isKindOfClass:[FSTDoubleValue class]] && - FSTDoubleBitwiseEquals(self.internalValue, ((FSTDoubleValue *)other).internalValue); -} - -- (NSUInteger)hash { - return FSTDoubleBitwiseHash(self.internalValue); -} - -// NOTE: compare: is implemented in NumberValue. - -@end - -#pragma mark - FSTStringValue - -@interface FSTStringValue () -@property(nonatomic, copy, readonly) NSString *internalValue; -@end - -// TODO(b/37267885): Add truncation support -@implementation FSTStringValue - -+ (instancetype)stringValue:(NSString *)value { - return [[FSTStringValue alloc] initWithValue:value]; -} - -- (id)initWithValue:(NSString *)value { - self = [super init]; - if (self) { - _internalValue = [value copy]; - } - return self; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderString; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - return self.internalValue; -} - -- (BOOL)isEqual:(id)other { - return [other isKindOfClass:[FSTStringValue class]] && - [self.internalValue isEqualToString:((FSTStringValue *)other).internalValue]; -} - -- (NSUInteger)hash { - return self.internalValue ? 1 : 0; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTStringValue class]]) { - return FSTCompareStrings(self.internalValue, ((FSTStringValue *)other).internalValue); - } else { - return [self defaultCompare:other]; - } -} - -@end - -#pragma mark - FSTTimestampValue - -@interface FSTTimestampValue () -@property(nonatomic, strong, readonly) FSTTimestamp *internalValue; -@end - -@implementation FSTTimestampValue - -+ (instancetype)timestampValue:(FSTTimestamp *)value { - return [[FSTTimestampValue alloc] initWithValue:value]; -} - -- (id)initWithValue:(FSTTimestamp *)value { - self = [super init]; - if (self) { - _internalValue = value; // FSTTimestamp is immutable. - } - return self; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderTimestamp; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - // For developers, we expose Timestamps as Dates. - return self.internalValue.approximateDateValue; -} - -- (BOOL)isEqual:(id)other { - return [other isKindOfClass:[FSTTimestampValue class]] && - [self.internalValue isEqual:((FSTTimestampValue *)other).internalValue]; -} - -- (NSUInteger)hash { - return [self.internalValue hash]; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTTimestampValue class]]) { - return [self.internalValue compare:((FSTTimestampValue *)other).internalValue]; - } else if ([other isKindOfClass:[FSTServerTimestampValue class]]) { - // Concrete timestamps come before server timestamps. - return NSOrderedAscending; - } else { - return [self defaultCompare:other]; - } -} - -@end - -#pragma mark - FSTServerTimestampValue - -@implementation FSTServerTimestampValue - -+ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime - previousValue:(nullable FSTFieldValue *)previousValue { - return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime - previousValue:previousValue]; -} - -- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime - previousValue:(nullable FSTFieldValue *)previousValue { - self = [super init]; - if (self) { - _localWriteTime = localWriteTime; - _previousValue = previousValue; - } - return self; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderTimestamp; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - switch (options.serverTimestampBehavior) { - case FSTServerTimestampBehaviorNone: - return [NSNull null]; - case FSTServerTimestampBehaviorEstimate: - return [self.localWriteTime approximateDateValue]; - case FSTServerTimestampBehaviorPrevious: - return self.previousValue ? [self.previousValue valueWithOptions:options] : [NSNull null]; - default: - FSTFail(@"Unexpected server timestamp option: %d", (int)options.serverTimestampBehavior); - } -} - -- (BOOL)isEqual:(id)other { - return [other isKindOfClass:[FSTServerTimestampValue class]] && - [self.localWriteTime isEqual:((FSTServerTimestampValue *)other).localWriteTime]; -} - -- (NSUInteger)hash { - return [self.localWriteTime hash]; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", self.localWriteTime]; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTServerTimestampValue class]]) { - return [self.localWriteTime compare:((FSTServerTimestampValue *)other).localWriteTime]; - } else if ([other isKindOfClass:[FSTTimestampValue class]]) { - // Server timestamps come after all concrete timestamps. - return NSOrderedDescending; - } else { - return [self defaultCompare:other]; - } -} - -@end - -#pragma mark - FSTGeoPointValue - -@interface FSTGeoPointValue () -@property(nonatomic, strong, readonly) FIRGeoPoint *internalValue; -@end - -@implementation FSTGeoPointValue - -+ (instancetype)geoPointValue:(FIRGeoPoint *)value { - return [[FSTGeoPointValue alloc] initWithValue:value]; -} - -- (id)initWithValue:(FIRGeoPoint *)value { - self = [super init]; - if (self) { - _internalValue = value; // FIRGeoPoint is immutable. - } - return self; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderGeoPoint; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - return self.internalValue; -} - -- (BOOL)isEqual:(id)other { - return [other isKindOfClass:[FSTGeoPointValue class]] && - [self.internalValue isEqual:((FSTGeoPointValue *)other).internalValue]; -} - -- (NSUInteger)hash { - return [self.internalValue hash]; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTGeoPointValue class]]) { - return [self.internalValue compare:((FSTGeoPointValue *)other).internalValue]; - } else { - return [self defaultCompare:other]; - } -} - -@end - -#pragma mark - FSTBlobValue - -@interface FSTBlobValue () -@property(nonatomic, copy, readonly) NSData *internalValue; -@end - -// TODO(b/37267885): Add truncation support -@implementation FSTBlobValue - -+ (instancetype)blobValue:(NSData *)value { - return [[FSTBlobValue alloc] initWithValue:value]; -} - -- (id)initWithValue:(NSData *)value { - self = [super init]; - if (self) { - _internalValue = [value copy]; - } - return self; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderBlob; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - return self.internalValue; -} - -- (BOOL)isEqual:(id)other { - return [other isKindOfClass:[FSTBlobValue class]] && - [self.internalValue isEqual:((FSTBlobValue *)other).internalValue]; -} - -- (NSUInteger)hash { - return [self.internalValue hash]; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTBlobValue class]]) { - return FSTCompareBytes(self.internalValue, ((FSTBlobValue *)other).internalValue); - } else { - return [self defaultCompare:other]; - } -} - -@end - -#pragma mark - FSTReferenceValue - -@interface FSTReferenceValue () -@property(nonatomic, strong, readonly) FSTDocumentKey *key; -@end - -@implementation FSTReferenceValue - -+ (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID { - return [[FSTReferenceValue alloc] initWithValue:value databaseID:databaseID]; -} - -- (id)initWithValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID { - self = [super init]; - if (self) { - _key = value; - _databaseID = databaseID; - } - return self; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - return self.key; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderReference; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTReferenceValue class]]) { - return NO; - } - - FSTReferenceValue *otherRef = (FSTReferenceValue *)other; - return [self.key isEqualToKey:otherRef.key] && - [self.databaseID isEqualToDatabaseId:otherRef.databaseID]; -} - -- (NSUInteger)hash { - NSUInteger result = [self.databaseID hash]; - result = 31 * result + [self.key hash]; - return result; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTReferenceValue class]]) { - FSTReferenceValue *ref = (FSTReferenceValue *)other; - NSComparisonResult cmp = [self.databaseID compare:ref.databaseID]; - return cmp != NSOrderedSame ? cmp : [self.key compare:ref.key]; - } else { - return [self defaultCompare:other]; - } -} - -@end - -#pragma mark - FSTObjectValue - -@interface FSTObjectValue () -@property(nonatomic, strong, readonly) - FSTImmutableSortedDictionary *internalValue; -@end - -@implementation FSTObjectValue - -+ (instancetype)objectValue { - static FSTObjectValue *sharedEmptyInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - FSTImmutableSortedDictionary *empty = - [FSTImmutableSortedDictionary dictionaryWithComparator:FSTStringComparator]; - sharedEmptyInstance = [[FSTObjectValue alloc] initWithImmutableDictionary:empty]; - }); - return sharedEmptyInstance; -} - -- (instancetype)initWithImmutableDictionary: - (FSTImmutableSortedDictionary *)value { - self = [super init]; - if (self) { - _internalValue = value; // FSTImmutableSortedDictionary is immutable. - } - return self; -} - -- (id)initWithDictionary:(NSDictionary *)value { - FSTImmutableSortedDictionary *dictionary = - [FSTImmutableSortedDictionary dictionaryWithDictionary:value comparator:FSTStringComparator]; - return [self initWithImmutableDictionary:dictionary]; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - [self.internalValue - enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { - result[key] = [obj valueWithOptions:options]; - }]; - return result; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderObject; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[FSTObjectValue class]]) { - return NO; - } - - FSTObjectValue *otherObj = other; - return [self.internalValue isEqual:otherObj.internalValue]; -} - -- (NSUInteger)hash { - return [self.internalValue hash]; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTObjectValue class]]) { - FSTImmutableSortedDictionary *selfDict = self.internalValue; - FSTImmutableSortedDictionary *otherDict = ((FSTObjectValue *)other).internalValue; - NSEnumerator *enumerator1 = [selfDict keyEnumerator]; - NSEnumerator *enumerator2 = [otherDict keyEnumerator]; - NSString *key1 = [enumerator1 nextObject]; - NSString *key2 = [enumerator2 nextObject]; - while (key1 && key2) { - NSComparisonResult keyCompare = [key1 compare:key2]; - if (keyCompare != NSOrderedSame) { - return keyCompare; - } - NSComparisonResult valueCompare = [selfDict[key1] compare:otherDict[key2]]; - if (valueCompare != NSOrderedSame) { - return valueCompare; - } - key1 = [enumerator1 nextObject]; - key2 = [enumerator2 nextObject]; - } - // Only equal if both enumerators are exhausted. - return FSTCompareBools(key1 != nil, key2 != nil); - } else { - return [self defaultCompare:other]; - } -} - -- (nullable FSTFieldValue *)valueForPath:(FSTFieldPath *)fieldPath { - FSTFieldValue *value = self; - for (int i = 0, max = fieldPath.length; value && i < max; i++) { - if (![value isMemberOfClass:[FSTObjectValue class]]) { - return nil; - } - - NSString *fieldName = fieldPath[i]; - value = ((FSTObjectValue *)value).internalValue[fieldName]; - } - - return value; -} - -- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forPath:(FSTFieldPath *)fieldPath { - FSTAssert([fieldPath length] > 0, @"Cannot set value with an empty path"); - - NSString *childName = [fieldPath firstSegment]; - if ([fieldPath length] == 1) { - // Recursive base case: - return [self objectBySettingValue:value forField:childName]; - } else { - // Nested path. Recursively generate a new sub-object and then wrap a new FSTObjectValue around - // the result. - FSTFieldValue *child = [_internalValue objectForKey:childName]; - FSTObjectValue *childObject; - if ([child isKindOfClass:[FSTObjectValue class]]) { - childObject = (FSTObjectValue *)child; - } else { - // If the child is not found or is a primitive type, pretend as if an empty object lived - // there. - childObject = [FSTObjectValue objectValue]; - } - FSTFieldValue *newChild = - [childObject objectBySettingValue:value forPath:[fieldPath pathByRemovingFirstSegment]]; - return [self objectBySettingValue:newChild forField:childName]; - } -} - -- (FSTObjectValue *)objectByDeletingPath:(FSTFieldPath *)fieldPath { - FSTAssert([fieldPath length] > 0, @"Cannot delete an empty path"); - NSString *childName = [fieldPath firstSegment]; - if ([fieldPath length] == 1) { - return [[FSTObjectValue alloc] - initWithImmutableDictionary:[_internalValue dictionaryByRemovingObjectForKey:childName]]; - } else { - FSTFieldValue *child = _internalValue[childName]; - if ([child isKindOfClass:[FSTObjectValue class]]) { - FSTObjectValue *newChild = - [((FSTObjectValue *)child) objectByDeletingPath:[fieldPath pathByRemovingFirstSegment]]; - return [self objectBySettingValue:newChild forField:childName]; - } else { - // If the child is not found or is a primitive type, make no modifications - return self; - } - } -} - -- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forField:(NSString *)field { - return [[FSTObjectValue alloc] - initWithImmutableDictionary:[_internalValue dictionaryBySettingObject:value forKey:field]]; -} - -@end - -@interface FSTArrayValue () -@property(nonatomic, strong, readonly) NSArray *internalValue; -@end - -#pragma mark - FSTArrayValue - -@implementation FSTArrayValue - -- (id)initWithValueNoCopy:(NSArray *)value { - self = [super init]; - if (self) { - // Does not copy, assumes the caller has already copied. - _internalValue = value; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isKindOfClass:[self class]]) { - return NO; - } - - // NSArray's isEqual does the right thing for our purposes. - FSTArrayValue *otherArray = other; - return [self.internalValue isEqual:otherArray.internalValue]; -} - -- (NSUInteger)hash { - return [self.internalValue hash]; -} - -- (id)valueWithOptions:(FSTFieldValueOptions *)options { - NSMutableArray *result = [NSMutableArray arrayWithCapacity:_internalValue.count]; - [self.internalValue enumerateObjectsUsingBlock:^(FSTFieldValue *obj, NSUInteger idx, BOOL *stop) { - [result addObject:[obj value]]; - }]; - return result; -} - -- (FSTTypeOrder)typeOrder { - return FSTTypeOrderArray; -} - -- (NSComparisonResult)compare:(FSTFieldValue *)other { - if ([other isKindOfClass:[FSTArrayValue class]]) { - NSArray *selfArray = self.internalValue; - NSArray *otherArray = ((FSTArrayValue *)other).internalValue; - NSUInteger minLength = MIN(selfArray.count, otherArray.count); - for (NSUInteger i = 0; i < minLength; i++) { - NSComparisonResult cmp = [selfArray[i] compare:otherArray[i]]; - if (cmp != NSOrderedSame) { - return cmp; - } - } - return FSTCompareUIntegers(selfArray.count, otherArray.count); - } else { - return [self defaultCompare:other]; - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTFieldValue.mm b/Firestore/Source/Model/FSTFieldValue.mm new file mode 100644 index 0000000..8ffc98e --- /dev/null +++ b/Firestore/Source/Model/FSTFieldValue.mm @@ -0,0 +1,931 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/Source/Model/FSTFieldValue.h" + +#include "Firestore/core/src/firebase/firestore/util/comparison.h" +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" + +#import "Firestore/Source/API/FIRGeoPoint+Internal.h" +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" + +using firebase::firestore::util::Comparator; +using firebase::firestore::util::CompareMixedNumber; +using firebase::firestore::util::DoubleBitwiseEquals; +using firebase::firestore::util::DoubleBitwiseHash; +using firebase::firestore::util::MakeStringView; +using firebase::firestore::util::ReverseOrder; +using firebase::firestore::util::WrapCompare; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldValueOptions + +@implementation FSTFieldValueOptions + ++ (instancetype)optionsForSnapshotOptions:(FIRSnapshotOptions *)options { + if (options.serverTimestampBehavior == FSTServerTimestampBehaviorNone) { + static FSTFieldValueOptions *defaultInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + defaultInstance = [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; + }); + return defaultInstance; + } else { + return [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:options.serverTimestampBehavior]; + } +} + +- (instancetype)initWithServerTimestampBehavior: + (FSTServerTimestampBehavior)serverTimestampBehavior { + self = [super init]; + + if (self) { + _serverTimestampBehavior = serverTimestampBehavior; + } + return self; +} + +@end + +#pragma mark - FSTFieldValue + +@interface FSTFieldValue () +- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other; +@end + +@implementation FSTFieldValue + +- (FSTTypeOrder)typeOrder { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (id)value { + return [self valueWithOptions:[FSTFieldValueOptions + optionsForSnapshotOptions:[FIRSnapshotOptions defaultOptions]]]; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (BOOL)isEqual:(id)other { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSUInteger)hash { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSString *)description { + return [[self value] description]; +} + +- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other { + if (self.typeOrder > other.typeOrder) { + return NSOrderedDescending; + } else { + FSTAssert(self.typeOrder < other.typeOrder, + @"defaultCompare should not be used for values of same type."); + return NSOrderedAscending; + } +} + +@end + +#pragma mark - FSTNullValue + +@implementation FSTNullValue + ++ (instancetype)nullValue { + static FSTNullValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTNullValue alloc] init]; + }); + return sharedInstance; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderNull; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + return [NSNull null]; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[self class]]; +} + +- (NSUInteger)hash { + return 47; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[self class]]) { + return NSOrderedSame; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTBooleanValue + +@interface FSTBooleanValue () +@property(nonatomic, assign, readonly) BOOL internalValue; +@end + +@implementation FSTBooleanValue + ++ (instancetype)trueValue { + static FSTBooleanValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTBooleanValue alloc] initWithValue:YES]; + }); + return sharedInstance; +} + ++ (instancetype)falseValue { + static FSTBooleanValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTBooleanValue alloc] initWithValue:NO]; + }); + return sharedInstance; +} + ++ (instancetype)booleanValue:(BOOL)value { + return value ? [FSTBooleanValue trueValue] : [FSTBooleanValue falseValue]; +} + +- (id)initWithValue:(BOOL)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderBoolean; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + return self.internalValue ? @YES : @NO; +} + +- (BOOL)isEqual:(id)other { + // Since we create shared instances for true / false, we can use reference equality. + return self == other; +} + +- (NSUInteger)hash { + return self.internalValue ? 1231 : 1237; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTBooleanValue class]]) { + return WrapCompare(self.internalValue, ((FSTBooleanValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTNumberValue + +@implementation FSTNumberValue + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderNumber; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if (![other isKindOfClass:[FSTNumberValue class]]) { + return [self defaultCompare:other]; + } else { + if ([self isKindOfClass:[FSTDoubleValue class]]) { + double thisDouble = ((FSTDoubleValue *)self).internalValue; + if ([other isKindOfClass:[FSTDoubleValue class]]) { + return WrapCompare(thisDouble, ((FSTDoubleValue *)other).internalValue); + } else { + FSTAssert([other isKindOfClass:[FSTIntegerValue class]], @"Unknown number value: %@", + other); + auto result = CompareMixedNumber(thisDouble, ((FSTIntegerValue *)other).internalValue); + return static_cast(result); + } + } else { + int64_t thisInt = ((FSTIntegerValue *)self).internalValue; + if ([other isKindOfClass:[FSTIntegerValue class]]) { + return WrapCompare(thisInt, ((FSTIntegerValue *)other).internalValue); + } else { + FSTAssert([other isKindOfClass:[FSTDoubleValue class]], @"Unknown number value: %@", other); + double otherDouble = ((FSTDoubleValue *)other).internalValue; + auto result = ReverseOrder(CompareMixedNumber(otherDouble, thisInt)); + return static_cast(result); + } + } + } +} + +@end + +#pragma mark - FSTIntegerValue + +@interface FSTIntegerValue () +@property(nonatomic, assign, readonly) int64_t internalValue; +@end + +@implementation FSTIntegerValue + ++ (instancetype)integerValue:(int64_t)value { + return [[FSTIntegerValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(int64_t)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + return @(self.internalValue); +} + +- (BOOL)isEqual:(id)other { + // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them + // equal via isEqual: + return [other isKindOfClass:[FSTIntegerValue class]] && + self.internalValue == ((FSTIntegerValue *)other).internalValue; +} + +- (NSUInteger)hash { + return (((NSUInteger)self.internalValue) ^ (NSUInteger)(self.internalValue >> 32)); +} + +// NOTE: compare: is implemented in NumberValue. + +@end + +#pragma mark - FSTDoubleValue + +@interface FSTDoubleValue () +@property(nonatomic, assign, readonly) double internalValue; +@end + +@implementation FSTDoubleValue + ++ (instancetype)doubleValue:(double)value { + // Normalize NaNs to match the behavior on the backend (which uses Double.doubletoLongBits()). + if (isnan(value)) { + return [FSTDoubleValue nanValue]; + } + return [[FSTDoubleValue alloc] initWithValue:value]; +} + ++ (instancetype)nanValue { + static FSTDoubleValue *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTDoubleValue alloc] initWithValue:NAN]; + }); + return sharedInstance; +} + +- (id)initWithValue:(double)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + return @(self.internalValue); +} + +- (BOOL)isEqual:(id)other { + // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them + // equal via isEqual: + + // NOTE: isEqual: should compare NaN equal to itself and -0.0 not equal to 0.0. + + return [other isKindOfClass:[FSTDoubleValue class]] && + DoubleBitwiseEquals(self.internalValue, ((FSTDoubleValue *)other).internalValue); +} + +- (NSUInteger)hash { + return DoubleBitwiseHash(self.internalValue); +} + +// NOTE: compare: is implemented in NumberValue. + +@end + +#pragma mark - FSTStringValue + +/** + * Specialization of Comparator for NSStrings. + */ +template <> +struct Comparator { + bool operator()(NSString *left, NSString *right) const { + Comparator lessThan; + return lessThan(MakeStringView(left), MakeStringView(right)); + } +}; + +@interface FSTStringValue () +@property(nonatomic, copy, readonly) NSString *internalValue; +@end + +// TODO(b/37267885): Add truncation support +@implementation FSTStringValue + ++ (instancetype)stringValue:(NSString *)value { + return [[FSTStringValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(NSString *)value { + self = [super init]; + if (self) { + _internalValue = [value copy]; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderString; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTStringValue class]] && + [self.internalValue isEqualToString:((FSTStringValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return self.internalValue ? 1 : 0; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTStringValue class]]) { + return WrapCompare(self.internalValue, ((FSTStringValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTTimestampValue + +@interface FSTTimestampValue () +@property(nonatomic, strong, readonly) FSTTimestamp *internalValue; +@end + +@implementation FSTTimestampValue + ++ (instancetype)timestampValue:(FSTTimestamp *)value { + return [[FSTTimestampValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(FSTTimestamp *)value { + self = [super init]; + if (self) { + _internalValue = value; // FSTTimestamp is immutable. + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderTimestamp; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + // For developers, we expose Timestamps as Dates. + return self.internalValue.approximateDateValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTTimestampValue class]] && + [self.internalValue isEqual:((FSTTimestampValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTTimestampValue class]]) { + return [self.internalValue compare:((FSTTimestampValue *)other).internalValue]; + } else if ([other isKindOfClass:[FSTServerTimestampValue class]]) { + // Concrete timestamps come before server timestamps. + return NSOrderedAscending; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTServerTimestampValue + +@implementation FSTServerTimestampValue + ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue { + return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime + previousValue:previousValue]; +} + +- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue { + self = [super init]; + if (self) { + _localWriteTime = localWriteTime; + _previousValue = previousValue; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderTimestamp; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + switch (options.serverTimestampBehavior) { + case FSTServerTimestampBehaviorNone: + return [NSNull null]; + case FSTServerTimestampBehaviorEstimate: + return [self.localWriteTime approximateDateValue]; + case FSTServerTimestampBehaviorPrevious: + return self.previousValue ? [self.previousValue valueWithOptions:options] : [NSNull null]; + default: + FSTFail(@"Unexpected server timestamp option: %d", (int)options.serverTimestampBehavior); + } +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTServerTimestampValue class]] && + [self.localWriteTime isEqual:((FSTServerTimestampValue *)other).localWriteTime]; +} + +- (NSUInteger)hash { + return [self.localWriteTime hash]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.localWriteTime]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTServerTimestampValue class]]) { + return [self.localWriteTime compare:((FSTServerTimestampValue *)other).localWriteTime]; + } else if ([other isKindOfClass:[FSTTimestampValue class]]) { + // Server timestamps come after all concrete timestamps. + return NSOrderedDescending; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTGeoPointValue + +@interface FSTGeoPointValue () +@property(nonatomic, strong, readonly) FIRGeoPoint *internalValue; +@end + +@implementation FSTGeoPointValue + ++ (instancetype)geoPointValue:(FIRGeoPoint *)value { + return [[FSTGeoPointValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(FIRGeoPoint *)value { + self = [super init]; + if (self) { + _internalValue = value; // FIRGeoPoint is immutable. + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderGeoPoint; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTGeoPointValue class]] && + [self.internalValue isEqual:((FSTGeoPointValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTGeoPointValue class]]) { + return [self.internalValue compare:((FSTGeoPointValue *)other).internalValue]; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTBlobValue + +static NSComparisonResult CompareBytes(NSData *left, NSData *right) { + NSUInteger minLength = MIN(left.length, right.length); + int result = memcmp(left.bytes, right.bytes, minLength); + if (result < 0) { + return NSOrderedAscending; + } else if (result > 0) { + return NSOrderedDescending; + } else if (left.length < right.length) { + return NSOrderedAscending; + } else if (left.length > right.length) { + return NSOrderedDescending; + } else { + return NSOrderedSame; + } +} + +@interface FSTBlobValue () +@property(nonatomic, copy, readonly) NSData *internalValue; +@end + +// TODO(b/37267885): Add truncation support +@implementation FSTBlobValue + ++ (instancetype)blobValue:(NSData *)value { + return [[FSTBlobValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(NSData *)value { + self = [super init]; + if (self) { + _internalValue = [value copy]; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderBlob; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTBlobValue class]] && + [self.internalValue isEqual:((FSTBlobValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTBlobValue class]]) { + return CompareBytes(self.internalValue, ((FSTBlobValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTReferenceValue + +@interface FSTReferenceValue () +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@end + +@implementation FSTReferenceValue + ++ (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID { + return [[FSTReferenceValue alloc] initWithValue:value databaseID:databaseID]; +} + +- (id)initWithValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _key = value; + _databaseID = databaseID; + } + return self; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + return self.key; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderReference; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTReferenceValue class]]) { + return NO; + } + + FSTReferenceValue *otherRef = (FSTReferenceValue *)other; + return [self.key isEqualToKey:otherRef.key] && + [self.databaseID isEqualToDatabaseId:otherRef.databaseID]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.databaseID hash]; + result = 31 * result + [self.key hash]; + return result; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTReferenceValue class]]) { + FSTReferenceValue *ref = (FSTReferenceValue *)other; + NSComparisonResult cmp = [self.databaseID compare:ref.databaseID]; + return cmp != NSOrderedSame ? cmp : [self.key compare:ref.key]; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTObjectValue + +static const NSComparator StringComparator = ^NSComparisonResult(NSString *left, NSString *right) { + return WrapCompare(left, right); +}; + +@interface FSTObjectValue () +@property(nonatomic, strong, readonly) + FSTImmutableSortedDictionary *internalValue; +@end + +@implementation FSTObjectValue + ++ (instancetype)objectValue { + static FSTObjectValue *sharedEmptyInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + FSTImmutableSortedDictionary *empty = + [FSTImmutableSortedDictionary dictionaryWithComparator:StringComparator]; + sharedEmptyInstance = [[FSTObjectValue alloc] initWithImmutableDictionary:empty]; + }); + return sharedEmptyInstance; +} + +- (instancetype)initWithImmutableDictionary: + (FSTImmutableSortedDictionary *)value { + self = [super init]; + if (self) { + _internalValue = value; // FSTImmutableSortedDictionary is immutable. + } + return self; +} + +- (id)initWithDictionary:(NSDictionary *)value { + FSTImmutableSortedDictionary *dictionary = + [FSTImmutableSortedDictionary dictionaryWithDictionary:value comparator:StringComparator]; + return [self initWithImmutableDictionary:dictionary]; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [self.internalValue + enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { + result[key] = [obj valueWithOptions:options]; + }]; + return result; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderObject; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTObjectValue class]]) { + return NO; + } + + FSTObjectValue *otherObj = other; + return [self.internalValue isEqual:otherObj.internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTObjectValue class]]) { + FSTImmutableSortedDictionary *selfDict = self.internalValue; + FSTImmutableSortedDictionary *otherDict = ((FSTObjectValue *)other).internalValue; + NSEnumerator *enumerator1 = [selfDict keyEnumerator]; + NSEnumerator *enumerator2 = [otherDict keyEnumerator]; + NSString *key1 = [enumerator1 nextObject]; + NSString *key2 = [enumerator2 nextObject]; + while (key1 && key2) { + NSComparisonResult keyCompare = [key1 compare:key2]; + if (keyCompare != NSOrderedSame) { + return keyCompare; + } + NSComparisonResult valueCompare = [selfDict[key1] compare:otherDict[key2]]; + if (valueCompare != NSOrderedSame) { + return valueCompare; + } + key1 = [enumerator1 nextObject]; + key2 = [enumerator2 nextObject]; + } + // Only equal if both enumerators are exhausted. + return WrapCompare(key1 != nil, key2 != nil); + } else { + return [self defaultCompare:other]; + } +} + +- (nullable FSTFieldValue *)valueForPath:(FSTFieldPath *)fieldPath { + FSTFieldValue *value = self; + for (int i = 0, max = fieldPath.length; value && i < max; i++) { + if (![value isMemberOfClass:[FSTObjectValue class]]) { + return nil; + } + + NSString *fieldName = fieldPath[i]; + value = ((FSTObjectValue *)value).internalValue[fieldName]; + } + + return value; +} + +- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forPath:(FSTFieldPath *)fieldPath { + FSTAssert([fieldPath length] > 0, @"Cannot set value with an empty path"); + + NSString *childName = [fieldPath firstSegment]; + if ([fieldPath length] == 1) { + // Recursive base case: + return [self objectBySettingValue:value forField:childName]; + } else { + // Nested path. Recursively generate a new sub-object and then wrap a new FSTObjectValue around + // the result. + FSTFieldValue *child = [_internalValue objectForKey:childName]; + FSTObjectValue *childObject; + if ([child isKindOfClass:[FSTObjectValue class]]) { + childObject = (FSTObjectValue *)child; + } else { + // If the child is not found or is a primitive type, pretend as if an empty object lived + // there. + childObject = [FSTObjectValue objectValue]; + } + FSTFieldValue *newChild = + [childObject objectBySettingValue:value forPath:[fieldPath pathByRemovingFirstSegment]]; + return [self objectBySettingValue:newChild forField:childName]; + } +} + +- (FSTObjectValue *)objectByDeletingPath:(FSTFieldPath *)fieldPath { + FSTAssert([fieldPath length] > 0, @"Cannot delete an empty path"); + NSString *childName = [fieldPath firstSegment]; + if ([fieldPath length] == 1) { + return [[FSTObjectValue alloc] + initWithImmutableDictionary:[_internalValue dictionaryByRemovingObjectForKey:childName]]; + } else { + FSTFieldValue *child = _internalValue[childName]; + if ([child isKindOfClass:[FSTObjectValue class]]) { + FSTObjectValue *newChild = + [((FSTObjectValue *)child) objectByDeletingPath:[fieldPath pathByRemovingFirstSegment]]; + return [self objectBySettingValue:newChild forField:childName]; + } else { + // If the child is not found or is a primitive type, make no modifications + return self; + } + } +} + +- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forField:(NSString *)field { + return [[FSTObjectValue alloc] + initWithImmutableDictionary:[_internalValue dictionaryBySettingObject:value forKey:field]]; +} + +@end + +@interface FSTArrayValue () +@property(nonatomic, strong, readonly) NSArray *internalValue; +@end + +#pragma mark - FSTArrayValue + +@implementation FSTArrayValue + +- (id)initWithValueNoCopy:(NSArray *)value { + self = [super init]; + if (self) { + // Does not copy, assumes the caller has already copied. + _internalValue = value; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[self class]]) { + return NO; + } + + // NSArray's isEqual does the right thing for our purposes. + FSTArrayValue *otherArray = other; + return [self.internalValue isEqual:otherArray.internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:_internalValue.count]; + [self.internalValue enumerateObjectsUsingBlock:^(FSTFieldValue *obj, NSUInteger idx, BOOL *stop) { + [result addObject:[obj value]]; + }]; + return result; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderArray; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTArrayValue class]]) { + NSArray *selfArray = self.internalValue; + NSArray *otherArray = ((FSTArrayValue *)other).internalValue; + NSUInteger minLength = MIN(selfArray.count, otherArray.count); + for (NSUInteger i = 0; i < minLength; i++) { + NSComparisonResult cmp = [selfArray[i] compare:otherArray[i]]; + if (cmp != NSOrderedSame) { + return cmp; + } + } + return WrapCompare(selfArray.count, otherArray.count); + } else { + return [self defaultCompare:other]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTComparison.h b/Firestore/Source/Util/FSTComparison.h deleted file mode 100644 index e6e57e6..0000000 --- a/Firestore/Source/Util/FSTComparison.h +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** Compares two NSStrings. */ -NSComparisonResult FSTCompareStrings(NSString *left, NSString *right); - -/** Compares two BOOLs. */ -NSComparisonResult FSTCompareBools(BOOL left, BOOL right); - -/** Compares two integers. */ -NSComparisonResult FSTCompareInts(int left, int right); - -/** Compares two int32_t. */ -NSComparisonResult FSTCompareInt32s(int32_t left, int32_t right); - -/** Compares two int64_t. */ -NSComparisonResult FSTCompareInt64s(int64_t left, int64_t right); - -/** Compares two NSUIntegers. */ -NSComparisonResult FSTCompareUIntegers(NSUInteger left, NSUInteger right); - -/** Compares two doubles (using Firestore semantics for NaN). */ -NSComparisonResult FSTCompareDoubles(double left, double right); - -/** Compares a double and an int64_t. */ -NSComparisonResult FSTCompareMixed(double doubleValue, int64_t longValue); - -/** Compare two NSData byte sequences. */ -NSComparisonResult FSTCompareBytes(NSData *left, NSData *right); - -/** A simple NSComparator for comparing NSNumber instances. */ -extern const NSComparator FSTNumberComparator; - -/** A simple NSComparator for comparing NSString instances. */ -extern const NSComparator FSTStringComparator; - -/** - * Compares the bitwise representation of two doubles, but normalizes NaN values. This is - * similar to what the backend and android clients do, including comparing -0.0 as not equal to 0.0. - */ -BOOL FSTDoubleBitwiseEquals(double left, double right); - -/** - * Computes a bitwise hash of a double, but normalizes NaN values, suitable for use when using - * FSTDoublesAreBitwiseEqual for equality. - */ -NSUInteger FSTDoubleBitwiseHash(double d); - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTComparison.m b/Firestore/Source/Util/FSTComparison.m deleted file mode 100644 index 9c5c3eb..0000000 --- a/Firestore/Source/Util/FSTComparison.m +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Util/FSTComparison.h" - -NS_ASSUME_NONNULL_BEGIN - -union DoubleBits { - double d; - uint64_t bits; -}; - -const NSComparator FSTNumberComparator = ^NSComparisonResult(NSNumber *left, NSNumber *right) { - return [left compare:right]; -}; - -const NSComparator FSTStringComparator = ^NSComparisonResult(NSString *left, NSString *right) { - return FSTCompareStrings(left, right); -}; - -NSComparisonResult FSTCompareStrings(NSString *left, NSString *right) { - // NOTE: NSLiteralSearch is necessary to compare the raw character codes. By default, - // precomposed characters are considered equivalent to their decomposed equivalents. - return [left compare:right options:NSLiteralSearch]; -} - -NSComparisonResult FSTCompareBools(BOOL left, BOOL right) { - if (!left) { - return right ? NSOrderedAscending : NSOrderedSame; - } else { - return right ? NSOrderedSame : NSOrderedDescending; - } -} - -NSComparisonResult FSTCompareInts(int left, int right) { - if (left > right) { - return NSOrderedDescending; - } - if (right > left) { - return NSOrderedAscending; - } - return NSOrderedSame; -} - -NSComparisonResult FSTCompareInt32s(int32_t left, int32_t right) { - if (left > right) { - return NSOrderedDescending; - } - if (right > left) { - return NSOrderedAscending; - } - return NSOrderedSame; -} - -NSComparisonResult FSTCompareInt64s(int64_t left, int64_t right) { - if (left > right) { - return NSOrderedDescending; - } - if (right > left) { - return NSOrderedAscending; - } - return NSOrderedSame; -} - -NSComparisonResult FSTCompareUIntegers(NSUInteger left, NSUInteger right) { - if (left > right) { - return NSOrderedDescending; - } - if (right > left) { - return NSOrderedAscending; - } - return NSOrderedSame; -} - -NSComparisonResult FSTCompareDoubles(double left, double right) { - // NaN sorts equal to itself and before any other number. - if (left < right) { - return NSOrderedAscending; - } else if (left > right) { - return NSOrderedDescending; - } else if (left == right) { - return NSOrderedSame; - } else { - // One or both left and right is NaN. - if (isnan(left)) { - return isnan(right) ? NSOrderedSame : NSOrderedAscending; - } else { - return NSOrderedDescending; - } - } -} - -static const double LONG_MIN_VALUE_AS_DOUBLE = (double)LLONG_MIN; -static const double LONG_MAX_VALUE_AS_DOUBLE = (double)LLONG_MAX; - -NSComparisonResult FSTCompareMixed(double doubleValue, int64_t longValue) { - // LLONG_MIN has an exact representation as double, so to check for a value outside the range - // representable by long, we have to check for strictly less than LLONG_MIN. Note that this also - // handles negative infinity. - if (doubleValue < LONG_MIN_VALUE_AS_DOUBLE) { - return NSOrderedAscending; - } - - // LLONG_MAX has no exact representation as double (casting as we've done makes 2^63, which is - // larger than LLONG_MAX), so consider any value greater than or equal to the threshold to be out - // of range. This also handles positive infinity. - if (doubleValue >= LONG_MAX_VALUE_AS_DOUBLE) { - return NSOrderedDescending; - } - - // In Firestore NaN is defined to compare before all other numbers. - if (isnan(doubleValue)) { - return NSOrderedAscending; - } - - int64_t doubleAsLong = (int64_t)doubleValue; - NSComparisonResult cmp = FSTCompareInt64s(doubleAsLong, longValue); - if (cmp != NSOrderedSame) { - return cmp; - } - - // At this point the long representations are equal but this could be due to rounding. - double longAsDouble = (double)longValue; - return FSTCompareDoubles(doubleValue, longAsDouble); -} - -NSComparisonResult FSTCompareBytes(NSData *left, NSData *right) { - NSUInteger minLength = MIN(left.length, right.length); - int result = memcmp(left.bytes, right.bytes, minLength); - if (result < 0) { - return NSOrderedAscending; - } else if (result > 0) { - return NSOrderedDescending; - } else if (left.length < right.length) { - return NSOrderedAscending; - } else if (left.length > right.length) { - return NSOrderedDescending; - } else { - return NSOrderedSame; - } -} - -/** Helper to normalize a double and then return the raw bits as a uint64_t. */ -uint64_t FSTDoubleBits(double d) { - if (isnan(d)) { - d = NAN; - } - union DoubleBits converter = {.d = d}; - return converter.bits; -} - -BOOL FSTDoubleBitwiseEquals(double left, double right) { - return FSTDoubleBits(left) == FSTDoubleBits(right); -} - -NSUInteger FSTDoubleBitwiseHash(double d) { - uint64_t bits = FSTDoubleBits(d); - // Note that x ^ (x >> 32) works fine for both 32 and 64 bit definitions of NSUInteger - return (((NSUInteger)bits) ^ (NSUInteger)(bits >> 32)); -} - -NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt index 737173b..09db164 100644 --- a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt @@ -49,6 +49,7 @@ cc_library( string_apple.h DEPENDS FirebaseCore + absl_strings EXCLUDE_FROM_ALL ) @@ -106,6 +107,8 @@ cc_library( SOURCES autoid.cc autoid.h + comparison.cc + comparison.h config.h firebase_assert.h log.h diff --git a/Firestore/core/src/firebase/firestore/util/comparison.cc b/Firestore/core/src/firebase/firestore/util/comparison.cc new file mode 100644 index 0000000..4bef843 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/comparison.cc @@ -0,0 +1,112 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Firestore/core/src/firebase/firestore/util/comparison.h" + +#include + +#include + +namespace firebase { +namespace firestore { +namespace util { + +bool Comparator::operator()( + const absl::string_view& left, const absl::string_view& right) const { + // TODO(wilhuff): truncation aware comparison + return left < right; +} + +bool Comparator::operator()(double left, double right) const { + // NaN sorts equal to itself and before any other number. + if (left < right) { + return true; + } else if (left >= right) { + return false; + } else { + // One or both left and right is NaN. + return isnan(left) && !isnan(right); + } +} + +static constexpr double INT64_MIN_VALUE_AS_DOUBLE = + static_cast(std::numeric_limits::min()); + +static constexpr double INT64_MAX_VALUE_AS_DOUBLE = + static_cast(std::numeric_limits::max()); + +ComparisonResult CompareMixedNumber(double double_value, int64_t int64_value) { + // LLONG_MIN has an exact representation as double, so to check for a value + // outside the range representable by long, we have to check for strictly less + // than LLONG_MIN. Note that this also handles negative infinity. + if (double_value < INT64_MIN_VALUE_AS_DOUBLE) { + return ComparisonResult::Ascending; + } + + // LLONG_MAX has no exact representation as double (casting as we've done + // makes 2^63, which is larger than LLONG_MAX), so consider any value greater + // than or equal to the threshold to be out of range. This also handles + // positive infinity. + if (double_value >= INT64_MAX_VALUE_AS_DOUBLE) { + return ComparisonResult::Descending; + } + + // In Firestore NaN is defined to compare before all other numbers. + if (isnan(double_value)) { + return ComparisonResult::Ascending; + } + + auto double_as_int64 = static_cast(double_value); + ComparisonResult cmp = Compare(double_as_int64, int64_value); + if (cmp != ComparisonResult::Same) { + return cmp; + } + + // At this point the long representations are equal but this could be due to + // rounding. + double int64_as_double = static_cast(int64_value); + return Compare(double_value, int64_as_double); +} + +/** Helper to normalize a double and then return the raw bits as a uint64_t. */ +uint64_t DoubleBits(double d) { + if (isnan(d)) { + d = NAN; + } + + // Unlike C, C++ does not define type punning through a union type. + + // TODO(wilhuff): replace with absl::bit_cast + static_assert(sizeof(double) == sizeof(uint64_t), "doubles must be 8 bytes"); + uint64_t bits; + memcpy(&bits, &d, sizeof(bits)); + return bits; +} + +bool DoubleBitwiseEquals(double left, double right) { + return DoubleBits(left) == DoubleBits(right); +} + +size_t DoubleBitwiseHash(double d) { + uint64_t bits = DoubleBits(d); + // Note that x ^ (x >> 32) works fine for both 32 and 64 bit definitions of + // size_t + return static_cast(bits) ^ static_cast(bits >> 32); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/comparison.h b/Firestore/core/src/firebase/firestore/util/comparison.h new file mode 100644 index 0000000..6fd1e2b --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/comparison.h @@ -0,0 +1,181 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_COMPARISON_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_COMPARISON_H_ + +#if __OBJC__ +#import +#endif + +#include +#include + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "absl/strings/string_view.h" + +namespace firebase { +namespace firestore { +namespace util { + +/** + * An enumeration describing the result of a three-way comparison among + * strongly-ordered values (i.e. where comparison between values always yields + * less-than, equal-to, or greater-than). + * + * This is equivalent to: + * + * * NSComparisonResult from the iOS/macOS Foundation framework. + * * std::strong_ordering from C++20 + * + * The values of the constants are specifically chosen so as to make casting + * between this type and NSComparisonResult possible. + */ +enum class ComparisonResult { + /** The left hand side was less than the right. */ + Ascending = -1, + + /** The left hand side was equal to the right. */ + Same = 0, + + /** The left hand side was greater than the right. */ + Descending = 1 +}; + +/** + * Returns the reverse order (i.e. Ascending => Descending) etc. + */ +constexpr ComparisonResult ReverseOrder(ComparisonResult result) { + return static_cast(-static_cast(result)); +} + +/** + * A generalized comparator for types in Firestore, with ordering defined + * according to Firestore's semantics. This is useful as argument to e.g. + * std::sort. + * + * Comparators are only defined for the limited set of types for which + * Firestore defines an ordering. + */ +template +struct Comparator { + // By default comparison is not defined +}; + +/** Compares two strings. */ +template <> +struct Comparator { + bool operator()(const absl::string_view& left, + const absl::string_view& right) const; +}; + +/** Compares two bools: false < true. */ +template <> +struct Comparator : public std::less {}; + +/** Compares two int32_t. */ +template <> +struct Comparator : public std::less {}; + +/** Compares two int64_t. */ +template <> +struct Comparator : public std::less {}; + +/** Compares two doubles (using Firestore semantics for NaN). */ +template <> +struct Comparator { + bool operator()(double left, double right) const; +}; + +/** Compare two byte sequences. */ +// TODO(wilhuff): perhaps absl::Span would be better? +template <> +struct Comparator> + : public std::less> {}; + +/** + * Perform a three-way comparison between the left and right values using + * the appropriate Comparator for the values based on their type. + */ +template +ComparisonResult Compare(const T& left, const T& right) { + Comparator less_than; + if (less_than(left, right)) { + return ComparisonResult::Ascending; + } else if (less_than(right, left)) { + return ComparisonResult::Descending; + } else { + return ComparisonResult::Same; + } +} + +#if __OBJC__ +/** + * Returns true if the given ComparisonResult and NSComparisonResult have the + * same integer values (at compile time). + */ +constexpr bool EqualValue(ComparisonResult lhs, NSComparisonResult rhs) { + return static_cast(lhs) == static_cast(rhs); +} + +/** + * Performs a three-way comparison, identically to Compare, but converts the + * result to an NSComparisonResult. + * + * This function exists for interoperation with Objective-C++ and should + * eventually be removed. + */ +template +inline NSComparisonResult WrapCompare(const T& left, const T& right) { + static_assert(EqualValue(ComparisonResult::Ascending, NSOrderedAscending), + "Ascending invalid"); + static_assert(EqualValue(ComparisonResult::Same, NSOrderedSame), + "Same invalid"); + static_assert(EqualValue(ComparisonResult::Descending, NSOrderedDescending), + "Descending invalid"); + + return static_cast(Compare(left, right)); +} +#endif + +/** Compares a double and an int64_t. */ +ComparisonResult CompareMixedNumber(double doubleValue, int64_t longValue); + +/** Normalizes a double and then return the raw bits as a uint64_t. */ +uint64_t DoubleBits(double d); + +/** + * Compares the bitwise representation of two doubles, but normalizes NaN + * values. This is similar to what the backend and android clients do, including + * comparing -0.0 as not equal to 0.0. + */ +bool DoubleBitwiseEquals(double left, double right); + +/** + * Computes a bitwise hash of a double, but normalizes NaN values, suitable for + * use when using FSTDoublesAreBitwiseEqual for equality. + */ +size_t DoubleBitwiseHash(double d); + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_COMPARISON_H_ diff --git a/Firestore/core/src/firebase/firestore/util/string_apple.h b/Firestore/core/src/firebase/firestore/util/string_apple.h index e1be8c3..108ade7 100644 --- a/Firestore/core/src/firebase/firestore/util/string_apple.h +++ b/Firestore/core/src/firebase/firestore/util/string_apple.h @@ -17,8 +17,13 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STRING_APPLE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STRING_APPLE_H_ +// Everything in this header exists for compatibility with Objective-C. +#if __OBJC__ + #import +#include "absl/strings/string_view.h" + namespace firebase { namespace firestore { namespace util { @@ -32,8 +37,16 @@ inline NSString* WrapNSStringNoCopy(const char* c_str) { freeWhenDone:NO]; } +// Creates an absl::string_view wrapper for the contents of the given NSString. +inline absl::string_view MakeStringView(NSString* str) { + return absl::string_view( + [str UTF8String], [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); +} + } // namespace util } // namespace firestore } // namespace firebase +#endif // __OBJC__ + #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STRING_APPLE_H_ diff --git a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt index 468c62e..5e7612c 100644 --- a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt @@ -36,6 +36,7 @@ cc_test( firebase_firestore_util_test SOURCES autoid_test.cc + comparison_test.cc string_printf_test.cc DEPENDS firebase_firestore_util diff --git a/Firestore/core/test/firebase/firestore/util/comparison_test.cc b/Firestore/core/test/firebase/firestore/util/comparison_test.cc new file mode 100644 index 0000000..ecbed4a --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/comparison_test.cc @@ -0,0 +1,210 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Firestore/core/src/firebase/firestore/util/comparison.h" + +#include + +#include + +#include "Firestore/core/src/firebase/firestore/util/string_printf.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +#define ASSERT_SAME(comparison) \ + do { \ + ASSERT_EQ(ComparisonResult::Same, comparison); \ + } while (0) + +#define ASSERT_ASCENDING(comparison) \ + do { \ + ASSERT_EQ(ComparisonResult::Ascending, comparison); \ + } while (0) + +#define ASSERT_DESCENDING(comparison) \ + do { \ + ASSERT_EQ(ComparisonResult::Descending, comparison); \ + } while (0) + +TEST(Comparison, ReverseOrder) { + ASSERT_ASCENDING(ReverseOrder(ComparisonResult::Descending)); + ASSERT_DESCENDING(ReverseOrder(ComparisonResult::Ascending)); + ASSERT_SAME(ReverseOrder(ComparisonResult::Same)); +} + +TEST(Comparison, StringCompare) { + ASSERT_ASCENDING(Compare("", "a")); + ASSERT_ASCENDING(Compare("a", "b")); + ASSERT_ASCENDING(Compare("a", "aa")); + + ASSERT_DESCENDING(Compare("a", "")); + ASSERT_DESCENDING(Compare("b", "a")); + ASSERT_DESCENDING(Compare("aa", "a")); + + ASSERT_SAME(Compare("", "")); + ASSERT_SAME(Compare("", std::string())); + ASSERT_SAME(Compare("a", "a")); +} + +TEST(Comparison, BooleanCompare) { + ASSERT_SAME(Compare(false, false)); + ASSERT_SAME(Compare(true, true)); + ASSERT_ASCENDING(Compare(false, true)); + ASSERT_DESCENDING(Compare(true, false)); +} + +TEST(Comparison, DoubleCompare) { + ASSERT_SAME(Compare(NAN, NAN)); + ASSERT_ASCENDING(Compare(NAN, 0)); + ASSERT_DESCENDING(Compare(0, NAN)); + + ASSERT_SAME(Compare(-INFINITY, -INFINITY)); + ASSERT_SAME(Compare(INFINITY, INFINITY)); + ASSERT_ASCENDING(Compare(-INFINITY, INFINITY)); + ASSERT_DESCENDING(Compare(INFINITY, -INFINITY)); + + ASSERT_SAME(Compare(0, 0)); + ASSERT_SAME(Compare(-0, -0)); + ASSERT_SAME(Compare(-0, 0)); +} + +#define ASSERT_BIT_EQUALS(expected, actual) \ + do { \ + uint64_t expectedBits = DoubleBits(expected); \ + uint64_t actualBits = DoubleBits(actual); \ + if (expectedBits != actualBits) { \ + std::string message = StringPrintf( \ + "Expected <%f> to compare equal to <%f> " \ + "with bits <%llX> equal to <%llX>", \ + actual, expected, actualBits, expectedBits); \ + FAIL() << message; \ + } \ + } while (0); + +#define ASSERT_MIXED_SAME(doubleValue, longValue) \ + do { \ + ComparisonResult result = CompareMixedNumber(doubleValue, longValue); \ + if (result != ComparisonResult::Same) { \ + std::string message = StringPrintf( \ + "Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + FAIL() << message; \ + } \ + } while (0); + +#define ASSERT_MIXED_DESCENDING(doubleValue, longValue) \ + do { \ + ComparisonResult result = CompareMixedNumber(doubleValue, longValue); \ + if (result != ComparisonResult::Descending) { \ + std::string message = StringPrintf( \ + "Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + FAIL() << message; \ + } \ + } while (0); + +#define ASSERT_MIXED_ASCENDING(doubleValue, longValue) \ + do { \ + ComparisonResult result = CompareMixedNumber(doubleValue, longValue); \ + if (result != ComparisonResult::Ascending) { \ + std::string message = StringPrintf( \ + "Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + FAIL() << message; \ + } \ + } while (0); + +TEST(Comparison, MixedNumberCompare) { + // Infinities + ASSERT_MIXED_ASCENDING(-INFINITY, LLONG_MIN); + ASSERT_MIXED_ASCENDING(-INFINITY, LLONG_MAX); + ASSERT_MIXED_ASCENDING(-INFINITY, 0LL); + + ASSERT_MIXED_DESCENDING(INFINITY, LLONG_MIN); + ASSERT_MIXED_DESCENDING(INFINITY, LLONG_MAX); + ASSERT_MIXED_DESCENDING(INFINITY, 0LL); + + // NaN + ASSERT_MIXED_ASCENDING(NAN, LLONG_MIN); + ASSERT_MIXED_ASCENDING(NAN, LLONG_MAX); + ASSERT_MIXED_ASCENDING(NAN, 0LL); + + // Large values (note DBL_MIN is positive and near zero). + ASSERT_MIXED_ASCENDING(-DBL_MAX, LLONG_MIN); + + // Tests around LLONG_MIN + ASSERT_BIT_EQUALS((double)LLONG_MIN, -0x1.0p63); + ASSERT_MIXED_SAME(-0x1.0p63, LLONG_MIN); + ASSERT_MIXED_ASCENDING(-0x1.0p63, LLONG_MIN + 1); + + ASSERT_LT(-0x1.0000000000001p63, -0x1.0p63); + ASSERT_MIXED_ASCENDING(-0x1.0000000000001p63, LLONG_MIN); + ASSERT_MIXED_DESCENDING(-0x1.FFFFFFFFFFFFFp62, LLONG_MIN); + + // Tests around LLONG_MAX + // Note LLONG_MAX cannot be exactly represented by a double, so the system + // rounds it to the nearest double, which is 2^63. This number, in turn is + // larger than the maximum representable as a long. + ASSERT_BIT_EQUALS(0x1.0p63, (double)LLONG_MAX); + ASSERT_MIXED_DESCENDING(0x1.0p63, LLONG_MAX); + + // The largest value with an exactly long representation + ASSERT_EQ((int64_t)0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL); + ASSERT_MIXED_SAME(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL); + + ASSERT_MIXED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFB00LL); + ASSERT_MIXED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFBFFLL); + ASSERT_MIXED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC01LL); + ASSERT_MIXED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFD00LL); + + ASSERT_MIXED_ASCENDING(0x1.FFFFFFFFFFFFEp62, 0x7FFFFFFFFFFFFC00LL); + + // Tests around MAX_SAFE_INTEGER + ASSERT_MIXED_SAME(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFFLL); + ASSERT_MIXED_DESCENDING(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFELL); + ASSERT_MIXED_ASCENDING(0x1.FFFFFFFFFFFFEp52, 0x1FFFFFFFFFFFFFLL); + ASSERT_MIXED_ASCENDING(0x1.FFFFFFFFFFFFFp52, 0x20000000000000LL); + + // Tests around MIN_SAFE_INTEGER + ASSERT_MIXED_SAME(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFFLL); + ASSERT_MIXED_ASCENDING(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFELL); + ASSERT_MIXED_DESCENDING(-0x1.FFFFFFFFFFFFEp52, -0x1FFFFFFFFFFFFFLL); + ASSERT_MIXED_DESCENDING(-0x1.FFFFFFFFFFFFFp52, -0x20000000000000LL); + + // Tests around zero. + ASSERT_MIXED_SAME(-0.0, 0LL); + ASSERT_MIXED_SAME(0.0, 0LL); + + // The smallest representable positive value should be greater than zero + ASSERT_MIXED_DESCENDING(DBL_MIN, 0LL); + ASSERT_MIXED_ASCENDING(-DBL_MIN, 0LL); + + // Note that 0x1.0p-1074 is a hex floating point literal representing the + // minimum subnormal number: . + double minSubNormal = 0x1.0p-1074; + ASSERT_MIXED_DESCENDING(minSubNormal, 0LL); + ASSERT_MIXED_ASCENDING(-minSubNormal, 0LL); + + // Other sanity checks + ASSERT_MIXED_ASCENDING(0.5, 1LL); + ASSERT_MIXED_DESCENDING(0.5, 0LL); + ASSERT_MIXED_ASCENDING(1.5, 2LL); + ASSERT_MIXED_DESCENDING(1.5, 1LL); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/third_party/abseil-cpp/absl/base/macros.h b/Firestore/third_party/abseil-cpp/absl/base/macros.h index d414087..07c5cf7 100644 --- a/Firestore/third_party/abseil-cpp/absl/base/macros.h +++ b/Firestore/third_party/abseil-cpp/absl/base/macros.h @@ -196,7 +196,7 @@ enum LinkerInitialized { #define ABSL_ASSERT(expr) (false ? (void)(expr) : (void)0) #else #define ABSL_ASSERT(expr) \ - (ABSL_PREDICT_TRUE((expr)) ? (void)0 : [] { assert(false && #expr); }()) + (void) (ABSL_PREDICT_TRUE((expr)) ? (void)0 : [] { assert(false && #expr); }()) #endif #endif // ABSL_BASE_MACROS_H_ -- cgit v1.2.3