Skip to content

Commit 7d86138

Browse files
Delete index entries for deleted documents (#3209)
1 parent 072ee4c commit 7d86138

14 files changed

+140
-58
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ private DocumentChangeResult populateDocumentChanges(
494494
@Nullable Map<DocumentKey, SnapshotVersion> documentVersions,
495495
SnapshotVersion globalVersion) {
496496
Map<DocumentKey, MutableDocument> changedDocs = new HashMap<>();
497+
List<DocumentKey> removedDocs = new ArrayList<>();
497498
Set<DocumentKey> conditionChanged = new HashSet<>();
498499

499500
// Each loop iteration only affects its "own" doc, so it's safe to get all the remote
@@ -517,7 +518,7 @@ private DocumentChangeResult populateDocumentChanges(
517518
if (doc.isNoDocument() && doc.getVersion().equals(SnapshotVersion.NONE)) {
518519
// NoDocuments with SnapshotVersion.NONE are used in manufactured events. We remove
519520
// these documents from cache since we lost access.
520-
remoteDocuments.remove(doc.getKey());
521+
removedDocs.add(doc.getKey());
521522
changedDocs.put(key, doc);
522523
} else if (!existingDoc.isValidDocument()
523524
|| doc.getVersion().compareTo(existingDoc.getVersion()) > 0
@@ -537,6 +538,7 @@ private DocumentChangeResult populateDocumentChanges(
537538
doc.getVersion());
538539
}
539540
}
541+
remoteDocuments.removeAll(removedDocs);
540542
return new DocumentChangeResult(changedDocs, conditionChanged);
541543
}
542544

firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryEagerReferenceDelegate.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
import com.google.firebase.firestore.core.ListenSequence;
1818
import com.google.firebase.firestore.model.DocumentKey;
19+
import java.util.ArrayList;
1920
import java.util.HashSet;
21+
import java.util.List;
2022
import java.util.Set;
2123

2224
/** Provides eager garbage collection for MemoryPersistence. */
@@ -72,11 +74,13 @@ public void onTransactionStarted() {
7274
@Override
7375
public void onTransactionCommitted() {
7476
MemoryRemoteDocumentCache remoteDocuments = persistence.getRemoteDocumentCache();
77+
List<DocumentKey> docsToRemove = new ArrayList<>();
7578
for (DocumentKey key : orphanedDocuments) {
7679
if (!isReferenced(key)) {
77-
remoteDocuments.remove(key);
80+
docsToRemove.add(key);
7881
}
7982
}
83+
remoteDocuments.removeAll(docsToRemove);
8084
orphanedDocuments = null;
8185
}
8286

firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryLruReferenceDelegate.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
import android.util.SparseArray;
2020
import com.google.firebase.firestore.core.ListenSequence;
21+
import com.google.firebase.firestore.model.Document;
2122
import com.google.firebase.firestore.model.DocumentKey;
22-
import com.google.firebase.firestore.model.MutableDocument;
2323
import com.google.firebase.firestore.util.Consumer;
24+
import java.util.ArrayList;
2425
import java.util.HashMap;
26+
import java.util.List;
2527
import java.util.Map;
2628

2729
/** Provides LRU garbage collection functionality for MemoryPersistence. */
@@ -115,17 +117,17 @@ public int removeTargets(long upperBound, SparseArray<?> activeTargetIds) {
115117

116118
@Override
117119
public int removeOrphanedDocuments(long upperBound) {
118-
int count = 0;
119120
MemoryRemoteDocumentCache cache = persistence.getRemoteDocumentCache();
120-
for (MutableDocument doc : cache.getDocuments()) {
121+
List<DocumentKey> docsToRemove = new ArrayList<>();
122+
for (Document doc : cache.getDocuments()) {
121123
DocumentKey key = doc.getKey();
122124
if (!isPinned(key, upperBound)) {
123-
cache.remove(key);
125+
docsToRemove.add(key);
124126
orphanedSequenceNumbers.remove(key);
125-
count++;
126127
}
127128
}
128-
return count;
129+
cache.removeAll(docsToRemove);
130+
return docsToRemove.size();
129131
}
130132

131133
@Override

firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414

1515
package com.google.firebase.firestore.local;
1616

17+
import static com.google.firebase.firestore.model.DocumentCollections.emptyDocumentMap;
1718
import static com.google.firebase.firestore.util.Assert.hardAssert;
1819

1920
import androidx.annotation.NonNull;
2021
import com.google.firebase.database.collection.ImmutableSortedMap;
22+
import com.google.firebase.firestore.model.Document;
2123
import com.google.firebase.firestore.model.DocumentKey;
2224
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2325
import com.google.firebase.firestore.model.MutableDocument;
2426
import com.google.firebase.firestore.model.ResourcePath;
2527
import com.google.firebase.firestore.model.SnapshotVersion;
28+
import java.util.Collection;
2629
import java.util.HashMap;
2730
import java.util.Iterator;
2831
import java.util.Map;
@@ -60,8 +63,16 @@ public void add(MutableDocument document, SnapshotVersion readTime) {
6063
}
6164

6265
@Override
63-
public void remove(DocumentKey key) {
64-
docs = docs.remove(key);
66+
public void removeAll(Collection<DocumentKey> keys) {
67+
hardAssert(indexManager != null, "setIndexManager() not called");
68+
69+
ImmutableSortedMap<DocumentKey, Document> deletedDocs = emptyDocumentMap();
70+
for (DocumentKey key : keys) {
71+
docs = docs.remove(key);
72+
deletedDocs =
73+
deletedDocs.insert(key, MutableDocument.newNoDocument(key, SnapshotVersion.NONE));
74+
}
75+
indexManager.updateIndexEntries(deletedDocs);
6576
}
6677

6778
@Override

firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.firebase.firestore.model.MutableDocument;
2020
import com.google.firebase.firestore.model.ResourcePath;
2121
import com.google.firebase.firestore.model.SnapshotVersion;
22+
import java.util.Collection;
2223
import java.util.Map;
2324

2425
/**
@@ -44,8 +45,8 @@ interface RemoteDocumentCache {
4445
*/
4546
void add(MutableDocument document, SnapshotVersion readTime);
4647

47-
/** Removes the cached entry for the given key (no-op if no entry exists). */
48-
void remove(DocumentKey documentKey);
48+
/** Removes the cached entries for the given keys (no-op if no entry exists). */
49+
void removeAll(Collection<DocumentKey> keys);
4950

5051
/**
5152
* Looks up an entry in the cache.

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ public void deleteFieldIndex(FieldIndex index) {
225225
@Override
226226
public void updateIndexEntries(ImmutableSortedMap<DocumentKey, Document> documents) {
227227
hardAssert(started, "IndexManager not started");
228+
if (!Persistence.INDEXING_SUPPORT_ENABLED) return;
229+
228230
for (Map.Entry<DocumentKey, Document> entry : documents) {
229231
Collection<FieldIndex> fieldIndexes = getFieldIndexes(entry.getKey().getCollectionGroup());
230232
for (FieldIndex fieldIndex : fieldIndexes) {

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteLruReferenceDelegate.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import com.google.firebase.firestore.model.DocumentKey;
2222
import com.google.firebase.firestore.model.ResourcePath;
2323
import com.google.firebase.firestore.util.Consumer;
24+
import java.util.ArrayList;
25+
import java.util.List;
2426

2527
/** Provides LRU functionality for SQLite persistence. */
2628
class SQLiteLruReferenceDelegate implements ReferenceDelegate, LruDelegate {
@@ -159,6 +161,7 @@ public int removeOrphanedDocuments(long upperBound) {
159161

160162
boolean resultsRemaining = true;
161163

164+
List<DocumentKey> docsToRemove = new ArrayList<>();
162165
while (resultsRemaining) {
163166
int rowsProccessed =
164167
persistence
@@ -171,14 +174,15 @@ public int removeOrphanedDocuments(long upperBound) {
171174
DocumentKey key = DocumentKey.fromPath(path);
172175
if (!isPinned(key)) {
173176
count[0]++;
174-
persistence.getRemoteDocumentCache().remove(key);
177+
docsToRemove.add(key);
175178
removeSentinel(key);
176179
}
177180
});
178181

179182
resultsRemaining = (rowsProccessed == REMOVE_ORPHANED_DOCUMENTS_BATCH_SIZE);
180183
}
181184

185+
persistence.getRemoteDocumentCache().removeAll(docsToRemove);
182186
return count[0];
183187
}
184188

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.firebase.firestore.util.Assert.fail;
1818
import static com.google.firebase.firestore.util.Assert.hardAssert;
19+
import static com.google.firebase.firestore.util.Util.repeatSequence;
1920

2021
import android.content.Context;
2122
import android.database.Cursor;
@@ -675,23 +676,27 @@ boolean hasMoreSubqueries() {
675676
return argsIter.hasNext();
676677
}
677678

678-
/** Performs the next subquery and returns a {@link Query} object for method chaining. */
679-
Query performNextSubquery() {
680-
++subqueriesPerformed;
681-
679+
private Object[] getNextSubqueryArgs() {
682680
List<Object> subqueryArgs = new ArrayList<>(argsHead);
683-
StringBuilder placeholdersBuilder = new StringBuilder();
684681
for (int i = 0; argsIter.hasNext() && i < LIMIT - argsHead.size(); i++) {
685-
if (i > 0) {
686-
placeholdersBuilder.append(", ");
687-
}
688-
placeholdersBuilder.append("?");
689-
690682
subqueryArgs.add(argsIter.next());
691683
}
692-
String placeholders = placeholdersBuilder.toString();
684+
return subqueryArgs.toArray();
685+
}
693686

694-
return db.query(head + placeholders + tail).binding(subqueryArgs.toArray());
687+
/** Performs the next subquery and returns a {@link Query} object for method chaining. */
688+
Query performNextSubquery() {
689+
++subqueriesPerformed;
690+
Object[] subqueryArgs = getNextSubqueryArgs();
691+
return db.query(head + repeatSequence("?", subqueryArgs.length, ", ") + tail)
692+
.binding(subqueryArgs);
693+
}
694+
695+
/** Executes the next subquery. */
696+
void executeNextSubquery() {
697+
++subqueriesPerformed;
698+
Object[] subqueryArgs = getNextSubqueryArgs();
699+
db.execute(head + repeatSequence("?", subqueryArgs.length, ", ") + tail, subqueryArgs);
695700
}
696701

697702
/** How many subqueries were performed. */

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414

1515
package com.google.firebase.firestore.local;
1616

17+
import static com.google.firebase.firestore.model.DocumentCollections.emptyDocumentMap;
1718
import static com.google.firebase.firestore.util.Assert.fail;
1819
import static com.google.firebase.firestore.util.Assert.hardAssert;
1920
import static com.google.firebase.firestore.util.Util.firstNEntries;
2021
import static com.google.firebase.firestore.util.Util.repeatSequence;
2122

2223
import androidx.annotation.VisibleForTesting;
2324
import com.google.firebase.Timestamp;
25+
import com.google.firebase.database.collection.ImmutableSortedMap;
26+
import com.google.firebase.firestore.model.Document;
2427
import com.google.firebase.firestore.model.DocumentKey;
2528
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2629
import com.google.firebase.firestore.model.MutableDocument;
@@ -31,6 +34,7 @@
3134
import com.google.protobuf.InvalidProtocolBufferException;
3235
import com.google.protobuf.MessageLite;
3336
import java.util.ArrayList;
37+
import java.util.Collection;
3438
import java.util.Collections;
3539
import java.util.HashMap;
3640
import java.util.List;
@@ -79,10 +83,26 @@ public void add(MutableDocument document, SnapshotVersion readTime) {
7983
}
8084

8185
@Override
82-
public void remove(DocumentKey documentKey) {
83-
String path = EncodedPath.encode(documentKey.getPath());
86+
public void removeAll(Collection<DocumentKey> keys) {
87+
if (keys.isEmpty()) return;
88+
89+
List<Object> encodedPaths = new ArrayList<>();
90+
ImmutableSortedMap<DocumentKey, Document> deletedDocs = emptyDocumentMap();
91+
92+
for (DocumentKey key : keys) {
93+
encodedPaths.add(EncodedPath.encode(key.getPath()));
94+
deletedDocs =
95+
deletedDocs.insert(key, MutableDocument.newNoDocument(key, SnapshotVersion.NONE));
96+
}
97+
98+
SQLitePersistence.LongQuery longQuery =
99+
new SQLitePersistence.LongQuery(
100+
db, "DELETE FROM remote_documents WHERE path IN (", encodedPaths, ")");
101+
while (longQuery.hasMoreSubqueries()) {
102+
longQuery.executeNextSubquery();
103+
}
84104

85-
db.execute("DELETE FROM remote_documents WHERE path = ?", path);
105+
indexManager.updateIndexEntries(deletedDocs);
86106
}
87107

88108
@Override

firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.firebase.firestore.model.mutation.Mutation;
2929
import com.google.firebase.firestore.model.mutation.MutationBatch;
3030
import com.google.protobuf.ByteString;
31+
import java.util.Collection;
3132
import java.util.List;
3233
import java.util.Map;
3334

@@ -124,8 +125,8 @@ public void add(MutableDocument document, SnapshotVersion readTime) {
124125
}
125126

126127
@Override
127-
public void remove(DocumentKey documentKey) {
128-
subject.remove(documentKey);
128+
public void removeAll(Collection<DocumentKey> keys) {
129+
subject.removeAll(keys);
129130
}
130131

131132
@Override

firebase-firestore/src/test/java/com/google/firebase/firestore/local/IndexingEnabledSQLiteLocalStoreTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
import static com.google.common.truth.Truth.assertThat;
1818
import static com.google.firebase.firestore.testutil.TestUtil.addedRemoteEvent;
19+
import static com.google.firebase.firestore.testutil.TestUtil.deletedDoc;
1920
import static com.google.firebase.firestore.testutil.TestUtil.doc;
2021
import static com.google.firebase.firestore.testutil.TestUtil.fieldIndex;
2122
import static com.google.firebase.firestore.testutil.TestUtil.filter;
2223
import static com.google.firebase.firestore.testutil.TestUtil.map;
2324
import static com.google.firebase.firestore.testutil.TestUtil.query;
25+
import static com.google.firebase.firestore.testutil.TestUtil.updateRemoteEvent;
26+
import static java.util.Collections.emptyList;
2427
import static java.util.Collections.singletonList;
2528

2629
import com.google.firebase.firestore.core.Query;
@@ -100,4 +103,30 @@ public void testUsesPartialIndexesWhenAvailable() {
100103
assertRemoteDocumentsRead(/* byKey= */ 1, /* byQuery= */ 1);
101104
assertQueryReturned("coll/a", "coll/b");
102105
}
106+
107+
@Test
108+
public void testDeletedDocumentRemovesIndex() {
109+
FieldIndex index = fieldIndex("coll", 0, FieldIndex.INITIAL_STATE, "matches", Kind.ASCENDING);
110+
configureFieldIndexes(singletonList(index));
111+
112+
Query query = query("coll").filter(filter("matches", "==", true));
113+
int targetId = allocateQuery(query);
114+
115+
applyRemoteEvent(addedRemoteEvent(doc("coll/a", 10, map("matches", true)), targetId));
116+
117+
// Add the document to the index
118+
backfillIndexes();
119+
120+
executeQuery(query);
121+
assertRemoteDocumentsRead(/* byKey= */ 1, /* byQuery= */ 0);
122+
assertQueryReturned("coll/a");
123+
124+
applyRemoteEvent(
125+
updateRemoteEvent(deletedDoc("coll/a", 0), singletonList(targetId), emptyList()));
126+
127+
// No backfill needed for deleted document.
128+
executeQuery(query);
129+
assertRemoteDocumentsRead(/* byKey= */ 0, /* byQuery= */ 0);
130+
assertQueryReturned();
131+
}
103132
}

firebase-firestore/src/test/java/com/google/firebase/firestore/local/MutationQueueTestCase.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public void setUp() {
7272
persistence.getReferenceDelegate().setInMemoryPins(new ReferenceSet());
7373
IndexManager indexManager = persistence.getIndexManager(User.UNAUTHENTICATED);
7474
indexManager.start();
75+
persistence.getRemoteDocumentCache().setIndexManager(indexManager);
7576
mutationQueue = persistence.getMutationQueue(User.UNAUTHENTICATED, indexManager);
7677
mutationQueue.start();
7778
}

0 commit comments

Comments
 (0)