Skip to content

Commit 46fe136

Browse files
Support descending queries (#6075)
1 parent 7343a55 commit 46fe136

File tree

11 files changed

+511
-177
lines changed

11 files changed

+511
-177
lines changed

packages/firestore/src/core/target.ts

Lines changed: 146 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import { DocumentKey } from '../model/document_key';
2020
import {
2121
FieldIndex,
2222
fieldIndexGetArraySegment,
23-
fieldIndexGetDirectionalSegments
23+
fieldIndexGetDirectionalSegments,
24+
IndexKind
2425
} from '../model/field_index';
2526
import { FieldPath, ResourcePath } from '../model/path';
2627
import {
@@ -304,70 +305,25 @@ export function targetGetLowerBound(
304305
// For each segment, retrieve a lower bound if there is a suitable filter or
305306
// startAt.
306307
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
307-
let segmentValue: ProtoValue | undefined = undefined;
308-
let segmentInclusive = true;
309-
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) {
308+
const segmentBound =
309+
segment.kind === IndexKind.ASCENDING
310+
? targetGetLowerBoundForField(target, segment.fieldPath, target.startAt)
311+
: targetGetUpperBoundForField(
312+
target,
313+
segment.fieldPath,
314+
target.startAt
315+
);
316+
317+
if (!segmentBound.value) {
363318
// No lower bound exists
364319
return null;
365320
}
366-
values.push(segmentValue);
367-
inclusive &&= segmentInclusive;
321+
values.push(segmentBound.value);
322+
inclusive &&= segmentBound.inclusive;
368323
}
369324
return new Bound(values, inclusive);
370325
}
326+
371327
/**
372328
* Returns an upper bound of field values that can be used as an ending point
373329
* when scanning the index defined by `fieldIndex`. Returns `null` if no
@@ -383,71 +339,149 @@ export function targetGetUpperBound(
383339
// For each segment, retrieve an upper bound if there is a suitable filter or
384340
// endAt.
385341
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
386-
let segmentValue: ProtoValue | undefined = undefined;
387-
let segmentInclusive = true;
342+
const segmentBound =
343+
segment.kind === IndexKind.ASCENDING
344+
? targetGetUpperBoundForField(target, segment.fieldPath, target.endAt)
345+
: targetGetLowerBoundForField(target, segment.fieldPath, target.endAt);
388346

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;
347+
if (!segmentBound.value) {
348+
// No upper bound exists
349+
return null;
350+
}
351+
values.push(segmentBound.value);
352+
inclusive &&= segmentBound.inclusive;
353+
}
396354

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-
}
355+
return new Bound(values, inclusive);
356+
}
419357

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

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;
397+
if (valuesMax(value, filterValue) === filterValue) {
398+
value = filterValue;
399+
inclusive = filterInclusive;
400+
}
401+
}
402+
403+
// If there is an additional bound, compare the values against the existing
404+
// range to see if we can narrow the scope.
405+
if (bound !== null) {
406+
for (let i = 0; i < target.orderBy.length; ++i) {
407+
const orderBy = target.orderBy[i];
408+
if (orderBy.field.isEqual(fieldPath)) {
409+
const cursorValue = bound.position[i];
410+
if (valuesMax(value, cursorValue) === cursorValue) {
411+
value = cursorValue;
412+
inclusive = bound.inclusive;
438413
}
414+
break;
439415
}
440416
}
417+
}
441418

442-
if (segmentValue === undefined) {
443-
// No upper bound exists
444-
return null;
419+
return { value, inclusive };
420+
}
421+
422+
/**
423+
* Returns the value to use as the upper bound for ascending index segment at
424+
* the provided `fieldPath` (or the lower bound for an descending segment).
425+
*/
426+
function targetGetUpperBoundForField(
427+
target: Target,
428+
fieldPath: FieldPath,
429+
bound: Bound | null
430+
): { value: ProtoValue | undefined; inclusive: boolean } {
431+
let value: ProtoValue | undefined = undefined;
432+
let inclusive = true;
433+
434+
// Process all filters to find a value for the current field segment
435+
for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
436+
let filterValue: ProtoValue | undefined = undefined;
437+
let filterInclusive = true;
438+
439+
switch (fieldFilter.op) {
440+
case Operator.GREATER_THAN_OR_EQUAL:
441+
case Operator.GREATER_THAN:
442+
filterValue = valuesGetUpperBound(fieldFilter.value);
443+
filterInclusive = false;
444+
break;
445+
case Operator.EQUAL:
446+
case Operator.IN:
447+
case Operator.LESS_THAN_OR_EQUAL:
448+
filterValue = fieldFilter.value;
449+
break;
450+
case Operator.LESS_THAN:
451+
filterValue = fieldFilter.value;
452+
filterInclusive = false;
453+
break;
454+
case Operator.NOT_EQUAL:
455+
case Operator.NOT_IN:
456+
filterValue = MAX_VALUE;
457+
break;
458+
default:
459+
// Remaining filters cannot be used as upper bounds.
460+
}
461+
462+
if (valuesMin(value, filterValue) === filterValue) {
463+
value = filterValue;
464+
inclusive = filterInclusive;
445465
}
446-
values.push(segmentValue);
447-
inclusive &&= segmentInclusive;
448466
}
449467

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

453487
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)