Skip to content

Commit 0d2e108

Browse files
author
Hui-Wu
authored
Fix missing query results when it is a match after a local patch mutation. (#1957)
When a local patch mutation is not acked by server yet, but the mutation makes a document matching a query, the document might not show up in the compensated query results. This PR fixes the bug. See firebase/firebase-android-sdk#155
1 parent fb5c762 commit 0d2e108

File tree

2 files changed

+97
-19
lines changed

2 files changed

+97
-19
lines changed

packages/firestore/src/local/local_documents_view.ts

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Query } from '../core/query';
1919
import { SnapshotVersion } from '../core/snapshot_version';
2020
import {
2121
DocumentKeySet,
22+
documentKeySet,
2223
DocumentMap,
2324
documentMap,
2425
MaybeDocumentMap,
@@ -34,6 +35,7 @@ import { ResourcePath } from '../model/path';
3435
import { assert } from '../util/assert';
3536
import { IndexManager } from './index_manager';
3637
import { MutationQueue } from './mutation_queue';
38+
import { PatchMutation } from '../model/mutation';
3739
import { PersistenceTransaction } from './persistence';
3840
import { PersistencePromise } from './persistence_promise';
3941
import { RemoteDocumentCache } from './remote_document_cache';
@@ -49,7 +51,7 @@ export class LocalDocumentsView {
4951
private remoteDocumentCache: RemoteDocumentCache,
5052
private mutationQueue: MutationQueue,
5153
private indexManager: IndexManager
52-
) {}
54+
) { }
5355

5456
/**
5557
* Get the local view of the document identified by `key`.
@@ -208,6 +210,7 @@ export class LocalDocumentsView {
208210
): PersistencePromise<DocumentMap> {
209211
// Query the remote documents and overlay mutations.
210212
let results: DocumentMap;
213+
let mutationBatches: MutationBatch[];
211214
return this.remoteDocumentCache
212215
.getDocumentsMatchingQuery(transaction, query)
213216
.next(queryResults => {
@@ -218,27 +221,41 @@ export class LocalDocumentsView {
218221
);
219222
})
220223
.next(matchingMutationBatches => {
221-
for (const batch of matchingMutationBatches) {
222-
for (const mutation of batch.mutations) {
223-
const key = mutation.key;
224-
// Only process documents belonging to the collection.
225-
if (!query.path.isImmediateParentOf(key.path)) {
226-
continue;
224+
mutationBatches = matchingMutationBatches;
225+
// It is possible that a PatchMutation can make a document match a query, even if
226+
// the version in the RemoteDocumentCache is not a match yet (waiting for server
227+
// to ack). To handle this, we find all document keys affected by the PatchMutations
228+
// that are not in `result` yet, and back fill them via `remoteDocumentCache.getEntries`,
229+
// otherwise those `PatchMutations` will be ignored because no base document can be found,
230+
// and lead to missing result for the query.
231+
return this.getMissingBaseDocuments(
232+
transaction,
233+
mutationBatches,
234+
results
235+
).next(missingBaseDocuments => {
236+
missingBaseDocuments.forEach((key, doc) => {
237+
if (doc !== null && doc instanceof Document) {
238+
results = results.insert(key, doc);
227239
}
240+
});
228241

229-
const baseDoc = results.get(key);
230-
const mutatedDoc = mutation.applyToLocalView(
231-
baseDoc,
232-
baseDoc,
233-
batch.localWriteTime
234-
);
235-
if (mutatedDoc instanceof Document) {
236-
results = results.insert(key, mutatedDoc);
237-
} else {
238-
results = results.remove(key);
242+
for (const batch of mutationBatches) {
243+
for (const mutation of batch.mutations) {
244+
const key = mutation.key;
245+
const baseDoc = results.get(key);
246+
const mutatedDoc = mutation.applyToLocalView(
247+
baseDoc,
248+
baseDoc,
249+
batch.localWriteTime
250+
);
251+
if (mutatedDoc instanceof Document) {
252+
results = results.insert(key, mutatedDoc);
253+
} else {
254+
results = results.remove(key);
255+
}
239256
}
240257
}
241-
}
258+
});
242259
})
243260
.next(() => {
244261
// Finally, filter out any documents that don't actually match
@@ -252,4 +269,28 @@ export class LocalDocumentsView {
252269
return results;
253270
});
254271
}
272+
273+
private getMissingBaseDocuments(
274+
transaction: PersistenceTransaction,
275+
matchingMutationBatches: MutationBatch[],
276+
existingDocuments: DocumentMap
277+
): PersistencePromise<NullableMaybeDocumentMap> {
278+
let missingBaseDocEntriesForPatching = documentKeySet();
279+
for (const batch of matchingMutationBatches) {
280+
for (const mutation of batch.mutations) {
281+
if (
282+
mutation instanceof PatchMutation &&
283+
existingDocuments.get(mutation.key) === null
284+
) {
285+
missingBaseDocEntriesForPatching = missingBaseDocEntriesForPatching.add(
286+
mutation.key
287+
);
288+
}
289+
}
290+
}
291+
return this.remoteDocumentCache.getEntries(
292+
transaction,
293+
missingBaseDocEntriesForPatching
294+
);
295+
}
255296
}

packages/firestore/test/unit/specs/query_spec.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import { Query } from '../../../src/core/query';
19-
import { doc, filter } from '../../util/helpers';
19+
import { doc, filter, path } from '../../util/helpers';
2020

2121
import { Document } from '../../../src/model/document';
2222
import { ResourcePath } from '../../../src/model/path';
@@ -98,4 +98,41 @@ describeSpec('Queries:', [], () => {
9898
hasPendingWrites: true
9999
});
100100
});
101+
102+
specTest(
103+
'Latency-compensated updates are included in query results',
104+
[],
105+
() => {
106+
const fullQuery = Query.atPath(path('collection'));
107+
const filteredQuery = fullQuery.addFilter(filter('match', '==', true));
108+
const docA = doc('collection/a', 1000, { match: false });
109+
const docAv2Local = doc(
110+
'collection/a',
111+
1000,
112+
{ match: true },
113+
{ hasLocalMutations: true }
114+
);
115+
116+
return (
117+
spec()
118+
.userListens(fullQuery)
119+
.watchAcksFull(fullQuery, 1000, docA)
120+
.expectEvents(fullQuery, { added: [docA] })
121+
122+
// patch docA so that it will now match the filtered query.
123+
.userPatches('collection/a', { match: true })
124+
.expectEvents(fullQuery, {
125+
modified: [docAv2Local],
126+
hasPendingWrites: true
127+
})
128+
// Make sure docA shows up in filtered query.
129+
.userListens(filteredQuery)
130+
.expectEvents(filteredQuery, {
131+
added: [docAv2Local],
132+
fromCache: true,
133+
hasPendingWrites: true
134+
})
135+
);
136+
}
137+
);
101138
});

0 commit comments

Comments
 (0)