@@ -1285,6 +1285,164 @@ - (void)testResumingAQueryShouldUseBloomFilterToAvoidFullRequery {
1285
1285
}
1286
1286
}
1287
1287
1288
+ - (void )
1289
+ testBloomFilterShouldAvertAFullRequeryWhenDocumentsWereAddedDeletedRemovedUpdatedAndUnchangedSinceTheResumeToken {
1290
+ // TODO(b/291365820): Stop skipping this test when running against the Firestore emulator once
1291
+ // the emulator is improved to include a bloom filter in the existence filter messages that it
1292
+ // sends.
1293
+ XCTSkipIf ([FSTIntegrationTestCase isRunningAgainstEmulator ],
1294
+ " Skip this test when running against the Firestore emulator because the emulator does "
1295
+ " not include a bloom filter when it sends existence filter messages, making it "
1296
+ " impossible for this test to verify the correctness of the bloom filter." );
1297
+
1298
+ // Set this test to stop when the first failure occurs because some test assertion failures make
1299
+ // the rest of the test not applicable or will even crash.
1300
+ [self setContinueAfterFailure: NO ];
1301
+
1302
+ // Prepare the names and contents of the 20 documents to create.
1303
+ NSMutableDictionary <NSString *, NSDictionary <NSString *, id > *> *testDocs =
1304
+ [[NSMutableDictionary alloc ] init ];
1305
+ for (int i = 0 ; i < 20 ; i++) {
1306
+ [testDocs setValue: @{@" key" : @42 , @" removed" : @NO }
1307
+ forKey: [NSString stringWithFormat: @" doc%@ " , @(1000 + i)]];
1308
+ }
1309
+
1310
+ // Each iteration of the "while" loop below runs a single iteration of the test. The test will
1311
+ // be run multiple times only if a bloom filter false positive occurs.
1312
+ int attemptNumber = 0 ;
1313
+ while (true ) {
1314
+ attemptNumber++;
1315
+
1316
+ // Create 20 documents in a new collection.
1317
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments: testDocs];
1318
+ FIRQuery *query = [collRef queryWhereField: @" removed" isEqualTo: @NO ];
1319
+
1320
+ // Run a query to populate the local cache with the 20 documents and a resume token.
1321
+ FIRQuerySnapshot *querySnapshot1 = [self readDocumentSetForRef: query
1322
+ source: FIRFirestoreSourceDefault];
1323
+ XCTAssertEqual (querySnapshot1.count , 20u , @" querySnapshot1.count has an unexpected value" );
1324
+ NSArray <FIRDocumentReference *> *createdDocuments =
1325
+ FIRDocumentReferenceArrayFromQuerySnapshot (querySnapshot1);
1326
+
1327
+ // Out of the 20 existing documents, leave 5 docs untouched, delete 5 docs, remove 5 docs,
1328
+ // update 5 docs, and add 15 new docs.
1329
+ NSSet <NSString *> *deletedDocumentIds;
1330
+ NSSet <NSString *> *removedDocumentIds;
1331
+ NSSet <NSString *> *updatedDocumentIds;
1332
+ NSMutableArray <NSString *> *addedDocumentIds = [[NSMutableArray alloc ] init ];
1333
+
1334
+ {
1335
+ FIRFirestore *db2 = [self firestore ];
1336
+ FIRWriteBatch *batch = [db2 batch ];
1337
+
1338
+ NSMutableArray <NSString *> *deletedDocumentIdsAccumulator = [[NSMutableArray alloc ] init ];
1339
+ for (decltype (createdDocuments.count ) i = 0 ; i < createdDocuments.count ; i += 4 ) {
1340
+ FIRDocumentReference *documentToDelete = [db2 documentWithPath: createdDocuments[i].path];
1341
+ [batch deleteDocument: documentToDelete];
1342
+ [deletedDocumentIdsAccumulator addObject: documentToDelete.documentID];
1343
+ }
1344
+ deletedDocumentIds = [NSSet setWithArray: deletedDocumentIdsAccumulator];
1345
+ XCTAssertEqual (deletedDocumentIds.count , 5u , @" deletedDocumentIds has the wrong size" );
1346
+
1347
+ // Update 5 documents to no longer match the query.
1348
+ NSMutableArray <NSString *> *removedDocumentIdsAccumulator = [[NSMutableArray alloc ] init ];
1349
+ for (decltype (createdDocuments.count ) i = 1 ; i < createdDocuments.count ; i += 4 ) {
1350
+ FIRDocumentReference *documentToRemove = [db2 documentWithPath: createdDocuments[i].path];
1351
+ [batch updateData: @{@" removed" : @YES } forDocument: documentToRemove];
1352
+ [removedDocumentIdsAccumulator addObject: documentToRemove.documentID];
1353
+ }
1354
+ removedDocumentIds = [NSSet setWithArray: removedDocumentIdsAccumulator];
1355
+ XCTAssertEqual (removedDocumentIds.count , 5u , @" removedDocumentIds has the wrong size" );
1356
+
1357
+ // Update 5 documents, but ensure they still match the query.
1358
+ NSMutableArray <NSString *> *updatedDocumentIdsAccumulator = [[NSMutableArray alloc ] init ];
1359
+ for (decltype (createdDocuments.count ) i = 2 ; i < createdDocuments.count ; i += 4 ) {
1360
+ FIRDocumentReference *documentToUpdate = [db2 documentWithPath: createdDocuments[i].path];
1361
+ [batch updateData: @{@" key" : @43 } forDocument: documentToUpdate];
1362
+ [updatedDocumentIdsAccumulator addObject: documentToUpdate.documentID];
1363
+ }
1364
+ updatedDocumentIds = [NSSet setWithArray: updatedDocumentIdsAccumulator];
1365
+ XCTAssertEqual (updatedDocumentIds.count , 5u , @" updatedDocumentIds has the wrong size" );
1366
+
1367
+ for (int i = 0 ; i < 15 ; i += 1 ) {
1368
+ FIRDocumentReference *documentToAdd = [db2
1369
+ documentWithPath: [NSString stringWithFormat: @" %@ /newDoc%@ " , collRef.path, @(1000 + i)]];
1370
+ [batch setData: @{@" key" : @42 , @" removed" : @NO } forDocument: documentToAdd];
1371
+ [addedDocumentIds addObject: documentToAdd.documentID];
1372
+ }
1373
+
1374
+ // Ensure the documentIds above are mutually exclusive.
1375
+ NSMutableSet <NSString *> *mergedSet = [NSMutableSet setWithArray: addedDocumentIds];
1376
+ [mergedSet unionSet: deletedDocumentIds];
1377
+ [mergedSet unionSet: removedDocumentIds];
1378
+ [mergedSet unionSet: updatedDocumentIds];
1379
+ XCTAssertEqual (mergedSet.count , 30u , @" There are documents experienced multiple operations." );
1380
+
1381
+ [self commitWriteBatch: batch];
1382
+ }
1383
+
1384
+ // Wait for 10 seconds, during which Watch will stop tracking the query and will send an
1385
+ // existence filter rather than "delete" events when the query is resumed.
1386
+ [NSThread sleepForTimeInterval: 10 .0f ];
1387
+
1388
+ // Resume the query and save the resulting snapshot for verification. Use some internal testing
1389
+ // hooks to "capture" the existence filter mismatches to verify that Watch sent a bloom
1390
+ // filter, and it was used to avert a full requery.
1391
+ __block FIRQuerySnapshot *querySnapshot2;
1392
+ NSArray <FSTTestingHooksExistenceFilterMismatchInfo *> *existenceFilterMismatches =
1393
+ [FSTTestingHooks captureExistenceFilterMismatchesDuringBlock: ^{
1394
+ querySnapshot2 = [self readDocumentSetForRef: query source: FIRFirestoreSourceDefault];
1395
+ }];
1396
+ XCTAssertEqual (querySnapshot2.count , 25u , @" querySnapshot1.count has an unexpected value" );
1397
+
1398
+ // Verify that the snapshot from the resumed query contains the expected documents; that is, 10
1399
+ // existing documents that still match the query, and 15 documents that are newly added.
1400
+ {
1401
+ NSMutableArray <NSString *> *expectedDocumentIds = [[NSMutableArray alloc ] init ];
1402
+ for (FIRDocumentReference *documentRef in createdDocuments) {
1403
+ if (![deletedDocumentIds containsObject: documentRef.documentID] &&
1404
+ ![removedDocumentIds containsObject: documentRef.documentID]) {
1405
+ [expectedDocumentIds addObject: documentRef.documentID];
1406
+ }
1407
+ }
1408
+ [expectedDocumentIds addObjectsFromArray: addedDocumentIds];
1409
+ XCTAssertEqualObjects ([NSSet setWithArray: FIRQuerySnapshotGetIDs (querySnapshot2)],
1410
+ [NSSet setWithArray: expectedDocumentIds],
1411
+ @" querySnapshot2 has the wrong documents" );
1412
+ }
1413
+
1414
+ // Verify that Watch sent an existence filter with the correct counts when the query was
1415
+ // resumed.
1416
+ XCTAssertEqual (existenceFilterMismatches.count , 1u ,
1417
+ @" Watch should have sent exactly 1 existence filter" );
1418
+ FSTTestingHooksExistenceFilterMismatchInfo *existenceFilterMismatchInfo =
1419
+ existenceFilterMismatches[0 ];
1420
+ XCTAssertEqual (existenceFilterMismatchInfo.localCacheCount , 35 );
1421
+ XCTAssertEqual (existenceFilterMismatchInfo.existenceFilterCount , 25 );
1422
+
1423
+ // Verify that Watch sent a valid bloom filter.
1424
+ FSTTestingHooksBloomFilter *bloomFilter = existenceFilterMismatchInfo.bloomFilter ;
1425
+ XCTAssertNotNil (bloomFilter,
1426
+ " Watch should have included a bloom filter in the existence filter" );
1427
+
1428
+ // Verify that the bloom filter was successfully used to avert a full requery. If a false
1429
+ // positive occurred then retry the entire test. Although statistically rare, false positives
1430
+ // are expected to happen occasionally. When a false positive _does_ happen, just retry the test
1431
+ // with a different set of documents. If that retry _also_ experiences a false positive, then
1432
+ // fail the test because that is so improbable that something must have gone wrong.
1433
+ if (attemptNumber == 1 && !bloomFilter.applied ) {
1434
+ continue ;
1435
+ }
1436
+
1437
+ XCTAssertTrue (bloomFilter.applied ,
1438
+ @" The bloom filter should have been successfully applied with attemptNumber=%@ " ,
1439
+ @(attemptNumber));
1440
+
1441
+ // Break out of the test loop now that the test passes.
1442
+ break ;
1443
+ }
1444
+ }
1445
+
1288
1446
- (void )testBloomFilterShouldCorrectlyEncodeComplexUnicodeCharacters {
1289
1447
// TODO(b/291365820): Stop skipping this test when running against the Firestore emulator once
1290
1448
// the emulator is improved to include a bloom filter in the existence filter messages that it
0 commit comments