Skip to content

Update query-document mapping from bundles. #3620

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

Merged
merged 6 commits into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions packages/firestore/src/core/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,17 @@ export class BundleLoader {
'Bundled documents ends with a document metadata and missing document.'
);

const result = await applyBundleDocuments(this.localStore, this.documents);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, returning the map of matching documents from applyBundleDocuments saves one iteration. This code would be less complex if you extracted the mapping in this function (ideally, via a call to a helper function). This would remove your changes from applyBundleDocuments and some of the changes from LocalStoreTester.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It IS nicer this way!

for (const q of this.queries) {
await saveNamedQuery(this.localStore, q);
await saveNamedQuery(
this.localStore,
q,
result.queryDocumentMap.get(q.name!)
);
}

const changedDocs = await applyBundleDocuments(
this.localStore,
this.documents
);

this.progress.taskState = 'Success';
return new BundleLoadResult({ ...this.progress }, changedDocs);
return new BundleLoadResult({ ...this.progress }, result.changedDocuments);
}
}
43 changes: 38 additions & 5 deletions packages/firestore/src/local/local_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,11 @@ export async function ignoreIfPrimaryLeaseLoss(
}
}

export interface ApplyBundleDocumentsResult {
changedDocuments: MaybeDocumentMap;
queryDocumentMap: Map<string, DocumentKeySet>;
}

/**
* Applies the documents from a bundle to the "ground-state" (remote)
* documents.
Expand All @@ -1259,11 +1264,12 @@ export async function ignoreIfPrimaryLeaseLoss(
export function applyBundleDocuments(
localStore: LocalStore,
documents: BundledDocuments
): Promise<MaybeDocumentMap> {
): Promise<ApplyBundleDocumentsResult> {
const localStoreImpl = debugCast(localStore, LocalStoreImpl);
const bundleConverter = new BundleConverter(localStoreImpl.serializer);
let documentMap = maybeDocumentMap();
let versionMap = documentVersionMap();
const queryDocumentMap: Map<string, DocumentKeySet> = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const queryDocumentMap: Map<string, DocumentKeySet> = new Map();
const queryDocumentMap = new Map<string, DocumentKeySet>();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

for (const bundleDoc of documents) {
const documentKey = bundleConverter.toDocumentKey(bundleDoc.metadata.name!);
documentMap = documentMap.insert(
Expand All @@ -1274,6 +1280,14 @@ export function applyBundleDocuments(
documentKey,
bundleConverter.toSnapshotVersion(bundleDoc.metadata.readTime!)
);
if (bundleDoc.metadata.queries) {
for (const queryName of bundleDoc.metadata.queries) {
const documentKeys = (
queryDocumentMap.get(queryName) || documentKeySet()
).add(documentKey);
queryDocumentMap.set(queryName, documentKeys);
}
}
}

const documentBuffer = localStoreImpl.remoteDocuments.newChangeBuffer({
Expand All @@ -1300,6 +1314,12 @@ export function applyBundleDocuments(
txn,
changedDocs
);
})
.next(changedDocuments => {
return PersistencePromise.resolve({
changedDocuments,
queryDocumentMap
});
});
}
);
Expand Down Expand Up @@ -1373,7 +1393,8 @@ export function getNamedQuery(
*/
export async function saveNamedQuery(
localStore: LocalStore,
query: bundleProto.NamedQuery
query: bundleProto.NamedQuery,
documents: DocumentKeySet = documentKeySet()
): Promise<void> {
// Allocate a target for the named query such that it can be resumed
// from associated read time if users use it to listen.
Expand All @@ -1400,10 +1421,22 @@ export async function saveNamedQuery(
transaction,
newTargetData
);
localStoreImpl.targetDataByTarget = localStoreImpl.targetDataByTarget.insert(
newTargetData.targetId,
newTargetData
);
}
return updateReadTime.next(() =>
localStoreImpl.bundleCache.saveNamedQuery(transaction, query)
);
return updateReadTime
.next(() =>
localStoreImpl.bundleCache.saveNamedQuery(transaction, query)
)
.next(() =>
localStoreImpl.targetCache.addMatchingKeys(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should only do this if you updated the readTime above. On top of that, you need to also remove all existing keys, otherwise your mapping will not match the backend's mapping.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree to both.

transaction,
documents,
allocated.targetId
)
);
}
);
}
3 changes: 3 additions & 0 deletions packages/firestore/src/protos/firestore/bundle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ message BundledDocumentMetadata {

// Whether the document exists.
bool exists = 3;

// The names of the queries in this bundle that this document matches to.
repeated string queries = 4;
}

