diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index c7b745d3f56..535598246d8 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,5 +1,11 @@ # Unreleased +# v0.12.6 +- [fixed] Fixed an issue where queries returned fewer results than they should, + caused by documents that were cached as deleted when they should not have + been (#1548). Some cache data is cleared and so clients may use extra + bandwidth the first time they launch with this version of the SDK. + # v0.12.5 - [changed] Internal improvements. diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm index e6ab720a523..4f8c575ee80 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm @@ -18,22 +18,29 @@ #include #include +#include #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" #import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLevelDBKey.h" #import "Firestore/Source/Local/FSTLevelDBMigrations.h" +#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h" #import "Firestore/Source/Local/FSTLevelDBQueryCache.h" #include "Firestore/core/src/firebase/firestore/util/ordered_code.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +#include "absl/strings/match.h" #include "leveldb/db.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" NS_ASSUME_NONNULL_BEGIN +using firebase::firestore::FirestoreErrorCode; using firebase::firestore::local::LevelDbTransaction; using firebase::firestore::util::OrderedCode; +using firebase::firestore::testutil::Key; using leveldb::DB; using leveldb::Options; using leveldb::Status; @@ -64,54 +71,136 @@ - (void)tearDown { - (void)testAddsTargetGlobal { FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()]; XCTAssertNil(metadata, @"Not expecting metadata yet, we should have an empty db"); - LevelDbTransaction transaction(_db.get(), "testAddsTargetGlobal"); - [FSTLevelDBMigrations runMigrationsWithTransaction:&transaction]; - transaction.Commit(); + [FSTLevelDBMigrations runMigrationsWithDatabase:_db.get()]; + metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()]; XCTAssertNotNil(metadata, @"Migrations should have added the metadata"); } - (void)testSetsVersionNumber { - LevelDbTransaction transaction(_db.get(), "testSetsVersionNumber"); - FSTLevelDBSchemaVersion initial = - [FSTLevelDBMigrations schemaVersionWithTransaction:&transaction]; - XCTAssertEqual(0, initial, "No version should be equivalent to 0"); - - // Pick an arbitrary high migration number and migrate to it. - [FSTLevelDBMigrations runMigrationsWithTransaction:&transaction]; - FSTLevelDBSchemaVersion actual = [FSTLevelDBMigrations schemaVersionWithTransaction:&transaction]; - XCTAssertGreaterThan(actual, 0, @"Expected to migrate to a schema version > 0"); + { + LevelDbTransaction transaction(_db.get(), "testSetsVersionNumber before"); + FSTLevelDBSchemaVersion initial = + [FSTLevelDBMigrations schemaVersionWithTransaction:&transaction]; + XCTAssertEqual(0, initial, "No version should be equivalent to 0"); + } + + { + // Pick an arbitrary high migration number and migrate to it. + [FSTLevelDBMigrations runMigrationsWithDatabase:_db.get()]; + + LevelDbTransaction transaction(_db.get(), "testSetsVersionNumber after"); + FSTLevelDBSchemaVersion actual = + [FSTLevelDBMigrations schemaVersionWithTransaction:&transaction]; + XCTAssertGreaterThan(actual, 0, @"Expected to migrate to a schema version > 0"); + } } -- (void)testCountsQueries { - NSUInteger expected = 50; +#define ASSERT_NOT_FOUND(transaction, key) \ + do { \ + std::string unused_result; \ + Status status = transaction.Get(key, &unused_result); \ + XCTAssertTrue(status.IsNotFound()); \ + } while (0) + +#define ASSERT_FOUND(transaction, key) \ + do { \ + std::string unused_result; \ + Status status = transaction.Get(key, &unused_result); \ + XCTAssertTrue(status.ok()); \ + } while (0) + +- (void)testDropsTheQueryCache { + NSString *userID = @"user"; + FSTBatchID batchID = 1; + FSTTargetID targetID = 2; + + FSTDocumentKey *key1 = Key("documents/1"); + FSTDocumentKey *key2 = Key("documents/2"); + + std::string targetKeys[] = { + [FSTLevelDBTargetKey keyWithTargetID:targetID], + [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key1], + [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key2], + [FSTLevelDBDocumentTargetKey keyWithDocumentKey:key1 targetID:targetID], + [FSTLevelDBDocumentTargetKey keyWithDocumentKey:key2 targetID:targetID], + [FSTLevelDBQueryTargetKey keyWithCanonicalID:"foo.bar.baz" targetID:targetID], + }; + + // Keys that should not be modified by the dropping the query cache + std::string preservedKeys[] = { + [self dummyKeyForTable:"targetA"], + [FSTLevelDBMutationQueueKey keyWithUserID:userID], + [FSTLevelDBMutationKey keyWithUserID:userID batchID:batchID], + }; + + [FSTLevelDBMigrations runMigrationsWithDatabase:_db.get() upToVersion:2]; { // Setup some targets to be counted in the migration. - LevelDbTransaction transaction(_db.get(), "testCountsQueries setup"); - for (int i = 0; i < expected; i++) { - std::string key = [FSTLevelDBTargetKey keyWithTargetID:i]; - transaction.Put(key, "dummy"); + LevelDbTransaction transaction(_db.get(), "testDropsTheQueryCache setup"); + for (const std::string &key : targetKeys) { + transaction.Put(key, "target"); + } + for (const std::string &key : preservedKeys) { + transaction.Put(key, "preserved"); } - // Add a dummy entry after the targets to make sure the iteration is correctly bounded. - // Use a table that would sort logically right after that table 'target'. - std::string dummyKey; - // Magic number that indicates a table name follows. Needed to mimic the prefix to the target - // table. - OrderedCode::WriteSignedNumIncreasing(&dummyKey, 5); - OrderedCode::WriteString(&dummyKey, "targetA"); - transaction.Put(dummyKey, "dummy"); transaction.Commit(); } + [FSTLevelDBMigrations runMigrationsWithDatabase:_db.get() upToVersion:3]; { - LevelDbTransaction transaction(_db.get(), "testCountsQueries"); - [FSTLevelDBMigrations runMigrationsWithTransaction:&transaction]; - transaction.Commit(); + LevelDbTransaction transaction(_db.get(), "testDropsTheQueryCache"); + for (const std::string &key : targetKeys) { + ASSERT_NOT_FOUND(transaction, key); + } + for (const std::string &key : preservedKeys) { + ASSERT_FOUND(transaction, key); + } + FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()]; - XCTAssertEqual(expected, metadata.targetCount, @"Failed to count all of the targets we added"); + XCTAssertNotNil(metadata, @"Metadata should have been added"); + XCTAssertEqual(metadata.targetCount, 0); } } +- (void)testDropsTheQueryCacheWithThousandsOfEntries { + [FSTLevelDBMigrations runMigrationsWithDatabase:_db.get() upToVersion:2]; + { + // Setup some targets to be destroyed. + LevelDbTransaction transaction(_db.get(), "testDropsTheQueryCacheWithThousandsOfEntries setup"); + for (int i = 0; i < 10000; ++i) { + transaction.Put([FSTLevelDBTargetKey keyWithTargetID:i], ""); + } + transaction.Commit(); + } + + [FSTLevelDBMigrations runMigrationsWithDatabase:_db.get() upToVersion:3]; + { + LevelDbTransaction transaction(_db.get(), "Verify"); + std::string prefix = [FSTLevelDBTargetKey keyPrefix]; + + auto it = transaction.NewIterator(); + std::vector found_keys; + for (it->Seek(prefix); it->Valid() && absl::StartsWith(it->key(), prefix); it->Next()) { + found_keys.push_back(std::string{it->key()}); + } + + XCTAssertEqual(found_keys, std::vector{}); + } +} + +/** + * Creates the name of a dummy entry to make sure the iteration is correctly bounded. + */ +- (std::string)dummyKeyForTable:(const char *)tableName { + std::string dummyKey; + // Magic number that indicates a table name follows. Needed to mimic the prefix to the target + // table. + OrderedCode::WriteSignedNumIncreasing(&dummyKey, 5); + OrderedCode::WriteString(&dummyKey, tableName); + return dummyKey; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm index 63c3d7225ab..e27fc65a80d 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -138,7 +138,7 @@ - (void)failStreamWithError:(NSError *)error { #pragma mark - Helper methods. -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(const SnapshotVersion &)snap { +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(SnapshotVersion)snap { if ([change isKindOfClass:[FSTWatchTargetChange class]]) { FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change; if (targetChange.cause) { @@ -152,6 +152,11 @@ - (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(const Snapsho [self.activeTargets removeObjectForKey:targetID]; } } + if ([targetChange.targetIDs count] != 0) { + // If the list of target IDs is not empty, we reset the snapshot version to NONE as + // done in `FSTSerializerBeta.versionFromListenResponse:`. + snap = SnapshotVersion::None(); + } } [self.delegate watchStreamDidChange:change snapshotVersion:snap]; } diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index c131f7e4152..7fe6434145e 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -194,25 +194,25 @@ - (void)doDelete:(NSString *)key { [self.driver writeUserMutation:FSTTestDeleteMutation(key)]; } -- (void)doWatchAck:(NSArray *)ackedTargets snapshot:(NSNumber *)watchSnapshot { +- (void)doWatchAck:(NSArray *)ackedTargets { FSTWatchTargetChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded targetIDs:ackedTargets cause:nil]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; } -- (void)doWatchCurrent:(NSArray *)currentSpec snapshot:(NSNumber *)watchSnapshot { +- (void)doWatchCurrent:(NSArray *)currentSpec { NSArray *currentTargets = currentSpec[0]; NSData *resumeToken = [currentSpec[1] dataUsingEncoding:NSUTF8StringEncoding]; FSTWatchTargetChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:currentTargets resumeToken:resumeToken]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; } -- (void)doWatchRemove:(NSDictionary *)watchRemoveSpec snapshot:(NSNumber *)watchSnapshot { +- (void)doWatchRemove:(NSDictionary *)watchRemoveSpec { NSError *error = nil; NSDictionary *cause = watchRemoveSpec[@"cause"]; if (cause) { @@ -226,19 +226,16 @@ - (void)doWatchRemove:(NSDictionary *)watchRemoveSpec snapshot:(NSNumber *)watch [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved targetIDs:watchRemoveSpec[@"targetIds"] cause:error]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; // Unlike web, the FSTMockDatastore detects a watch removal with cause and will remove active // targets } -- (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable)watchSnapshot { +- (void)doWatchEntity:(NSDictionary *)watchEntity { if (watchEntity[@"docs"]) { HARD_ASSERT(!watchEntity[@"doc"], "Exactly one of |doc| or |docs| needs to be set."); - int count = 0; NSArray *docs = watchEntity[@"docs"]; for (NSDictionary *doc in docs) { - count++; - bool isLast = (count == docs.count); NSMutableDictionary *watchSpec = [NSMutableDictionary dictionary]; watchSpec[@"doc"] = doc; if (watchEntity[@"targets"]) { @@ -247,11 +244,7 @@ - (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable) if (watchEntity[@"removedTargets"]) { watchSpec[@"removedTargets"] = watchEntity[@"removedTargets"]; } - NSNumber *_Nullable version = nil; - if (isLast) { - version = watchSnapshot; - } - [self doWatchEntity:watchSpec snapshot:version]; + [self doWatchEntity:watchSpec]; } } else if (watchEntity[@"doc"]) { NSArray *docSpec = watchEntity[@"doc"]; @@ -270,7 +263,7 @@ - (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable) removedTargetIDs:watchEntity[@"removedTargets"] documentKey:doc.key document:doc]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; } else if (watchEntity[@"key"]) { FSTDocumentKey *docKey = FSTTestDocKey(watchEntity[@"key"]); FSTWatchChange *change = @@ -278,13 +271,13 @@ - (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable) removedTargetIDs:watchEntity[@"removedTargets"] documentKey:docKey document:nil]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; } else { HARD_FAIL("Either key, doc or docs must be set."); } } -- (void)doWatchFilter:(NSArray *)watchFilter snapshot:(NSNumber *_Nullable)watchSnapshot { +- (void)doWatchFilter:(NSArray *)watchFilter { NSArray *targets = watchFilter[0]; HARD_ASSERT(targets.count == 1, "ExistenceFilters currently support exactly one target only."); @@ -294,15 +287,29 @@ - (void)doWatchFilter:(NSArray *)watchFilter snapshot:(NSNumber *_Nullable)watch FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:keyCount]; FSTExistenceFilterWatchChange *change = [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:targets[0].intValue]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; } -- (void)doWatchReset:(NSArray *)watchReset snapshot:(NSNumber *_Nullable)watchSnapshot { +- (void)doWatchReset:(NSArray *)watchReset { FSTWatchTargetChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset targetIDs:watchReset cause:nil]; - [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; +} + +- (void)doWatchSnapshot:(NSDictionary *)watchSnapshot { + // The client will only respond to watchSnapshots if they are on a target change with an empty + // set of target IDs. + NSArray *targetIDs = + watchSnapshot[@"targetIds"] ? watchSnapshot[@"targetIds"] : [NSArray array]; + NSData *resumeToken = [watchSnapshot[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange + targetIDs:targetIDs + resumeToken:resumeToken]; + [self.driver receiveWatchChange:change + snapshotVersion:[self parseVersion:watchSnapshot[@"version"]]]; } - (void)doWatchStreamClose:(NSDictionary *)closeSpec { @@ -415,17 +422,19 @@ - (void)doStep:(NSDictionary *)step { } else if (step[@"userDelete"]) { [self doDelete:step[@"userDelete"]]; } else if (step[@"watchAck"]) { - [self doWatchAck:step[@"watchAck"] snapshot:step[@"watchSnapshot"]]; + [self doWatchAck:step[@"watchAck"]]; } else if (step[@"watchCurrent"]) { - [self doWatchCurrent:step[@"watchCurrent"] snapshot:step[@"watchSnapshot"]]; + [self doWatchCurrent:step[@"watchCurrent"]]; } else if (step[@"watchRemove"]) { - [self doWatchRemove:step[@"watchRemove"] snapshot:step[@"watchSnapshot"]]; + [self doWatchRemove:step[@"watchRemove"]]; } else if (step[@"watchEntity"]) { - [self doWatchEntity:step[@"watchEntity"] snapshot:step[@"watchSnapshot"]]; + [self doWatchEntity:step[@"watchEntity"]]; } else if (step[@"watchFilter"]) { - [self doWatchFilter:step[@"watchFilter"] snapshot:step[@"watchSnapshot"]]; + [self doWatchFilter:step[@"watchFilter"]]; } else if (step[@"watchReset"]) { - [self doWatchReset:step[@"watchReset"] snapshot:step[@"watchSnapshot"]]; + [self doWatchReset:step[@"watchReset"]]; + } else if (step[@"watchSnapshot"]) { + [self doWatchSnapshot:step[@"watchSnapshot"]]; } else if (step[@"watchStreamClose"]) { [self doWatchStreamClose:step[@"watchStreamClose"]]; } else if (step[@"watchProto"]) { diff --git a/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json index ef41afe0ff4..3b177734e95 100644 --- a/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json @@ -56,8 +56,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json index 3e5d4fb264e..d7a617577b2 100644 --- a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json @@ -56,8 +56,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -86,8 +90,12 @@ 2 ], "collection/1" - ], - "watchSnapshot": 2000 + ] + }, + { + "watchSnapshot": { + "version": 2000 + } } ] }, @@ -132,8 +140,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -169,8 +181,12 @@ 2 ], "collection/1" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -236,8 +252,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -257,8 +277,12 @@ 2 ], "collection/1" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -331,8 +355,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -436,8 +464,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -517,8 +549,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -554,8 +590,12 @@ 2 ], "collection/1" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -615,8 +655,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [ "collection/2" @@ -652,8 +696,12 @@ 1 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -755,8 +803,12 @@ 2 ], "existence-filter-resume-token" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -818,8 +870,12 @@ 2 ], "collection/1" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -879,8 +935,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [ "collection/2" @@ -916,8 +976,12 @@ 1 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -1012,8 +1076,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -1059,8 +1127,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -1143,8 +1215,12 @@ 2 ], "resume-token-3000" - ], - "watchSnapshot": 3000, + ] + }, + { + "watchSnapshot": { + "version": 3000 + }, "expect": [ { "query": { @@ -1226,8 +1302,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -1255,8 +1335,12 @@ [ 2 ] - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -1345,8 +1429,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -1382,8 +1470,12 @@ 2 ], "collection/1" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -1443,8 +1535,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [ "collection/2" @@ -1491,7 +1587,6 @@ }, "limboDocs": [] }, - "watchSnapshot": 3000, "expect": [ { "query": { diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json index a186496f029..29b2b981cc3 100644 --- a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json @@ -56,8 +56,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -91,8 +95,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "stateExpect": { "limboDocs": [ "collection/a" @@ -140,8 +148,12 @@ 1 ], "resume-token-2" - ], - "watchSnapshot": 1002, + ] + }, + { + "watchSnapshot": { + "version": 1002 + }, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -236,8 +248,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -271,8 +287,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "stateExpect": { "limboDocs": [ "collection/a" @@ -327,8 +347,12 @@ 1 ], "resume-token-1002" - ], - "watchSnapshot": 1002, + ] + }, + { + "watchSnapshot": { + "version": 1002 + }, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -435,8 +459,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -476,8 +504,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "stateExpect": { "limboDocs": [ "collection/a" @@ -553,8 +585,12 @@ 1 ], "resume-token-1002" - ], - "watchSnapshot": 1002, + ] + }, + { + "watchSnapshot": { + "version": 1002 + }, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -673,8 +709,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -762,8 +802,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "stateExpect": { "limboDocs": [ "collection/a" @@ -859,8 +903,12 @@ 4 ], "resume-token-1002" - ], - "watchSnapshot": 1002, + ] + }, + { + "watchSnapshot": { + "version": 1002 + }, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -953,8 +1001,12 @@ 1 ], "resume-token-1003" - ], - "watchSnapshot": 1003 + ] + }, + { + "watchSnapshot": { + "version": 1003 + } } ] }, @@ -1022,8 +1074,12 @@ 2 ], "resume-token-1002" - ], - "watchSnapshot": 1002, + ] + }, + { + "watchSnapshot": { + "version": 1002 + }, "expect": [ { "query": { @@ -1059,8 +1115,12 @@ "removedTargets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 1003 }, - "watchSnapshot": 1003, "stateExpect": { "limboDocs": [ "collection/b" @@ -1108,8 +1168,12 @@ 1 ], "resume-token-1004" - ], - "watchSnapshot": 1004, + ] + }, + { + "watchSnapshot": { + "version": 1004 + }, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -1147,9 +1211,9 @@ } ] }, - "Limbo documents handle receiving ack and then current": { + "Limbo resolution handles snapshot before CURRENT": { "describeName": "Limbo Documents:", - "itName": "Limbo documents handle receiving ack and then current", + "itName": "Limbo resolution handles snapshot before CURRENT", "tags": [], "config": { "useGarbageCollection": false @@ -1213,8 +1277,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -1352,8 +1420,12 @@ 4 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -1473,14 +1545,23 @@ ] } }, + { + "watchSnapshot": { + "version": 2000 + } + }, { "watchCurrent": [ [ 1 ], "resume-token-3000" - ], - "watchSnapshot": 3000 + ] + }, + { + "watchSnapshot": { + "version": 3000 + } }, { "watchEntity": { @@ -1514,8 +1595,12 @@ "targets": [ 4 ] + } + }, + { + "watchSnapshot": { + "version": 4000 }, - "watchSnapshot": 4000, "expect": [ { "query": { @@ -1557,5 +1642,413 @@ } } ] + }, + "Limbo resolution handles snapshot before CURRENT [no document update]": { + "describeName": "Limbo Documents:", + "itName": "Limbo resolution handles snapshot before CURRENT [no document update]", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ], + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ], + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-2000" + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userPatch": [ + "collection/a", + { + "include": false + } + ], + "expect": [ + { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "removed": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "limboDocs": [ + "collection/b" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/b", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchSnapshot": { + "version": 2000 + } + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-3000" + ] + }, + { + "watchSnapshot": { + "version": 3000 + }, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "4": { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "removed": [ + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ] + ], + "removedTargets": [ + 4 + ] + } + }, + { + "watchSnapshot": { + "version": 4000 + } + } + ] } } diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json index 6aa1daa0029..890744ebac6 100644 --- a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json @@ -65,8 +65,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { @@ -127,8 +131,12 @@ "removedTargets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 1002 }, - "watchSnapshot": 1002, "expect": [ { "query": { @@ -229,8 +237,12 @@ 2 ], "resume-token-1002" - ], - "watchSnapshot": 1002, + ] + }, + { + "watchSnapshot": { + "version": 1002 + }, "expect": [ { "query": { @@ -295,8 +307,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [ "collection/a" @@ -346,8 +362,12 @@ 1 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -462,8 +482,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { @@ -516,8 +540,12 @@ "removedTargets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 1002 }, - "watchSnapshot": 1002, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -701,8 +729,12 @@ 4 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { @@ -783,8 +815,12 @@ "removedTargets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 1002 }, - "watchSnapshot": 1002, "stateExpect": { "limboDocs": [], "activeTargets": { @@ -928,8 +964,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { @@ -1081,8 +1121,12 @@ 4 ], "resume-token-1005" - ], - "watchSnapshot": 1005, + ] + }, + { + "watchSnapshot": { + "version": 1005 + }, "expect": [ { "query": { @@ -1160,8 +1204,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [ "collection/a", @@ -1228,8 +1276,12 @@ 1 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "stateExpect": { "limboDocs": [ "collection/b", @@ -1340,8 +1392,12 @@ 3 ], "resume-token-2001" - ], - "watchSnapshot": 2001, + ] + }, + { + "watchSnapshot": { + "version": 2001 + }, "stateExpect": { "limboDocs": [ "collection/c", @@ -1452,8 +1508,12 @@ 5 ], "resume-token-2002" - ], - "watchSnapshot": 2002, + ] + }, + { + "watchSnapshot": { + "version": 2002 + }, "stateExpect": { "limboDocs": [ "collection/d" @@ -1555,8 +1615,12 @@ 7 ], "resume-token-2003" - ], - "watchSnapshot": 2003, + ] + }, + { + "watchSnapshot": { + "version": 2003 + }, "stateExpect": { "limboDocs": [], "activeTargets": { diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json index e838d2fda72..427cd76b76e 100644 --- a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json @@ -2,7 +2,10 @@ "Contents of query are cleared when listen is removed.": { "describeName": "Listens:", "itName": "Contents of query are cleared when listen is removed.", - "tags": [], + "tags": [ + "no-lru" + ], + "comment": "Explicitly tests eager GC behavior", "config": { "useGarbageCollection": true }, @@ -56,8 +59,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -174,8 +181,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -212,8 +223,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -373,8 +388,12 @@ { "watchAck": [ 6 - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -476,8 +495,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -559,7 +582,7 @@ "Does not raise event for initial document delete": { "describeName": "Listens:", "itName": "Does not raise event for initial document delete", - "tags": [""], + "tags": [], "config": { "useGarbageCollection": true }, @@ -603,8 +626,12 @@ "removedTargets": [ 2 ] - }, - "watchSnapshot": 1000 + } + }, + { + "watchSnapshot": { + "version": 1000 + } }, { "watchCurrent": [ @@ -612,8 +639,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -833,8 +864,12 @@ 4 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -916,8 +951,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -954,8 +993,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -1068,8 +1111,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000 + ] + }, + { + "watchSnapshot": { + "version": 1000 + } }, { "watchEntity": { @@ -1085,8 +1132,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -1159,8 +1210,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -1197,8 +1252,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -1298,8 +1357,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000 + ] + }, + { + "watchSnapshot": { + "version": 1000 + } }, { "watchEntity": { @@ -1315,8 +1378,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -1402,8 +1469,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -1531,8 +1602,12 @@ 4 ], "resume-token-4000" - ], - "watchSnapshot": 4000, + ] + }, + { + "watchSnapshot": { + "version": 4000 + }, "expect": [ { "query": { @@ -1627,7 +1702,7 @@ } ] ], - "targets": [ + "removedTargets": [ 2 ] } @@ -1638,8 +1713,12 @@ 2 ], "resume-token-5000" - ], - "watchSnapshot": 5000, + ] + }, + { + "watchSnapshot": { + "version": 5000 + }, "expect": [ { "query": { @@ -1748,8 +1827,12 @@ 4 ], "resume-token-6000" - ], - "watchSnapshot": 6000, + ] + }, + { + "watchSnapshot": { + "version": 6000 + }, "expect": [ { "query": { @@ -1765,12 +1848,12 @@ } ] }, - "Listens are reestablished after network disconnect": { + "Individual (deleted) documents cannot revert": { "describeName": "Listens:", - "itName": "Listens are reestablished after network disconnect", + "itName": "Individual (deleted) documents cannot revert", "tags": [], "config": { - "useGarbageCollection": true + "useGarbageCollection": false }, "steps": [ { @@ -1778,7 +1861,13 @@ 2, { "path": "collection", - "filters": [], + "filters": [ + [ + "visible", + "==", + true + ] + ], "orderBys": [] } ], @@ -1787,13 +1876,18 @@ "2": { "query": { "path": "collection", - "filters": [], + "filters": [ + [ + "visible", + "==", + true + ] + ], "orderBys": [] }, "resumeToken": "" } - }, - "watchStreamRequestCount": 1 + } } }, { @@ -1808,7 +1902,8 @@ "collection/a", 1000, { - "key": "a" + "v": "v1000", + "visible": true } ] ], @@ -1823,13 +1918,23 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { "path": "collection", - "filters": [], + "filters": [ + [ + "visible", + "==", + true + ] + ], "orderBys": [] }, "added": [ @@ -1837,7 +1942,8 @@ "collection/a", 1000, { - "key": "a" + "v": "v1000", + "visible": true } ] ], @@ -1848,10 +1954,51 @@ ] }, { - "enableNetwork": false, + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], "stateExpect": { - "activeTargets": {}, - "limboDocs": [] + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } }, "expect": [ { @@ -1860,57 +2007,53 @@ "filters": [], "orderBys": [] }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000", + "visible": true + } + ] + ], "errorCode": 0, "fromCache": true, "hasPendingWrites": false } ] }, - { - "enableNetwork": true, - "stateExpect": { - "activeTargets": { - "2": { - "query": { - "path": "collection", - "filters": [], - "orderBys": [] - }, - "resumeToken": "resume-token-1000" - } - }, - "watchStreamRequestCount": 2 - } - }, { "watchAck": [ - 2 + 4 ] }, { "watchEntity": { "docs": [ [ - "collection/b", - 2000, - { - "key": "b" - } + "collection/a", + 3000, + null ] ], - "targets": [ - 2 + "removedTargets": [ + 4 ] } }, { "watchCurrent": [ [ - 2 + 4 ], - "resume-token-2000" - ], - "watchSnapshot": 2000, + "resume-token-4000" + ] + }, + { + "watchSnapshot": { + "version": 4000 + }, "expect": [ { "query": { @@ -1918,12 +2061,13 @@ "filters": [], "orderBys": [] }, - "added": [ + "removed": [ [ - "collection/b", - 2000, + "collection/a", + 1000, { - "key": "b" + "v": "v1000", + "visible": true } ] ], @@ -1932,21 +2076,655 @@ "hasPendingWrites": false } ] - } - ] - }, - "Synthesizes deletes for missing document": { - "describeName": "Listens:", - "itName": "Synthesizes deletes for missing document", - "tags": [], - "config": { - "useGarbageCollection": false - }, - "steps": [ + }, { - "userListen": [ - 2, - { + "userUnlisten": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 4 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000", + "visible": false + } + ] + ], + "removedTargets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-5000" + ] + }, + { + "watchSnapshot": { + "version": 5000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-4000" + } + } + } + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-6000" + ] + }, + { + "watchSnapshot": { + "version": 6000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Deleted documents in cache are fixed": { + "describeName": "Listens:", + "itName": "Deleted documents in cache are fixed", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + null + ] + ], + "removedTargets": [ + 2 + ] + } + }, + { + "watchSnapshot": { + "version": 2000, + "targetIds": [ + 2 + ], + "resumeToken": "resume-token-2000" + } + }, + { + "watchSnapshot": { + "version": 2000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-3000" + ] + }, + { + "watchSnapshot": { + "version": 3000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Listens are reestablished after network disconnect": { + "describeName": "Listens:", + "itName": "Listens are reestablished after network disconnect", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + }, + "watchStreamRequestCount": 1 + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "enableNetwork": false, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [] + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "enableNetwork": true, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + }, + "watchStreamRequestCount": 2 + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 2000, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 2000, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Synthesizes deletes for missing document": { + "describeName": "Listens:", + "itName": "Synthesizes deletes for missing document", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { "path": "collection", "filters": [], "orderBys": [] @@ -1999,8 +2777,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -2167,8 +2949,12 @@ 4 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -2313,8 +3099,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -2425,8 +3215,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -2508,8 +3302,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -2559,8 +3357,12 @@ "targets": [ 2 ] - }, - "watchSnapshot": 2000 + } + }, + { + "watchSnapshot": { + "version": 2000 + } }, { "watchRemove": { @@ -2663,8 +3465,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 1000 }, - "watchSnapshot": 1000, "expect": [ { "query": { @@ -2693,8 +3499,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -2765,5 +3575,229 @@ ] } ] + }, + "Persists resume token sent with target": { + "describeName": "Listens:", + "itName": "Persists resume token sent with target", + "tags": [ + "exclusive" + ], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchSnapshot": { + "version": 2000, + "targetIds": [ + 2 + ], + "resumeToken": "resume-token-2000" + } + }, + { + "watchSnapshot": { + "version": 2000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 2000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-2000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 2000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-3000" + ] + }, + { + "watchSnapshot": { + "version": 3000 + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] } } diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json index 1af4c16e2f8..dbc6d10d9d7 100644 --- a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json @@ -176,7 +176,10 @@ "Removing all listeners delays \"Offline\" status on next listen": { "describeName": "Offline:", "itName": "Removing all listeners delays \"Offline\" status on next listen", - "tags": [], + "tags": [ + "no-lru" + ], + "comment": "Marked as no-lru because when a listen is re-added, it gets a new target id rather than reusing one", "config": { "useGarbageCollection": true }, @@ -366,8 +369,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -460,8 +467,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -534,8 +545,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -569,8 +584,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "stateExpect": { "limboDocs": [ "collection/a" @@ -673,8 +692,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001 + ] + }, + { + "watchSnapshot": { + "version": 1001 + } }, { "watchAck": [ @@ -695,8 +718,12 @@ 1 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { @@ -824,8 +851,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { diff --git a/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json index 100920687b9..58b5d16bcb7 100644 --- a/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json @@ -119,8 +119,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { diff --git a/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json index 158e3370869..7303e36a821 100644 --- a/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json @@ -196,8 +196,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -331,8 +335,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -697,8 +705,12 @@ 2 ], "resume-token-500" - ], - "watchSnapshot": 500, + ] + }, + { + "watchSnapshot": { + "version": 500 + }, "expect": [ { "query": { diff --git a/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json index 6852c9017b1..876496375fc 100644 --- a/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json @@ -91,8 +91,12 @@ 2 ], "resume-token" - ], - "watchSnapshot": 1000 + ] + }, + { + "watchSnapshot": { + "version": 1000 + } }, { "watchRemove": { @@ -128,8 +132,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { @@ -316,8 +324,12 @@ 2 ], "resume-token" - ], - "watchSnapshot": 1000 + ] + }, + { + "watchSnapshot": { + "version": 1000 + } }, { "watchRemove": { @@ -353,8 +365,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001 + ] + }, + { + "watchSnapshot": { + "version": 1001 + } }, { "watchRemove": { @@ -390,8 +406,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001 + ] + }, + { + "watchSnapshot": { + "version": 1001 + } }, { "watchRemove": { @@ -427,8 +447,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { @@ -519,8 +543,12 @@ 2 ], "resume-token-1001" - ], - "watchSnapshot": 1001, + ] + }, + { + "watchSnapshot": { + "version": 1001 + }, "expect": [ { "query": { diff --git a/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json index f411d983813..cee3c2a44c1 100644 --- a/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json @@ -56,8 +56,12 @@ 2 ], "custom-query-resume-token" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -160,8 +164,12 @@ 2 ], "custom-query-resume-token" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -243,8 +251,12 @@ { "watchAck": [ 2 - ], - "watchSnapshot": 1001 + ] + }, + { + "watchSnapshot": { + "version": 1001 + } } ] } diff --git a/Firestore/Example/Tests/SpecTests/json/write_spec_test.json b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json index d4d1e7c3fe4..5422f21f597 100644 --- a/Firestore/Example/Tests/SpecTests/json/write_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json @@ -63,8 +63,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -138,8 +142,12 @@ "targets": [ 2 ] - }, - "watchSnapshot": 2000 + } + }, + { + "watchSnapshot": { + "version": 2000 + } }, { "writeAck": { @@ -212,8 +220,12 @@ "targets": [ 2 ] - }, - "watchSnapshot": 3000 + } + }, + { + "watchSnapshot": { + "version": 3000 + } }, { "writeAck": { @@ -301,8 +313,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -369,8 +385,12 @@ "targets": [ 2 ] - }, - "watchSnapshot": 2000 + } + }, + { + "watchSnapshot": { + "version": 2000 + } }, { "writeAck": { @@ -458,8 +478,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -526,8 +550,12 @@ "targets": [ 2 ] - }, - "watchSnapshot": 10000 + } + }, + { + "watchSnapshot": { + "version": 10000 + } }, { "writeAck": { @@ -615,8 +643,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -689,8 +721,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -772,8 +808,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -846,8 +886,12 @@ "targets": [ 2 ] - }, - "watchSnapshot": 2000 + } + }, + { + "watchSnapshot": { + "version": 2000 + } }, { "watchEntity": { @@ -870,8 +914,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 3000 }, - "watchSnapshot": 3000, "expect": [ { "query": { @@ -1421,8 +1469,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 1000 }, - "watchSnapshot": 1000, "expect": [ { "query": { @@ -1465,8 +1517,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -1509,8 +1565,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 3000 }, - "watchSnapshot": 3000, "expect": [ { "query": { @@ -1553,8 +1613,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 4000 }, - "watchSnapshot": 4000, "expect": [ { "query": { @@ -1597,8 +1661,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 5000 }, - "watchSnapshot": 5000, "expect": [ { "query": { @@ -1641,8 +1709,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 6000 }, - "watchSnapshot": 6000, "expect": [ { "query": { @@ -1685,8 +1757,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 7000 }, - "watchSnapshot": 7000, "expect": [ { "query": { @@ -1729,8 +1805,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 8000 }, - "watchSnapshot": 8000, "expect": [ { "query": { @@ -1773,8 +1853,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 9000 }, - "watchSnapshot": 9000, "expect": [ { "query": { @@ -1817,8 +1901,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 10000 }, - "watchSnapshot": 10000, "expect": [ { "query": { @@ -1861,8 +1949,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 11000 }, - "watchSnapshot": 11000, "expect": [ { "query": { @@ -1905,8 +1997,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 12000 }, - "watchSnapshot": 12000, "expect": [ { "query": { @@ -1949,8 +2045,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 13000 }, - "watchSnapshot": 13000, "expect": [ { "query": { @@ -1993,8 +2093,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 14000 }, - "watchSnapshot": 14000, "expect": [ { "query": { @@ -2037,8 +2141,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 15000 }, - "watchSnapshot": 15000, "expect": [ { "query": { @@ -3058,8 +3166,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -3191,8 +3303,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -3266,8 +3382,12 @@ 2 ], "resume-token-500" - ], - "watchSnapshot": 500, + ] + }, + { + "watchSnapshot": { + "version": 500 + }, "expect": [ { "query": { @@ -3374,8 +3494,12 @@ "targets": [ 2 ] + } + }, + { + "watchSnapshot": { + "version": 2000 }, - "watchSnapshot": 2000, "expect": [ { "query": { @@ -3456,8 +3580,12 @@ 2 ], "resume-token-500" - ], - "watchSnapshot": 500, + ] + }, + { + "watchSnapshot": { + "version": 500 + }, "expect": [ { "query": { @@ -3573,8 +3701,12 @@ 2 ], "resume-token-2000" - ], - "watchSnapshot": 2000, + ] + }, + { + "watchSnapshot": { + "version": 2000 + }, "expect": [ { "query": { @@ -3602,7 +3734,10 @@ "Held writes are released when there are no queries left.": { "describeName": "Writes:", "itName": "Held writes are released when there are no queries left.", - "tags": [], + "tags": [ + "no-lru" + ], + "comment": "This test expects a new target id for a new listen, but without eager gc, the same target id is reused", "config": { "useGarbageCollection": true }, @@ -3648,8 +3783,12 @@ 2 ], "resume-token-500" - ], - "watchSnapshot": 500, + ] + }, + { + "watchSnapshot": { + "version": 500 + }, "expect": [ { "query": { @@ -4735,8 +4874,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -4862,8 +5005,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -4989,8 +5136,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -5116,8 +5267,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -5243,8 +5398,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -5370,8 +5529,12 @@ 2 ], "resume-token-1000" - ], - "watchSnapshot": 1000, + ] + }, + { + "watchSnapshot": { + "version": 1000 + }, "expect": [ { "query": { @@ -5456,8 +5619,12 @@ 2 ], "resume-token-500" - ], - "watchSnapshot": 500, + ] + }, + { + "watchSnapshot": { + "version": 500 + }, "expect": [ { "query": { @@ -5534,8 +5701,12 @@ "targets": [ 2 ] - }, - "watchSnapshot": 2000 + } + }, + { + "watchSnapshot": { + "version": 2000 + } }, { "writeAck": { diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm index bf7b053964b..dad81d4cd3b 100644 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ b/Firestore/Source/Core/FSTSyncEngine.mm @@ -114,6 +114,27 @@ - (instancetype)initWithQuery:(FSTQuery *)query @end +#pragma mark - LimboResolution + +/** Tracks a limbo resolution. */ +class LimboResolution { + public: + LimboResolution() { + } + + explicit LimboResolution(const DocumentKey &key) : key{key} { + } + + DocumentKey key; + + /** + * Set to true once we've received a document. This is used in remoteKeysForTarget and + * ultimately used by FSTWatchChangeAggregator to decide whether it needs to manufacture a delete + * event for the target once the target is CURRENT. + */ + bool document_received = false; +}; + #pragma mark - FSTSyncEngine @interface FSTSyncEngine () @@ -151,8 +172,11 @@ @implementation FSTSyncEngine { */ std::map _limboTargetsByKey; - /** The inverse of _limboTargetsByKey, a map of TargetId to the key of the limbo doc. */ - std::map _limboKeysByTarget; + /** + * Basically the inverse of limboTargetsByKey, a map of target ID to a LimboResolution (which + * includes the DocumentKey as well as whether we've received a document for the target). + */ + std::map _limboResolutionsByTarget; User _currentUser; } @@ -288,6 +312,35 @@ - (void)transactionWithRetries:(int)retries - (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { [self assertDelegateExistsForSelector:_cmd]; + // Update `receivedDocument` as appropriate for any limbo targets. + for (const auto &entry : remoteEvent.targetChanges) { + FSTTargetID targetID = entry.first; + FSTTargetChange *change = entry.second; + const auto iter = _limboResolutionsByTarget.find(targetID); + if (iter != _limboResolutionsByTarget.end()) { + LimboResolution &limboResolution = iter->second; + // Since this is a limbo resolution lookup, it's for a single document and it could be + // added, modified, or removed, but not a combination. + HARD_ASSERT(change.addedDocuments.size() + change.modifiedDocuments.size() + + change.removedDocuments.size() <= + 1, + "Limbo resolution for single document contains multiple changes."); + + if (change.addedDocuments.size() > 0) { + limboResolution.document_received = true; + } else if (change.modifiedDocuments.size() > 0) { + HARD_ASSERT(limboResolution.document_received, + "Received change for limbo target document without add."); + } else if (change.removedDocuments.size() > 0) { + HARD_ASSERT(limboResolution.document_received, + "Received remove for limbo target document without add."); + limboResolution.document_received = false; + } else { + // This was probably just a CURRENT targetChange or similar. + } + } + } + FSTMaybeDocumentDictionary *changes = [self.localStore applyRemoteEvent:remoteEvent]; [self emitNewSnapshotsWithChanges:changes remoteEvent:remoteEvent]; } @@ -310,13 +363,13 @@ - (void)applyChangedOnlineState:(FSTOnlineState)onlineState { - (void)rejectListenWithTargetID:(const TargetId)targetID error:(NSError *)error { [self assertDelegateExistsForSelector:_cmd]; - const auto iter = _limboKeysByTarget.find(targetID); - if (iter != _limboKeysByTarget.end()) { - const DocumentKey limboKey = iter->second; + const auto iter = _limboResolutionsByTarget.find(targetID); + if (iter != _limboResolutionsByTarget.end()) { + const DocumentKey limboKey = iter->second.key; // Since this query failed, we won't want to manually unlisten to it. // So go ahead and remove it from bookkeeping. _limboTargetsByKey.erase(limboKey); - _limboKeysByTarget.erase(targetID); + _limboResolutionsByTarget.erase(targetID); // TODO(dimond): Retry on transient errors? @@ -484,7 +537,7 @@ - (void)trackLimboChange:(FSTLimboDocumentChange *)limboChange { targetID:limboTargetID listenSequenceNumber:kIrrelevantSequenceNumber purpose:FSTQueryPurposeLimboResolution]; - _limboKeysByTarget[limboTargetID] = key; + _limboResolutionsByTarget.emplace(limboTargetID, LimboResolution{key}); [self.remoteStore listenToTargetWithQueryData:queryData]; _limboTargetsByKey[key] = limboTargetID; } @@ -499,7 +552,7 @@ - (void)removeLimboTargetForKey:(const DocumentKey &)key { TargetId limboTargetID = iter->second; [self.remoteStore stopListeningToTargetID:limboTargetID]; _limboTargetsByKey.erase(key); - _limboKeysByTarget.erase(limboTargetID); + _limboResolutionsByTarget.erase(limboTargetID); } // Used for testing @@ -520,8 +573,13 @@ - (void)userDidChange:(const User &)user { } - (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetId { - FSTQueryView *queryView = self.queryViewsByTarget[targetId]; - return queryView ? queryView.view.syncedDocuments : DocumentKeySet{}; + const auto iter = _limboResolutionsByTarget.find([targetId intValue]); + if (iter != _limboResolutionsByTarget.end() && iter->second.document_received) { + return DocumentKeySet{iter->second.key}; + } else { + FSTQueryView *queryView = self.queryViewsByTarget[targetId]; + return queryView ? queryView.view.syncedDocuments : DocumentKeySet{}; + } } @end diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index 9dc50a2c8a2..db9992e3eaa 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -150,9 +150,7 @@ - (BOOL)start:(NSError **)error { return NO; } _ptr.reset(database); - LevelDbTransaction transaction(_ptr.get(), "Start LevelDB"); - [FSTLevelDBMigrations runMigrationsWithTransaction:&transaction]; - transaction.Commit(); + [FSTLevelDBMigrations runMigrationsWithDatabase:_ptr.get()]; return YES; } diff --git a/Firestore/Source/Local/FSTLevelDBMigrations.h b/Firestore/Source/Local/FSTLevelDBMigrations.h index 1724edfa453..0987da53c61 100644 --- a/Firestore/Source/Local/FSTLevelDBMigrations.h +++ b/Firestore/Source/Local/FSTLevelDBMigrations.h @@ -36,7 +36,13 @@ typedef int32_t FSTLevelDBSchemaVersion; /** * Runs any migrations needed to bring the given database up to the current schema version */ -+ (void)runMigrationsWithTransaction:(firebase::firestore::local::LevelDbTransaction *)transaction; ++ (void)runMigrationsWithDatabase:(leveldb::DB *)database; + +/** + * Runs any migrations needed to bring the given database up to the given schema version + */ ++ (void)runMigrationsWithDatabase:(leveldb::DB *)database + upToVersion:(FSTLevelDBSchemaVersion)version; @end diff --git a/Firestore/Source/Local/FSTLevelDBMigrations.mm b/Firestore/Source/Local/FSTLevelDBMigrations.mm index bd72c97a99b..80348cefbc0 100644 --- a/Firestore/Source/Local/FSTLevelDBMigrations.mm +++ b/Firestore/Source/Local/FSTLevelDBMigrations.mm @@ -23,32 +23,35 @@ #import "Firestore/Source/Local/FSTLevelDBQueryCache.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/base/macros.h" +#include "absl/memory/memory.h" #include "absl/strings/match.h" #include "leveldb/write_batch.h" NS_ASSUME_NONNULL_BEGIN -// Current version of the schema defined in this file. -static FSTLevelDBSchemaVersion kSchemaVersion = 2; +/** + * Schema version for the iOS client. + * + * Note that tables aren't a concept in LevelDB. They exist in our schema as just prefixes on keys. + * This means tables don't need to be created but they also can't easily be dropped and re-created. + * + * Migrations: + * * Migration 1 used to ensure the target_global row existed, without clearing it. No longer + * required because migration 3 unconditionally clears it. + * * Migration 2 used to ensure that the target_global row had a correct count of targets. No + * longer required because migration 3 deletes them all. + * * Migration 3 deletes the entire query cache to deal with cache corruption related to + * limbo resolution. Addresses https://github.com/firebase/firebase-ios-sdk/issues/1548. + */ +static FSTLevelDBSchemaVersion kSchemaVersion = 3; using firebase::firestore::local::LevelDbTransaction; -using leveldb::DB; using leveldb::Iterator; using leveldb::Status; using leveldb::Slice; using leveldb::WriteOptions; -/** - * Ensures that the global singleton target metadata row exists in LevelDB. - */ -static void EnsureTargetGlobal(LevelDbTransaction *transaction) { - FSTPBTargetGlobal *targetGlobal = - [FSTLevelDBQueryCache readTargetMetadataWithTransaction:transaction]; - if (!targetGlobal) { - transaction->Put([FSTLevelDBTargetGlobalKey key], [FSTPBTargetGlobal message]); - } -} - /** * Save the given version number as the current version of the schema of the database. * @param version The version to save @@ -60,30 +63,39 @@ static void SaveVersion(FSTLevelDBSchemaVersion version, LevelDbTransaction *tra transaction->Put(key, version_string); } -/** - * This function counts the number of targets that currently exist in the given db. It - * then reads the target global row, adds the count to the metadata from that row, and writes - * the metadata back. - * - * It assumes the metadata has already been written and is able to be read in this transaction. - */ -static void AddTargetCount(LevelDbTransaction *transaction) { - auto it = transaction->NewIterator(); - std::string start_key = [FSTLevelDBTargetKey keyPrefix]; - it->Seek(start_key); - - int32_t count = 0; - while (it->Valid() && absl::StartsWith(it->key(), start_key)) { - count++; - it->Next(); +static void DeleteEverythingWithPrefix(const std::string &prefix, leveldb::DB *db) { + bool more_deletes = true; + while (more_deletes) { + LevelDbTransaction transaction(db, "Delete everything with prefix"); + auto it = transaction.NewIterator(); + + more_deletes = false; + for (it->Seek(prefix); it->Valid() && absl::StartsWith(it->key(), prefix); it->Next()) { + if (transaction.changed_keys() >= 1000) { + more_deletes = true; + break; + } + transaction.Delete(it->key()); + } + + transaction.Commit(); } +} + +/** Migration 3. */ +static void ClearQueryCache(leveldb::DB *db) { + DeleteEverythingWithPrefix([FSTLevelDBTargetKey keyPrefix], db); + DeleteEverythingWithPrefix([FSTLevelDBDocumentTargetKey keyPrefix], db); + DeleteEverythingWithPrefix([FSTLevelDBTargetDocumentKey keyPrefix], db); + DeleteEverythingWithPrefix([FSTLevelDBQueryTargetKey keyPrefix], db); + + LevelDbTransaction transaction(db, "Drop query cache"); - FSTPBTargetGlobal *targetGlobal = - [FSTLevelDBQueryCache readTargetMetadataWithTransaction:transaction]; - HARD_ASSERT(targetGlobal != nil, - "We should have a metadata row as it was added in an earlier migration"); - targetGlobal.targetCount = count; - transaction->Put([FSTLevelDBTargetGlobalKey key], targetGlobal); + // Reset the target global entry too (to reset the target count). + transaction.Put([FSTLevelDBTargetGlobalKey key], [FSTPBTargetGlobal message]); + + SaveVersion(3, &transaction); + transaction.Commit(); } @implementation FSTLevelDBMigrations @@ -100,23 +112,19 @@ + (FSTLevelDBSchemaVersion)schemaVersionWithTransaction: } } -+ (void)runMigrationsWithTransaction:(firebase::firestore::local::LevelDbTransaction *)transaction { - FSTLevelDBSchemaVersion currentVersion = [self schemaVersionWithTransaction:transaction]; - // Each case in this switch statement intentionally falls through. This lets us - // start at the current schema version and apply any migrations that have not yet - // been applied, to bring us up to current, as defined by the kSchemaVersion constant. - switch (currentVersion) { - case 0: - EnsureTargetGlobal(transaction); - // Fallthrough - case 1: - // We're now guaranteed that the target global exists. We can safely add a count to it. - AddTargetCount(transaction); - // Fallthrough - default: - if (currentVersion < kSchemaVersion) { - SaveVersion(kSchemaVersion, transaction); - } ++ (void)runMigrationsWithDatabase:(leveldb::DB *)database { + [self runMigrationsWithDatabase:database upToVersion:kSchemaVersion]; +} + ++ (void)runMigrationsWithDatabase:(leveldb::DB *)database + upToVersion:(FSTLevelDBSchemaVersion)toVersion { + LevelDbTransaction transaction{database, "Read schema version"}; + FSTLevelDBSchemaVersion fromVersion = [self schemaVersionWithTransaction:&transaction]; + + // This must run unconditionally because schema migrations were added to iOS after the first + // release. There may be clients that have never run any migrations that have existing targets. + if (fromVersion < 3 && toVersion >= 3) { + ClearQueryCache(database); } } diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index 6aab78e8b6e..aed0e552ef5 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -268,6 +268,7 @@ - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { FSTListenSequenceNumber sequenceNumber = [self.listenSequence next]; id queryCache = self.queryCache; + DocumentKeySet authoritativeUpdates; for (const auto &entry : remoteEvent.targetChanges) { FSTTargetID targetID = entry.first; FSTBoxedTargetID *boxedTargetID = @(targetID); @@ -279,6 +280,21 @@ - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { continue; } + // When a global snapshot contains updates (either add or modify) we can completely trust + // these updates as authoritative and blindly apply them to our cache (as a defensive measure + // to promote self-healing in the unfortunate case that our cache is ever somehow corrupted / + // out-of-sync). + // + // If the document is only updated while removing it from a target then watch isn't obligated + // to send the absolute latest version: it can send the first version that caused the document + // not to match. + for (const DocumentKey &key : change.addedDocuments) { + authoritativeUpdates = authoritativeUpdates.insert(key); + } + for (const DocumentKey &key : change.modifiedDocuments) { + authoritativeUpdates = authoritativeUpdates.insert(key); + } + [queryCache removeMatchingKeys:change.removedDocuments forTargetID:targetID]; [queryCache addMatchingKeys:change.addedDocuments forTargetID:targetID]; @@ -303,11 +319,12 @@ - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { FSTMaybeDocument *doc = kv.second; changedDocKeys = changedDocKeys.insert(key); FSTMaybeDocument *existingDoc = [self.remoteDocumentCache entryForKey:key]; - // Make sure we don't apply an old document version to the remote cache, though we - // make an exception for SnapshotVersion::None() which can happen for manufactured - // events (e.g. in the case of a limbo document resolution failing). - if (!existingDoc || SnapshotVersion{doc.version} == SnapshotVersion::None() || - SnapshotVersion{doc.version} >= SnapshotVersion{existingDoc.version}) { + + // If a document update isn't authoritative, make sure we don't apply an old document version + // to the remote cache. We make an exception for SnapshotVersion.MIN which can happen for + // manufactured events (e.g. in the case of a limbo document resolution failing). + if (!existingDoc || doc.version == SnapshotVersion::None() || + authoritativeUpdates.contains(doc.key) || doc.version >= existingDoc.version) { [self.remoteDocumentCache addEntry:doc]; } else { LOG_DEBUG( diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_transaction.h b/Firestore/core/src/firebase/firestore/local/leveldb_transaction.h index a6ddce2d5d9..9b308fcf86c 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_transaction.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_transaction.h @@ -144,6 +144,10 @@ class LevelDbTransaction { */ static const leveldb::WriteOptions& DefaultWriteOptions(); + size_t changed_keys() const { + return mutations_.size() + deletions_.size(); + } + /** * Remove the database entry (if any) for "key". It is not an error if "key" * did not exist in the database.