Skip to content

Commit c2ab3f5

Browse files
Support descending queries
1 parent 2d672ce commit c2ab3f5

File tree

10 files changed

+433
-162
lines changed

10 files changed

+433
-162
lines changed

packages/firestore/src/core/target.ts

Lines changed: 140 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import { DocumentKey } from '../model/document_key';
2020
import {
2121
FieldIndex,
2222
fieldIndexGetArraySegment,
23-
fieldIndexGetDirectionalSegments
23+
fieldIndexGetDirectionalSegments,
24+
IndexKind,
25+
IndexSegment
2426
} from '../model/field_index';
2527
import { FieldPath, ResourcePath } from '../model/path';
2628
import {
@@ -304,70 +306,21 @@ export function targetGetLowerBound(
304306
// For each segment, retrieve a lower bound if there is a suitable filter or
305307
// startAt.
306308
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
307-
let segmentValue: ProtoValue | undefined = undefined;
308-
let segmentInclusive = true;
309+
const segmentBound =
310+
segment.kind === IndexKind.ASCENDING
311+
? targetGetAscendingBound(target, segment, target.startAt)
312+
: targetGetDescendingBound(target, segment, target.startAt);
309313

310-
// Process all filters to find a value for the current field segment
311-
for (const fieldFilter of targetGetFieldFiltersForPath(
312-
target,
313-
segment.fieldPath
314-
)) {
315-
let filterValue: ProtoValue | undefined = undefined;
316-
let filterInclusive = true;
317-
318-
switch (fieldFilter.op) {
319-
case Operator.LESS_THAN:
320-
case Operator.LESS_THAN_OR_EQUAL:
321-
filterValue = valuesGetLowerBound(fieldFilter.value);
322-
break;
323-
case Operator.EQUAL:
324-
case Operator.IN:
325-
case Operator.GREATER_THAN_OR_EQUAL:
326-
filterValue = fieldFilter.value;
327-
break;
328-
case Operator.GREATER_THAN:
329-
filterValue = fieldFilter.value;
330-
filterInclusive = false;
331-
break;
332-
case Operator.NOT_EQUAL:
333-
case Operator.NOT_IN:
334-
filterValue = MIN_VALUE;
335-
break;
336-
default:
337-
// Remaining filters cannot be used as lower bounds.
338-
}
339-
340-
if (valuesMax(segmentValue, filterValue) === filterValue) {
341-
segmentValue = filterValue;
342-
segmentInclusive = filterInclusive;
343-
}
344-
}
345-
346-
// If there is a startAt bound, compare the values against the existing
347-
// boundary to see if we can narrow the scope.
348-
if (target.startAt !== null) {
349-
for (let i = 0; i < target.orderBy.length; ++i) {
350-
const orderBy = target.orderBy[i];
351-
if (orderBy.field.isEqual(segment.fieldPath)) {
352-
const cursorValue = target.startAt.position[i];
353-
if (valuesMax(segmentValue, cursorValue) === cursorValue) {
354-
segmentValue = cursorValue;
355-
segmentInclusive = target.startAt.inclusive;
356-
}
357-
break;
358-
}
359-
}
360-
}
361-
362-
if (segmentValue === undefined) {
314+
if (!segmentBound.value) {
363315
// No lower bound exists
364316
return null;
365317
}
366-
values.push(segmentValue);
367-
inclusive &&= segmentInclusive;
318+
values.push(segmentBound.value);
319+
inclusive &&= segmentBound.inclusive;
368320
}
369321
return new Bound(values, inclusive);
370322
}
323+
371324
/**
372325
* Returns an upper bound of field values that can be used as an ending point
373326
* when scanning the index defined by `fieldIndex`. Returns `null` if no
@@ -383,71 +336,147 @@ export function targetGetUpperBound(
383336
// For each segment, retrieve an upper bound if there is a suitable filter or
384337
// endAt.
385338
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
386-
let segmentValue: ProtoValue | undefined = undefined;
387-
let segmentInclusive = true;
339+
const segmentBound =
340+
segment.kind === IndexKind.ASCENDING
341+
? targetGetDescendingBound(target, segment, target.endAt)
342+
: targetGetAscendingBound(target, segment, target.endAt);
388343

389-
// Process all filters to find a value for the current field segment
390-
for (const fieldFilter of targetGetFieldFiltersForPath(
391-
target,
392-
segment.fieldPath
393-
)) {
394-
let filterValue: ProtoValue | undefined = undefined;
395-
let filterInclusive = true;
344+
if (!segmentBound.value) {
345+
// No upper bound exists
346+
return null;
347+
}
348+
values.push(segmentBound.value);
349+
inclusive &&= segmentBound.inclusive;
350+
}
396351

397-
switch (fieldFilter.op) {
398-
case Operator.GREATER_THAN_OR_EQUAL:
399-
case Operator.GREATER_THAN:
400-
filterValue = valuesGetUpperBound(fieldFilter.value);
401-
filterInclusive = false;
402-
break;
403-
case Operator.EQUAL:
404-
case Operator.IN:
405-
case Operator.LESS_THAN_OR_EQUAL:
406-
filterValue = fieldFilter.value;
407-
break;
408-
case Operator.LESS_THAN:
409-
filterValue = fieldFilter.value;
410-
filterInclusive = false;
411-
break;
412-
case Operator.NOT_EQUAL:
413-
case Operator.NOT_IN:
414-
filterValue = MAX_VALUE;
415-
break;
416-
default:
417-
// Remaining filters cannot be used as upper bounds.
418-
}
352+
return new Bound(values, inclusive);
353+
}
419354

420-
if (valuesMin(segmentValue, filterValue) === filterValue) {
421-
segmentValue = filterValue;
422-
segmentInclusive = filterInclusive;
423-
}
355+
function targetGetAscendingBound(
356+
target: Target,
357+
segment: IndexSegment,
358+
bound: Bound | null
359+
): { value: ProtoValue | undefined; inclusive: boolean } {
360+
let value: ProtoValue | undefined = undefined;
361+
let inclusive = true;
362+
363+
// Process all filters to find a value for the current field segment
364+
for (const fieldFilter of targetGetFieldFiltersForPath(
365+
target,
366+
segment.fieldPath
367+
)) {
368+
let filterValue: ProtoValue | undefined = undefined;
369+
let filterInclusive = true;
370+
371+
switch (fieldFilter.op) {
372+
case Operator.LESS_THAN:
373+
case Operator.LESS_THAN_OR_EQUAL:
374+
filterValue = valuesGetLowerBound(fieldFilter.value);
375+
break;
376+
case Operator.EQUAL:
377+
case Operator.IN:
378+
case Operator.GREATER_THAN_OR_EQUAL:
379+
filterValue = fieldFilter.value;
380+
break;
381+
case Operator.GREATER_THAN:
382+
filterValue = fieldFilter.value;
383+
filterInclusive = false;
384+
break;
385+
case Operator.NOT_EQUAL:
386+
case Operator.NOT_IN:
387+
filterValue = MIN_VALUE;
388+
break;
389+
default:
390+
// Remaining filters cannot be used as lower bounds.
424391
}
425392

426-
// If there is a endAt bound, compare the values against the existing
427-
// boundary to see if we can narrow the scope.
428-
if (target.endAt !== null) {
429-
for (let i = 0; i < target.orderBy.length; ++i) {
430-
const orderBy = target.orderBy[i];
431-
if (orderBy.field.isEqual(segment.fieldPath)) {
432-
const cursorValue = target.endAt.position[i];
433-
if (valuesMin(segmentValue, cursorValue) === cursorValue) {
434-
segmentValue = cursorValue;
435-
segmentInclusive = target.endAt.inclusive;
436-
}
437-
break;
393+
if (valuesMax(value, filterValue) === filterValue) {
394+
value = filterValue;
395+
inclusive = filterInclusive;
396+
}
397+
}
398+
399+
// If there is an additional bound, compare the values against the existing
400+
// range to see if we can narrow the scope.
401+
if (bound !== null) {
402+
for (let i = 0; i < target.orderBy.length; ++i) {
403+
const orderBy = target.orderBy[i];
404+
if (orderBy.field.isEqual(segment.fieldPath)) {
405+
const cursorValue = bound.position[i];
406+
if (valuesMax(value, cursorValue) === cursorValue) {
407+
value = cursorValue;
408+
inclusive = bound.inclusive;
438409
}
410+
break;
439411
}
440412
}
413+
}
441414

442-
if (segmentValue === undefined) {
443-
// No upper bound exists
444-
return null;
415+
return { value, inclusive };
416+
}
417+
418+
function targetGetDescendingBound(
419+
target: Target,
420+
segment: IndexSegment,
421+
bound: Bound | null
422+
): { value: ProtoValue | undefined; inclusive: boolean } {
423+
let value: ProtoValue | undefined = undefined;
424+
let inclusive = true;
425+
426+
// Process all filters to find a value for the current field segment
427+
for (const fieldFilter of targetGetFieldFiltersForPath(
428+
target,
429+
segment.fieldPath
430+
)) {
431+
let filterValue: ProtoValue | undefined = undefined;
432+
let filterInclusive = true;
433+
434+
switch (fieldFilter.op) {
435+
case Operator.GREATER_THAN_OR_EQUAL:
436+
case Operator.GREATER_THAN:
437+
filterValue = valuesGetUpperBound(fieldFilter.value);
438+
filterInclusive = false;
439+
break;
440+
case Operator.EQUAL:
441+
case Operator.IN:
442+
case Operator.LESS_THAN_OR_EQUAL:
443+
filterValue = fieldFilter.value;
444+
break;
445+
case Operator.LESS_THAN:
446+
filterValue = fieldFilter.value;
447+
filterInclusive = false;
448+
break;
449+
case Operator.NOT_EQUAL:
450+
case Operator.NOT_IN:
451+
filterValue = MAX_VALUE;
452+
break;
453+
default:
454+
// Remaining filters cannot be used as upper bounds.
455+
}
456+
457+
if (valuesMin(value, filterValue) === filterValue) {
458+
value = filterValue;
459+
inclusive = filterInclusive;
445460
}
446-
values.push(segmentValue);
447-
inclusive &&= segmentInclusive;
448461
}
449462

450-
return new Bound(values, inclusive);
463+
// If there is an additional bound, compare the values against the existing
464+
// range to see if we can narrow the scope.
465+
if (bound !== null) {
466+
for (let i = 0; i < target.orderBy.length; ++i) {
467+
const orderBy = target.orderBy[i];
468+
if (orderBy.field.isEqual(segment.fieldPath)) {
469+
const cursorValue = bound.position[i];
470+
if (valuesMin(value, cursorValue) === cursorValue) {
471+
value = cursorValue;
472+
inclusive = bound.inclusive;
473+
}
474+
break;
475+
}
476+
}
477+
}
478+
479+
return { value, inclusive };
451480
}
452481

453482
export abstract class Filter {

packages/firestore/src/local/index_manager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
*/
1717

1818
import { Target } from '../core/target';
19-
import { DocumentKeySet, DocumentMap } from '../model/collections';
19+
import { DocumentMap } from '../model/collections';
20+
import { DocumentKey } from '../model/document_key';
2021
import { FieldIndex, IndexOffset } from '../model/field_index';
2122
import { ResourcePath } from '../model/path';
2223

@@ -108,7 +109,7 @@ export interface IndexManager {
108109
getDocumentsMatchingTarget(
109110
transaction: PersistenceTransaction,
110111
target: Target
111-
): PersistencePromise<DocumentKeySet | null>;
112+
): PersistencePromise<DocumentKey[] | null>;
112113

113114
/**
114115
* Returns the next collection group to update. Returns `null` if no group

0 commit comments

Comments
 (0)