// Metadata describing the bundle file/stream.
Expand Down
3 changes: 3 additions & 0 deletions packages/firestore/src/protos/firestore_bundle_proto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export interface BundledDocumentMetadata {

/** BundledDocumentMetadata exists */
exists?: boolean | null;

/** The names of the queries in this bundle that this document matches to. */
queries?: string[] | null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Proto3, is not possible to have a missing array. Missing arrays are represented as empty lists, so this should be queries: string[].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

/** Properties of a BundleMetadata. */
Expand Down
125 changes: 120 additions & 5 deletions packages/firestore/test/unit/local/local_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { IndexFreeQueryEngine } from '../../../src/local/index_free_query_engine
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
import {
applyBundleDocuments,
ApplyBundleDocumentsResult,
getNamedQuery,
hasNewerBundle,
LocalStore,
Expand All @@ -48,6 +49,7 @@ import { LocalViewChanges } from '../../../src/local/local_view_changes';
import { Persistence } from '../../../src/local/persistence';
import { SimpleQueryEngine } from '../../../src/local/simple_query_engine';
import {
DocumentKeySet,
documentKeySet,
MaybeDocumentMap
} from '../../../src/model/collections';
Expand Down Expand Up @@ -91,6 +93,7 @@ import {
query,
setMutation,
TestBundledDocuments,
TestNamedQuery,
TestSnapshotVersion,
transformMutation,
unknownDoc,
Expand All @@ -113,6 +116,7 @@ export interface LocalStoreComponents {
class LocalStoreTester {
private promiseChain: Promise<void> = Promise.resolve();
private lastChanges: MaybeDocumentMap | null = null;
private lastBundleQueryDocumentMap: Map<string, DocumentKeySet> | null = null;
private lastTargetId: TargetId | null = null;
private batches: MutationBatch[] = [];

Expand All @@ -125,6 +129,7 @@ class LocalStoreTester {
private prepareNextStep(): void {
this.promiseChain = this.promiseChain.then(() => {
this.lastChanges = null;
this.lastBundleQueryDocumentMap = null;
this.lastTargetId = null;
this.queryEngine.resetCounts();
});
Expand All @@ -137,7 +142,7 @@ class LocalStoreTester {
| RemoteEvent
| LocalViewChanges
| TestBundledDocuments
| bundleProto.NamedQuery
| TestNamedQuery
): LocalStoreTester {
if (op instanceof Mutation) {
return this.afterMutations([op]);
Expand Down Expand Up @@ -188,17 +193,22 @@ class LocalStoreTester {

this.promiseChain = this.promiseChain
.then(() => applyBundleDocuments(this.localStore, documents))
.then((result: MaybeDocumentMap) => {
this.lastChanges = result;
.then((result: ApplyBundleDocumentsResult) => {
this.lastChanges = result.changedDocuments;
this.lastBundleQueryDocumentMap = result.queryDocumentMap;
});
return this;
}

afterNamedQuery(namedQuery: bundleProto.NamedQuery): LocalStoreTester {
afterNamedQuery(testQuery: TestNamedQuery): LocalStoreTester {
this.prepareNextStep();

this.promiseChain = this.promiseChain.then(() =>
saveNamedQuery(this.localStore, namedQuery)
saveNamedQuery(
this.localStore,
testQuery.namedQuery,
testQuery.matchingDocuments
)
);
return this;
}
Expand Down Expand Up @@ -372,6 +382,25 @@ class LocalStoreTester {
return this;
}

toReturnQueryDocumentMap(
expected: Map<string, DocumentKeySet>
): LocalStoreTester {
this.promiseChain = this.promiseChain.then(() => {
debugAssert(
!!this.lastBundleQueryDocumentMap,
'Expecting query document map to be returned from applying bundled documents'
);
for (const k of Array.from(this.lastBundleQueryDocumentMap.keys())) {
expect(expected.has(k)).to.be.true;
expect(
this.lastBundleQueryDocumentMap.get(k)!.isEqual(expected.get(k)!)
).to.be.true;
}
this.lastBundleQueryDocumentMap = null;
});
return this;
}

toReturnRemoved(...keyStrings: string[]): LocalStoreTester {
this.promiseChain = this.promiseChain.then(() => {
debugAssert(
Expand Down Expand Up @@ -432,6 +461,29 @@ class LocalStoreTester {
return this;
}

toHaveQueryDocumentMapping(
persistence: Persistence,
targetId: TargetId,
expectedKeys: DocumentKeySet
): LocalStoreTester {
this.promiseChain = this.promiseChain.then(() => {
return persistence.runTransaction(
'toHaveQueryDocumentMapping',
'readonly',
transaction => {
return persistence
.getTargetCache()
.getMatchingKeysForTargetId(transaction, targetId)
.next(matchedKeys => {
expect(matchedKeys.isEqual(expectedKeys)).to.be.true;
});
}
);
});

return this;
}

toHaveNewerBundle(
metadata: bundleProto.BundleMetadata,
expected: boolean
Expand Down Expand Up @@ -1706,6 +1758,69 @@ function genericLocalStoreTests(
.finish();
});

it('handles loading named queries allocates targets and updates target document mapping', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('handles loading named queries allocates targets and updates target document mapping', async () => {
it('loading named queries allocates targets and updates target document mapping', async () => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const expectedQueryDocumentMap = new Map([
['query-1', documentKeySet(key('foo1/bar'))],
['query-2', documentKeySet(key('foo2/bar'))]
]);
const version1 = SnapshotVersion.fromTimestamp(Timestamp.fromMillis(10000));
const version2 = SnapshotVersion.fromTimestamp(Timestamp.fromMillis(20000));

return expectLocalStore()
.after(
bundledDocuments(
[doc('foo1/bar', 1, { sum: 1337 }), doc('foo2/bar', 2, { sum: 42 })],
[['query-1'], ['query-2']]
)
)
.toReturnChanged(
doc('foo1/bar', 1, { sum: 1337 }),
doc('foo2/bar', 2, { sum: 42 })
)
.toReturnQueryDocumentMap(expectedQueryDocumentMap)
.toContain(doc('foo1/bar', 1, { sum: 1337 }))
.toContain(doc('foo2/bar', 2, { sum: 42 }))
.after(
namedQuery(
'query-1',
query('foo1'),
/* limitType */ 'FIRST',
version1,
expectedQueryDocumentMap.get('query-1')
)
)
.toHaveNamedQuery({
name: 'query-1',
query: query('foo1'),
readTime: version1
})
.toHaveQueryDocumentMapping(
persistence,
/*targetId*/ 2,
/*expectedKeys*/ documentKeySet(key('foo1/bar'))
)
.after(
namedQuery(
'query-2',
query('foo2'),
/* limitType */ 'FIRST',
version2,
expectedQueryDocumentMap.get('query-2')
)
)
.toHaveNamedQuery({
name: 'query-2',
query: query('foo2'),
readTime: version2
})
.toHaveQueryDocumentMapping(
persistence,
/*targetId*/ 4,
/*expectedKeys*/ documentKeySet(key('foo2/bar'))
)
.finish();
});

it('handles saving and loading limit to last queries', async () => {
const now = Timestamp.now();
return expectLocalStore()
Expand Down
39 changes: 26 additions & 13 deletions packages/firestore/test/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,14 +445,16 @@ export class TestBundledDocuments {
}

export function bundledDocuments(
documents: MaybeDocument[]
documents: MaybeDocument[],
queryNames?: string[][]
): TestBundledDocuments {
const result = documents.map(d => {
const result = documents.map((d, index) => {
return {
metadata: {
name: toName(JSON_SERIALIZER, d.key),
readTime: toVersion(JSON_SERIALIZER, d.version),
exists: d instanceof Document
exists: d instanceof Document,
queries: queryNames ? queryNames[index] : undefined
},
document:
d instanceof Document ? toDocument(JSON_SERIALIZER, d) : undefined
Expand All @@ -462,21 +464,32 @@ export function bundledDocuments(
return new TestBundledDocuments(result);
}

export class TestNamedQuery {
constructor(
public namedQuery: bundleProto.NamedQuery,
public matchingDocuments: DocumentKeySet
) {}
}

export function namedQuery(
name: string,
query: Query,
limitType: bundleProto.LimitType,
readTime: SnapshotVersion
): bundleProto.NamedQuery {
readTime: SnapshotVersion,
matchingDocuments: DocumentKeySet = documentKeySet()
): TestNamedQuery {
return {
name,
readTime: toTimestamp(JSON_SERIALIZER, readTime.toTimestamp()),
bundledQuery: {
parent: toQueryTarget(JSON_SERIALIZER, queryToTarget(query)).parent,
limitType,
structuredQuery: toQueryTarget(JSON_SERIALIZER, queryToTarget(query))
.structuredQuery
}
namedQuery: {
name,
readTime: toTimestamp(JSON_SERIALIZER, readTime.toTimestamp()),
bundledQuery: {
parent: toQueryTarget(JSON_SERIALIZER, queryToTarget(query)).parent,
limitType,
structuredQuery: toQueryTarget(JSON_SERIALIZER, queryToTarget(query))
.structuredQuery
}
},
matchingDocuments
};
}

Expand Down