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 3 commits
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: 10 additions & 5 deletions packages/firestore/src/core/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { MaybeDocument, NoDocument } from '../model/document';
import { debugAssert } from '../util/assert';
import {
applyBundleDocuments,
getQueryDocumentMapping,
LocalStore,
saveNamedQuery
} from '../local/local_store';
Expand Down Expand Up @@ -218,16 +219,20 @@ export class BundleLoader {
'Bundled documents ends with a document metadata and missing document.'
);

for (const q of this.queries) {
await saveNamedQuery(this.localStore, q);
}
const changedDocuments = await applyBundleDocuments(
this.localStore,
this.documents
);

const changedDocs = await applyBundleDocuments(
const queryDocumentMap = getQueryDocumentMapping(
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.

Nit: Empty line below.

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 q of this.queries) {
await saveNamedQuery(this.localStore, q, queryDocumentMap.get(q.name!));
}

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

export function getQueryDocumentMapping(
localStore: LocalStore,
documents: BundledDocuments
): Map<string, DocumentKeySet> {
const localStoreImpl = debugCast(localStore, LocalStoreImpl);
const queryDocumentMap = new Map<string, DocumentKeySet>();
const bundleConverter = new BundleConverter(localStoreImpl.serializer);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to get the serializer from the callsite of this function, drop its dependency on "LocalStore" and move it to bundle.ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to add a getSerializer to localstore interface, but that still seems better than current.

for (const bundleDoc of documents) {
if (bundleDoc.metadata.queries) {
const documentKey = bundleConverter.toDocumentKey(
bundleDoc.metadata.name!
);
for (const queryName of bundleDoc.metadata.queries) {
const documentKeys = (
queryDocumentMap.get(queryName) || documentKeySet()
).add(documentKey);
queryDocumentMap.set(queryName, documentKeys);
}
}
}

return queryDocumentMap;
}

/**
* Applies the documents from a bundle to the "ground-state" (remote)
* documents.
Expand Down Expand Up @@ -1300,6 +1324,9 @@ export function applyBundleDocuments(
txn,
changedDocs
);
})
.next(changedDocuments => {
return PersistencePromise.resolve(changedDocuments);
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like a no-op.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

});
}
);
Expand Down Expand Up @@ -1373,7 +1400,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 @@ -1389,21 +1417,41 @@ export async function saveNamedQuery(
'readwrite',
transaction => {
// Update allocated target's read time, if the bundle's read time is newer.
let updateReadTime = PersistencePromise.resolve();
let updateReadTime = PersistencePromise.resolve(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

If still needed: s/updateReadTime/updatedReadTime (or readTimeUpdated)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

const readTime = fromVersion(query.readTime!);
if (allocated.snapshotVersion.compareTo(readTime) < 0) {
const newTargetData = allocated.withResumeToken(
ByteString.EMPTY_BYTE_STRING,
readTime
);
updateReadTime = localStoreImpl.targetCache.updateTargetData(
transaction,
updateReadTime = localStoreImpl.targetCache
.updateTargetData(transaction, newTargetData)
.next(
() => true,
() => false
);
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs explanation (but I think it could be removed if you update the target mapping right here)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

localStoreImpl.targetDataByTarget = localStoreImpl.targetDataByTarget.insert(
newTargetData.targetId,
newTargetData
);
}
return updateReadTime.next(() =>
localStoreImpl.bundleCache.saveNamedQuery(transaction, query)
);
return updateReadTime
.next(updated => {
if (updated) {
return localStoreImpl.targetCache
.removeMatchingKeysForTargetId(transaction, allocated.targetId)
.next(() =>
localStoreImpl.targetCache.addMatchingKeys(
transaction,
documents,
allocated.targetId
)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this not just be chained off the call to "updateTargetData"?

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.

}
})
.next(() =>
localStoreImpl.bundleCache.saveNamedQuery(transaction, query)
);
}
);
}
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[];
}

/** Properties of a BundleMetadata. */
Expand Down
99 changes: 95 additions & 4 deletions packages/firestore/test/unit/local/local_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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 +92,7 @@ import {
query,
setMutation,
TestBundledDocuments,
TestNamedQuery,
TestSnapshotVersion,
transformMutation,
unknownDoc,
Expand Down Expand Up @@ -137,7 +139,7 @@ class LocalStoreTester {
| RemoteEvent
| LocalViewChanges
| TestBundledDocuments
| bundleProto.NamedQuery
| TestNamedQuery
): LocalStoreTester {
if (op instanceof Mutation) {
return this.afterMutations([op]);
Expand Down Expand Up @@ -188,17 +190,21 @@ class LocalStoreTester {

this.promiseChain = this.promiseChain
.then(() => applyBundleDocuments(this.localStore, documents))
.then((result: MaybeDocumentMap) => {
.then(result => {
this.lastChanges = result;
});
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 @@ -432,6 +438,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 +1735,68 @@ function genericLocalStoreTests(
.finish();
});

it('loading named queries allocates targets and updates target document mapping', async () => {
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 })
)
.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