Skip to content

Commit 05a6aff

Browse files
IndexeddbIndexManager.updateEntries() (#5999)
1 parent ea98957 commit 05a6aff

File tree

9 files changed

+471
-21
lines changed

9 files changed

+471
-21
lines changed

packages/firestore/src/index/index_byte_encoder.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
import { IndexKind } from '../model/field_index';
1718
import { ByteString } from '../util/byte_string';
1819

1920
import { DirectionalIndexByteEncoder } from './directional_index_byte_encoder';
@@ -69,8 +70,8 @@ export class IndexByteEncoder {
6970
this.orderedCode.seed(encodedBytes);
7071
}
7172

72-
forKind(kind: 'asc' | 'desc'): DirectionalIndexByteEncoder {
73-
return kind === 'asc' ? this.ascending : this.descending;
73+
forKind(kind: IndexKind): DirectionalIndexByteEncoder {
74+
return kind === IndexKind.ASCENDING ? this.ascending : this.descending;
7475
}
7576

7677
encodedBytes(): Uint8Array {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @license
3+
* Copyright 2022 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { DocumentKey } from '../model/document_key';
19+
20+
/** Represents an index entry saved by the SDK in persisted storage. */
21+
export class IndexEntry {
22+
constructor(
23+
readonly indexId: number,
24+
readonly documentKey: DocumentKey,
25+
readonly arrayValue: Uint8Array,
26+
readonly directionalValue: Uint8Array
27+
) {}
28+
}
29+
30+
export function indexEntryComparator(
31+
left: IndexEntry,
32+
right: IndexEntry
33+
): number {
34+
let cmp = left.indexId - right.indexId;
35+
if (cmp !== 0) {
36+
return cmp;
37+
}
38+
39+
cmp = DocumentKey.comparator(left.documentKey, right.documentKey);
40+
if (cmp !== 0) {
41+
return cmp;
42+
}
43+
44+
cmp = compareByteArrays(left.arrayValue, right.arrayValue);
45+
if (cmp !== 0) {
46+
return cmp;
47+
}
48+
49+
return compareByteArrays(left.directionalValue, right.directionalValue);
50+
}
51+
52+
function compareByteArrays(left: Uint8Array, right: Uint8Array): number {
53+
for (let i = 0; i < left.length && i < right.length; ++i) {
54+
const compare = left[i] - right[i];
55+
if (compare !== 0) {
56+
return compare;
57+
}
58+
}
59+
return left.length - right.length;
60+
}

packages/firestore/src/local/indexeddb_index_manager.ts

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,30 @@
1717

1818
import { User } from '../auth/user';
1919
import { Target } from '../core/target';
20+
import { FirestoreIndexValueWriter } from '../index/firestore_index_value_writer';
21+
import { IndexByteEncoder } from '../index/index_byte_encoder';
22+
import { IndexEntry, indexEntryComparator } from '../index/index_entry';
2023
import {
2124
documentKeySet,
2225
DocumentKeySet,
2326
DocumentMap
2427
} from '../model/collections';
25-
import { FieldIndex, IndexOffset } from '../model/field_index';
28+
import { Document } from '../model/document';
29+
import { DocumentKey } from '../model/document_key';
30+
import {
31+
FieldIndex,
32+
fieldIndexGetArraySegment,
33+
fieldIndexGetDirectionalSegments,
34+
IndexKind,
35+
IndexOffset
36+
} from '../model/field_index';
2637
import { ResourcePath } from '../model/path';
38+
import { isArray } from '../model/values';
39+
import { Value as ProtoValue } from '../protos/firestore_proto_api';
2740
import { debugAssert } from '../util/assert';
41+
import { logDebug } from '../util/log';
2842
import { immediateSuccessor } from '../util/misc';
43+
import { diffSortedSets, SortedSet } from '../util/sorted_set';
2944

3045
import {
3146
decodeResourcePath,
@@ -53,6 +68,8 @@ import { PersistencePromise } from './persistence_promise';
5368
import { PersistenceTransaction } from './persistence_transaction';
5469
import { SimpleDbStore } from './simple_db';
5570

71+
const LOG_TAG = 'IndexedDbIndexManager';
72+
5673
/**
5774
* A persisted implementation of IndexManager.
5875
*
@@ -194,6 +211,40 @@ export class IndexedDbIndexManager implements IndexManager {
194211
return PersistencePromise.resolve<FieldIndex | null>(null);
195212
}
196213

214+
/**
215+
* Returns the byte encoded form of the directional values in the field index.
216+
* Returns `null` if the document does not have all fields specified in the
217+
* index.
218+
*/
219+
private encodeDirectionalElements(
220+
fieldIndex: FieldIndex,
221+
document: Document
222+
): Uint8Array | null {
223+
const encoder = new IndexByteEncoder();
224+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
225+
const field = document.data.field(segment.fieldPath);
226+
if (field == null) {
227+
return null;
228+
}
229+
const directionalEncoder = encoder.forKind(segment.kind);
230+
FirestoreIndexValueWriter.INSTANCE.writeIndexValue(
231+
field,
232+
directionalEncoder
233+
);
234+
}
235+
return encoder.encodedBytes();
236+
}
237+
238+
/** Encodes a single value to the ascending index format. */
239+
private encodeSingleElement(value: ProtoValue): Uint8Array {
240+
const encoder = new IndexByteEncoder();
241+
FirestoreIndexValueWriter.INSTANCE.writeIndexValue(
242+
value,
243+
encoder.forKind(IndexKind.ASCENDING)
244+
);
245+
return encoder.encodedBytes();
246+
}
247+
197248
getFieldIndexes(
198249
transaction: PersistenceTransaction,
199250
collectionGroup?: string
@@ -269,8 +320,181 @@ export class IndexedDbIndexManager implements IndexManager {
269320
transaction: PersistenceTransaction,
270321
documents: DocumentMap
271322
): PersistencePromise<void> {
272-
// TODO(indexing): Implement
273-
return PersistencePromise.resolve();
323+
// Porting Note: `getFieldIndexes()` on Web does not cache index lookups as
324+
// it could be used across different IndexedDB transactions. As any cached
325+
// data might be invalidated by other multi-tab clients, we can only trust
326+
// data within a single IndexedDB transaction. We therefore add a cache
327+
// here.
328+
const memoizedIndexes = new Map<string, FieldIndex[]>();
329+
return PersistencePromise.forEach(documents, (key, doc) => {
330+
const memoizedCollectionIndexes = memoizedIndexes.get(
331+
key.collectionGroup
332+
);
333+
const fieldIndexes = memoizedCollectionIndexes
334+
? PersistencePromise.resolve(memoizedCollectionIndexes)
335+
: this.getFieldIndexes(transaction, key.collectionGroup);
336+
337+
return fieldIndexes.next(fieldIndexes => {
338+
memoizedIndexes.set(key.collectionGroup, fieldIndexes);
339+
return PersistencePromise.forEach(
340+
fieldIndexes,
341+
(fieldIndex: FieldIndex) => {
342+
return this.getExistingIndexEntries(
343+
transaction,
344+
key,
345+
fieldIndex
346+
).next(existingEntries => {
347+
const newEntries = this.computeIndexEntries(doc, fieldIndex);
348+
if (!existingEntries.isEqual(newEntries)) {
349+
return this.updateEntries(
350+
transaction,
351+
doc,
352+
existingEntries,
353+
newEntries
354+
);
355+
}
356+
return PersistencePromise.resolve();
357+
});
358+
}
359+
);
360+
});
361+
});
362+
}
363+
364+
private addIndexEntry(
365+
transaction: PersistenceTransaction,
366+
document: Document,
367+
indexEntry: IndexEntry
368+
): PersistencePromise<void> {
369+
const indexEntries = indexEntriesStore(transaction);
370+
return indexEntries.put(
371+
new DbIndexEntry(
372+
indexEntry.indexId,
373+
this.uid,
374+
indexEntry.arrayValue,
375+
indexEntry.directionalValue,
376+
encodeResourcePath(document.key.path)
377+
)
378+
);
379+
}
380+
381+
private deleteIndexEntry(
382+
transaction: PersistenceTransaction,
383+
document: Document,
384+
indexEntry: IndexEntry
385+
): PersistencePromise<void> {
386+
const indexEntries = indexEntriesStore(transaction);
387+
return indexEntries.delete([
388+
indexEntry.indexId,
389+
this.uid,
390+
indexEntry.arrayValue,
391+
indexEntry.directionalValue,
392+
encodeResourcePath(document.key.path)
393+
]);
394+
}
395+
396+
private getExistingIndexEntries(
397+
transaction: PersistenceTransaction,
398+
documentKey: DocumentKey,
399+
fieldIndex: FieldIndex
400+
): PersistencePromise<SortedSet<IndexEntry>> {
401+
const indexEntries = indexEntriesStore(transaction);
402+
let results = new SortedSet<IndexEntry>(indexEntryComparator);
403+
return indexEntries
404+
.iterate(
405+
{
406+
index: DbIndexEntry.documentKeyIndex,
407+
range: IDBKeyRange.only([
408+
fieldIndex.indexId,
409+
this.uid,
410+
encodeResourcePath(documentKey.path)
411+
])
412+
},
413+
(_, entry) => {
414+
results = results.add(
415+
new IndexEntry(
416+
fieldIndex.indexId,
417+
documentKey,
418+
entry.arrayValue,
419+
entry.directionalValue
420+
)
421+
);
422+
}
423+
)
424+
.next(() => results);
425+
}
426+
427+
/** Creates the index entries for the given document. */
428+
private computeIndexEntries(
429+
document: Document,
430+
fieldIndex: FieldIndex
431+
): SortedSet<IndexEntry> {
432+
let results = new SortedSet<IndexEntry>(indexEntryComparator);
433+
434+
const directionalValue = this.encodeDirectionalElements(
435+
fieldIndex,
436+
document
437+
);
438+
if (directionalValue == null) {
439+
return results;
440+
}
441+
442+
const arraySegment = fieldIndexGetArraySegment(fieldIndex);
443+
if (arraySegment != null) {
444+
const value = document.data.field(arraySegment.fieldPath);
445+
if (isArray(value)) {
446+
for (const arrayValue of value.arrayValue!.values || []) {
447+
results = results.add(
448+
new IndexEntry(
449+
fieldIndex.indexId,
450+
document.key,
451+
this.encodeSingleElement(arrayValue),
452+
directionalValue
453+
)
454+
);
455+
}
456+
}
457+
} else {
458+
results = results.add(
459+
new IndexEntry(
460+
fieldIndex.indexId,
461+
document.key,
462+
new Uint8Array(),
463+
directionalValue
464+
)
465+
);
466+
}
467+
468+
return results;
469+
}
470+
471+
/**
472+
* Updates the index entries for the provided document by deleting entries
473+
* that are no longer referenced in `newEntries` and adding all newly added
474+
* entries.
475+
*/
476+
private updateEntries(
477+
transaction: PersistenceTransaction,
478+
document: Document,
479+
existingEntries: SortedSet<IndexEntry>,
480+
newEntries: SortedSet<IndexEntry>
481+
): PersistencePromise<void> {
482+
logDebug(LOG_TAG, "Updating index entries for document '%s'", document.key);
483+
484+
const promises: Array<PersistencePromise<void>> = [];
485+
diffSortedSets(
486+
existingEntries,
487+
newEntries,
488+
indexEntryComparator,
489+
/* onAdd= */ entry => {
490+
promises.push(this.addIndexEntry(transaction, document, entry));
491+
},
492+
/* onRemove= */ entry => {
493+
promises.push(this.deleteIndexEntry(transaction, document, entry));
494+
}
495+
);
496+
497+
return PersistencePromise.waitFor(promises);
274498
}
275499

276500
private getNextSequenceNumber(

packages/firestore/src/local/indexeddb_schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,10 @@ export class DbIndexEntry {
808808
'documentKey'
809809
];
810810

811+
static documentKeyIndex = 'documentKeyIndex';
812+
813+
static documentKeyIndexPath = ['indexId', 'uid', 'documentKey'];
814+
811815
constructor(
812816
/** The index id for this entry. */
813817
public indexId: number,

packages/firestore/src/local/indexeddb_schema_converter.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,9 +545,14 @@ function createFieldIndex(db: IDBDatabase): void {
545545
{ unique: false }
546546
);
547547

548-
db.createObjectStore(DbIndexEntry.store, {
548+
const indexEntryStore = db.createObjectStore(DbIndexEntry.store, {
549549
keyPath: DbIndexEntry.keyPath
550550
});
551+
indexEntryStore.createIndex(
552+
DbIndexEntry.documentKeyIndex,
553+
DbIndexEntry.documentKeyIndexPath,
554+
{ unique: false }
555+
);
551556
}
552557

553558
function createDocumentOverlayStore(db: IDBDatabase): void {

packages/firestore/src/model/document_key.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ export class DocumentKey {
4343
return new DocumentKey(ResourcePath.emptyPath());
4444
}
4545

46+
get collectionGroup(): string {
47+
debugAssert(
48+
!this.path.isEmpty(),
49+
'Cannot get collection group for empty key'
50+
);
51+
return this.path.popLast().lastSegment();
52+
}
53+
4654
/** Returns true if the document is in the specified collectionId. */
4755
hasCollectionId(collectionId: string): boolean {
4856
return (

0 commit comments

Comments
 (0)