diff --git a/.changeset/healthy-peas-heal.md b/.changeset/healthy-peas-heal.md new file mode 100644 index 00000000000..3a02a6d9a2f --- /dev/null +++ b/.changeset/healthy-peas-heal.md @@ -0,0 +1,6 @@ +--- +'@firebase/firestore': patch +'firebase': patch +--- + +Implemented internal logic to delete all client-side indexes diff --git a/packages/firestore/externs.json b/packages/firestore/externs.json index a1746227316..f77855208b1 100644 --- a/packages/firestore/externs.json +++ b/packages/firestore/externs.json @@ -31,6 +31,7 @@ "packages/util/dist/src/environment.d.ts", "packages/util/dist/src/compat.d.ts", "packages/util/dist/src/obj.d.ts", + "packages/firestore/src/api/persistent_cache_index_manager.ts", "packages/firestore/src/protos/firestore_bundle_proto.ts", "packages/firestore/src/protos/firestore_proto_api.ts", "packages/firestore/src/util/error.ts", diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index 9f9ac38749a..49d71e2adee 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -205,6 +205,7 @@ export { export { PersistentCacheIndexManager, getPersistentCacheIndexManager, + deleteAllPersistentCacheIndexes, enablePersistentCacheIndexAutoCreation, disablePersistentCacheIndexAutoCreation } from './api/persistent_cache_index_manager'; diff --git a/packages/firestore/src/api/persistent_cache_index_manager.ts b/packages/firestore/src/api/persistent_cache_index_manager.ts index 96751fee074..7aa5d113622 100644 --- a/packages/firestore/src/api/persistent_cache_index_manager.ts +++ b/packages/firestore/src/api/persistent_cache_index_manager.ts @@ -16,11 +16,14 @@ */ import { + firestoreClientDeleteAllFieldIndexes, firestoreClientSetPersistentCacheIndexAutoCreationEnabled, - FirestoreClient + FirestoreClient, + TestingHooks as FirestoreClientTestingHooks } from '../core/firestore_client'; import { cast } from '../util/input_validation'; import { logDebug, logWarn } from '../util/log'; +import { testingHooksSpi } from '../util/testing_hooks_spi'; import { ensureFirestoreConfigured, Firestore } from './database'; @@ -99,6 +102,31 @@ export function disablePersistentCacheIndexAutoCreation( setPersistentCacheIndexAutoCreationEnabled(indexManager, false); } +/** + * Removes all persistent cache indexes. + * + * Please note this function will also deletes indexes generated by + * `setIndexConfiguration()`, which is deprecated. + * + * TODO(CSI) Remove @internal to make the API publicly available. + * @internal + */ +export function deleteAllPersistentCacheIndexes( + indexManager: PersistentCacheIndexManager +): void { + indexManager._client.verifyNotTerminated(); + + const promise = firestoreClientDeleteAllFieldIndexes(indexManager._client); + + promise + .then(_ => logDebug('deleting all persistent cache indexes succeeded')) + .catch(error => + logWarn('deleting all persistent cache indexes failed', error) + ); + + testingHooksSpi?.notifyPersistentCacheDeleteAllIndexes(promise); +} + function setPersistentCacheIndexAutoCreationEnabled( indexManager: PersistentCacheIndexManager, isEnabled: boolean @@ -124,6 +152,8 @@ function setPersistentCacheIndexAutoCreationEnabled( error ) ); + + testingHooksSpi?.notifyPersistentCacheIndexAutoCreationToggle(promise); } /** @@ -138,3 +168,25 @@ const persistentCacheIndexManagerByFirestore = new WeakMap< Firestore, PersistentCacheIndexManager >(); + +/** + * Test-only hooks into the SDK for use exclusively by tests. + */ +export class TestingHooks { + private constructor() { + throw new Error('creating instances is not supported'); + } + + static setIndexAutoCreationSettings( + indexManager: PersistentCacheIndexManager, + settings: { + indexAutoCreationMinCollectionSize?: number; + relativeIndexReadCostPerDocument?: number; + } + ): Promise { + return FirestoreClientTestingHooks.setPersistentCacheIndexAutoCreationSettings( + indexManager._client, + settings + ); + } +} diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 6b7950825f6..ff01420e92b 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -23,14 +23,17 @@ import { CredentialsProvider } from '../api/credentials'; import { User } from '../auth/user'; +import { IndexType } from '../local/index_manager'; import { LocalStore } from '../local/local_store'; import { localStoreConfigureFieldIndexes, + localStoreDeleteAllFieldIndexes, localStoreExecuteQuery, localStoreGetNamedQuery, localStoreHandleUserChange, localStoreReadDocument, - localStoreSetIndexAutoCreationEnabled + localStoreSetIndexAutoCreationEnabled, + TestingHooks as LocalStoreTestingHooks } from '../local/local_store_impl'; import { Persistence } from '../local/persistence'; import { Document } from '../model/document'; @@ -841,3 +844,47 @@ export function firestoreClientSetPersistentCacheIndexAutoCreationEnabled( ); }); } + +export function firestoreClientDeleteAllFieldIndexes( + client: FirestoreClient +): Promise { + return client.asyncQueue.enqueue(async () => { + return localStoreDeleteAllFieldIndexes(await getLocalStore(client)); + }); +} + +/** + * Test-only hooks into the SDK for use exclusively by tests. + */ +export class TestingHooks { + private constructor() { + throw new Error('creating instances is not supported'); + } + + static getQueryIndexType( + client: FirestoreClient, + query: Query + ): Promise { + return client.asyncQueue.enqueue(async () => { + const localStore = await getLocalStore(client); + return LocalStoreTestingHooks.getQueryIndexType(localStore, query); + }); + } + + static setPersistentCacheIndexAutoCreationSettings( + client: FirestoreClient, + settings: { + indexAutoCreationMinCollectionSize?: number; + relativeIndexReadCostPerDocument?: number; + } + ): Promise { + const settingsCopy = { ...settings }; + return client.asyncQueue.enqueue(async () => { + const localStore = await getLocalStore(client); + LocalStoreTestingHooks.setIndexAutoCreationSettings( + localStore, + settingsCopy + ); + }); + } +} diff --git a/packages/firestore/src/local/index_manager.ts b/packages/firestore/src/local/index_manager.ts index 78bc47a9471..d81693acc60 100644 --- a/packages/firestore/src/local/index_manager.ts +++ b/packages/firestore/src/local/index_manager.ts @@ -105,6 +105,11 @@ export interface IndexManager { index: FieldIndex ): PersistencePromise; + /** Removes all field indexes and deletes all index values. */ + deleteAllFieldIndexes( + transaction: PersistenceTransaction + ): PersistencePromise; + /** Creates a full matched field index which serves the given target. */ createTargetIndexes( transaction: PersistenceTransaction, diff --git a/packages/firestore/src/local/indexeddb_index_manager.ts b/packages/firestore/src/local/indexeddb_index_manager.ts index f7e5991a6be..71c5c9a70bc 100644 --- a/packages/firestore/src/local/indexeddb_index_manager.ts +++ b/packages/firestore/src/local/indexeddb_index_manager.ts @@ -252,6 +252,19 @@ export class IndexedDbIndexManager implements IndexManager { ); } + deleteAllFieldIndexes( + transaction: PersistenceTransaction + ): PersistencePromise { + const indexes = indexConfigurationStore(transaction); + const entries = indexEntriesStore(transaction); + const states = indexStateStore(transaction); + + return indexes + .deleteAll() + .next(() => entries.deleteAll()) + .next(() => states.deleteAll()); + } + createTargetIndexes( transaction: PersistenceTransaction, target: Target diff --git a/packages/firestore/src/local/local_store_impl.ts b/packages/firestore/src/local/local_store_impl.ts index 11459a1716c..1a106e57b7a 100644 --- a/packages/firestore/src/local/local_store_impl.ts +++ b/packages/firestore/src/local/local_store_impl.ts @@ -70,7 +70,7 @@ import { BATCHID_UNKNOWN } from '../util/types'; import { BundleCache } from './bundle_cache'; import { DocumentOverlayCache } from './document_overlay_cache'; -import { IndexManager } from './index_manager'; +import { IndexManager, IndexType } from './index_manager'; import { IndexedDbMutationQueue } from './indexeddb_mutation_queue'; import { IndexedDbPersistence } from './indexeddb_persistence'; import { IndexedDbTargetCache } from './indexeddb_target_cache'; @@ -1535,6 +1535,18 @@ export function localStoreSetIndexAutoCreationEnabled( localStoreImpl.queryEngine.indexAutoCreationEnabled = isEnabled; } +export function localStoreDeleteAllFieldIndexes( + localStore: LocalStore +): Promise { + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + const indexManager = localStoreImpl.indexManager; + return localStoreImpl.persistence.runTransaction( + 'Delete All Indexes', + 'readwrite', + transaction => indexManager.deleteAllFieldIndexes(transaction) + ); +} + /** * Test-only hooks into the SDK for use exclusively by tests. */ @@ -1560,4 +1572,17 @@ export class TestingHooks { settings.relativeIndexReadCostPerDocument; } } + + static getQueryIndexType( + localStore: LocalStore, + query: Query + ): Promise { + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + const target = queryToTarget(query); + return localStoreImpl.persistence.runTransaction( + 'local_store_impl TestingHooks getQueryIndexType', + 'readonly', + txn => localStoreImpl.indexManager.getIndexType(txn, target) + ); + } } diff --git a/packages/firestore/src/local/memory_index_manager.ts b/packages/firestore/src/local/memory_index_manager.ts index 5a363118767..2153cd197b7 100644 --- a/packages/firestore/src/local/memory_index_manager.ts +++ b/packages/firestore/src/local/memory_index_manager.ts @@ -66,6 +66,13 @@ export class MemoryIndexManager implements IndexManager { return PersistencePromise.resolve(); } + deleteAllFieldIndexes( + transaction: PersistenceTransaction + ): PersistencePromise { + // Field indices are not supported with memory persistence. + return PersistencePromise.resolve(); + } + createTargetIndexes( transaction: PersistenceTransaction, target: Target diff --git a/packages/firestore/src/util/testing_hooks.ts b/packages/firestore/src/util/testing_hooks.ts index 36422172a45..bb21e7ca7e5 100644 --- a/packages/firestore/src/util/testing_hooks.ts +++ b/packages/firestore/src/util/testing_hooks.ts @@ -15,8 +15,17 @@ * limitations under the License. */ +import { ensureFirestoreConfigured, Firestore } from '../api/database'; +import { + PersistentCacheIndexManager, + TestingHooks as PersistentCacheIndexManagerTestingHooks +} from '../api/persistent_cache_index_manager'; import { Unsubscribe } from '../api/reference_impl'; +import { TestingHooks as FirestoreClientTestingHooks } from '../core/firestore_client'; +import { Query } from '../lite-api/reference'; +import { IndexType } from '../local/index_manager'; +import { cast } from './input_validation'; import { setTestingHooksSpi, ExistenceFilterMismatchInfo, @@ -54,6 +63,106 @@ export class TestingHooks { ): Unsubscribe { return TestingHooksSpiImpl.instance.onExistenceFilterMismatch(callback); } + + /** + * Registers a callback to be notified when + * `enablePersistentCacheIndexAutoCreation()` or + * `disablePersistentCacheIndexAutoCreation()` is invoked. + * + * The relative order in which callbacks are notified is unspecified; do not + * rely on any particular ordering. If a given callback is registered multiple + * times then it will be notified multiple times, once per registration. + * + * @param callback the callback to invoke when + * `enablePersistentCacheIndexAutoCreation()` or + * `disablePersistentCacheIndexAutoCreation()` is invoked. + * + * @return a function that, when called, unregisters the given callback; only + * the first invocation of the returned function does anything; all subsequent + * invocations do nothing. + */ + static onPersistentCacheIndexAutoCreationToggle( + callback: PersistentCacheIndexAutoCreationToggleCallback + ): Unsubscribe { + return TestingHooksSpiImpl.instance.onPersistentCacheIndexAutoCreationToggle( + callback + ); + } + + /** + * Registers a callback to be notified when + * `deleteAllPersistentCacheIndexes()` is invoked. + * + * The relative order in which callbacks are notified is unspecified; do not + * rely on any particular ordering. If a given callback is registered multiple + * times then it will be notified multiple times, once per registration. + * + * @param callback the callback to invoke when + * `deleteAllPersistentCacheIndexes()` is invoked. + * + * @return a function that, when called, unregisters the given callback; only + * the first invocation of the returned function does anything; all subsequent + * invocations do nothing. + */ + static onPersistentCacheDeleteAllIndexes( + callback: PersistentCacheDeleteAllIndexesCallback + ): Unsubscribe { + return TestingHooksSpiImpl.instance.onPersistentCacheDeleteAllIndexes( + callback + ); + } + + /** + * Determines the type of client-side index that will be used when executing the + * given query against the local cache. + * + * @param query The query whose client-side index type to get; it is typed as + * `unknown` so that it is usable in the minified, bundled code, but it should + * be a `Query` object. + */ + static async getQueryIndexType( + query: unknown + ): Promise<'full' | 'partial' | 'none'> { + const query_ = cast(query as Query, Query); + const firestore = cast(query_.firestore, Firestore); + const client = ensureFirestoreConfigured(firestore); + + const indexType = await FirestoreClientTestingHooks.getQueryIndexType( + client, + query_._query + ); + + switch (indexType) { + case IndexType.NONE: + return 'none'; + case IndexType.PARTIAL: + return 'partial'; + case IndexType.FULL: + return 'full'; + default: + throw new Error(`unrecognized IndexType: ${indexType}`); + } + } + + /** + * Sets the persistent cache index auto-creation settings for the given + * Firestore instance. + * + * @return a Promise that is fulfilled when the settings are successfully + * applied, or rejected if applying the settings fails. + */ + static setPersistentCacheIndexAutoCreationSettings( + indexManager: PersistentCacheIndexManager, + settings: { + indexAutoCreationMinCollectionSize?: number; + relativeIndexReadCostPerDocument?: number; + } + ): Promise { + return PersistentCacheIndexManagerTestingHooks.setIndexAutoCreationSettings( + indexManager, + settings + ); + } } /** @@ -68,6 +177,39 @@ export type ExistenceFilterMismatchCallback = ( info: ExistenceFilterMismatchInfo ) => unknown; +/** + * The signature of callbacks registered with + * `TestingHooks.onPersistentCacheIndexAutoCreationToggle()`. + * + * The `promise` argument will be fulfilled when the asynchronous work started + * by the call to `enablePersistentCacheIndexAutoCreation()` or + * `disablePersistentCacheIndexAutoCreation()` completes successfully, or will + * be rejected if it fails. + * + * The return value, if any, is ignored. + * + * @internal + */ +export type PersistentCacheIndexAutoCreationToggleCallback = ( + promise: Promise +) => unknown; + +/** + * The signature of callbacks registered with + * `TestingHooks.onPersistentCacheDeleteAllIndexes()`. + * + * The `promise` argument will be fulfilled when the asynchronous work started + * by the call to `deleteAllPersistentCacheIndexes()` completes successfully, or + * will be rejected if it fails. + * + * The return value of the callback, if any, is ignored. + * + * @internal + */ +export type PersistentCacheDeleteAllIndexesCallback = ( + promise: Promise +) => unknown; + /** * The implementation of `TestingHooksSpi`. */ @@ -77,6 +219,14 @@ class TestingHooksSpiImpl implements TestingHooksSpi { ExistenceFilterMismatchCallback >(); + private readonly persistentCacheIndexAutoCreationToggleCallbacksById = + new Map(); + + private readonly persistentCacheDeleteAllIndexesCallbacksById = new Map< + Symbol, + PersistentCacheDeleteAllIndexesCallback + >(); + private constructor() {} static get instance(): TestingHooksSpiImpl { @@ -96,11 +246,50 @@ class TestingHooksSpiImpl implements TestingHooksSpi { onExistenceFilterMismatch( callback: ExistenceFilterMismatchCallback ): Unsubscribe { - const id = Symbol(); - const callbacks = this.existenceFilterMismatchCallbacksById; - callbacks.set(id, callback); - return () => callbacks.delete(id); + return registerCallback( + callback, + this.existenceFilterMismatchCallbacksById + ); } + + notifyPersistentCacheIndexAutoCreationToggle(promise: Promise): void { + this.persistentCacheIndexAutoCreationToggleCallbacksById.forEach(callback => + callback(promise) + ); + } + + onPersistentCacheIndexAutoCreationToggle( + callback: PersistentCacheIndexAutoCreationToggleCallback + ): Unsubscribe { + return registerCallback( + callback, + this.persistentCacheIndexAutoCreationToggleCallbacksById + ); + } + + notifyPersistentCacheDeleteAllIndexes(promise: Promise): void { + this.persistentCacheDeleteAllIndexesCallbacksById.forEach(callback => + callback(promise) + ); + } + + onPersistentCacheDeleteAllIndexes( + callback: PersistentCacheDeleteAllIndexesCallback + ): Unsubscribe { + return registerCallback( + callback, + this.persistentCacheDeleteAllIndexesCallbacksById + ); + } +} + +function registerCallback( + callback: T, + callbacks: Map +): Unsubscribe { + const id = Symbol(); + callbacks.set(id, callback); + return () => callbacks.delete(id); } let testingHooksSpiImplInstance: TestingHooksSpiImpl | null = null; diff --git a/packages/firestore/src/util/testing_hooks_spi.ts b/packages/firestore/src/util/testing_hooks_spi.ts index e6c729e3661..811e1a52e39 100644 --- a/packages/firestore/src/util/testing_hooks_spi.ts +++ b/packages/firestore/src/util/testing_hooks_spi.ts @@ -50,6 +50,20 @@ export interface TestingHooksSpi { * `TestingHooks.onExistenceFilterMismatch()` with the given info. */ notifyOnExistenceFilterMismatch(info: ExistenceFilterMismatchInfo): void; + + /** + * Invokes all callbacks registered with + * `TestingHooks.onPersistentCacheIndexAutoCreationToggle()` with the given + * promise. + */ + notifyPersistentCacheIndexAutoCreationToggle(promise: Promise): void; + + /** + * Invokes all callbacks registered with + * `TestingHooks.onPersistentCacheDeleteAllIndexes()` with the given + * promise. + */ + notifyPersistentCacheDeleteAllIndexes(promise: Promise): void; } /** diff --git a/packages/firestore/test/integration/api/persistent_cache_index_manager.test.ts b/packages/firestore/test/integration/api/persistent_cache_index_manager.test.ts index 5d09c948a64..0fa51169e8a 100644 --- a/packages/firestore/test/integration/api/persistent_cache_index_manager.test.ts +++ b/packages/firestore/test/integration/api/persistent_cache_index_manager.test.ts @@ -18,17 +18,17 @@ import { expect } from 'chai'; import { + deleteAllPersistentCacheIndexes, disablePersistentCacheIndexAutoCreation, - doc, enablePersistentCacheIndexAutoCreation, - getDoc, getDocs, getDocsFromCache, getPersistentCacheIndexManager, PersistentCacheIndexManager, query, terminate, - where + where, + _TestingHooks as TestingHooks } from '../util/firebase_export'; import { apiDescribe, @@ -36,6 +36,12 @@ import { withTestCollection, withTestDb } from '../util/helpers'; +import { + getQueryIndexType, + setPersistentCacheIndexAutoCreationSettings, + verifyPersistentCacheDeleteAllIndexesSucceedsDuring, + verifyPersistentCacheIndexAutoCreationToggleSucceedsDuring +} from '../util/testing_hooks_util'; apiDescribe('PersistentCacheIndexManager', persistence => { describe('getPersistentCacheIndexManager()', () => { @@ -71,40 +77,40 @@ apiDescribe('PersistentCacheIndexManager', persistence => { return; } - describe('enable/disable persistent index auto creation', () => { - it('enable on new instance should succeed', () => + describe('enablePersistentCacheIndexAutoCreation()', () => { + it('should return successfully', () => withTestDb(persistence, async db => { const indexManager = getPersistentCacheIndexManager(db)!; enablePersistentCacheIndexAutoCreation(indexManager); })); - it('disable on new instance should succeed', () => - withTestDb(persistence, async db => { + it('should successfully enable indexing when not yet enabled', () => + withTestDb(persistence, db => { const indexManager = getPersistentCacheIndexManager(db)!; - disablePersistentCacheIndexAutoCreation(indexManager); + return verifyPersistentCacheIndexAutoCreationToggleSucceedsDuring(() => + enablePersistentCacheIndexAutoCreation(indexManager) + ); })); - it('enable when already enabled should succeed', async () => - withTestDb(persistence, async db => { - const documentRef = doc(db, 'a/b'); + it('should successfully enable indexing when already enabled', () => + withTestDb(persistence, db => { const indexManager = getPersistentCacheIndexManager(db)!; - enablePersistentCacheIndexAutoCreation(indexManager); - await getDoc(documentRef); // flush the async queue - enablePersistentCacheIndexAutoCreation(indexManager); - enablePersistentCacheIndexAutoCreation(indexManager); + return verifyPersistentCacheIndexAutoCreationToggleSucceedsDuring(() => + enablePersistentCacheIndexAutoCreation(indexManager) + ); })); - it('disable when already disabled should succeed', async () => - withTestDb(persistence, async db => { - const documentRef = doc(db, 'a/b'); + it('should successfully enable indexing after being disabled', () => + withTestDb(persistence, db => { const indexManager = getPersistentCacheIndexManager(db)!; + enablePersistentCacheIndexAutoCreation(indexManager); disablePersistentCacheIndexAutoCreation(indexManager); - await getDoc(documentRef); // flush the async queue - disablePersistentCacheIndexAutoCreation(indexManager); - disablePersistentCacheIndexAutoCreation(indexManager); + return verifyPersistentCacheIndexAutoCreationToggleSucceedsDuring(() => + enablePersistentCacheIndexAutoCreation(indexManager) + ); })); - it('enabling after terminate() should throw', () => + it('should fail if invoked after terminate()', () => withTestDb(persistence, async db => { const indexManager = getPersistentCacheIndexManager(db)!; terminate(db).catch(e => expect.fail(`terminate() failed: ${e}`)); @@ -112,8 +118,43 @@ apiDescribe('PersistentCacheIndexManager', persistence => { enablePersistentCacheIndexAutoCreation(indexManager) ).to.throw('The client has already been terminated.'); })); + }); + + describe('disablePersistentCacheIndexAutoCreation(()', () => { + it('should return successfully', () => + withTestDb(persistence, async db => { + const indexManager = getPersistentCacheIndexManager(db)!; + disablePersistentCacheIndexAutoCreation(indexManager); + })); + + it('should successfully disable indexing when not yet enabled', () => + withTestDb(persistence, db => { + const indexManager = getPersistentCacheIndexManager(db)!; + return verifyPersistentCacheIndexAutoCreationToggleSucceedsDuring(() => + disablePersistentCacheIndexAutoCreation(indexManager) + ); + })); + + it('should successfully disable indexing when enabled', () => + withTestDb(persistence, db => { + const indexManager = getPersistentCacheIndexManager(db)!; + enablePersistentCacheIndexAutoCreation(indexManager); + return verifyPersistentCacheIndexAutoCreationToggleSucceedsDuring(() => + disablePersistentCacheIndexAutoCreation(indexManager) + ); + })); + + it('should successfully enable indexing after being disabled', () => + withTestDb(persistence, db => { + const indexManager = getPersistentCacheIndexManager(db)!; + enablePersistentCacheIndexAutoCreation(indexManager); + disablePersistentCacheIndexAutoCreation(indexManager); + return verifyPersistentCacheIndexAutoCreationToggleSucceedsDuring(() => + disablePersistentCacheIndexAutoCreation(indexManager) + ); + })); - it('disabling after terminate() should throw', () => + it('should fail if invoked after terminate()', () => withTestDb(persistence, async db => { const indexManager = getPersistentCacheIndexManager(db)!; terminate(db).catch(e => expect.fail(`terminate() failed: ${e}`)); @@ -121,32 +162,417 @@ apiDescribe('PersistentCacheIndexManager', persistence => { disablePersistentCacheIndexAutoCreation(indexManager) ).to.throw('The client has already been terminated.'); })); + }); + + describe('deleteAllPersistentCacheIndexes()', () => { + it('should return successfully', () => + withTestDb(persistence, async db => { + const indexManager = getPersistentCacheIndexManager(db)!; + deleteAllPersistentCacheIndexes(indexManager); + })); + + it('should be successful when auto-indexing is enabled', () => + withTestDb(persistence, db => { + const indexManager = getPersistentCacheIndexManager(db)!; + enablePersistentCacheIndexAutoCreation(indexManager); + return verifyPersistentCacheDeleteAllIndexesSucceedsDuring(() => + deleteAllPersistentCacheIndexes(indexManager) + ); + })); + + it('should be successful when auto-indexing is disabled', () => + withTestDb(persistence, db => { + const indexManager = getPersistentCacheIndexManager(db)!; + enablePersistentCacheIndexAutoCreation(indexManager); + disablePersistentCacheIndexAutoCreation(indexManager); + return verifyPersistentCacheDeleteAllIndexesSucceedsDuring(() => + deleteAllPersistentCacheIndexes(indexManager) + ); + })); + + it('should fail if invoked after terminate()', () => + withTestDb(persistence, async db => { + const indexManager = getPersistentCacheIndexManager(db)!; + terminate(db).catch(e => expect.fail(`terminate() failed: ${e}`)); + expect(() => deleteAllPersistentCacheIndexes(indexManager)).to.throw( + 'The client has already been terminated.' + ); + })); + }); + + describe('Query execution', () => { + it('Auto-indexing is disabled by default', () => + testIndexesGetAutoCreated({ + documentCounts: { matching: 1, notMatching: 5 }, + expectedIndexAutoCreated: false, + indexAutoCreationEnabled: false, + indexAutoCreationMinCollectionSize: 0, + relativeIndexReadCostPerDocument: 2 + })); + + it( + 'Default indexAutoCreationMinCollectionSize=100: ' + + 'index should *not* be auto-created if lookup scanned 99 documents', + () => + testIndexesGetAutoCreated({ + documentCounts: { matching: 1, notMatching: 98 }, + expectedIndexAutoCreated: false + }) + ); + + it( + 'Default indexAutoCreationMinCollectionSize=100' + + ' index *should* be auto-created if lookup scanned 100 documents', + () => + testIndexesGetAutoCreated({ + documentCounts: { matching: 1, notMatching: 99 }, + expectedIndexAutoCreated: true + }) + ); + + it( + 'Default relativeIndexReadCostPerDocument=2: ' + + 'index should *not* be auto-created if the relative index read cost is matched exactly', + () => + testIndexesGetAutoCreated({ + documentCounts: { matching: 50, notMatching: 50 }, + expectedIndexAutoCreated: false + }) + ); + + it( + 'Default relativeIndexReadCostPerDocument=2: ' + + 'index *should* be auto-created if the relative index read cost is exceeded slightly', + () => + testIndexesGetAutoCreated({ + documentCounts: { matching: 49, notMatching: 51 }, + expectedIndexAutoCreated: true + }) + ); - it('query returns correct results when index is auto-created', () => { + it('Indexes are only auto-created when enabled', async () => { const testDocs = partitionedTestDocs({ - matching: { documentData: { match: true }, documentCount: 1 }, - nonmatching: { documentData: { match: false }, documentCount: 100 } + FooMatches: { + documentData: { foo: 'match' }, + documentCount: 1 + }, + BarMatches: { + documentData: { bar: 'match' }, + documentCount: 2 + }, + BazMatches: { + documentData: { baz: 'match' }, + documentCount: 3 + }, + NeitherFooNorBarNorMazMatch: { + documentData: { foo: 'nomatch', bar: 'nomatch', baz: 'nomatch' }, + documentCount: 10 + } + }); + + return withTestCollection(persistence, testDocs, async (coll, db) => { + const indexManager = getPersistentCacheIndexManager(db)!; + expect(indexManager, 'indexManager').is.not.null; + await setPersistentCacheIndexAutoCreationSettings(indexManager, { + indexAutoCreationMinCollectionSize: 0, + relativeIndexReadCostPerDocument: 2 + }); + + // Populate the local cache with the entire collection. + await getDocs(coll); + + // Enable automatic index creation and run a query, ensuring that an + // appropriate index is created. + enablePersistentCacheIndexAutoCreation(indexManager); + const query1 = query(coll, where('foo', '==', 'match')); + const snapshot1 = await getDocsFromCache(query1); + expect(snapshot1.size, 'snapshot1.size').to.equal(1); + expect( + await getQueryIndexType(query1), + 'getQueryIndexType(query1)' + ).to.equal('full'); + + // Disable automatic index creation and run a query, ensuring that an + // appropriate index is _not_ created, and the other is maintained. + disablePersistentCacheIndexAutoCreation(indexManager); + const query2 = query(coll, where('bar', '==', 'match')); + const snapshot2 = await getDocsFromCache(query2); + expect(snapshot2.size, 'snapshot2.size').to.equal(2); + expect( + await getQueryIndexType(query2), + 'getQueryIndexType(query2)' + ).to.equal('none'); + expect( + await getQueryIndexType(query1), + 'getQueryIndexType(query1) check #2' + ).to.equal('full'); + + // Re-enable automatic index creation and run a query, ensuring that an + // appropriate index is created, and others are maintained. + enablePersistentCacheIndexAutoCreation(indexManager); + const query3 = query(coll, where('baz', '==', 'match')); + const snapshot3 = await getDocsFromCache(query3); + expect(snapshot3.size, 'snapshot3.size').to.equal(3); + expect( + await getQueryIndexType(query3), + 'getQueryIndexType(query3)' + ).to.equal('full'); + expect( + await getQueryIndexType(query2), + 'getQueryIndexType(query2) check #2' + ).to.equal('none'); + expect( + await getQueryIndexType(query1), + 'getQueryIndexType(query1) check #3' + ).to.equal('full'); + }); + }); + + it('A full index is not auto-created if there is a partial index match', async () => { + const testDocs = partitionedTestDocs({ + FooMatches: { + documentData: { foo: 'match' }, + documentCount: 5 + }, + FooAndBarBothMatch: { + documentData: { foo: 'match', bar: 'match' }, + documentCount: 1 + }, + NeitherFooNorBarMatch: { + documentData: { foo: 'nomatch', bar: 'nomatch' }, + documentCount: 15 + } + }); + + return withTestCollection(persistence, testDocs, async (coll, db) => { + const indexManager = getPersistentCacheIndexManager(db)!; + expect(indexManager, 'indexManager').is.not.null; + enablePersistentCacheIndexAutoCreation(indexManager); + await setPersistentCacheIndexAutoCreationSettings(indexManager, { + indexAutoCreationMinCollectionSize: 0, + relativeIndexReadCostPerDocument: 2 + }); + + // Populate the local cache with the entire collection. + await getDocs(coll); + + // Run a query to have an index on the 'foo' field created. + { + const fooQuery = query(coll, where('foo', '==', 'match')); + const fooSnapshot = await getDocsFromCache(fooQuery); + expect(fooSnapshot.size, 'fooSnapshot.size').to.equal(6); + expect( + await getQueryIndexType(fooQuery), + 'getQueryIndexType(fooQuery)' + ).to.equal('full'); + } + + // Run a query that filters on both 'foo' and 'bar' fields and ensure + // that the partial index (created by the previous query's execution) + // is NOT upgraded to a full index. Note that in the future we _may_ + // change this behavior since a full index would likely be beneficial to + // the query's execution performance. + { + const fooBarQuery = query( + coll, + where('foo', '==', 'match'), + where('bar', '==', 'match') + ); + expect( + await getQueryIndexType(fooBarQuery), + 'getQueryIndexType(fooBarQuery) before' + ).to.equal('partial'); + const fooBarSnapshot = await getDocsFromCache(fooBarQuery); + expect(fooBarSnapshot.size, 'fooBarSnapshot.size').to.equal(1); + expect( + await getQueryIndexType(fooBarQuery), + 'getQueryIndexType(fooBarQuery) after' + ).to.equal('partial'); + } + }); + }); + + it('Indexes can be deleted while index auto-creation is enabled', async () => { + const testDocs = partitionedTestDocs({ + FooMatches: { + documentData: { foo: 'match' }, + documentCount: 2 + }, + BarMatches: { + documentData: { bar: 'match' }, + documentCount: 3 + }, + NeitherFooNorBarMatch: { + documentData: { foo: 'nomatch', bar: 'nomatch' }, + documentCount: 5 + } + }); + + return withTestCollection(persistence, testDocs, async (coll, db) => { + const indexManager = getPersistentCacheIndexManager(db)!; + expect(indexManager, 'indexManager').is.not.null; + enablePersistentCacheIndexAutoCreation(indexManager); + await setPersistentCacheIndexAutoCreationSettings(indexManager, { + indexAutoCreationMinCollectionSize: 0, + relativeIndexReadCostPerDocument: 2 + }); + + // Populate the local cache with the entire collection. + await getDocs(coll); + + // Run a query to have an index on the 'foo' field created. + const fooQuery = query(coll, where('foo', '==', 'match')); + { + const fooSnapshot = await getDocsFromCache(fooQuery); + expect(fooSnapshot.size, 'fooSnapshot.size').to.equal(2); + expect( + await getQueryIndexType(fooQuery), + 'getQueryIndexType(fooQuery)' + ).to.equal('full'); + } + + // Run a query to have an index on the 'bar' field created. + const barQuery = query(coll, where('bar', '==', 'match')); + { + const barSnapshot = await getDocsFromCache(barQuery); + expect(barSnapshot.size, 'barSnapshot.size').to.equal(3); + expect( + await getQueryIndexType(barQuery), + 'getQueryIndexType(barQuery)' + ).to.equal('full'); + } + + // Delete the indexes that were auto-created. + deleteAllPersistentCacheIndexes(indexManager); + expect( + await getQueryIndexType(fooQuery), + 'getQueryIndexType(fooQuery) after delete' + ).to.equal('none'); + expect( + await getQueryIndexType(barQuery), + 'getQueryIndexType(barQuery) after delete' + ).to.equal('none'); }); + }); + + it('Indexes can be deleted while index auto-creation is disabled', async () => { + const testDocs = partitionedTestDocs({ + FooMatches: { + documentData: { foo: 'match' }, + documentCount: 2 + }, + BarMatches: { + documentData: { bar: 'match' }, + documentCount: 3 + }, + NeitherFooNorBarMatch: { + documentData: { foo: 'nomatch', bar: 'nomatch' }, + documentCount: 5 + } + }); + return withTestCollection(persistence, testDocs, async (coll, db) => { const indexManager = getPersistentCacheIndexManager(db)!; + expect(indexManager, 'indexManager').is.not.null; enablePersistentCacheIndexAutoCreation(indexManager); + await setPersistentCacheIndexAutoCreationSettings(indexManager, { + indexAutoCreationMinCollectionSize: 0, + relativeIndexReadCostPerDocument: 2 + }); - // Populate the local cache with the entire collection's contents. + // Populate the local cache with the entire collection. await getDocs(coll); - // Run a query that matches only one of the documents in the collection; - // this should cause an index to be auto-created. - const query_ = query(coll, where('match', '==', true)); - const snapshot1 = await getDocsFromCache(query_); - expect(snapshot1.size).to.equal(1); - - // Run the query that matches only one of the documents again, which - // should _still_ return the one and only document that matches. Since - // the public API surface does not reveal whether an index was used, - // there isn't anything else that can be verified. - const snapshot2 = await getDocsFromCache(query_); - expect(snapshot2.size).to.equal(1); + // Run a query to have an index on the 'foo' field created. + const fooQuery = query(coll, where('foo', '==', 'match')); + { + const fooSnapshot = await getDocsFromCache(fooQuery); + expect(fooSnapshot.size, 'fooSnapshot.size').to.equal(2); + expect( + await getQueryIndexType(fooQuery), + 'getQueryIndexType(fooQuery)' + ).to.equal('full'); + } + + // Run a query to have an index on the 'bar' field created. + const barQuery = query(coll, where('bar', '==', 'match')); + { + const barSnapshot = await getDocsFromCache(barQuery); + expect(barSnapshot.size, 'barSnapshot.size').to.equal(3); + expect( + await getQueryIndexType(barQuery), + 'getQueryIndexType(barQuery)' + ).to.equal('full'); + } + + // Disable index auto-creation so that the later step can verify that + // indexes can be deleted even while index auto-creation is disabled. + disablePersistentCacheIndexAutoCreation(indexManager); + + // Delete the indexes that were auto-created. + deleteAllPersistentCacheIndexes(indexManager); + expect( + await getQueryIndexType(fooQuery), + 'getQueryIndexType(fooQuery) after delete' + ).to.equal('none'); + expect( + await getQueryIndexType(barQuery), + 'getQueryIndexType(barQuery) after delete' + ).to.equal('none'); }); }); + + async function testIndexesGetAutoCreated(config: { + documentCounts: { matching: number; notMatching: number }; + expectedIndexAutoCreated: boolean; + indexAutoCreationEnabled?: boolean; + indexAutoCreationMinCollectionSize?: number; + relativeIndexReadCostPerDocument?: number; + }): Promise { + const testDocs = partitionedTestDocs({ + matching: { + documentData: { foo: 'match' }, + documentCount: config.documentCounts.matching + }, + notMatching: { + documentData: { foo: 'nomatch' }, + documentCount: config.documentCounts.notMatching + } + }); + + return withTestCollection(persistence, testDocs, async (coll, db) => { + // Populate the local cache with the entire collection. + await getDocs(coll); + + // Configure automatic index creation, as requested. + const indexManager = getPersistentCacheIndexManager(db)!; + expect(indexManager, 'indexManager').is.not.null; + if (config.indexAutoCreationEnabled ?? true) { + enablePersistentCacheIndexAutoCreation(indexManager); + } + if ( + config.indexAutoCreationMinCollectionSize !== undefined || + config.relativeIndexReadCostPerDocument !== undefined + ) { + await TestingHooks.setPersistentCacheIndexAutoCreationSettings( + indexManager, + config + ); + } + + // Run a query against the local cache that matches a _subset_ of the + // entire collection. + const query_ = query(coll, where('foo', '==', 'match')); + const snapshot = await getDocsFromCache(query_); + expect(snapshot.size, 'snapshot.size').to.equal( + config.documentCounts.matching + ); + + // Verify that an index was or was not created, as expected. + expect(await getQueryIndexType(query_), 'getQueryIndexType()').to.equal( + config.expectedIndexAutoCreated ? 'full' : 'none' + ); + }); + } }); }); diff --git a/packages/firestore/test/integration/util/testing_hooks_util.ts b/packages/firestore/test/integration/util/testing_hooks_util.ts index 72604f91a8d..cef32ae55c5 100644 --- a/packages/firestore/test/integration/util/testing_hooks_util.ts +++ b/packages/firestore/test/integration/util/testing_hooks_util.ts @@ -15,8 +15,13 @@ * limitations under the License. */ +import { expect } from 'chai'; + import { + DocumentData, DocumentReference, + PersistentCacheIndexManager, + Query, _TestingHooks as TestingHooks, _TestingHooksExistenceFilterMismatchInfo as ExistenceFilterMismatchInfoInternal } from './firebase_export'; @@ -93,3 +98,111 @@ function createExistenceFilterMismatchInfoFrom( return info; } + +/** + * Verifies than an invocation of `enablePersistentCacheIndexAutoCreation()` or + * `disablePersistentCacheIndexAutoCreation()` made during the execution of the + * given callback succeeds. + * + * @param callback The callback to invoke; this callback must invoke exactly one + * of `enablePersistentCacheIndexAutoCreation()` or + * `disablePersistentCacheIndexAutoCreation()` exactly once; this callback is + * called synchronously by this function, and is called exactly once. + * + * @return a promise that is fulfilled when the asynchronous work started by + * `enablePersistentCacheIndexAutoCreation()` or + * `disablePersistentCacheIndexAutoCreation()` completes successfully, or is + * rejected if it fails. + */ +export function verifyPersistentCacheIndexAutoCreationToggleSucceedsDuring( + callback: () => void +): Promise { + const promises: Array> = []; + + const unregister = TestingHooks.onPersistentCacheIndexAutoCreationToggle( + promise => promises.push(promise) + ); + + try { + callback(); + } finally { + unregister(); + } + + expect( + promises, + 'exactly one invocation of enablePersistentCacheIndexAutoCreation() or ' + + 'disablePersistentCacheIndexAutoCreation() should be made by the callback' + ).to.have.length(1); + + return promises[0]; +} + +/** + * Verifies than an invocation of `deleteAllPersistentCacheIndexes()` made + * during the execution of the given callback succeeds. + * + * @param callback The callback to invoke; this callback must invoke + * `deleteAllPersistentCacheIndexes()` exactly once; this callback is + * called synchronously by this function, and is called exactly once. + * + * @return a promise that is fulfilled when the asynchronous work started by + * `deleteAllPersistentCacheIndexes()` completes successfully, or is rejected + * if it fails. + */ +export function verifyPersistentCacheDeleteAllIndexesSucceedsDuring( + callback: () => void +): Promise { + const promises: Array> = []; + + const unregister = TestingHooks.onPersistentCacheDeleteAllIndexes(promise => + promises.push(promise) + ); + + try { + callback(); + } finally { + unregister(); + } + + expect( + promises, + 'exactly one invocation of deleteAllPersistentCacheIndexes() should be ' + + 'made by the callback' + ).to.have.length(1); + + return promises[0]; +} + +/** + * Determines the type of client-side index that will be used when executing the + * given query against the local cache. + */ +export function getQueryIndexType< + AppModelType, + DbModelType extends DocumentData +>( + query: Query +): Promise<'full' | 'partial' | 'none'> { + return TestingHooks.getQueryIndexType(query); +} + +/** + * Sets the persistent cache index auto-creation settings for the given + * Firestore instance. + * + * @return a Promise that is fulfilled when the settings are successfully + * applied, or rejected if applying the settings fails. + */ +export function setPersistentCacheIndexAutoCreationSettings( + indexManager: PersistentCacheIndexManager, + settings: { + indexAutoCreationMinCollectionSize?: number; + relativeIndexReadCostPerDocument?: number; + } +): Promise { + return TestingHooks.setPersistentCacheIndexAutoCreationSettings( + indexManager, + settings + ); +} diff --git a/packages/firestore/test/unit/local/index_manager.test.ts b/packages/firestore/test/unit/local/index_manager.test.ts index 1385064e4a0..0788dcabbde 100644 --- a/packages/firestore/test/unit/local/index_manager.test.ts +++ b/packages/firestore/test/unit/local/index_manager.test.ts @@ -1734,6 +1734,21 @@ describe('IndexedDbIndexManager', async () => { await validateIsFullIndex(query_); }); + it('deleteAllFieldIndexes() deletes all indexes', async () => { + // Create some indexes. + const query1 = queryWithAddedFilter(query('coll'), filter('a', '==', 42)); + await indexManager.createTargetIndexes(queryToTarget(query1)); + await validateIsFullIndex(query1); + const query2 = queryWithAddedFilter(query('coll'), filter('b', '==', 42)); + await indexManager.createTargetIndexes(queryToTarget(query2)); + await validateIsFullIndex(query2); + + // Verify that deleteAllFieldIndexes() deletes the indexes. + await indexManager.deleteAllFieldIndexes(); + await validateIsNoneIndex(query1); + await validateIsNoneIndex(query2); + }); + async function validateIsPartialIndex(query: Query): Promise { await validateIndexType(query, IndexType.PARTIAL); } diff --git a/packages/firestore/test/unit/local/local_store.test.ts b/packages/firestore/test/unit/local/local_store.test.ts index 66009fbe89e..845e73a6efb 100644 --- a/packages/firestore/test/unit/local/local_store.test.ts +++ b/packages/firestore/test/unit/local/local_store.test.ts @@ -39,6 +39,8 @@ import { localStoreAllocateTarget, localStoreApplyBundledDocuments, localStoreApplyRemoteEventToLocalCache, + localStoreConfigureFieldIndexes, + localStoreDeleteAllFieldIndexes, localStoreExecuteQuery, localStoreGetHighestUnacknowledgedBatchId, localStoreGetTargetData, @@ -64,6 +66,12 @@ import { DocumentMap } from '../../../src/model/collections'; import { Document } from '../../../src/model/document'; +import { + FieldIndex, + IndexKind, + IndexSegment, + IndexState +} from '../../../src/model/field_index'; import { FieldMask } from '../../../src/model/field_mask'; import { FieldTransform, @@ -78,6 +86,7 @@ import { MutationBatchResult } from '../../../src/model/mutation_batch'; import { ObjectValue } from '../../../src/model/object_value'; +import { FieldPath } from '../../../src/model/path'; import { serverTimestamp } from '../../../src/model/server_timestamps'; import { ServerTimestampTransform } from '../../../src/model/transform_operation'; import { BundleMetadata as ProtoBundleMetadata } from '../../../src/protos/firestore_bundle_proto'; @@ -367,6 +376,22 @@ class LocalStoreTester { return this; } + afterDeleteAllFieldIndexes(): LocalStoreTester { + this.prepareNextStep(); + this.promiseChain = this.promiseChain.then(() => + localStoreDeleteAllFieldIndexes(this.localStore) + ); + return this; + } + + afterConfigureFieldIndexes(fieldIndexes: FieldIndex[]): LocalStoreTester { + this.prepareNextStep(); + this.promiseChain = this.promiseChain.then(() => + localStoreConfigureFieldIndexes(this.localStore, fieldIndexes) + ); + return this; + } + afterBackfillIndexes(options?: { maxDocumentsToProcess?: number; }): LocalStoreTester { @@ -648,6 +673,18 @@ function compareDocsWithCreateTime( ); } +function fieldIndex( + collectionGroup: string, + indexId: number, + indexState: IndexState, + field: string, + kind: IndexKind +): FieldIndex { + const fieldPath = new FieldPath(field.split('.')); + const segments = [new IndexSegment(fieldPath, kind)]; + return new FieldIndex(indexId, collectionGroup, segments, indexState); +} + describe('LocalStore w/ Memory Persistence', () => { async function initialize(): Promise { const queryEngine = new CountingQueryEngine(); @@ -2987,4 +3024,78 @@ function indexedDbLocalStoreTests( .toReturnChanged('coll/a', 'coll/f') .finish(); }); + + it('delete all indexes works with index auto creation', () => { + const query_ = query('coll', filter('value', '==', 'match')); + return ( + expectLocalStore() + .afterAllocatingQuery(query_) + .toReturnTargetId(2) + .afterIndexAutoCreationConfigure({ + isEnabled: true, + indexAutoCreationMinCollectionSize: 0, + relativeIndexReadCostPerDocument: 2 + }) + .afterRemoteEvents([ + docAddedRemoteEvent(doc('coll/a', 10, { value: 'match' }), [2], []), + docAddedRemoteEvent( + doc('coll/b', 10, { value: Number.NaN }), + [2], + [] + ), + docAddedRemoteEvent(doc('coll/c', 10, { value: null }), [2], []), + docAddedRemoteEvent( + doc('coll/d', 10, { value: 'mismatch' }), + [2], + [] + ), + docAddedRemoteEvent(doc('coll/e', 10, { value: 'match' }), [2], []) + ]) + // First time query is running without indexes. + // Based on current heuristic, collection document counts (5) > + // 2 * resultSize (2). + // Full matched index should be created. + .afterExecutingQuery(query_) + .toHaveRead({ documentsByKey: 0, documentsByCollection: 2 }) + .toReturnChanged('coll/a', 'coll/e') + .afterIndexAutoCreationConfigure({ isEnabled: false }) + .afterBackfillIndexes() + .afterExecutingQuery(query_) + .toHaveRead({ documentsByKey: 2, documentsByCollection: 0 }) + .toReturnChanged('coll/a', 'coll/e') + .afterDeleteAllFieldIndexes() + .afterExecutingQuery(query_) + .toHaveRead({ documentsByKey: 0, documentsByCollection: 2 }) + .toReturnChanged('coll/a', 'coll/e') + .finish() + ); + }); + + it('delete all indexes works with manual added indexes', () => { + const query_ = query('coll', filter('matches', '==', true)); + return expectLocalStore() + .afterConfigureFieldIndexes([ + fieldIndex( + 'coll', + 0, + IndexState.empty(), + 'matches', + IndexKind.ASCENDING + ) + ]) + .afterAllocatingQuery(query_) + .toReturnTargetId(2) + .afterRemoteEvents([ + docAddedRemoteEvent(doc('coll/a', 10, { matches: true }), [2], []) + ]) + .afterBackfillIndexes() + .afterExecutingQuery(query_) + .toHaveRead({ documentsByKey: 1, documentsByCollection: 0 }) + .toReturnChanged('coll/a') + .afterDeleteAllFieldIndexes() + .afterExecutingQuery(query_) + .toHaveRead({ documentsByKey: 0, documentsByCollection: 1 }) + .toReturnChanged('coll/a') + .finish(); + }); } diff --git a/packages/firestore/test/unit/local/test_index_manager.ts b/packages/firestore/test/unit/local/test_index_manager.ts index 94509073925..c3b6c092652 100644 --- a/packages/firestore/test/unit/local/test_index_manager.ts +++ b/packages/firestore/test/unit/local/test_index_manager.ts @@ -71,6 +71,14 @@ export class TestIndexManager { ); } + deleteAllFieldIndexes(): Promise { + return this.persistence.runTransaction( + 'deleteAllFieldIndexes', + 'readwrite', + txn => this.indexManager.deleteAllFieldIndexes(txn) + ); + } + getFieldIndexes(collectionGroup?: string): Promise { return this.persistence.runTransaction('getFieldIndexes', 'readonly', txn => collectionGroup