Skip to content

Commit ea903a6

Browse files
IndexeddbIndexManager.updateEntries()
1 parent c1b9cf1 commit ea903a6

File tree

9 files changed

+440
-21
lines changed

9 files changed

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

packages/firestore/src/local/indexeddb_index_manager.ts

Lines changed: 222 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,176 @@ export class IndexedDbIndexManager implements IndexManager {
269320
transaction: PersistenceTransaction,
270321
documents: DocumentMap
271322
): PersistencePromise<void> {
272-
// TODO(indexing): Implement
273-
return PersistencePromise.resolve();
323+
const memoizedIndexes = new Map<string, FieldIndex[]>();
324+
return PersistencePromise.forEach(documents, (key, doc) => {
325+
const memoizedCollectionIndexes = memoizedIndexes.get(
326+
key.collectionGroup
327+
);
328+
const fieldIndexes = memoizedCollectionIndexes
329+
? PersistencePromise.resolve(memoizedCollectionIndexes)
330+
: this.getFieldIndexes(transaction, key.collectionGroup);
331+
332+
return fieldIndexes.next(fieldIndexes => {
333+
memoizedIndexes.set(key.collectionGroup, fieldIndexes);
334+
return PersistencePromise.forEach(
335+
fieldIndexes,
336+
(fieldIndex: FieldIndex) => {
337+
return this.getExistingIndexEntries(
338+
transaction,
339+
key,
340+
fieldIndex
341+
).next(existingEntries => {
342+
const newEntries = this.computeIndexEntries(doc, fieldIndex);
343+
if (!existingEntries.isEqual(newEntries)) {
344+
return this.updateEntries(
345+
transaction,
346+
doc,
347+
existingEntries,
348+
newEntries
349+
);
350+
}
351+
return PersistencePromise.resolve();
352+
});
353+
}
354+
);
355+
});
356+
});
357+
}
358+
359+
private addIndexEntry(
360+
transaction: PersistenceTransaction,
361+
document: Document,
362+
indexEntry: IndexEntry
363+
): PersistencePromise<void> {
364+
const indexEntries = indexEntriesStore(transaction);
365+
return indexEntries.put(
366+
new DbIndexEntry(
367+
indexEntry.indexId,
368+
this.uid,
369+
indexEntry.arrayValue,
370+
indexEntry.directionalValue,
371+
encodeResourcePath(document.key.path)
372+
)
373+
);
374+
}
375+
376+
private deleteIndexEntry(
377+
transaction: PersistenceTransaction,
378+
document: Document,
379+
indexEntry: IndexEntry
380+
): PersistencePromise<void> {
381+
const indexEntries = indexEntriesStore(transaction);
382+
return indexEntries.delete([
383+
indexEntry.indexId,
384+
this.uid,
385+
indexEntry.arrayValue,
386+
indexEntry.directionalValue,
387+
encodeResourcePath(document.key.path)
388+
]);
389+
}
390+
391+
private getExistingIndexEntries(
392+
transaction: PersistenceTransaction,
393+
documentKey: DocumentKey,
394+
fieldIndex: FieldIndex
395+
): PersistencePromise<SortedSet<IndexEntry>> {
396+
const indexEntries = indexEntriesStore(transaction);
397+
let results = new SortedSet<IndexEntry>(indexEntryComparator);
398+
return indexEntries
399+
.iterate(
400+
{
401+
index: DbIndexEntry.documentKeyIndex,
402+
range: IDBKeyRange.only([
403+
fieldIndex.indexId,
404+
this.uid,
405+
encodeResourcePath(documentKey.path)
406+
])
407+
},
408+
(_, entry) => {
409+
results = results.add(
410+
new IndexEntry(
411+
fieldIndex.indexId,
412+
documentKey,
413+
entry.arrayValue,
414+
entry.directionalValue
415+
)
416+
);
417+
}
418+
)
419+
.next(() => results);
420+
}
421+
422+
/** Creates the index entries for the given document. */
423+
private computeIndexEntries(
424+
document: Document,
425+
fieldIndex: FieldIndex
426+
): SortedSet<IndexEntry> {
427+
let results = new SortedSet<IndexEntry>(indexEntryComparator);
428+
429+
const directionalValue = this.encodeDirectionalElements(
430+
fieldIndex,
431+
document
432+
);
433+
if (directionalValue == null) {
434+
return results;
435+
}
436+
437+
const arraySegment = fieldIndexGetArraySegment(fieldIndex);
438+
if (arraySegment != null) {
439+
const value = document.data.field(arraySegment.fieldPath);
440+
if (isArray(value)) {
441+
for (const arrayValue of value.arrayValue!.values || []) {
442+
results = results.add(
443+
new IndexEntry(
444+
fieldIndex.indexId,
445+
document.key,
446+
this.encodeSingleElement(arrayValue),
447+
directionalValue
448+
)
449+
);
450+
}
451+
}
452+
} else {
453+
results = results.add(
454+
new IndexEntry(
455+
fieldIndex.indexId,
456+
document.key,
457+
new Uint8Array(),
458+
directionalValue
459+
)
460+
);
461+
}
462+
463+
return results;
464+
}
465+
466+
/**
467+
* Updates the index entries for the provided document by deleting entries
468+
* that are no longer referenced in `newEntries` and adding all newly added
469+
* entries.
470+
*/
471+
private updateEntries(
472+
transaction: PersistenceTransaction,
473+
document: Document,
474+
existingEntries: SortedSet<IndexEntry>,
475+
newEntries: SortedSet<IndexEntry>
476+
): PersistencePromise<void> {
477+
logDebug(LOG_TAG, "Updating index entries for document '%s'", document.key);
478+
479+
const promises: Array<PersistencePromise<void>> = [];
480+
diffSortedSets(
481+
existingEntries,
482+
newEntries,
483+
indexEntryComparator,
484+
entry => {
485+
promises.push(this.addIndexEntry(transaction, document, entry));
486+
},
487+
entry => {
488+
promises.push(this.deleteIndexEntry(transaction, document, entry));
489+
}
490+
);
491+
492+
return PersistencePromise.waitFor(promises);
274493
}
275494

276495
private getNextSequenceNumber(

packages/firestore/src/local/indexeddb_schema.ts

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

809+
static documentKeyIndex = 'sequenceNumberIndex';
810+
811+
static documentKeyIndexPath = ['indexId', 'uid', 'documentKey'];
812+
809813
constructor(
810814
/** The index id for this entry. */
811815
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
@@ -538,7 +538,12 @@ function createFieldIndex(db: IDBDatabase): void {
538538
{ unique: false }
539539
);
540540

541-
db.createObjectStore(DbIndexEntry.store, {
541+
const indexEntryStore = db.createObjectStore(DbIndexEntry.store, {
542542
keyPath: DbIndexEntry.keyPath
543543
});
544+
indexEntryStore.createIndex(
545+
DbIndexEntry.documentKeyIndex,
546+
DbIndexEntry.documentKeyIndexPath,
547+
{ unique: false }
548+
);
544549
}

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)