Skip to content

Commit eca7547

Browse files
Optimize query execution by deferring query evaluation
1 parent 759a50b commit eca7547

File tree

11 files changed

+109
-183
lines changed

11 files changed

+109
-183
lines changed

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

Lines changed: 11 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -322,26 +322,21 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
322322
/** Queries the remote documents and overlays by doing a full collection scan. */
323323
private ImmutableSortedMap<DocumentKey, Document>
324324
getDocumentsMatchingCollectionQueryFromOverlayCache(Query query, IndexOffset offset) {
325-
ImmutableSortedMap<DocumentKey, MutableDocument> remoteDocuments =
326-
remoteDocumentCache.getAllDocumentsMatchingQuery(query, offset);
325+
Map<DocumentKey, MutableDocument> remoteDocuments =
326+
remoteDocumentCache.getAll(query.getPath(), offset);
327327
Map<DocumentKey, Mutation> overlays = documentOverlayCache.getOverlays(query.getPath(), -1);
328328

329-
// As documents might match the query because of their overlay we need to include all documents
330-
// in the result.
331-
Set<DocumentKey> missingDocuments = new HashSet<>();
329+
// As documents might match the query because of their overlay we need to include documents
330+
// for all overlays in the initial document set.
332331
for (Map.Entry<DocumentKey, Mutation> entry : overlays.entrySet()) {
333332
if (!remoteDocuments.containsKey(entry.getKey())) {
334-
missingDocuments.add(entry.getKey());
333+
remoteDocuments.put(entry.getKey(), MutableDocument.newInvalidDocument(entry.getKey()));
335334
}
336335
}
337-
for (Map.Entry<DocumentKey, MutableDocument> entry :
338-
remoteDocumentCache.getAll(missingDocuments).entrySet()) {
339-
remoteDocuments = remoteDocuments.insert(entry.getKey(), entry.getValue());
340-
}
341336

342337
// Apply the overlays and match against the query.
343338
ImmutableSortedMap<DocumentKey, Document> results = emptyDocumentMap();
344-
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments) {
339+
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments.entrySet()) {
345340
Mutation overlay = overlays.get(docEntry.getKey());
346341
if (overlay != null) {
347342
overlay.applyToLocalView(docEntry.getValue(), null, Timestamp.now());
@@ -358,14 +353,12 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
358353
/** Queries the remote documents and mutation queue, by doing a full collection scan. */
359354
private ImmutableSortedMap<DocumentKey, Document>
360355
getDocumentsMatchingCollectionQueryFromMutationQueue(Query query, IndexOffset offset) {
361-
ImmutableSortedMap<DocumentKey, MutableDocument> remoteDocuments =
362-
remoteDocumentCache.getAllDocumentsMatchingQuery(query, offset);
356+
Map<DocumentKey, MutableDocument> remoteDocuments =
357+
remoteDocumentCache.getAll(query.getPath(), offset);
363358

364359
// TODO(indexing): We should plumb sinceReadTime through to the mutation queue
365360
List<MutationBatch> matchingBatches = mutationQueue.getAllMutationBatchesAffectingQuery(query);
366361

367-
remoteDocuments = addMissingBaseDocuments(matchingBatches, remoteDocuments);
368-
369362
for (MutationBatch batch : matchingBatches) {
370363
for (Mutation mutation : batch.getMutations()) {
371364
// Only process documents belonging to the collection.
@@ -378,18 +371,18 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
378371
if (document == null) {
379372
// Create invalid document to apply mutations on top of
380373
document = MutableDocument.newInvalidDocument(key);
381-
remoteDocuments = remoteDocuments.insert(key, document);
374+
remoteDocuments.put(key, document);
382375
}
383376
mutation.applyToLocalView(
384377
document, FieldMask.fromSet(new HashSet<>()), batch.getLocalWriteTime());
385378
if (!document.isFoundDocument()) {
386-
remoteDocuments = remoteDocuments.remove(key);
379+
remoteDocuments.remove(key);
387380
}
388381
}
389382
}
390383

391384
ImmutableSortedMap<DocumentKey, Document> results = emptyDocumentMap();
392-
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments) {
385+
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments.entrySet()) {
393386
// Finally, insert the documents that still match the query
394387
if (query.matches(docEntry.getValue())) {
395388
results = results.insert(docEntry.getKey(), docEntry.getValue());
@@ -398,35 +391,4 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
398391

399392
return results;
400393
}
401-
402-
/**
403-
* It is possible that a {@code PatchMutation} can make a document match a query, even if the
404-
* version in the {@code RemoteDocumentCache} is not a match yet (waiting for server to ack). To
405-
* handle this, we find all document keys affected by the {@code PatchMutation}s that are not in
406-
* {@code existingDocs} yet, and back fill them via {@code remoteDocumentCache.getAll}, otherwise
407-
* those {@code PatchMutation}s will be ignored because no base document can be found, and lead to
408-
* missing results for the query.
409-
*/
410-
private ImmutableSortedMap<DocumentKey, MutableDocument> addMissingBaseDocuments(
411-
List<MutationBatch> matchingBatches,
412-
ImmutableSortedMap<DocumentKey, MutableDocument> existingDocs) {
413-
HashSet<DocumentKey> missingDocKeys = new HashSet<>();
414-
for (MutationBatch batch : matchingBatches) {
415-
for (Mutation mutation : batch.getMutations()) {
416-
if (mutation instanceof PatchMutation && !existingDocs.containsKey(mutation.getKey())) {
417-
missingDocKeys.add(mutation.getKey());
418-
}
419-
}
420-
}
421-
422-
ImmutableSortedMap<DocumentKey, MutableDocument> mergedDocs = existingDocs;
423-
Map<DocumentKey, MutableDocument> missingDocs = remoteDocumentCache.getAll(missingDocKeys);
424-
for (Map.Entry<DocumentKey, MutableDocument> entry : missingDocs.entrySet()) {
425-
if (entry.getValue().isFoundDocument()) {
426-
mergedDocs = mergedDocs.insert(entry.getKey(), entry.getValue());
427-
}
428-
}
429-
430-
return mergedDocs;
431-
}
432394
}

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

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@
1414

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

17-
import static com.google.firebase.firestore.model.DocumentCollections.emptyMutableDocumentMap;
1817
import static com.google.firebase.firestore.util.Assert.hardAssert;
1918

2019
import androidx.annotation.NonNull;
2120
import com.google.firebase.database.collection.ImmutableSortedMap;
22-
import com.google.firebase.firestore.core.Query;
2321
import com.google.firebase.firestore.model.DocumentKey;
2422
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2523
import com.google.firebase.firestore.model.MutableDocument;
@@ -90,26 +88,11 @@ public Map<DocumentKey, MutableDocument> getAll(
9088
List<ResourcePath> collectionParents = indexManager.getCollectionParents(collectionGroup);
9189
Set<MutableDocument> allDocuments =
9290
new TreeSet<>((l, r) -> IndexOffset.fromDocument(l).compareTo(IndexOffset.fromDocument(r)));
93-
Map<DocumentKey, MutableDocument> matchingDocuments = new HashMap<>();
94-
9591
for (ResourcePath collectionParent : collectionParents) {
96-
ResourcePath documentParent = collectionParent.append(collectionGroup);
97-
Iterator<Map.Entry<DocumentKey, MutableDocument>> iterator =
98-
docs.iteratorFrom(DocumentKey.fromPath(documentParent.append("")));
99-
while (iterator.hasNext()) {
100-
Map.Entry<DocumentKey, MutableDocument> entry = iterator.next();
101-
DocumentKey key = entry.getKey();
102-
103-
if (!documentParent.isPrefixOf(key.getPath())) {
104-
break;
105-
}
106-
107-
if (IndexOffset.fromDocument(entry.getValue()).compareTo(offset) > 0) {
108-
allDocuments.add(entry.getValue());
109-
}
110-
}
92+
allDocuments.addAll(getAll(collectionParent.append(collectionGroup), offset).values());
11193
}
11294

95+
Map<DocumentKey, MutableDocument> matchingDocuments = new HashMap<>();
11396
Iterator<MutableDocument> it = allDocuments.iterator();
11497
while (it.hasNext() && matchingDocuments.size() < count) {
11598
MutableDocument document = it.next();
@@ -120,41 +103,35 @@ public Map<DocumentKey, MutableDocument> getAll(
120103
}
121104

122105
@Override
123-
public ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQuery(
124-
Query query, IndexOffset offset) {
125-
hardAssert(
126-
!query.isCollectionGroupQuery(),
127-
"CollectionGroup queries should be handled in LocalDocumentsView");
128-
ImmutableSortedMap<DocumentKey, MutableDocument> result = emptyMutableDocumentMap();
106+
public Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset) {
107+
Map<DocumentKey, MutableDocument> result = new HashMap<>();
129108

130109
// Documents are ordered by key, so we can use a prefix scan to narrow down the documents
131110
// we need to match the query against.
132-
ResourcePath queryPath = query.getPath();
133-
DocumentKey prefix = DocumentKey.fromPath(queryPath.append(""));
111+
DocumentKey prefix = DocumentKey.fromPath(collection.append(""));
134112
Iterator<Map.Entry<DocumentKey, MutableDocument>> iterator = docs.iteratorFrom(prefix);
135113

136114
while (iterator.hasNext()) {
137115
Map.Entry<DocumentKey, MutableDocument> entry = iterator.next();
116+
MutableDocument doc = entry.getValue();
138117

139118
DocumentKey key = entry.getKey();
140-
if (!queryPath.isPrefixOf(key.getPath())) {
119+
if (!collection.isPrefixOf(key.getPath())) {
120+
// We are now scanning the next collection. Abort.
141121
break;
142122
}
143123

144-
MutableDocument doc = entry.getValue();
145-
if (!doc.isFoundDocument()) {
124+
if (key.getPath().length() > collection.length() + 1) {
125+
// Exclude entries from subcollections.
146126
continue;
147127
}
148128

149129
if (IndexOffset.fromDocument(doc).compareTo(offset) <= 0) {
130+
// The document sorts before the offset.
150131
continue;
151132
}
152133

153-
if (!query.matches(doc)) {
154-
continue;
155-
}
156-
157-
result = result.insert(doc.getKey(), doc.clone());
134+
result.put(doc.getKey(), doc.clone());
158135
}
159136

160137
return result;

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414

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

17-
import com.google.firebase.database.collection.ImmutableSortedMap;
18-
import com.google.firebase.firestore.core.Query;
1917
import com.google.firebase.firestore.model.DocumentKey;
2018
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2119
import com.google.firebase.firestore.model.MutableDocument;
20+
import com.google.firebase.firestore.model.ResourcePath;
2221
import com.google.firebase.firestore.model.SnapshotVersion;
2322
import java.util.Map;
2423

@@ -80,19 +79,13 @@ interface RemoteDocumentCache {
8079
Map<DocumentKey, MutableDocument> getAll(String collectionGroup, IndexOffset offset, int count);
8180

8281
/**
83-
* Executes a query against the cached Document entries
82+
* Returns the documents from the provided collection.
8483
*
85-
* <p>Implementations may return extra documents if convenient. The results should be re-filtered
86-
* by the consumer before presenting them to the user.
87-
*
88-
* <p>Cached entries for non-existing documents have no bearing on query results.
89-
*
90-
* @param query The query to match documents against.
84+
* @param collection The collection to read.
9185
* @param offset The read time and document key to start scanning at (exclusive).
9286
* @return The set of matching documents.
9387
*/
94-
ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQuery(
95-
Query query, IndexOffset offset);
88+
Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset);
9689

9790
/** Returns the latest read time of any document in the cache. */
9891
SnapshotVersion getLatestReadTime();

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import androidx.annotation.Nullable;
2525
import com.google.firebase.Timestamp;
2626
import com.google.firebase.database.collection.ImmutableSortedMap;
27-
import com.google.firebase.database.collection.LLRBNode;
2827
import com.google.firebase.firestore.auth.User;
2928
import com.google.firebase.firestore.core.Bound;
3029
import com.google.firebase.firestore.core.FieldFilter;

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

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@
2020

2121
import androidx.annotation.VisibleForTesting;
2222
import com.google.firebase.Timestamp;
23-
import com.google.firebase.database.collection.ImmutableSortedMap;
24-
import com.google.firebase.firestore.core.Query;
25-
import com.google.firebase.firestore.model.DocumentCollections;
2623
import com.google.firebase.firestore.model.DocumentKey;
2724
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2825
import com.google.firebase.firestore.model.MutableDocument;
@@ -229,23 +226,9 @@ private Map<DocumentKey, MutableDocument> getAll(
229226
}
230227

231228
@Override
232-
public ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQuery(
233-
final Query query, IndexOffset offset) {
234-
hardAssert(
235-
!query.isCollectionGroupQuery(),
236-
"CollectionGroup queries should be handled in LocalDocumentsView");
237-
238-
Map<DocumentKey, MutableDocument> allDocuments =
239-
getAll(Collections.singletonList(query.getPath()), offset, Integer.MAX_VALUE);
240-
241-
ImmutableSortedMap<DocumentKey, MutableDocument> matchingDocuments =
242-
DocumentCollections.emptyMutableDocumentMap();
243-
for (MutableDocument document : allDocuments.values()) {
244-
if (document.isFoundDocument() && query.matches(document)) {
245-
matchingDocuments = matchingDocuments.insert(document.getKey(), document);
246-
}
247-
}
248-
return matchingDocuments;
229+
public Map<DocumentKey, MutableDocument> getAll(
230+
final ResourcePath collection, IndexOffset offset) {
231+
return getAll(Collections.singletonList(collection), offset, Integer.MAX_VALUE);
249232
}
250233

251234
@Override

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class SQLiteSchema {
4949
* The version of the schema. Increase this by one for each migration added to runMigrations
5050
* below.
5151
*/
52-
static final int VERSION = 13;
52+
static final int VERSION = 14;
5353

5454
static final int OVERLAY_SUPPORT_VERSION = VERSION + 1;
5555

@@ -178,6 +178,10 @@ void runSchemaUpgrades(int fromVersion, int toVersion) {
178178
ensurePathLength();
179179
}
180180

181+
if (fromVersion < 14 && toVersion >= 14) {
182+
ensureReadTime();
183+
}
184+
181185
/*
182186
* Adding a new migration? READ THIS FIRST!
183187
*
@@ -658,6 +662,12 @@ private void ensurePathLength() {
658662
} while (resultsRemaining[0]);
659663
}
660664

665+
/** Initialize the remote_document's read_time column with 0 values if they are not set. */
666+
private void ensureReadTime() {
667+
db.execSQL(
668+
"UPDATE remote_documents SET read_time_seconds = 0, read_time_nanos = 0 WHERE read_time_seconds IS NULL");
669+
}
670+
661671
private void createBundleCache() {
662672
ifTablesDontExist(
663673
new String[] {"bundles", "named_queries"},

firebase-firestore/src/main/java/com/google/firebase/firestore/model/MutableDocument.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,25 @@ private enum DocumentState {
6262
private final DocumentKey key;
6363
private DocumentType documentType;
6464
private SnapshotVersion version;
65-
private SnapshotVersion readTime = SnapshotVersion.NONE;
65+
private SnapshotVersion readTime;
6666
private ObjectValue value;
6767
private DocumentState documentState;
6868

6969
private MutableDocument(DocumentKey key) {
7070
this.key = key;
71+
this.readTime = SnapshotVersion.NONE;
7172
}
7273

7374
private MutableDocument(
7475
DocumentKey key,
7576
DocumentType documentType,
7677
SnapshotVersion version,
78+
SnapshotVersion readTime,
7779
ObjectValue value,
7880
DocumentState documentState) {
7981
this.key = key;
8082
this.version = version;
83+
this.readTime = readTime;
8184
this.documentType = documentType;
8285
this.documentState = documentState;
8386
this.value = value;
@@ -92,6 +95,7 @@ public static MutableDocument newInvalidDocument(DocumentKey documentKey) {
9295
documentKey,
9396
DocumentType.INVALID,
9497
SnapshotVersion.NONE,
98+
SnapshotVersion.NONE,
9599
new ObjectValue(),
96100
DocumentState.SYNCED);
97101
}
@@ -226,7 +230,7 @@ public boolean isUnknownDocument() {
226230
@Override
227231
@NonNull
228232
public MutableDocument clone() {
229-
return new MutableDocument(key, documentType, version, value.clone(), documentState);
233+
return new MutableDocument(key, documentType, version, readTime, value.clone(), documentState);
230234
}
231235

232236
@Override
@@ -238,6 +242,8 @@ public boolean equals(Object o) {
238242

239243
if (!key.equals(document.key)) return false;
240244
if (!version.equals(document.version)) return false;
245+
// TODO(mrschmidt): Include readTime (requires a lot of test updates)
246+
// if (!readTime.equals(document.readTime)) return false;
241247
if (!documentType.equals(document.documentType)) return false;
242248
if (!documentState.equals(document.documentState)) return false;
243249
return value.equals(document.value);

0 commit comments

Comments
 (0)