From 69aa7b02df3b4d1f9832b7713951936b6bf32ca9 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 18 Mar 2022 09:49:37 -0600 Subject: [PATCH] RemoteDocumentCache APIs for Indexing (#5988) --- .changeset/seven-dolphins-breathe.md | 5 + packages/firestore/externs.json | 1 + .../firestore/src/core/snapshot_version.ts | 4 + .../local/indexeddb_mutation_batch_impl.ts | 5 +- .../local/indexeddb_remote_document_cache.ts | 305 ++++++++++++------ .../firestore/src/local/indexeddb_schema.ts | 41 ++- .../src/local/indexeddb_schema_converter.ts | 133 ++++++-- .../src/local/indexeddb_schema_legacy.ts | 37 +++ .../src/local/indexeddb_sentinels.ts | 81 +++-- .../src/local/local_documents_view.ts | 19 +- .../firestore/src/local/local_serializer.ts | 46 ++- .../src/local/memory_remote_document_cache.ts | 27 +- packages/firestore/src/local/query_engine.ts | 12 +- .../src/local/remote_document_cache.ts | 25 +- packages/firestore/src/model/field_index.ts | 13 +- packages/firestore/src/model/path.ts | 1 + .../test/unit/local/counting_query_engine.ts | 22 +- .../unit/local/indexeddb_persistence.test.ts | 164 ++++++---- .../test/unit/local/query_engine.test.ts | 10 +- .../unit/local/remote_document_cache.test.ts | 126 +++++++- .../unit/local/test_remote_document_cache.ts | 27 +- 21 files changed, 815 insertions(+), 289 deletions(-) create mode 100644 .changeset/seven-dolphins-breathe.md create mode 100644 packages/firestore/src/local/indexeddb_schema_legacy.ts diff --git a/.changeset/seven-dolphins-breathe.md b/.changeset/seven-dolphins-breathe.md new file mode 100644 index 00000000000..dbf89ff8152 --- /dev/null +++ b/.changeset/seven-dolphins-breathe.md @@ -0,0 +1,5 @@ +--- +"@firebase/firestore": patch +--- + +The format of some of the IndexedDB data changed. This increases the performance of document lookups after an initial migration. If you do not want to migrate data, you can call `clearIndexedDbPersistence()` before invoking `enableIndexedDbPersistence()`. diff --git a/packages/firestore/externs.json b/packages/firestore/externs.json index 003eed2d069..a4c36074978 100644 --- a/packages/firestore/externs.json +++ b/packages/firestore/externs.json @@ -34,6 +34,7 @@ "packages/firestore/src/protos/firestore_proto_api.ts", "packages/firestore/src/util/error.ts", "packages/firestore/src/local/indexeddb_schema.ts", + "packages/firestore/src/local/indexeddb_schema_legacy.ts", "packages/firestore/src/local/shared_client_state_schema.ts" ] } diff --git a/packages/firestore/src/core/snapshot_version.ts b/packages/firestore/src/core/snapshot_version.ts index 9f2e5581a7f..d5acc66b233 100644 --- a/packages/firestore/src/core/snapshot_version.ts +++ b/packages/firestore/src/core/snapshot_version.ts @@ -30,6 +30,10 @@ export class SnapshotVersion { return new SnapshotVersion(new Timestamp(0, 0)); } + static max(): SnapshotVersion { + return new SnapshotVersion(new Timestamp(253402300799, 1e9 - 1)); + } + private constructor(private timestamp: Timestamp) {} compareTo(other: SnapshotVersion): number { diff --git a/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts b/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts index c642b44bdac..16b157accb2 100644 --- a/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts +++ b/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts @@ -23,6 +23,7 @@ import { DbMutationBatch, DbRemoteDocument } from './indexeddb_schema'; +import { DbRemoteDocument as DbRemoteDocumentLegacy } from './indexeddb_schema_legacy'; import { DbDocumentMutationKey, DbDocumentMutationStore, @@ -84,7 +85,9 @@ export function removeMutationBatch( /** * Returns an approximate size for the given document. */ -export function dbDocumentSize(doc: DbRemoteDocument | null): number { +export function dbDocumentSize( + doc: DbRemoteDocument | DbRemoteDocumentLegacy | null +): number { if (!doc) { return 0; } diff --git a/packages/firestore/src/local/indexeddb_remote_document_cache.ts b/packages/firestore/src/local/indexeddb_remote_document_cache.ts index 50e43fff7a4..a8a12ff92e3 100644 --- a/packages/firestore/src/local/indexeddb_remote_document_cache.ts +++ b/packages/firestore/src/local/indexeddb_remote_document_cache.ts @@ -24,6 +24,7 @@ import { } from '../model/collections'; import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { IndexOffset } from '../model/field_index'; import { ResourcePath } from '../model/path'; import { debugAssert, debugCast, hardAssert } from '../util/assert'; import { primitiveComparator } from '../util/misc'; @@ -35,12 +36,14 @@ import { IndexManager } from './index_manager'; import { dbDocumentSize } from './indexeddb_mutation_batch_impl'; import { DbRemoteDocument, DbRemoteDocumentGlobal } from './indexeddb_schema'; import { - DbRemoteDocumentCollectionReadTimeIndex, + DbRemoteDocumentCollectionGroupIndex, + DbRemoteDocumentDocumentKeyIndex, DbRemoteDocumentGlobalKey, DbRemoteDocumentGlobalStore, DbRemoteDocumentKey, DbRemoteDocumentReadTimeIndex, - DbRemoteDocumentStore + DbRemoteDocumentStore, + DbTimestampKey } from './indexeddb_sentinels'; import { getStore } from './indexeddb_transaction'; import { @@ -54,7 +57,7 @@ import { PersistencePromise } from './persistence_promise'; import { PersistenceTransaction } from './persistence_transaction'; import { RemoteDocumentCache } from './remote_document_cache'; import { RemoteDocumentChangeBuffer } from './remote_document_change_buffer'; -import { IterateOptions, SimpleDbStore } from './simple_db'; +import { SimpleDbStore } from './simple_db'; export interface DocumentSizeEntry { document: MutableDocument; @@ -91,7 +94,7 @@ class IndexedDbRemoteDocumentCacheImpl implements IndexedDbRemoteDocumentCache { doc: DbRemoteDocument ): PersistencePromise { const documentStore = remoteDocumentsStore(transaction); - return documentStore.put(dbKey(key), doc); + return documentStore.put(doc); } /** @@ -102,11 +105,11 @@ class IndexedDbRemoteDocumentCacheImpl implements IndexedDbRemoteDocumentCache { */ removeEntry( transaction: PersistenceTransaction, - documentKey: DocumentKey + documentKey: DocumentKey, + readTime: SnapshotVersion ): PersistencePromise { const store = remoteDocumentsStore(transaction); - const key = dbKey(documentKey); - return store.delete(key); + return store.delete(dbReadTimeKey(documentKey, readTime)); } /** @@ -129,11 +132,18 @@ class IndexedDbRemoteDocumentCacheImpl implements IndexedDbRemoteDocumentCache { transaction: PersistenceTransaction, documentKey: DocumentKey ): PersistencePromise { + let doc = MutableDocument.newInvalidDocument(documentKey); return remoteDocumentsStore(transaction) - .get(dbKey(documentKey)) - .next(dbRemoteDoc => { - return this.maybeDecodeDocument(documentKey, dbRemoteDoc); - }); + .iterate( + { + index: DbRemoteDocumentDocumentKeyIndex, + range: IDBKeyRange.only(dbKey(documentKey)) + }, + (_, dbRemoteDoc) => { + doc = this.maybeDecodeDocument(documentKey, dbRemoteDoc); + } + ) + .next(() => doc); } /** @@ -146,15 +156,24 @@ class IndexedDbRemoteDocumentCacheImpl implements IndexedDbRemoteDocumentCache { transaction: PersistenceTransaction, documentKey: DocumentKey ): PersistencePromise { + let result = { + size: 0, + document: MutableDocument.newInvalidDocument(documentKey) + }; return remoteDocumentsStore(transaction) - .get(dbKey(documentKey)) - .next(dbRemoteDoc => { - const doc = this.maybeDecodeDocument(documentKey, dbRemoteDoc); - return { - document: doc, - size: dbDocumentSize(dbRemoteDoc) - }; - }); + .iterate( + { + index: DbRemoteDocumentDocumentKeyIndex, + range: IDBKeyRange.only(dbKey(documentKey)) + }, + (_, dbRemoteDoc) => { + result = { + document: this.maybeDecodeDocument(documentKey, dbRemoteDoc), + size: dbDocumentSize(dbRemoteDoc) + }; + } + ) + .next(() => result); } getEntries( @@ -207,36 +226,45 @@ class IndexedDbRemoteDocumentCacheImpl implements IndexedDbRemoteDocumentCache { return PersistencePromise.resolve(); } + let sortedKeys = new SortedSet(dbKeyComparator); + documentKeys.forEach(e => (sortedKeys = sortedKeys.add(e))); const range = IDBKeyRange.bound( - documentKeys.first()!.path.toArray(), - documentKeys.last()!.path.toArray() + dbKey(sortedKeys.first()!), + dbKey(sortedKeys.last()!) ); - const keyIter = documentKeys.getIterator(); + const keyIter = sortedKeys.getIterator(); let nextKey: DocumentKey | null = keyIter.getNext(); return remoteDocumentsStore(transaction) - .iterate({ range }, (potentialKeyRaw, dbRemoteDoc, control) => { - const potentialKey = DocumentKey.fromSegments(potentialKeyRaw); - - // Go through keys not found in cache. - while (nextKey && DocumentKey.comparator(nextKey!, potentialKey) < 0) { - callback(nextKey!, null); - nextKey = keyIter.getNext(); - } - - if (nextKey && nextKey!.isEqual(potentialKey)) { - // Key found in cache. - callback(nextKey!, dbRemoteDoc); - nextKey = keyIter.hasNext() ? keyIter.getNext() : null; + .iterate( + { index: DbRemoteDocumentDocumentKeyIndex, range }, + (_, dbRemoteDoc, control) => { + const potentialKey = DocumentKey.fromSegments([ + ...dbRemoteDoc.prefixPath, + dbRemoteDoc.collectionGroup, + dbRemoteDoc.documentId + ]); + + // Go through keys not found in cache. + while (nextKey && dbKeyComparator(nextKey!, potentialKey) < 0) { + callback(nextKey!, null); + nextKey = keyIter.getNext(); + } + + if (nextKey && nextKey!.isEqual(potentialKey)) { + // Key found in cache. + callback(nextKey!, dbRemoteDoc); + nextKey = keyIter.hasNext() ? keyIter.getNext() : null; + } + + // Skip to the next key (if there is one). + if (nextKey) { + control.skip(dbKey(nextKey)); + } else { + control.done(); + } } - - // Skip to the next key (if there is one). - if (nextKey) { - control.skip(nextKey!.path.toArray()); - } else { - control.done(); - } - }) + ) .next(() => { // The rest of the keys are not in the cache. One case where `iterate` // above won't go through them is when the cache is empty. @@ -247,55 +275,79 @@ class IndexedDbRemoteDocumentCacheImpl implements IndexedDbRemoteDocumentCache { }); } - getAll( + getAllFromCollection( transaction: PersistenceTransaction, collection: ResourcePath, - sinceReadTime: SnapshotVersion + offset: IndexOffset ): PersistencePromise { - let results = mutableDocumentMap(); - - const immediateChildrenPathLength = collection.length + 1; - - const iterationOptions: IterateOptions = {}; - if (sinceReadTime.isEqual(SnapshotVersion.min())) { - // Documents are ordered by key, so we can use a prefix scan to narrow - // down the documents we need to match the query against. - const startKey = collection.toArray(); - iterationOptions.range = IDBKeyRange.lowerBound(startKey); - } else { - // Execute an index-free query and filter by read time. This is safe - // since all document changes to queries that have a - // lastLimboFreeSnapshotVersion (`sinceReadTime`) have a read time set. - const collectionKey = collection.toArray(); - const readTimeKey = toDbTimestampKey(sinceReadTime); - iterationOptions.range = IDBKeyRange.lowerBound( - [collectionKey, readTimeKey], - /* open= */ true - ); - iterationOptions.index = DbRemoteDocumentCollectionReadTimeIndex; - } + const startKey = [ + collection.popLast().toArray(), + collection.lastSegment(), + toDbTimestampKey(offset.readTime), + offset.documentKey.path.isEmpty() + ? '' + : offset.documentKey.path.lastSegment() + ]; + const endKey: DbRemoteDocumentKey = [ + collection.popLast().toArray(), + collection.lastSegment(), + [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], + '' + ]; return remoteDocumentsStore(transaction) - .iterate(iterationOptions, (key, dbRemoteDoc, control) => { - // The query is actually returning any path that starts with the query - // path prefix which may include documents in subcollections. For - // example, a query on 'rooms' will return rooms/abc/messages/xyx but we - // shouldn't match it. Fix this by discarding rows with document keys - // more than one segment longer than the query path. - if (key.length !== immediateChildrenPathLength) { - return; + .loadAll(IDBKeyRange.bound(startKey, endKey, true)) + .next(dbRemoteDocs => { + let results = mutableDocumentMap(); + for (const dbRemoteDoc of dbRemoteDocs) { + const document = this.maybeDecodeDocument( + DocumentKey.fromSegments( + dbRemoteDoc.prefixPath.concat( + dbRemoteDoc.collectionGroup, + dbRemoteDoc.documentId + ) + ), + dbRemoteDoc + ); + results = results.insert(document.key, document); } + return results; + }); + } - const document = this.maybeDecodeDocument( - DocumentKey.fromSegments(key), - dbRemoteDoc - ); - if (collection.isPrefixOf(document.key.path)) { + getAllFromCollectionGroup( + transaction: PersistenceTransaction, + collectionGroup: string, + offset: IndexOffset, + limit: number + ): PersistencePromise { + debugAssert(limit > 0, 'Limit should be at least 1'); + let results = mutableDocumentMap(); + + const startKey = dbCollectionGroupKey(collectionGroup, offset); + const endKey = dbCollectionGroupKey(collectionGroup, IndexOffset.max()); + return remoteDocumentsStore(transaction) + .iterate( + { + index: DbRemoteDocumentCollectionGroupIndex, + range: IDBKeyRange.bound(startKey, endKey, true) + }, + (_, dbRemoteDoc, control) => { + const document = this.maybeDecodeDocument( + DocumentKey.fromSegments( + dbRemoteDoc.prefixPath.concat( + dbRemoteDoc.collectionGroup, + dbRemoteDoc.documentId + ) + ), + dbRemoteDoc + ); results = results.insert(document.key, document); - } else { - control.done(); + if (results.size === limit) { + control.done(); + } } - }) + ) .next(() => results); } @@ -438,8 +490,12 @@ export function remoteDocumentCacheGetLastReadTime( * when we apply the changes. */ class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer { - // A map of document sizes prior to applying the changes in this buffer. - protected documentSizes: ObjectMap = new ObjectMap( + // A map of document sizes and read times prior to applying the changes in + // this buffer. + protected documentStates: ObjectMap< + DocumentKey, + { size: number; readTime: SnapshotVersion } + > = new ObjectMap( key => key.toString(), (l, r) => l.isEqual(r) ); @@ -468,11 +524,14 @@ class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer { ); this.changes.forEach((key, documentChange) => { - const previousSize = this.documentSizes.get(key); + const previousDoc = this.documentStates.get(key); debugAssert( - previousSize !== undefined, + previousDoc !== undefined, `Cannot modify a document that wasn't read (for ${key})` ); + promises.push( + this.documentCache.removeEntry(transaction, key, previousDoc.readTime) + ); if (documentChange.isValidDocument()) { debugAssert( !documentChange.readTime.isEqual(SnapshotVersion.min()), @@ -485,10 +544,10 @@ class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer { collectionParents = collectionParents.add(key.path.popLast()); const size = dbDocumentSize(doc); - sizeDelta += size - previousSize!; + sizeDelta += size - previousDoc.size; promises.push(this.documentCache.addEntry(transaction, key, doc)); } else { - sizeDelta -= previousSize!; + sizeDelta -= previousDoc.size; if (this.trackRemovals) { // In order to track removals, we store a "sentinel delete" in the // RemoteDocumentCache. This entry is represented by a NoDocument @@ -501,8 +560,6 @@ class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer { promises.push( this.documentCache.addEntry(transaction, key, deletedDoc) ); - } else { - promises.push(this.documentCache.removeEntry(transaction, key)); } } }); @@ -529,7 +586,10 @@ class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer { return this.documentCache .getSizedEntry(transaction, documentKey) .next(getResult => { - this.documentSizes.set(documentKey, getResult.size); + this.documentStates.set(documentKey, { + size: getResult.size, + readTime: getResult.document.readTime + }); return getResult.document; }); } @@ -547,7 +607,10 @@ class IndexedDbRemoteDocumentChangeBuffer extends RemoteDocumentChangeBuffer { // keys to `DocumentSizeEntry`s. This is to allow returning the // `MutableDocumentMap` directly, without a conversion. sizeMap.forEach((documentKey, size) => { - this.documentSizes.set(documentKey, size); + this.documentStates.set(documentKey, { + size, + readTime: documents.get(documentKey)!.readTime + }); }); return documents; }); @@ -575,6 +638,58 @@ function remoteDocumentsStore( ); } -function dbKey(docKey: DocumentKey): DbRemoteDocumentKey { - return docKey.path.toArray(); +/** + * Returns a key that can be used for document lookups on the + * `DbRemoteDocumentDocumentKeyIndex` index. + */ +function dbKey(documentKey: DocumentKey): [string[], string, string] { + const path = documentKey.path.toArray(); + return [ + /* prefix path */ path.slice(0, path.length - 2), + /* collection id */ path[path.length - 2], + /* document id */ path[path.length - 1] + ]; +} + +/** + * Returns a key that can be used for document lookups via the primary key of + * the DbRemoteDocument object store. + */ +function dbReadTimeKey( + documentKey: DocumentKey, + readTime: SnapshotVersion +): DbRemoteDocumentKey { + const path = documentKey.path.toArray(); + return [ + /* prefix path */ path.slice(0, path.length - 2), + /* collection id */ path[path.length - 2], + toDbTimestampKey(readTime), + /* document id */ path[path.length - 1] + ]; +} + +/** + * Returns a key that can be used for document lookups on the + * `DbRemoteDocumentDocumentCollectionGroupIndex` index. + */ +function dbCollectionGroupKey( + collectionGroup: string, + offset: IndexOffset +): [string, DbTimestampKey, string[], string] { + const path = offset.documentKey.path.toArray(); + return [ + /* collection id */ collectionGroup, + toDbTimestampKey(offset.readTime), + /* prefix path */ path.slice(0, path.length - 2), + /* document id */ path.length > 0 ? path[path.length - 1] : '' + ]; +} +/** + * Comparator that compares document keys according to the primary key sorting + * used by the `DbRemoteDocumentDocument` store (by collection path and then + * document ID). + */ +function dbKeyComparator(l: DocumentKey, r: DocumentKey): number { + const cmp = l.path.length - r.path.length; + return cmp !== 0 ? cmp : DocumentKey.comparator(l, r); } diff --git a/packages/firestore/src/local/indexeddb_schema.ts b/packages/firestore/src/local/indexeddb_schema.ts index b4fae101c4d..621df66783c 100644 --- a/packages/firestore/src/local/indexeddb_schema.ts +++ b/packages/firestore/src/local/indexeddb_schema.ts @@ -31,7 +31,7 @@ import { DbTimestampKey } from './indexeddb_sentinels'; // TODO(indexing): Remove this constant const INDEXING_ENABLED = false; -export const INDEXING_SCHEMA_VERSION = 13; +export const INDEXING_SCHEMA_VERSION = 14; /** * Schema Version for the Web client: @@ -52,10 +52,12 @@ export const INDEXING_SCHEMA_VERSION = 13; * 10. Rewrite the canonical IDs to the explicit Protobuf-based format. * 11. Add bundles and named_queries for bundle support. * 12. Add document overlays. - * 13. Add indexing support. + * 13. Rewrite the keys of the remote document cache to allow for efficient + * document lookup via `getAll()`. + * 14. Add indexing support. */ -export const SCHEMA_VERSION = INDEXING_ENABLED ? INDEXING_SCHEMA_VERSION : 12; +export const SCHEMA_VERSION = INDEXING_ENABLED ? INDEXING_SCHEMA_VERSION : 13; /** * Wrapper class to store timestamps (seconds and nanos) in IndexedDb objects. @@ -194,15 +196,24 @@ export interface DbUnknownDocument { * - An "unknown document" representing a document that is known to exist (at * some version) but whose contents are unknown. * + * The document key is split up across `prefixPath`, `collectionGroup` and + * `documentId`. + * * Note: This is the persisted equivalent of a MaybeDocument and could perhaps * be made more general if necessary. */ export interface DbRemoteDocument { - // TODO: We are currently storing full document keys almost three times - // (once as part of the primary key, once - partly - as `parentPath` and once - // inside the encoded documents). During our next migration, we should - // rewrite the primary key as parentPath + document ID which would allow us - // to drop one value. + /** The path to the document's collection (excluding). */ + prefixPath: string[]; + + /** The collection ID the document is direclty nested under. */ + collectionGroup: string; + + /** The document ID. */ + documentId: string; + + /** When the document was read from the backend. */ + readTime: DbTimestampKey; /** * Set to an instance of DbUnknownDocument if the data for a document is @@ -226,19 +237,7 @@ export interface DbRemoteDocument { * documents are potentially inconsistent with the backend's copy and use * the write's commit version as their document version. */ - hasCommittedMutations?: boolean; - - /** - * When the document was read from the backend. Undefined for data written - * prior to schema version 9. - */ - readTime?: DbTimestampKey; - - /** - * The path of the collection this document is part of. Undefined for data - * written prior to schema version 9. - */ - parentPath?: string[]; + hasCommittedMutations: boolean; } /** diff --git a/packages/firestore/src/local/indexeddb_schema_converter.ts b/packages/firestore/src/local/indexeddb_schema_converter.ts index 6f1e47f870f..2a46273e1e4 100644 --- a/packages/firestore/src/local/indexeddb_schema_converter.ts +++ b/packages/firestore/src/local/indexeddb_schema_converter.ts @@ -16,8 +16,9 @@ */ import { SnapshotVersion } from '../core/snapshot_version'; +import { DocumentKey } from '../model/document_key'; import { ResourcePath } from '../model/path'; -import { debugAssert, hardAssert } from '../util/assert'; +import { debugAssert, fail, hardAssert } from '../util/assert'; import { BATCHID_UNKNOWN } from '../util/types'; import { @@ -40,6 +41,11 @@ import { DbTargetGlobal, INDEXING_SCHEMA_VERSION } from './indexeddb_schema'; +import { + DbRemoteDocument as DbRemoteDocumentLegacy, + DbRemoteDocumentStore as DbRemoteDocumentStoreLegacy, + DbRemoteDocumentKey as DbRemoteDocumentKeyLegacy +} from './indexeddb_schema_legacy'; import { DbBundleKeyPath, DbBundleStore, @@ -79,11 +85,14 @@ import { DbNamedQueryKeyPath, DbNamedQueryStore, DbPrimaryClientStore, - DbRemoteDocumentCollectionReadTimeIndex, - DbRemoteDocumentCollectionReadTimeIndexPath, + DbRemoteDocumentCollectionGroupIndex, + DbRemoteDocumentCollectionGroupIndexPath, + DbRemoteDocumentDocumentKeyIndex, + DbRemoteDocumentDocumentKeyIndexPath, DbRemoteDocumentGlobalKey, DbRemoteDocumentGlobalStore, DbRemoteDocumentKey, + DbRemoteDocumentKeyPath, DbRemoteDocumentReadTimeIndex, DbRemoteDocumentReadTimeIndexPath, DbRemoteDocumentStore, @@ -140,7 +149,7 @@ export class SchemaConverter implements SimpleDbSchemaConverter { createPrimaryClientStore(db); createMutationQueue(db); createQueryCache(db); - createRemoteDocumentCache(db); + createLegacyRemoteDocumentCache(db); } // Migration 2 to populate the targetGlobal object no longer needed since @@ -202,7 +211,9 @@ export class SchemaConverter implements SimpleDbSchemaConverter { // to the DbRemoteDocument object store itself. Since the previous change // log only contained transient data, we can drop its object store. dropRemoteDocumentChangesStore(db); - createRemoteDocumentReadTimeIndex(txn); + + // Note: Schema version 9 used to create a read time index for the + // RemoteDocumentCache. This is now done with schema version 13. }); } @@ -224,6 +235,13 @@ export class SchemaConverter implements SimpleDbSchemaConverter { } if (fromVersion < 13 && toVersion >= 13) { + p = p + .next(() => createRemoteDocumentCache(db)) + .next(() => this.rewriteRemoteDocumentCache(db, simpleDbTransaction)) + .next(() => db.deleteObjectStore(DbRemoteDocumentStoreLegacy)); + } + + if (fromVersion < 14 && toVersion >= 14) { p = p.next(() => { createFieldIndex(db); }); @@ -237,7 +255,9 @@ export class SchemaConverter implements SimpleDbSchemaConverter { ): PersistencePromise { let byteSize = 0; return txn - .store(DbRemoteDocumentStore) + .store( + DbRemoteDocumentStoreLegacy + ) .iterate((_, doc) => { byteSize += dbDocumentSize(doc); }) @@ -301,9 +321,10 @@ export class SchemaConverter implements SimpleDbSchemaConverter { DbTargetDocumentKey, DbTargetDocument >(DbTargetDocumentStore); - const documentsStore = txn.store( - DbRemoteDocumentStore - ); + const documentsStore = txn.store< + DbRemoteDocumentKeyLegacy, + DbRemoteDocumentKeyLegacy + >(DbRemoteDocumentStoreLegacy); const globalTargetStore = txn.store( DbTargetGlobalStore ); @@ -373,7 +394,9 @@ export class SchemaConverter implements SimpleDbSchemaConverter { // Index existing remote documents. return txn - .store(DbRemoteDocumentStore) + .store( + DbRemoteDocumentStoreLegacy + ) .iterate({ keysOnly: true }, (pathSegments, _) => { const path = new ResourcePath(pathSegments); return addEntry(path.popLast()); @@ -401,6 +424,39 @@ export class SchemaConverter implements SimpleDbSchemaConverter { return targetStore.put(updatedDbTarget); }); } + + private rewriteRemoteDocumentCache( + db: IDBDatabase, + transaction: SimpleDbTransaction + ): PersistencePromise { + const legacyRemoteDocumentStore = transaction.store< + DbRemoteDocumentKeyLegacy, + DbRemoteDocumentLegacy + >(DbRemoteDocumentStoreLegacy); + + const writes: Array> = []; + return legacyRemoteDocumentStore + .iterate((_, legacyDocument) => { + const remoteDocumentStore = transaction.store< + DbRemoteDocumentKey, + DbRemoteDocument + >(DbRemoteDocumentStore); + + const path = extractKey(legacyDocument).path.toArray(); + const dbRemoteDocument = { + prefixPath: path.slice(0, path.length - 2), + collectionGroup: path[path.length - 2], + documentId: path[path.length - 1], + readTime: legacyDocument.readTime || [0, 0], + unknownDocument: legacyDocument.unknownDocument, + noDocument: legacyDocument.noDocument, + document: legacyDocument.document, + hasCommittedMutations: !!legacyDocument.hasCommittedMutations + }; + writes.push(remoteDocumentStore.put(dbRemoteDocument)); + }) + .next(() => PersistencePromise.waitFor(writes)); + } } function sentinelKey(path: ResourcePath): DbTargetDocumentKey { @@ -464,8 +520,27 @@ function upgradeMutationBatchSchemaAndMigrateData( }); } +function createLegacyRemoteDocumentCache(db: IDBDatabase): void { + db.createObjectStore(DbRemoteDocumentStoreLegacy); +} + function createRemoteDocumentCache(db: IDBDatabase): void { - db.createObjectStore(DbRemoteDocumentStore); + const remoteDocumentStore = db.createObjectStore(DbRemoteDocumentStore, { + keyPath: DbRemoteDocumentKeyPath + }); + remoteDocumentStore.createIndex( + DbRemoteDocumentDocumentKeyIndex, + DbRemoteDocumentDocumentKeyIndexPath + ); + remoteDocumentStore.createIndex( + DbRemoteDocumentReadTimeIndex, + DbRemoteDocumentReadTimeIndexPath, + { unique: false } + ); + remoteDocumentStore.createIndex( + DbRemoteDocumentCollectionGroupIndex, + DbRemoteDocumentCollectionGroupIndexPath + ); } function createDocumentGlobalStore(db: IDBDatabase): void { @@ -527,24 +602,6 @@ function writeEmptyTargetGlobalEntry( return globalStore.put(DbTargetGlobalKey, metadata); } -/** - * Creates indices on the RemoteDocuments store used for both multi-tab - * and Index-Free queries. - */ -function createRemoteDocumentReadTimeIndex(txn: IDBTransaction): void { - const remoteDocumentStore = txn.objectStore(DbRemoteDocumentStore); - remoteDocumentStore.createIndex( - DbRemoteDocumentReadTimeIndex, - DbRemoteDocumentReadTimeIndexPath, - { unique: false } - ); - remoteDocumentStore.createIndex( - DbRemoteDocumentCollectionReadTimeIndex, - DbRemoteDocumentCollectionReadTimeIndexPath, - { unique: false } - ); -} - function createClientMetadataStore(db: IDBDatabase): void { db.createObjectStore(DbClientMetadataStore, { keyPath: DbClientMetadataKeyPath @@ -564,14 +621,14 @@ function createNamedQueriesStore(db: IDBDatabase): void { } function createFieldIndex(db: IDBDatabase): void { - const indexConfiguratioStore = db.createObjectStore( + const indexConfigurationStore = db.createObjectStore( DbIndexConfigurationStore, { keyPath: DbIndexConfigurationKeyPath, autoIncrement: true } ); - indexConfiguratioStore.createIndex( + indexConfigurationStore.createIndex( DbIndexConfigurationCollectionGroupIndex, DbIndexConfigurationCollectionGroupIndexPath, { unique: false } @@ -611,3 +668,17 @@ function createDocumentOverlayStore(db: IDBDatabase): void { { unique: false } ); } + +function extractKey(remoteDoc: DbRemoteDocumentLegacy): DocumentKey { + if (remoteDoc.document) { + return new DocumentKey( + ResourcePath.fromString(remoteDoc.document.name!).popFirst(5) + ); + } else if (remoteDoc.noDocument) { + return DocumentKey.fromSegments(remoteDoc.noDocument.path); + } else if (remoteDoc.unknownDocument) { + return DocumentKey.fromSegments(remoteDoc.unknownDocument.path); + } else { + return fail('Unexpected DbRemoteDocument'); + } +} diff --git a/packages/firestore/src/local/indexeddb_schema_legacy.ts b/packages/firestore/src/local/indexeddb_schema_legacy.ts new file mode 100644 index 00000000000..909f2cf1d41 --- /dev/null +++ b/packages/firestore/src/local/indexeddb_schema_legacy.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document as ProtoDocument } from '../protos/firestore_proto_api'; + +import { DbNoDocument, DbUnknownDocument } from './indexeddb_schema'; +import { DbTimestampKey } from './indexeddb_sentinels'; + +// This file contains older schema definitions for object stores that were +// migrated to newer schema versions. These object stores should only be used +// during schema migrations. + +export interface DbRemoteDocument { + unknownDocument?: DbUnknownDocument; + noDocument?: DbNoDocument; + document?: ProtoDocument; + hasCommittedMutations?: boolean; + readTime?: DbTimestampKey; + parentPath?: string[]; +} + +export type DbRemoteDocumentKey = string[]; +export const DbRemoteDocumentStore = 'remoteDocuments'; diff --git a/packages/firestore/src/local/indexeddb_sentinels.ts b/packages/firestore/src/local/indexeddb_sentinels.ts index 189175b905d..3623b5a0641 100644 --- a/packages/firestore/src/local/indexeddb_sentinels.ts +++ b/packages/firestore/src/local/indexeddb_sentinels.ts @@ -24,6 +24,7 @@ import { encodeResourcePath } from './encoded_resource_path'; import { DbDocumentMutation } from './indexeddb_schema'; +import { DbRemoteDocumentStore as DbRemoteDocumentStoreLegacy } from './indexeddb_schema_legacy'; // This file contains static constants and helper functions for IndexedDB. // It is split from indexeddb_schema to allow for minification. @@ -123,7 +124,29 @@ export const DbDocumentMutationPlaceholder: DbDocumentMutation = {}; export const DbDocumentMutationStore = 'documentMutations'; -export const DbRemoteDocumentStore = 'remoteDocuments'; +export const DbRemoteDocumentStore = 'remoteDocumentsV14'; + +/** + * A key in the 'remoteDocumentsV14' object store is an array containing the + * collection path, the collection group, the read time and the document id. + */ +export type DbRemoteDocumentKey = [ + /** path to collection */ string[], + /** collection group */ string, + /** read time */ DbTimestampKey, + /** document ID */ string +]; + +/** + * The primary key of the remote documents store, which allows for efficient + * access by collection path and read time. + */ +export const DbRemoteDocumentKeyPath = [ + 'prefixPath', + 'collectionGroup', + 'readTime', + 'documentId' +]; /** * An index that provides access to all entries sorted by read time (which @@ -133,21 +156,31 @@ export const DbRemoteDocumentStore = 'remoteDocuments'; */ export const DbRemoteDocumentReadTimeIndex = 'readTimeIndex'; +// TODO(indexing): Consider re-working Multi-Tab to use the collectionGroupIndex export const DbRemoteDocumentReadTimeIndexPath = 'readTime'; +/** An index that provides access to documents by key. */ +export const DbRemoteDocumentDocumentKeyIndex = 'documentKeyIndex'; + +export const DbRemoteDocumentDocumentKeyIndexPath = [ + 'prefixPath', + 'collectionGroup', + 'documentId' +]; + /** - * An index that provides access to documents in a collection sorted by read + * An index that provides access to documents by collection group and read * time. * - * This index is used to allow the RemoteDocumentCache to fetch newly changed - * documents in a collection. + * This index is used by the index backfiller. */ -export const DbRemoteDocumentCollectionReadTimeIndex = - 'collectionReadTimeIndex'; +export const DbRemoteDocumentCollectionGroupIndex = 'collectionGroupIndex'; -export const DbRemoteDocumentCollectionReadTimeIndexPath = [ - 'parentPath', - 'readTime' +export const DbRemoteDocumentCollectionGroupIndexPath = [ + 'collectionGroup', + 'readTime', + 'prefixPath', + 'documentId' ]; export const DbRemoteDocumentGlobalStore = 'remoteDocumentGlobal'; @@ -346,7 +379,7 @@ export const V1_STORES = [ DbMutationQueueStore, DbMutationBatchStore, DbDocumentMutationStore, - DbRemoteDocumentStore, + DbRemoteDocumentStoreLegacy, DbTargetStore, DbPrimaryClientStore, DbTargetGlobalStore, @@ -362,7 +395,23 @@ export const V8_STORES = [...V6_STORES, DbCollectionParentStore]; export const V11_STORES = [...V8_STORES, DbBundleStore, DbNamedQueryStore]; export const V12_STORES = [...V11_STORES, DbDocumentOverlayStore]; export const V13_STORES = [ - ...V12_STORES, + DbMutationQueueStore, + DbMutationBatchStore, + DbDocumentMutationStore, + DbRemoteDocumentStore, + DbTargetStore, + DbPrimaryClientStore, + DbTargetGlobalStore, + DbTargetDocumentStore, + DbClientMetadataStore, + DbRemoteDocumentGlobalStore, + DbCollectionParentStore, + DbBundleStore, + DbNamedQueryStore, + DbDocumentOverlayStore +]; +export const V14_STORES = [ + ...V13_STORES, DbIndexConfigurationStore, DbIndexStateStore, DbIndexEntryStore @@ -377,7 +426,9 @@ export const ALL_STORES = V12_STORES; /** Returns the object stores for the provided schema. */ export function getObjectStores(schemaVersion: number): string[] { - if (schemaVersion === 13) { + if (schemaVersion === 14) { + return V14_STORES; + } else if (schemaVersion === 13) { return V13_STORES; } else if (schemaVersion === 12) { return V12_STORES; @@ -387,9 +438,3 @@ export function getObjectStores(schemaVersion: number): string[] { fail('Only schema version 11 and 12 and 13 are supported'); } } - -/** - * A key in the 'remoteDocuments' object store is a string array containing the - * segments that make up the path. - */ -export type DbRemoteDocumentKey = string[]; diff --git a/packages/firestore/src/local/local_documents_view.ts b/packages/firestore/src/local/local_documents_view.ts index cf60ca6dbf1..3e0edb081dd 100644 --- a/packages/firestore/src/local/local_documents_view.ts +++ b/packages/firestore/src/local/local_documents_view.ts @@ -22,7 +22,6 @@ import { Query, queryMatches } from '../core/query'; -import { SnapshotVersion } from '../core/snapshot_version'; import { DocumentKeySet, DocumentMap, @@ -31,6 +30,7 @@ import { } from '../model/collections'; import { Document, MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { IndexOffset } from '../model/field_index'; import { mutationApplyToLocalView } from '../model/mutation'; import { MutationBatch } from '../model/mutation_batch'; import { ResourcePath } from '../model/path'; @@ -134,13 +134,12 @@ export class LocalDocumentsView { * * @param transaction - The persistence transaction. * @param query - The query to match documents against. - * @param sinceReadTime - If not set to SnapshotVersion.min(), return only - * documents that have been read since this snapshot version (exclusive). + * @param offset - Read time and key to start scanning by (exclusive). */ getDocumentsMatchingQuery( transaction: PersistenceTransaction, query: Query, - sinceReadTime: SnapshotVersion + offset: IndexOffset ): PersistencePromise { if (isDocumentQuery(query)) { return this.getDocumentsMatchingDocumentQuery(transaction, query.path); @@ -148,13 +147,13 @@ export class LocalDocumentsView { return this.getDocumentsMatchingCollectionGroupQuery( transaction, query, - sinceReadTime + offset ); } else { return this.getDocumentsMatchingCollectionQuery( transaction, query, - sinceReadTime + offset ); } } @@ -178,7 +177,7 @@ export class LocalDocumentsView { private getDocumentsMatchingCollectionGroupQuery( transaction: PersistenceTransaction, query: Query, - sinceReadTime: SnapshotVersion + offset: IndexOffset ): PersistencePromise { debugAssert( query.path.isEmpty(), @@ -199,7 +198,7 @@ export class LocalDocumentsView { return this.getDocumentsMatchingCollectionQuery( transaction, collectionQuery, - sinceReadTime + offset ).next(r => { r.forEach((key, doc) => { results = results.insert(key, doc); @@ -212,12 +211,12 @@ export class LocalDocumentsView { private getDocumentsMatchingCollectionQuery( transaction: PersistenceTransaction, query: Query, - sinceReadTime: SnapshotVersion + offset: IndexOffset ): PersistencePromise { // Query the remote documents and overlay mutations. let results: MutableDocumentMap; return this.remoteDocumentCache - .getAll(transaction, query.path, sinceReadTime) + .getAllFromCollection(transaction, query.path, offset) .next(queryResults => { results = queryResults; return this.mutationQueue.getAllMutationBatchesAffectingQuery( diff --git a/packages/firestore/src/local/local_serializer.ts b/packages/firestore/src/local/local_serializer.ts index 9cf68484e3d..3923e6777a3 100644 --- a/packages/firestore/src/local/local_serializer.ts +++ b/packages/firestore/src/local/local_serializer.ts @@ -117,37 +117,31 @@ export function toDbRemoteDocument( localSerializer: LocalSerializer, document: MutableDocument ): DbRemoteDocument { - const parentPath = document.key.path.popLast().toArray(); - const readTime = toDbTimestampKey(document.readTime); + const key = document.key; + const remoteDoc: DbRemoteDocument = { + prefixPath: key.getCollectionPath().popLast().toArray(), + collectionGroup: key.collectionGroup, + documentId: key.path.lastSegment(), + readTime: toDbTimestampKey(document.readTime), + hasCommittedMutations: document.hasCommittedMutations + }; + if (document.isFoundDocument()) { - const doc = toDocument(localSerializer.remoteSerializer, document); - const hasCommittedMutations = document.hasCommittedMutations; - return { - document: doc, - hasCommittedMutations, - readTime, - parentPath - }; + remoteDoc.document = toDocument(localSerializer.remoteSerializer, document); } else if (document.isNoDocument()) { - const path = document.key.path.toArray(); - const hasCommittedMutations = document.hasCommittedMutations; - return { - noDocument: { path, readTime: toDbTimestamp(document.version) }, - hasCommittedMutations, - readTime, - parentPath + remoteDoc.noDocument = { + path: key.path.toArray(), + readTime: toDbTimestamp(document.version) }; } else if (document.isUnknownDocument()) { - const path = document.key.path.toArray(); - return { - unknownDocument: { path, version: toDbTimestamp(document.version) }, - hasCommittedMutations: true, - readTime, - parentPath + remoteDoc.unknownDocument = { + path: key.path.toArray(), + version: toDbTimestamp(document.version) }; } else { return fail('Unexpected Document ' + document); } + return remoteDoc; } export function toDbTimestampKey( @@ -164,7 +158,7 @@ export function fromDbTimestampKey( return SnapshotVersion.fromTimestamp(timestamp); } -function toDbTimestamp(snapshotVersion: SnapshotVersion): DbTimestamp { +export function toDbTimestamp(snapshotVersion: SnapshotVersion): DbTimestamp { const timestamp = snapshotVersion.toTimestamp(); return { seconds: timestamp.seconds, nanoseconds: timestamp.nanoseconds }; } @@ -439,8 +433,8 @@ export function toDbDocumentOverlayKey( userId: string, docKey: DocumentKey ): DbDocumentOverlayKey { - const docId: string = docKey.path.lastSegment(); - const collectionPath: string = encodeResourcePath(docKey.path.popLast()); + const docId = docKey.path.lastSegment(); + const collectionPath = encodeResourcePath(docKey.path.popLast()); return [userId, collectionPath, docId]; } diff --git a/packages/firestore/src/local/memory_remote_document_cache.ts b/packages/firestore/src/local/memory_remote_document_cache.ts index b8be4f866f6..796e461295d 100644 --- a/packages/firestore/src/local/memory_remote_document_cache.ts +++ b/packages/firestore/src/local/memory_remote_document_cache.ts @@ -23,8 +23,13 @@ import { } from '../model/collections'; import { Document, MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { + IndexOffset, + indexOffsetComparator, + newIndexOffsetFromDocument +} from '../model/field_index'; import { ResourcePath } from '../model/path'; -import { debugAssert } from '../util/assert'; +import { debugAssert, fail } from '../util/assert'; import { SortedMap } from '../util/sorted_map'; import { IndexManager } from './index_manager'; @@ -154,10 +159,10 @@ class MemoryRemoteDocumentCacheImpl implements MemoryRemoteDocumentCache { return PersistencePromise.resolve(results); } - getAll( + getAllFromCollection( transaction: PersistenceTransaction, collectionPath: ResourcePath, - sinceReadTime: SnapshotVersion + offset: IndexOffset ): PersistencePromise { let results = mutableDocumentMap(); @@ -177,7 +182,10 @@ class MemoryRemoteDocumentCacheImpl implements MemoryRemoteDocumentCache { // Exclude entries from subcollections. continue; } - if (document.readTime.compareTo(sinceReadTime) <= 0) { + if ( + indexOffsetComparator(newIndexOffsetFromDocument(document), offset) <= 0 + ) { + // The document sorts before the offset. continue; } results = results.insert(document.key, document.mutableCopy()); @@ -185,6 +193,17 @@ class MemoryRemoteDocumentCacheImpl implements MemoryRemoteDocumentCache { return PersistencePromise.resolve(results); } + getAllFromCollectionGroup( + transaction: PersistenceTransaction, + collectionGroup: string, + offset: IndexOffset, + limti: number + ): PersistencePromise { + // This method should only be called from the IndexBackfiller if persistence + // is enabled. + fail('getAllFromCollectionGroup() is not supported.'); + } + forEachDocumentKey( transaction: PersistenceTransaction, f: (key: DocumentKey) => PersistencePromise diff --git a/packages/firestore/src/local/query_engine.ts b/packages/firestore/src/local/query_engine.ts index 3cf3ed956e1..53755663361 100644 --- a/packages/firestore/src/local/query_engine.ts +++ b/packages/firestore/src/local/query_engine.ts @@ -28,6 +28,11 @@ import { import { SnapshotVersion } from '../core/snapshot_version'; import { DocumentKeySet, DocumentMap } from '../model/collections'; import { Document } from '../model/document'; +import { + IndexOffset, + INITIAL_LARGEST_BATCH_ID, + newIndexOffsetSuccessorFromReadTime +} from '../model/field_index'; import { debugAssert } from '../util/assert'; import { getLogLevel, LogLevel, logDebug } from '../util/log'; import { SortedSet } from '../util/sorted_set'; @@ -117,7 +122,10 @@ export class QueryEngine { return this.localDocumentsView!.getDocumentsMatchingQuery( transaction, query, - lastLimboFreeSnapshotVersion + newIndexOffsetSuccessorFromReadTime( + lastLimboFreeSnapshotVersion, + INITIAL_LARGEST_BATCH_ID + ) ).next(updatedResults => { // We merge `previousResults` into `updateResults`, since // `updateResults` is already a DocumentMap. If a document is @@ -207,7 +215,7 @@ export class QueryEngine { return this.localDocumentsView!.getDocumentsMatchingQuery( transaction, query, - SnapshotVersion.min() + IndexOffset.min() ); } } diff --git a/packages/firestore/src/local/remote_document_cache.ts b/packages/firestore/src/local/remote_document_cache.ts index 5a8933348af..55c15826d3a 100644 --- a/packages/firestore/src/local/remote_document_cache.ts +++ b/packages/firestore/src/local/remote_document_cache.ts @@ -15,10 +15,10 @@ * limitations under the License. */ -import { SnapshotVersion } from '../core/snapshot_version'; import { DocumentKeySet, MutableDocumentMap } from '../model/collections'; import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { IndexOffset } from '../model/field_index'; import { ResourcePath } from '../model/path'; import { IndexManager } from './index_manager'; @@ -65,14 +65,29 @@ export interface RemoteDocumentCache { * Returns the documents from the provided collection. * * @param collection - The collection to read. - * @param sinceReadTime - If not set to SnapshotVersion.min(), return only - * documents that have been read since this snapshot version (exclusive). + * @param offset - The offset to start the scan at (exclusive). * @returns The set of matching documents. */ - getAll( + getAllFromCollection( transaction: PersistenceTransaction, collection: ResourcePath, - sinceReadTime: SnapshotVersion + offset: IndexOffset + ): PersistencePromise; + + /** + * Looks up the next `limit` documents for a collection group based on the + * provided offset. The ordering is based on the document's read time and key. + * + * @param collectionGroup - The collection group to scan. + * @param offset - The offset to start the scan at (exclusive). + * @param limit - The maximum number of results to return. + * @returns The set of matching documents. + */ + getAllFromCollectionGroup( + transaction: PersistenceTransaction, + collectionGroup: string, + offset: IndexOffset, + limit: number ): PersistencePromise; /** diff --git a/packages/firestore/src/model/field_index.ts b/packages/firestore/src/model/field_index.ts index 71b2ce634b7..eff7d22e269 100644 --- a/packages/firestore/src/model/field_index.ts +++ b/packages/firestore/src/model/field_index.ts @@ -27,7 +27,7 @@ import { FieldPath } from './path'; * The initial mutation batch id for each index. Gets updated during index * backfill. */ -const INITIAL_LARGEST_BATCH_ID = -1; +export const INITIAL_LARGEST_BATCH_ID = -1; /** * The initial sequence number for each index. Gets updated during index @@ -222,7 +222,7 @@ export class IndexOffset { readonly largestBatchId: number ) {} - /** The state of an index that has not yet been backfilled. */ + /** Returns an offset that sorts before all regular offsets. */ static min(): IndexOffset { return new IndexOffset( SnapshotVersion.min(), @@ -230,6 +230,15 @@ export class IndexOffset { INITIAL_LARGEST_BATCH_ID ); } + + /** Returns an offset that sorts after all regular offsets. */ + static max(): IndexOffset { + return new IndexOffset( + SnapshotVersion.max(), + DocumentKey.empty(), + INITIAL_LARGEST_BATCH_ID + ); + } } export function indexOffsetComparator( diff --git a/packages/firestore/src/model/path.ts b/packages/firestore/src/model/path.ts index 8ae933eb355..ecdb9ed086c 100644 --- a/packages/firestore/src/model/path.ts +++ b/packages/firestore/src/model/path.ts @@ -112,6 +112,7 @@ abstract class BasePath> { } lastSegment(): string { + debugAssert(!this.isEmpty(), "Can't call lastSegment() on empty path"); return this.get(this.length - 1); } diff --git a/packages/firestore/test/unit/local/counting_query_engine.ts b/packages/firestore/test/unit/local/counting_query_engine.ts index 08e7b662da5..784625e9cd3 100644 --- a/packages/firestore/test/unit/local/counting_query_engine.ts +++ b/packages/firestore/test/unit/local/counting_query_engine.ts @@ -96,9 +96,27 @@ export class CountingQueryEngine extends QueryEngine { setIndexManager: (indexManager: IndexManager) => { subject.setIndexManager(indexManager); }, - getAll: (transaction, collectionGroup, sinceReadTime) => { + getAllFromCollection: (transaction, collection, sinceReadTime) => { return subject - .getAll(transaction, collectionGroup, sinceReadTime) + .getAllFromCollection(transaction, collection, sinceReadTime) + .next(result => { + this.documentsReadByCollection += result.size; + return result; + }); + }, + getAllFromCollectionGroup: ( + transaction, + collectionGroup, + sinceReadTime, + limit + ) => { + return subject + .getAllFromCollectionGroup( + transaction, + collectionGroup, + sinceReadTime, + limit + ) .next(result => { this.documentsReadByCollection += result.size; return result; diff --git a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts index 3d69deb2fe2..d34412d31c2 100644 --- a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts +++ b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts @@ -41,6 +41,11 @@ import { SCHEMA_VERSION } from '../../../src/local/indexeddb_schema'; import { SchemaConverter } from '../../../src/local/indexeddb_schema_converter'; +import { + DbRemoteDocument as DbRemoteDocumentLegacy, + DbRemoteDocumentStore as DbRemoteDocumentStoreLegacy, + DbRemoteDocumentKey as DbRemoteDocumentKeyLegacy +} from '../../../src/local/indexeddb_schema_legacy'; import { DbCollectionParentKey, DbCollectionParentStore, @@ -53,7 +58,6 @@ import { DbMutationQueueStore, DbPrimaryClientKey, DbPrimaryClientStore, - DbRemoteDocumentCollectionReadTimeIndex, DbRemoteDocumentGlobalKey, DbRemoteDocumentGlobalStore, DbRemoteDocumentKey, @@ -68,6 +72,7 @@ import { newDbDocumentMutationKey, V12_STORES, V13_STORES, + V14_STORES, V1_STORES, V3_STORES, V4_STORES, @@ -76,8 +81,10 @@ import { } from '../../../src/local/indexeddb_sentinels'; import { fromDbTarget, + LocalSerializer, toDbRemoteDocument, toDbTarget, + toDbTimestamp, toDbTimestampKey } from '../../../src/local/local_serializer'; import { LruParams } from '../../../src/local/lru_garbage_collector'; @@ -85,9 +92,14 @@ import { PersistencePromise } from '../../../src/local/persistence_promise'; import { ClientId } from '../../../src/local/shared_client_state'; import { SimpleDb, SimpleDbTransaction } from '../../../src/local/simple_db'; import { TargetData, TargetPurpose } from '../../../src/local/target_data'; +import { MutableDocument } from '../../../src/model/document'; import { getWindow } from '../../../src/platform/dom'; import { firestoreV1ApiClientInterfaces } from '../../../src/protos/firestore_proto_api'; -import { JsonProtoSerializer } from '../../../src/remote/serializer'; +import { + JsonProtoSerializer, + toDocument +} from '../../../src/remote/serializer'; +import { fail } from '../../../src/util/assert'; import { AsyncQueue, TimerId } from '../../../src/util/async_queue'; import { AsyncQueueImpl, @@ -238,7 +250,7 @@ function addDocs( return PersistencePromise.forEach(keys, (key: string) => { const remoteDoc = doc(key, version, { data: 'foo' }); const dbRemoteDoc = toDbRemoteDocument(TEST_SERIALIZER, remoteDoc); - return remoteDocumentStore.put(remoteDoc.key.path.toArray(), dbRemoteDoc); + return remoteDocumentStore.put(dbRemoteDoc); }); } @@ -608,23 +620,21 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { doc('docs/b', 2, { baz: false }), doc('docs/c', 3, { a: 1, b: [5, 'foo'] }) ]; - const dbRemoteDocs = docs.map(doc => ({ - dbKey: doc.key.path.toArray(), - dbDoc: toDbRemoteDocument(TEST_SERIALIZER, doc) - })); // V5 stores doesn't exist return db.runTransaction( this.test!.fullTitle(), 'readwrite', V4_STORES, txn => { - const store = txn.store( - DbRemoteDocumentStore - ); - return PersistencePromise.forEach( - dbRemoteDocs, - ({ dbKey, dbDoc }: { dbKey: string[]; dbDoc: DbRemoteDocument }) => - store.put(dbKey, dbDoc) + const store = txn.store< + DbRemoteDocumentKeyLegacy, + DbRemoteDocumentLegacy + >(DbRemoteDocumentStoreLegacy); + return PersistencePromise.forEach(docs, (doc: MutableDocument) => + store.put( + doc.key.path.toArray(), + toLegacyDbRemoteDocument(TEST_SERIALIZER, doc) + ) ); } ); @@ -666,9 +676,9 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { DbTargetGlobal >(DbTargetGlobalStore); const remoteDocumentStore = txn.store< - DbRemoteDocumentKey, - DbRemoteDocument - >(DbRemoteDocumentStore); + DbRemoteDocumentKeyLegacy, + DbRemoteDocumentLegacy + >(DbRemoteDocumentStoreLegacy); const targetDocumentStore = txn.store< DbTargetDocumentKey, DbTargetDocument @@ -689,7 +699,7 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { promises.push( remoteDocumentStore.put( document.key.path.toArray(), - toDbRemoteDocument(serializer, document) + toLegacyDbRemoteDocument(serializer, document) ) ); if (i % 2 === 1) { @@ -771,9 +781,9 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { V6_STORES, txn => { const remoteDocumentStore = txn.store< - DbRemoteDocumentKey, - DbRemoteDocument - >(DbRemoteDocumentStore); + DbRemoteDocumentKeyLegacy, + DbRemoteDocumentLegacy + >(DbRemoteDocumentStoreLegacy); const documentMutationStore = txn.store< DbDocumentMutationKey, DbDocumentMutation @@ -798,7 +808,7 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { const remoteDoc = doc(path, /*version=*/ 1, { data: 1 }); return remoteDocumentStore.put( remoteDoc.key.path.toArray(), - toDbRemoteDocument(TEST_SERIALIZER, remoteDoc) + toLegacyDbRemoteDocument(TEST_SERIALIZER, remoteDoc) ); } ); @@ -909,9 +919,9 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { V8_STORES, txn => { const remoteDocumentStore = txn.store< - DbRemoteDocumentKey, - DbRemoteDocument - >(DbRemoteDocumentStore); + DbRemoteDocumentKeyLegacy, + DbRemoteDocumentLegacy + >(DbRemoteDocumentStoreLegacy); // Write the remote document entries. return PersistencePromise.forEach( @@ -919,7 +929,7 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { (path: string) => { const remoteDoc = doc(path, /*version=*/ 1, { data: 1 }); - const dbRemoteDoc = toDbRemoteDocument( + const dbRemoteDoc = toLegacyDbRemoteDocument( TEST_SERIALIZER, remoteDoc ); @@ -937,12 +947,12 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { ); }); - // Migrate to v9 and verify that new documents are indexed. - await withDb(9, db => { + // Migrate to v13 and verify that new documents are indexed. + await withDb(13, db => { return db.runTransaction( this.test!.fullTitle(), 'readwrite', - V8_STORES, + V13_STORES, txn => { const remoteDocumentStore = txn.store< DbRemoteDocumentKey, @@ -967,18 +977,16 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { // read time. const lastReadTime = toDbTimestampKey(version(1)); const range = IDBKeyRange.lowerBound( - [['coll2'], lastReadTime], + [[], 'coll2', lastReadTime, ''], true ); - return remoteDocumentStore - .loadAll(DbRemoteDocumentCollectionReadTimeIndex, range) - .next(docsRead => { - const keys = docsRead.map(dbDoc => dbDoc.document!.name); - expect(keys).to.have.members([ - 'projects/test-project/databases/(default)/documents/coll2/doc3', - 'projects/test-project/databases/(default)/documents/coll2/doc4' - ]); - }); + return remoteDocumentStore.loadAll(range).next(docsRead => { + const keys = docsRead.map(dbDoc => dbDoc.document!.name); + expect(keys).to.have.members([ + 'projects/test-project/databases/(default)/documents/coll2/doc3', + 'projects/test-project/databases/(default)/documents/coll2/doc4' + ]); + }); }); } ); @@ -989,11 +997,11 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { const oldDocPaths = ['coll/doc1', 'coll/doc2', 'abc/doc1']; const newDocPaths = ['coll/doc3', 'coll/doc4', 'abc/doc2']; - await withDb(9, db => { + await withDb(13, db => { return db.runTransaction( this.test!.fullTitle(), 'readwrite', - V8_STORES, + V13_STORES, txn => { return addDocs(txn, oldDocPaths, /* version= */ 1).next(() => addDocs(txn, newDocPaths, /* version= */ 2).next(() => { @@ -1002,20 +1010,18 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { DbRemoteDocument >(DbRemoteDocumentStore); - const lastReadTime = toDbTimestampKey(version(1)); + const lastReadTime = toDbTimestampKey(version(2)); const range = IDBKeyRange.lowerBound( - [['coll'], lastReadTime], + [[], 'coll', lastReadTime, ''], true ); - return remoteDocumentStore - .loadAll(DbRemoteDocumentCollectionReadTimeIndex, range) - .next(docsRead => { - const keys = docsRead.map(dbDoc => dbDoc.document!.name); - expect(keys).to.have.members([ - 'projects/test-project/databases/(default)/documents/coll/doc3', - 'projects/test-project/databases/(default)/documents/coll/doc4' - ]); - }); + return remoteDocumentStore.loadAll(range).next(docsRead => { + const keys = docsRead.map(dbDoc => dbDoc.document!.name); + expect(keys).to.have.members([ + 'projects/test-project/databases/(default)/documents/coll/doc3', + 'projects/test-project/databases/(default)/documents/coll/doc4' + ]); + }); }) ); } @@ -1027,11 +1033,11 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { const oldDocPaths = ['coll1/old', 'coll2/old']; const newDocPaths = ['coll1/new', 'coll2/new']; - await withDb(9, db => { + await withDb(13, db => { return db.runTransaction( this.test!.fullTitle(), 'readwrite', - V8_STORES, + V13_STORES, txn => { return addDocs(txn, oldDocPaths, /* version= */ 1).next(() => addDocs(txn, newDocPaths, /* version= */ 2).next(() => { @@ -1074,6 +1080,14 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { }); }); + it('can upgrade from version 13 to 14', async () => { + await withDb(13, async () => {}); + await withDb(14, async (db, version, objectStores) => { + expect(version).to.have.equal(14); + expect(objectStores).to.have.members(V14_STORES); + }); + }); + it('downgrading throws a custom error', async function (this: Context) { // Upgrade to latest version await withDb(SCHEMA_VERSION, async (db, version) => { @@ -1385,3 +1399,45 @@ describe('IndexedDb', () => { }); }); }); + +/** + * Converts a document to the format expected by schema version v13 and older. + */ +function toLegacyDbRemoteDocument( + localSerializer: LocalSerializer, + document: MutableDocument +): DbRemoteDocumentLegacy { + const dbReadTime = toDbTimestampKey(document.readTime); + const parentPath = document.key.path.popLast().toArray(); + if (document.isFoundDocument()) { + const doc = toDocument(localSerializer.remoteSerializer, document); + const hasCommittedMutations = document.hasCommittedMutations; + return { + document: doc, + hasCommittedMutations, + readTime: dbReadTime, + parentPath + }; + } else if (document.isNoDocument()) { + const path = document.key.path.toArray(); + const readTime = toDbTimestamp(document.version); + const hasCommittedMutations = document.hasCommittedMutations; + return { + noDocument: { path, readTime }, + hasCommittedMutations, + readTime: dbReadTime, + parentPath + }; + } else if (document.isUnknownDocument()) { + const path = document.key.path.toArray(); + const version = toDbTimestamp(document.version); + return { + unknownDocument: { path, version }, + hasCommittedMutations: true, + readTime: dbReadTime, + parentPath + }; + } else { + return fail('Unexpected Document ' + document); + } +} diff --git a/packages/firestore/test/unit/local/query_engine.test.ts b/packages/firestore/test/unit/local/query_engine.test.ts index 430ce67bd57..f4a829c7e75 100644 --- a/packages/firestore/test/unit/local/query_engine.test.ts +++ b/packages/firestore/test/unit/local/query_engine.test.ts @@ -33,6 +33,10 @@ import { documentKeySet, DocumentMap } from '../../../src/model/collections'; import { Document, MutableDocument } from '../../../src/model/document'; import { DocumentKey } from '../../../src/model/document_key'; import { DocumentSet } from '../../../src/model/document_set'; +import { + IndexOffset, + indexOffsetComparator +} from '../../../src/model/field_index'; import { debugAssert } from '../../../src/util/assert'; import { doc, filter, key, orderBy, query, version } from '../../util/helpers'; @@ -67,17 +71,17 @@ class TestLocalDocumentsView extends LocalDocumentsView { getDocumentsMatchingQuery( transaction: PersistenceTransaction, query: Query, - sinceReadTime: SnapshotVersion + offset: IndexOffset ): PersistencePromise { const skipsDocumentsBeforeSnapshot = - !SnapshotVersion.min().isEqual(sinceReadTime); + indexOffsetComparator(IndexOffset.min(), offset) !== 0; expect(skipsDocumentsBeforeSnapshot).to.eq( !this.expectFullCollectionScan, 'Observed query execution mode did not match expectation' ); - return super.getDocumentsMatchingQuery(transaction, query, sinceReadTime); + return super.getDocumentsMatchingQuery(transaction, query, offset); } } diff --git a/packages/firestore/test/unit/local/remote_document_cache.test.ts b/packages/firestore/test/unit/local/remote_document_cache.test.ts index d69c0d588e2..25af62da353 100644 --- a/packages/firestore/test/unit/local/remote_document_cache.test.ts +++ b/packages/firestore/test/unit/local/remote_document_cache.test.ts @@ -23,6 +23,12 @@ import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { remoteDocumentCacheGetLastReadTime } from '../../../src/local/indexeddb_remote_document_cache'; import { documentKeySet, DocumentMap } from '../../../src/model/collections'; import { MutableDocument, Document } from '../../../src/model/document'; +import { + IndexOffset, + INITIAL_LARGEST_BATCH_ID, + newIndexOffsetFromDocument, + newIndexOffsetSuccessorFromReadTime +} from '../../../src/model/field_index'; import { deletedDoc, doc, @@ -178,6 +184,78 @@ describe('IndexedDbRemoteDocumentCache', () => { ); }); + it('can get next documents from collection group', async () => { + await cache.addEntries([ + doc('a/1', 1, DOC_DATA), + doc('a/2', 2, DOC_DATA), + doc('b/1', 3, DOC_DATA) + ]); + + const results = await cache.getAllFromCollectionGroup( + 'a', + IndexOffset.min(), + Number.MAX_SAFE_INTEGER + ); + assertMatches([doc('a/1', 1, DOC_DATA), doc('a/2', 2, DOC_DATA)], results); + }); + + it('cen get next documents from collection group with limit', async () => { + await cache.addEntries([ + doc('a/1', 1, DOC_DATA), + doc('b/2/a/2', 2, DOC_DATA), + doc('a/3', 3, DOC_DATA) + ]); + + const results = await cache.getAllFromCollectionGroup( + 'a', + IndexOffset.min(), + 2 + ); + assertMatches( + [doc('a/1', 1, DOC_DATA), doc('b/2/a/2', 2, DOC_DATA)], + results + ); + }); + + it('cen get next documents from collection group with read time offset', async () => { + await cache.addEntries([ + doc('a/1', 1, DOC_DATA), + doc('a/2', 2, DOC_DATA), + doc('a/3', 3, DOC_DATA) + ]); + + const results = await cache.getAllFromCollectionGroup( + 'a', + newIndexOffsetSuccessorFromReadTime(version(1), INITIAL_LARGEST_BATCH_ID), + 2 + ); + assertMatches([doc('a/2', 2, DOC_DATA), doc('a/3', 3, DOC_DATA)], results); + }); + + it('cen get next documents from collection group with document key offset', async () => { + await cache.addEntries([ + doc('a/1', 1, DOC_DATA), + doc('a/2', 1, DOC_DATA), + doc('a/3', 1, DOC_DATA) + ]); + + const results = await cache.getAllFromCollectionGroup( + 'a', + newIndexOffsetFromDocument(doc('a/1', 1, DOC_DATA)), + Number.MAX_SAFE_INTEGER + ); + assertMatches([doc('a/2', 1, DOC_DATA), doc('a/3', 1, DOC_DATA)], results); + }); + + it('can get next documents from non-existing collection group', async () => { + const results = await cache.getAllFromCollectionGroup( + 'a', + IndexOffset.min(), + Number.MAX_SAFE_INTEGER + ); + assertMatches([], results); + }); + genericRemoteDocumentCacheTests(async () => cache); lruRemoteDocumentCacheTests(async () => cache); @@ -314,13 +392,26 @@ function genericRemoteDocumentCacheTests( const key2 = key(LONG_DOC_PATH); return cache .addEntries(docs) - .then(() => cache.getEntries(documentKeySet().add(key1).add(key2))) + .then(() => cache.getEntries(documentKeySet(key1, key2))) .then(read => { expectEqual(read.get(key1), docs[0]); expectEqual(read.get(key2), docs[1]); }); }); + it('can set and read several documents with overlapping keys', () => { + // This test verifies that the sorting works correctly in IndexedDB, + // which sorts by prefix path first. + const keys = ['a/b', 'a/b/c/d', 'a/b/c/d/e/f', 'a/c', 'c/b/d/e', 'f/g']; + const docs = keys.map(k => doc(k, VERSION, DOC_DATA)); + return cache + .addEntries(docs) + .then(() => cache.getEntries(documentKeySet(...keys.map(k => key(k))))) + .then(read => { + expect(read.size).to.equal(keys.length); + }); + }); + it('can set and read several documents including missing document', () => { const docs = [ doc(DOC_PATH, VERSION, DOC_DATA), @@ -331,9 +422,7 @@ function genericRemoteDocumentCacheTests( const missingKey = key('foo/nonexistent'); return cache .addEntries(docs) - .then(() => - cache.getEntries(documentKeySet().add(key1).add(key2).add(missingKey)) - ) + .then(() => cache.getEntries(documentKeySet(key1, key2, missingKey))) .then(read => { expectEqual(read.get(key1), docs[0]); expectEqual(read.get(key2), docs[1]); @@ -364,18 +453,33 @@ function genericRemoteDocumentCacheTests( await cache.addEntries([ doc('a/1', VERSION, DOC_DATA), doc('b/1', VERSION, DOC_DATA), - doc('b/1/z/1', VERSION, DOC_DATA), doc('b/2', VERSION, DOC_DATA), doc('c/1', VERSION, DOC_DATA) ]); - const matchingDocs = await cache.getAll(path('b'), SnapshotVersion.min()); + const matchingDocs = await cache.getAllFromCollection( + path('b'), + IndexOffset.min() + ); assertMatches( [doc('b/1', VERSION, DOC_DATA), doc('b/2', VERSION, DOC_DATA)], matchingDocs ); }); + it('getAll() excludes subcollection', async () => { + await cache.addEntries([ + doc('a/1', VERSION, DOC_DATA), + doc('a/1/b/1', VERSION, DOC_DATA) + ]); + + const matchingDocs = await cache.getAllFromCollection( + path('a'), + IndexOffset.min() + ); + assertMatches([doc('a/1', VERSION, DOC_DATA)], matchingDocs); + }); + it('can get all documents since read time', async () => { await cache.addEntries([ doc('b/old', 1, DOC_DATA).setReadTime(version(11)) @@ -387,9 +491,9 @@ function genericRemoteDocumentCacheTests( doc('b/new', 3, DOC_DATA).setReadTime(version(13)) ]); - const matchingDocs = await cache.getAll( + const matchingDocs = await cache.getAllFromCollection( path('b'), - /* sinceReadTime= */ version(12) + newIndexOffsetSuccessorFromReadTime(version(12), INITIAL_LARGEST_BATCH_ID) ); assertMatches([doc('b/new', 3, DOC_DATA)], matchingDocs); }); @@ -398,9 +502,9 @@ function genericRemoteDocumentCacheTests( await cache.addEntries([doc('b/old', 1, DOC_DATA).setReadTime(version(2))]); await cache.addEntries([doc('b/new', 2, DOC_DATA).setReadTime(version(1))]); - const matchingDocs = await cache.getAll( + const matchingDocs = await cache.getAllFromCollection( path('b'), - /* sinceReadTime= */ version(1) + newIndexOffsetSuccessorFromReadTime(version(1), INITIAL_LARGEST_BATCH_ID) ); assertMatches([doc('b/old', 1, DOC_DATA)], matchingDocs); }); @@ -434,7 +538,7 @@ function genericRemoteDocumentCacheTests( document.data.set(field('state'), wrap('new')); document = await cache - .getAll(path('coll'), SnapshotVersion.min()) + .getAllFromCollection(path('coll'), IndexOffset.min()) .then(m => m.get(key('coll/doc'))!); verifyOldValue(document); diff --git a/packages/firestore/test/unit/local/test_remote_document_cache.ts b/packages/firestore/test/unit/local/test_remote_document_cache.ts index ec0b3c09ec0..7e6d523d822 100644 --- a/packages/firestore/test/unit/local/test_remote_document_cache.ts +++ b/packages/firestore/test/unit/local/test_remote_document_cache.ts @@ -28,6 +28,7 @@ import { } from '../../../src/model/collections'; import { Document, MutableDocument } from '../../../src/model/document'; import { DocumentKey } from '../../../src/model/document_key'; +import { IndexOffset } from '../../../src/model/field_index'; import { ResourcePath } from '../../../src/model/path'; /** @@ -109,14 +110,32 @@ export class TestRemoteDocumentCache { }); } - getAll( + getAllFromCollection( collection: ResourcePath, - sinceReadTime: SnapshotVersion + offset: IndexOffset ): Promise { return this.persistence.runTransaction( - 'getDocumentsMatchingQuery', + 'getAllFromCollection', 'readonly', - txn => this.cache.getAll(txn, collection, sinceReadTime) + txn => this.cache.getAllFromCollection(txn, collection, offset) + ); + } + + getAllFromCollectionGroup( + collectionGroup: string, + offset: IndexOffset, + limit: number + ): Promise { + return this.persistence.runTransaction( + 'getAllFromCollectionGroup', + 'readonly', + txn => + this.cache.getAllFromCollectionGroup( + txn, + collectionGroup, + offset, + limit + ) ); }