Skip to content

Commit ba1507f

Browse files
Use precompute bounds
1 parent b87e324 commit ba1507f

File tree

4 files changed

+196
-119
lines changed

4 files changed

+196
-119
lines changed

packages/firestore/src/core/target.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ export function targetGetUpperBound(
440440
}
441441

442442
if (segmentValue === undefined) {
443-
// No lower bound exists
443+
// No upper bound exists
444444
return null;
445445
}
446446
values.push(segmentValue);

packages/firestore/src/index/index_entry.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,33 @@ export class IndexEntry {
2525
readonly arrayValue: Uint8Array,
2626
readonly directionalValue: Uint8Array
2727
) {}
28+
29+
/**
30+
* Returns an IndexEntry entry that sorts immediately after the current
31+
* directional value.
32+
*/
33+
successor(): IndexEntry {
34+
const currentLength = this.directionalValue.length;
35+
const newLength =
36+
currentLength === 0 || this.directionalValue[currentLength - 1] === 255
37+
? currentLength + 1
38+
: currentLength;
39+
40+
const successor = new Uint8Array(newLength);
41+
successor.set(this.directionalValue, 0);
42+
if (newLength !== currentLength) {
43+
successor.set([0], this.directionalValue.length);
44+
} else {
45+
++successor[successor.length - 1];
46+
}
47+
48+
return new IndexEntry(
49+
this.indexId,
50+
this.documentKey,
51+
this.arrayValue,
52+
successor
53+
);
54+
}
2855
}
2956

3057
export function indexEntryComparator(
@@ -36,17 +63,17 @@ export function indexEntryComparator(
3663
return cmp;
3764
}
3865

39-
cmp = DocumentKey.comparator(left.documentKey, right.documentKey);
66+
cmp = compareByteArrays(left.arrayValue, right.arrayValue);
4067
if (cmp !== 0) {
4168
return cmp;
4269
}
4370

44-
cmp = compareByteArrays(left.arrayValue, right.arrayValue);
71+
cmp = compareByteArrays(left.directionalValue, right.directionalValue);
4572
if (cmp !== 0) {
4673
return cmp;
4774
}
4875

49-
return compareByteArrays(left.directionalValue, right.directionalValue);
76+
return DocumentKey.comparator(left.documentKey, right.documentKey);
5077
}
5178

