Skip to content

Commit

Permalink
Firestore: deleteAllPersistentCacheIndexes() added, but hidden for now (
Browse files Browse the repository at this point in the history
  • Loading branch information
dconeybe authored Aug 25, 2023
1 parent 5f6304d commit 25cda8a
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changeset/healthy-peas-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/firestore': patch
'firebase': patch
---

Implemented internal logic to delete all client-side indexes
1 change: 1 addition & 0 deletions packages/firestore/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export {
export {
PersistentCacheIndexManager,
getPersistentCacheIndexManager,
deleteAllPersistentCacheIndexes,
enablePersistentCacheIndexAutoCreation,
disablePersistentCacheIndexAutoCreation
} from './api/persistent_cache_index_manager';
Expand Down
24 changes: 24 additions & 0 deletions packages/firestore/src/api/persistent_cache_index_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import {
firestoreClientDeleteAllFieldIndexes,
firestoreClientSetPersistentCacheIndexAutoCreationEnabled,
FirestoreClient
} from '../core/firestore_client';
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/firestore/src/core/firestore_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { User } from '../auth/user';
import { LocalStore } from '../local/local_store';
import {
localStoreConfigureFieldIndexes,
localStoreDeleteAllFieldIndexes,
localStoreExecuteQuery,
localStoreGetNamedQuery,
localStoreHandleUserChange,
Expand Down Expand Up @@ -841,3 +842,11 @@ export function firestoreClientSetPersistentCacheIndexAutoCreationEnabled(
);
});
}

export function firestoreClientDeleteAllFieldIndexes(
client: FirestoreClient
): Promise<void> {
return client.asyncQueue.enqueue(async () => {
return localStoreDeleteAllFieldIndexes(await getLocalStore(client));
});
}
5 changes: 5 additions & 0 deletions packages/firestore/src/local/index_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export interface IndexManager {
index: FieldIndex
): PersistencePromise<void>;

/** Removes all field indexes and deletes all index values. */
deleteAllFieldIndexes(
transaction: PersistenceTransaction
): PersistencePromise<void>;

/** Creates a full matched field index which serves the given target. */
createTargetIndexes(
transaction: PersistenceTransaction,
Expand Down
13 changes: 13 additions & 0 deletions packages/firestore/src/local/indexeddb_index_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,19 @@ export class IndexedDbIndexManager implements IndexManager {
);
}

deleteAllFieldIndexes(
transaction: PersistenceTransaction
): PersistencePromise<void> {
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
Expand Down
12 changes: 12 additions & 0 deletions packages/firestore/src/local/local_store_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,18 @@ export function localStoreSetIndexAutoCreationEnabled(
localStoreImpl.queryEngine.indexAutoCreationEnabled = isEnabled;
}

export function localStoreDeleteAllFieldIndexes(
localStore: LocalStore
): Promise<void> {
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.
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/firestore/src/local/memory_index_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ export class MemoryIndexManager implements IndexManager {
return PersistencePromise.resolve();
}

deleteAllFieldIndexes(
transaction: PersistenceTransaction
): PersistencePromise<void> {
// Field indices are not supported with memory persistence.
return PersistencePromise.resolve();
}

createTargetIndexes(
transaction: PersistenceTransaction,
target: Target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { expect } from 'chai';

import {
deleteAllPersistentCacheIndexes,
disablePersistentCacheIndexAutoCreation,
doc,
enablePersistentCacheIndexAutoCreation,
Expand Down Expand Up @@ -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);
});
});
});
});
15 changes: 15 additions & 0 deletions packages/firestore/test/unit/local/index_manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
await validateIndexType(query, IndexType.PARTIAL);
}
Expand Down
111 changes: 111 additions & 0 deletions packages/firestore/test/unit/local/local_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
localStoreAllocateTarget,
localStoreApplyBundledDocuments,
localStoreApplyRemoteEventToLocalCache,
localStoreConfigureFieldIndexes,
localStoreDeleteAllFieldIndexes,
localStoreExecuteQuery,
localStoreGetHighestUnacknowledgedBatchId,
localStoreGetTargetData,
Expand All @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<LocalStoreComponents> {
const queryEngine = new CountingQueryEngine();
Expand Down Expand Up @@ -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();
});
}
Loading

0 comments on commit 25cda8a

Please sign in to comment.