diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java index 0f3e03f8a7c..f607ddbbc85 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore; +import static com.google.common.truth.Truth.assertWithMessage; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.isRunningAgainstEmulator; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.nullList; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.querySnapshotToIds; @@ -39,6 +40,8 @@ import com.google.firebase.firestore.testutil.EventAccumulator; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -1030,6 +1033,73 @@ public void testMultipleUpdatesWhileOffline() { assertEquals(asList(map("foo", "zzyzx", "bar", "2")), querySnapshotToValues(snapshot2)); } + @Test + public void resumingAQueryShouldUseExistenceFilterToDetectDeletes() throws Exception { + // Prepare the names and contents of the 100 documents to create. + Map> testData = new HashMap<>(); + for (int i = 0; i < 100; i++) { + testData.put("doc" + (1000 + i), map("key", 42)); + } + + // Create 100 documents in a new collection. + CollectionReference collection = testCollectionWithDocs(testData); + + // Run a query to populate the local cache with the 100 documents and a resume token. + List createdDocuments = new ArrayList<>(); + { + QuerySnapshot querySnapshot = waitFor(collection.get()); + assertWithMessage("querySnapshot1").that(querySnapshot.size()).isEqualTo(100); + for (DocumentSnapshot documentSnapshot : querySnapshot.getDocuments()) { + createdDocuments.add(documentSnapshot.getReference()); + } + } + + // Delete 50 of the 100 documents. Do this in a transaction, rather than + // DocumentReference.delete(), to avoid affecting the local cache. + HashSet deletedDocumentIds = new HashSet<>(); + waitFor( + collection + .getFirestore() + .runTransaction( + transaction -> { + for (int i = 0; i < createdDocuments.size(); i += 2) { + DocumentReference documentToDelete = createdDocuments.get(i); + transaction.delete(documentToDelete); + deletedDocumentIds.add(documentToDelete.getId()); + } + return null; + })); + + // Wait for 10 seconds, during which Watch will stop tracking the query and will send an + // existence filter rather than "delete" events when the query is resumed. + Thread.sleep(10000); + + // Resume the query and save the resulting snapshot for verification. + QuerySnapshot snapshot2 = waitFor(collection.get()); + + // Verify that the snapshot from the resumed query contains the expected documents; that is, + // that it contains the 50 documents that were _not_ deleted. + // TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to + // send an existence filter. At the time of writing, the Firestore emulator fails to send an + // existence filter, resulting in the client including the deleted documents in the snapshot + // of the resumed query. + if (!(isRunningAgainstEmulator() && snapshot2.size() == 100)) { + HashSet actualDocumentIds = new HashSet<>(); + for (DocumentSnapshot documentSnapshot : snapshot2.getDocuments()) { + actualDocumentIds.add(documentSnapshot.getId()); + } + HashSet expectedDocumentIds = new HashSet<>(); + for (DocumentReference documentRef : createdDocuments) { + if (!deletedDocumentIds.contains(documentRef.getId())) { + expectedDocumentIds.add(documentRef.getId()); + } + } + assertWithMessage("snapshot2.docs") + .that(actualDocumentIds) + .containsExactlyElementsIn(expectedDocumentIds); + } + } + @Test public void testOrQueries() { Map> testDocs = diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index e14f8bffb48..4efa914633a 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -36,6 +36,7 @@ import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.MetadataChanges; import com.google.firebase.firestore.QuerySnapshot; +import com.google.firebase.firestore.WriteBatch; import com.google.firebase.firestore.auth.User; import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.model.DatabaseId; @@ -347,8 +348,27 @@ public static CollectionReference testCollectionWithDocs(Map> docs) { + WriteBatch writeBatch = null; + int writeBatchSize = 0; + for (Map.Entry> doc : docs.entrySet()) { - waitFor(collection.document(doc.getKey()).set(doc.getValue())); + if (writeBatch == null) { + writeBatch = collection.getFirestore().batch(); + } + + writeBatch.set(collection.document(doc.getKey()), doc.getValue()); + writeBatchSize++; + + // Write batches are capped at 500 writes. Use 400 just to be safe. + if (writeBatchSize == 400) { + waitFor(writeBatch.commit()); + writeBatch = null; + writeBatchSize = 0; + } + } + + if (writeBatch != null) { + waitFor(writeBatch.commit()); } }