5279
export function compareByteArrays(left: Uint8Array, right: Uint8Array): number {

packages/firestore/src/local/indexeddb_index_manager.ts

Lines changed: 108 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@ import {
3030
} from '../core/target';
3131
import { FirestoreIndexValueWriter } from '../index/firestore_index_value_writer';
3232
import { IndexByteEncoder } from '../index/index_byte_encoder';
33-
import {
34-
compareByteArrays,
35-
IndexEntry,
36-
indexEntryComparator
37-
} from '../index/index_entry';
33+
import { IndexEntry, indexEntryComparator } from '../index/index_entry';
3834
import {
3935
documentKeySet,
4036
DocumentKeySet,
@@ -325,7 +321,7 @@ export class IndexedDbIndexManager implements IndexManager {
325321
lowerBoundInclusive: boolean,
326322
upperBounds: Uint8Array[] | null,
327323
upperBoundInclusive: boolean,
328-
notInValues: Uint8Array[] | null
324+
notInValues: Uint8Array[]
329325
): IDBKeyRange[] {
330326
// The number of total index scans we union together. This is similar to a
331327
// distributed normal form, but adapted for array values. We create a single
@@ -345,31 +341,46 @@ export class IndexedDbIndexManager implements IndexManager {
345341
const arrayValue = arrayValues
346342
? this.encodeSingleElement(arrayValues[i / scansPerArrayElement])
347343
: EMPTY_VALUE;
344+
345+
const lowerBound = lowerBounds
346+
? this.generateLowerBound(
347+
indexId,
348+
arrayValue,
349+
lowerBounds[i % scansPerArrayElement],
350+
lowerBoundInclusive
351+
)
352+
: this.generateEmptyBound(indexId);
353+
const upperBound = upperBounds
354+
? this.generateUpperBound(
355+
indexId,
356+
arrayValue,
357+
upperBounds[i % scansPerArrayElement],
358+
upperBoundInclusive
359+
)
360+
: this.generateEmptyBound(indexId + 1);
361+
348362
indexRanges.push(
349-
IDBKeyRange.bound(
350-
lowerBounds
351-
? this.generateLowerBound(
363+
...this.createRange(
364+
lowerBound,
365+
/* lowerInclusive= */ true,
366+
upperBound,
367+
/* upperInclusive= */ false,
368+
notInValues.map(
369+
(
370+
notIn // make non-nullable
371+
) =>
372+
this.generateLowerBound(
352373
indexId,
353374
arrayValue,
354-
lowerBounds[i % scansPerArrayElement],
355-
lowerBoundInclusive
375+
notIn,
376+
/* inclusive= */ true
356377
)
357-
: this.generateEmptyBound(indexId),
358-
upperBounds
359-
? this.generateUpperBound(
360-
indexId,
361-
arrayValue,
362-
upperBounds[i % scansPerArrayElement],
363-
upperBoundInclusive
364-
)
365-
: this.generateEmptyBound(indexId + 1),
366-
/* lowerOpen= */ false,
367-
/* upperOpen= */ true
378+
)
368379
)
369380
);
370381
}
371382

372-
return this.applyNotIn(indexRanges, notInValues);
383+
return indexRanges;
373384
}
374385

375386
/** Generates the lower bound for `arrayValue` and `directionalValue`. */
@@ -378,14 +389,14 @@ export class IndexedDbIndexManager implements IndexManager {
378389
arrayValue: Uint8Array,
379390
directionalValue: Uint8Array,
380391
inclusive: boolean
381-
): DbIndexEntryKey {
382-
return [
392+
): IndexEntry {
393+
const entry = new IndexEntry(
383394
indexId,
384-
this.uid,
395+
DocumentKey.empty(),
385396
arrayValue,
386-
inclusive ? directionalValue : successor(directionalValue),
387-
''
388-
];
397+
directionalValue
398+
);
399+
return inclusive ? entry : entry.successor();
389400
}
390401

391402
/** Generates the upper bound for `arrayValue` and `directionalValue`. */
@@ -394,22 +405,27 @@ export class IndexedDbIndexManager implements IndexManager {
394405
arrayValue: Uint8Array,
395406
directionalValue: Uint8Array,
396407
inclusive: boolean
397-
): DbIndexEntryKey {
398-
return [
408+
): IndexEntry {
409+
const entry = new IndexEntry(
399410
indexId,
400-
this.uid,
411+
DocumentKey.empty(),
401412
arrayValue,
402-
inclusive ? successor(directionalValue) : directionalValue,
403-
''
404-
];
413+
directionalValue
414+
);
415+
return inclusive ? entry.successor() : entry;
405416
}
406417

407418
/**
408419
* Generates an empty bound that scopes the index scan to the current index
409420
* and user.
410421
*/
411-
private generateEmptyBound(indexId: number): DbIndexEntryKey {
412-
return [indexId, this.uid, EMPTY_VALUE, EMPTY_VALUE, ''];
422+
private generateEmptyBound(indexId: number): IndexEntry {
423+
return new IndexEntry(
424+
indexId,
425+
DocumentKey.empty(),
426+
EMPTY_VALUE,
427+
EMPTY_VALUE
428+
);
413429
}
414430

415431
getFieldIndex(
@@ -475,9 +491,9 @@ export class IndexedDbIndexManager implements IndexManager {
475491
fieldIndex: FieldIndex,
476492
target: Target,
477493
bound: ProtoValue[] | null
478-
): Uint8Array[] | null {
479-
if (bound == null) {
480-
return null;
494+
): Uint8Array[] {
495+
if (bound === null) {
496+
return [];
481497
}
482498

483499
let encoders: IndexByteEncoder[] = [];
@@ -835,90 +851,69 @@ export class IndexedDbIndexManager implements IndexManager {
835851
}
836852

837853
/**
838-
* Applies notIn and != filters by taking the provided ranges and excluding
854+
* Returns a new set of IDB ranges that splits the existing range and excludes
839855
* any values that match the `notInValue` from these ranges. As an example,
840856
* '[foo > 2 && foo != 3]` becomes `[foo > 2 && < 3, foo > 3]`.
841857
*/
842-
private applyNotIn(
843-
indexRanges: IDBKeyRange[],
844-
notInValues: Uint8Array[] | null
858+
private createRange(
859+
lower: IndexEntry,
860+
lowerInclusive: boolean,
861+
upper: IndexEntry,
862+
upperInclusive: boolean,
863+
notInValues: IndexEntry[]
845864
): IDBKeyRange[] {
846-
if (!notInValues) {
847-
return indexRanges;
848-
}
865+
// The notIb values need to be sorted and unique so that we can return a
866+
// sorted set of non-overlapping ranges.
867+
notInValues = notInValues
868+
.sort((l, r) => indexEntryComparator(l, r))
869+
.filter(
870+
(el, i, values) => !i || indexEntryComparator(el, values[i - 1]) !== 0
871+
);
849872

850-
// The values need to be sorted so that we can return a sorted set of
851-
// non-overlapping ranges.
852-
notInValues.sort((l, r) => compareByteArrays(l, r));
873+
const bounds: IndexEntry[] = [];
874+
bounds.push(lower);
875+
for (const notInValue of notInValues) {
876+
const sortsAfter = indexEntryComparator(notInValue, lower);
877+
const sortsBefore = indexEntryComparator(notInValue, upper);
878+
879+
if (sortsAfter > 0 && sortsBefore < 0) {
880+
bounds.push(notInValue);
881+
bounds.push(notInValue.successor());
882+
} else if (sortsAfter === 0) {
883+
// The lowest value in the range is excluded
884+
bounds[0] = lower.successor();
885+
} else if (sortsBefore === 0) {
886+
// The largest value in the range is excluded
887+
upperInclusive = false;
888+
}
889+
}
890+
bounds.push(upper);
853891

854892
const ranges: IDBKeyRange[] = [];
855-
856-
// Use the existing bounds and interleave the notIn values. This means
857-
// that we would split an existing range into multiple ranges that exclude
858-
// the values from any notIn filter.
859-
for (const indexRange of indexRanges) {
860-
debugAssert(
861-
indexRange.lower[0] === indexRange.upper[0],
862-
'Index ID must match for index range'
893+
for (let i = 0; i < bounds.length; i += 2) {
894+
ranges.push(
895+
IDBKeyRange.bound(
896+
[
897+
bounds[i].indexId,
898+
this.uid,
899+
bounds[i].arrayValue,
900+
bounds[i].directionalValue,
901+
''
902+
],
903+
[
904+
bounds[i + 1].indexId,
905+
this.uid,
906+
bounds[i + 1].arrayValue,
907+
bounds[i + 1].directionalValue,
908+
''
909+
],
910+
!lowerInclusive,
911+
!upperInclusive
912+
)
863913
);
864-
const lowerBound = new Uint8Array(indexRange.lower[3]);
865-
const upperBound = new Uint8Array(indexRange.upper[3]);
866-
867-
let lastBound = lowerBound;
868-
let lastOpen = indexRange.lowerOpen;
869-
870-
const bounds = [...notInValues, upperBound];
871-
for (const currentBound of bounds) {
872-
// Verify that the range in the bound is sensible, as the bound may get
873-
// rejected otherwise
874-
const sortsAfter = compareByteArrays(currentBound, lastBound);
875-
const sortsBefore = compareByteArrays(currentBound, upperBound);
876-
if ((lastOpen ? sortsAfter > 0 : sortsAfter >= 0) && sortsBefore <= 0) {
877-
ranges.push(
878-
IDBKeyRange.bound(
879-
this.generateBound(indexRange.lower, lastBound),
880-
this.generateBound(indexRange.upper, currentBound),
881-
lastOpen,
882-
/* upperOpen= */ indexRange.upperOpen
883-
)
884-
);
885-
lastOpen = true;
886-
lastBound = successor(currentBound);
887-
}
888-
}
889914
}
890915
return ranges;
891916
}
892-
893-
/**
894-
* Generates the index entry that can be used to create the cutoff for `value`
895-
* on the provided index range.
896-
*/
897-
private generateBound(
898-
existingRange: DbIndexEntryKey,
899-
value: Uint8Array
900-
): DbIndexEntryKey {
901-
return [
902-
/* indexId= */ existingRange[0],
903-
/* userId= */ existingRange[1],
904-
/* arrayValue= */ existingRange[2],
905-
value,
906-
/* documentKey= */ ''
907-
];
908-
}
909-
}
910-
911-
/** Creates a Uint8Array value that sorts immediately after `value`. */
912-
function successor(value: Uint8Array): Uint8Array {
913-
if (value.length === 0 || value[value.length - 1] === 255) {
914-
const successor = new Uint8Array(value.length + 1);
915-
successor.set(value, 0);
916-
successor.set([0], value.length);
917-
return successor;
918-
} else {
919-
++value[value.length - 1];
920-
}
921-
return value;
922917
}
923918

924919
/**

0 commit comments

Comments
 (0)