-
Notifications
You must be signed in to change notification settings - Fork 944
Fix missing query results when it is a match after a local patch mutation. #1957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
71f2411
a6b40ba
96aed4d
4c726ef
0a49cd0
a6c5635
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ import { Query } from '../core/query'; | |
import { SnapshotVersion } from '../core/snapshot_version'; | ||
import { | ||
DocumentKeySet, | ||
documentKeySet, | ||
DocumentMap, | ||
documentMap, | ||
MaybeDocumentMap, | ||
|
@@ -34,6 +35,7 @@ import { ResourcePath } from '../model/path'; | |
import { assert } from '../util/assert'; | ||
import { IndexManager } from './index_manager'; | ||
import { MutationQueue } from './mutation_queue'; | ||
import { PatchMutation } from '../model/mutation'; | ||
import { PersistenceTransaction } from './persistence'; | ||
import { PersistencePromise } from './persistence_promise'; | ||
import { RemoteDocumentCache } from './remote_document_cache'; | ||
|
@@ -49,7 +51,7 @@ export class LocalDocumentsView { | |
private remoteDocumentCache: RemoteDocumentCache, | ||
private mutationQueue: MutationQueue, | ||
private indexManager: IndexManager | ||
) {} | ||
) { } | ||
|
||
/** | ||
* Get the local view of the document identified by `key`. | ||
|
@@ -208,6 +210,7 @@ export class LocalDocumentsView { | |
): PersistencePromise<DocumentMap> { | ||
// Query the remote documents and overlay mutations. | ||
let results: DocumentMap; | ||
let mutationBatches: MutationBatch[]; | ||
return this.remoteDocumentCache | ||
.getDocumentsMatchingQuery(transaction, query) | ||
.next(queryResults => { | ||
|
@@ -218,27 +221,41 @@ export class LocalDocumentsView { | |
); | ||
}) | ||
.next(matchingMutationBatches => { | ||
for (const batch of matchingMutationBatches) { | ||
for (const mutation of batch.mutations) { | ||
const key = mutation.key; | ||
// Only process documents belonging to the collection. | ||
if (!query.path.isImmediateParentOf(key.path)) { | ||
continue; | ||
mutationBatches = matchingMutationBatches; | ||
// It is possible that a PatchMutation can make a document match a query, even if | ||
// the version in the RemoteDocumentCache is not a match yet (waiting for server | ||
// to ack). To handle this, we find all document keys affected by the PatchMutations | ||
// that are not in `result` yet, and back fill them via `remoteDocumentCache.getEntries`, | ||
// otherwise those `PatchMutations` will be ignored because no base document can be found, | ||
// and lead to missing result for the query. | ||
return this.getMissingBaseDocuments( | ||
transaction, | ||
mutationBatches, | ||
results | ||
).next(missingBaseDocuments => { | ||
missingBaseDocuments.forEach((key, doc) => { | ||
if (doc !== null && doc instanceof Document) { | ||
results = results.insert(key, doc); | ||
} | ||
}); | ||
|
||
const baseDoc = results.get(key); | ||
const mutatedDoc = mutation.applyToLocalView( | ||
baseDoc, | ||
baseDoc, | ||
batch.localWriteTime | ||
); | ||
if (mutatedDoc instanceof Document) { | ||
results = results.insert(key, mutatedDoc); | ||
} else { | ||
results = results.remove(key); | ||
for (const batch of mutationBatches) { | ||
for (const mutation of batch.mutations) { | ||
const key = mutation.key; | ||
const baseDoc = results.get(key); | ||
const mutatedDoc = mutation.applyToLocalView( | ||
baseDoc, | ||
baseDoc, | ||
batch.localWriteTime | ||
); | ||
if (mutatedDoc instanceof Document) { | ||
results = results.insert(key, mutatedDoc); | ||
} else { | ||
results = results.remove(key); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
}) | ||
.next(() => { | ||
// Finally, filter out any documents that don't actually match | ||
|
@@ -252,4 +269,28 @@ export class LocalDocumentsView { | |
return results; | ||
}); | ||
} | ||
|
||
private getMissingBaseDocuments( | ||
transaction: PersistenceTransaction, | ||
matchingMutationBatches: MutationBatch[], | ||
existingDocuments: DocumentMap | ||
): PersistencePromise<NullableMaybeDocumentMap> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense for this to return a DocumentMap? It would simplify the code above, but you would need to post-filter in here. Alternatively, you could also assemble the final result map here and make this a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My understanding is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can get return the combined set of Optional. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decide not to do it because it is not a significant improvement.. |
||
let missingBaseDocEntriesForPatching = documentKeySet(); | ||
for (const batch of matchingMutationBatches) { | ||
for (const mutation of batch.mutations) { | ||
if ( | ||
mutation instanceof PatchMutation && | ||
existingDocuments.get(mutation.key) === null | ||
) { | ||
missingBaseDocEntriesForPatching = missingBaseDocEntriesForPatching.add( | ||
mutation.key | ||
); | ||
} | ||
} | ||
} | ||
return this.remoteDocumentCache.getEntries( | ||
transaction, | ||
missingBaseDocEntriesForPatching | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,7 @@ | |
*/ | ||
|
||
import { Query } from '../../../src/core/query'; | ||
import { doc, filter } from '../../util/helpers'; | ||
import { doc, filter, path } from '../../util/helpers'; | ||
|
||
import { Document } from '../../../src/model/document'; | ||
import { ResourcePath } from '../../../src/model/path'; | ||
|
@@ -98,4 +98,41 @@ describeSpec('Queries:', [], () => { | |
hasPendingWrites: true | ||
}); | ||
}); | ||
|
||
specTest( | ||
'Latency-compensated updates are included in query results', | ||
[], | ||
() => { | ||
const fullQuery = Query.atPath(path('collection')); | ||
const filteredQuery = fullQuery.addFilter(filter('match', '==', true)); | ||
const docA = doc('collection/a', 1000, { match: false }); | ||
const docAv2Local = doc( | ||
'collection/a', | ||
1000, | ||
{ match: true }, | ||
{ hasLocalMutations: true } | ||
); | ||
|
||
return ( | ||
spec() | ||
.userListens(fullQuery) | ||
.watchAcksFull(fullQuery, 1000, docA) | ||
.expectEvents(fullQuery, { added: [docA] }) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that if you do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure i understand..can you explain more? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am myself no longer fully sold on my idea. But let me explain what I had in mind:
I believe this would work and it would remove the event in line 124 that is not directly related to your change. The reason I am no longer fully sold on this is that you would have to run the test with GC disabled to make this work. Otherwise, the SDK would delete There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, can confirm what you described. Without GC disabled this is not working. I am keep it as-is as that is slightly more likely what happens in real world. |
||
// patch docA so that it will now match the filtered query. | ||
.userPatches('collection/a', { match: true }) | ||
.expectEvents(fullQuery, { | ||
modified: [docAv2Local], | ||
hasPendingWrites: true | ||
}) | ||
// Make sure docA shows up in filtered query. | ||
.userListens(filteredQuery) | ||
.expectEvents(filteredQuery, { | ||
added: [docAv2Local], | ||
fromCache: true, | ||
hasPendingWrites: true | ||
}) | ||
); | ||
} | ||
); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.