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/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..4a6d11c2b3a 100644 --- a/packages/firestore/src/api/persistent_cache_index_manager.ts +++ b/packages/firestore/src/api/persistent_cache_index_manager.ts @@ -16,6 +16,7 @@ */ import { + firestoreClientDeleteAllFieldIndexes, firestoreClientSetPersistentCacheIndexAutoCreationEnabled, FirestoreClient } from '../core/firestore_client'; @@ -99,6 +100,29 @@ 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) + ); +} + function setPersistentCacheIndexAutoCreationEnabled( indexManager: PersistentCacheIndexManager, isEnabled: boolean diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 6b7950825f6..a36cf4fd5cd 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -26,6 +26,7 @@ import { User } from '../auth/user'; import { LocalStore } from '../local/local_store'; import { localStoreConfigureFieldIndexes, + localStoreDeleteAllFieldIndexes, localStoreExecuteQuery, localStoreGetNamedQuery, localStoreHandleUserChange, @@ -841,3 +842,11 @@ export function firestoreClientSetPersistentCacheIndexAutoCreationEnabled( ); }); } + +export function firestoreClientDeleteAllFieldIndexes( + client: FirestoreClient +): Promise { + return client.asyncQueue.enqueue(async () => { + return localStoreDeleteAllFieldIndexes(await getLocalStore(client)); + }); +} 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..56f2b96f8d1 100644 --- a/packages/firestore/src/local/local_store_impl.ts +++ b/packages/firestore/src/local/local_store_impl.ts @@ -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. */ 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/test/integration/api/persistent_cache_index_manager.test.ts b/packages/firestore/test/integration/api/persistent_cache_index_manager.test.ts index 5d09c948a64..a931fe90529 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,6 +18,7 @@ import { expect } from 'chai'; import { + deleteAllPersistentCacheIndexes, disablePersistentCacheIndexAutoCreation, doc, enablePersistentCacheIndexAutoCreation, @@ -149,4 +150,66 @@ apiDescribe('PersistentCacheIndexManager', persistence => { }); }); }); + + describe('delete all persistent cache indexes', () => { + it('deleteAllPersistentCacheIndexes() on new instance should succeed', () => + withTestDb(persistence, async db => { + const indexManager = getPersistentCacheIndexManager(db)!; + deleteAllPersistentCacheIndexes(indexManager); + })); + + it('deleteAllPersistentCacheIndexes() should be successful when auto-indexing is enabled', () => + withTestDb(persistence, async db => { + const indexManager = getPersistentCacheIndexManager(db)!; + enablePersistentCacheIndexAutoCreation(indexManager); + deleteAllPersistentCacheIndexes(indexManager); + })); + + it('deleteAllPersistentCacheIndexes() should be successful when auto-indexing is disabled', () => + withTestDb(persistence, async db => { + const indexManager = getPersistentCacheIndexManager(db)!; + enablePersistentCacheIndexAutoCreation(indexManager); + disablePersistentCacheIndexAutoCreation(indexManager); + deleteAllPersistentCacheIndexes(indexManager); + })); + + it('deleteAllPersistentCacheIndexes() after terminate() should throw', () => + 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.' + ); + })); + + it('query returns correct results when auto-created index has been deleted', () => { + const testDocs = partitionedTestDocs({ + matching: { documentData: { match: true }, documentCount: 1 }, + nonmatching: { documentData: { match: false }, documentCount: 100 } + }); + return withTestCollection(persistence, testDocs, async (coll, db) => { + const indexManager = getPersistentCacheIndexManager(db)!; + enablePersistentCacheIndexAutoCreation(indexManager); + + // Populate the local cache with the entire collection's contents. + 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); + + // Delete the index + deleteAllPersistentCacheIndexes(indexManager); + + // 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); + }); + }); + }); }); 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