Skip to content

Support descending queries #6075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 146 additions & 112 deletions packages/firestore/src/core/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { DocumentKey } from '../model/document_key';
import {
FieldIndex,
fieldIndexGetArraySegment,
fieldIndexGetDirectionalSegments
fieldIndexGetDirectionalSegments,
IndexKind
} from '../model/field_index';
import { FieldPath, ResourcePath } from '../model/path';
import {
Expand Down Expand Up @@ -304,70 +305,25 @@ export function targetGetLowerBound(
// For each segment, retrieve a lower bound if there is a suitable filter or
// startAt.
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
let segmentValue: ProtoValue | undefined = undefined;
let segmentInclusive = true;

// Process all filters to find a value for the current field segment
for (const fieldFilter of targetGetFieldFiltersForPath(
target,
segment.fieldPath
)) {
let filterValue: ProtoValue | undefined = undefined;
let filterInclusive = true;

switch (fieldFilter.op) {
case Operator.LESS_THAN:
case Operator.LESS_THAN_OR_EQUAL:
filterValue = valuesGetLowerBound(fieldFilter.value);
break;
case Operator.EQUAL:
case Operator.IN:
case Operator.GREATER_THAN_OR_EQUAL:
filterValue = fieldFilter.value;
break;
case Operator.GREATER_THAN:
filterValue = fieldFilter.value;
filterInclusive = false;
break;
case Operator.NOT_EQUAL:
case Operator.NOT_IN:
filterValue = MIN_VALUE;
break;
default:
// Remaining filters cannot be used as lower bounds.
}

if (valuesMax(segmentValue, filterValue) === filterValue) {
segmentValue = filterValue;
segmentInclusive = filterInclusive;
}
}

// If there is a startAt bound, compare the values against the existing
// boundary to see if we can narrow the scope.
if (target.startAt !== null) {
for (let i = 0; i < target.orderBy.length; ++i) {
const orderBy = target.orderBy[i];
if (orderBy.field.isEqual(segment.fieldPath)) {
const cursorValue = target.startAt.position[i];
if (valuesMax(segmentValue, cursorValue) === cursorValue) {
segmentValue = cursorValue;
segmentInclusive = target.startAt.inclusive;
}
break;
}
}
}

if (segmentValue === undefined) {
const segmentBound =
segment.kind === IndexKind.ASCENDING
? targetGetLowerBoundForField(target, segment.fieldPath, target.startAt)
: targetGetUpperBoundForField(
target,
segment.fieldPath,
target.startAt
);

if (!segmentBound.value) {
// No lower bound exists
return null;
}
values.push(segmentValue);
inclusive &&= segmentInclusive;
values.push(segmentBound.value);
inclusive &&= segmentBound.inclusive;
}
return new Bound(values, inclusive);
}

/**
* Returns an upper bound of field values that can be used as an ending point
* when scanning the index defined by `fieldIndex`. Returns `null` if no
Expand All @@ -383,71 +339,149 @@ export function targetGetUpperBound(
// For each segment, retrieve an upper bound if there is a suitable filter or
// endAt.
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
let segmentValue: ProtoValue | undefined = undefined;
let segmentInclusive = true;
const segmentBound =
segment.kind === IndexKind.ASCENDING
? targetGetUpperBoundForField(target, segment.fieldPath, target.endAt)
: targetGetLowerBoundForField(target, segment.fieldPath, target.endAt);

// Process all filters to find a value for the current field segment
for (const fieldFilter of targetGetFieldFiltersForPath(
target,
segment.fieldPath
)) {
let filterValue: ProtoValue | undefined = undefined;
let filterInclusive = true;
if (!segmentBound.value) {
// No upper bound exists
return null;
}
values.push(segmentBound.value);
inclusive &&= segmentBound.inclusive;
}

switch (fieldFilter.op) {
case Operator.GREATER_THAN_OR_EQUAL:
case Operator.GREATER_THAN:
filterValue = valuesGetUpperBound(fieldFilter.value);
filterInclusive = false;
break;
case Operator.EQUAL:
case Operator.IN:
case Operator.LESS_THAN_OR_EQUAL:
filterValue = fieldFilter.value;
break;
case Operator.LESS_THAN:
filterValue = fieldFilter.value;
filterInclusive = false;
break;
case Operator.NOT_EQUAL:
case Operator.NOT_IN:
filterValue = MAX_VALUE;
break;
default:
// Remaining filters cannot be used as upper bounds.
}
return new Bound(values, inclusive);
}

if (valuesMin(segmentValue, filterValue) === filterValue) {
segmentValue = filterValue;
segmentInclusive = filterInclusive;
}
/**
* Returns the value to use as the lower bound for ascending index segment at
* the provided `fieldPath` (or the upper bound for an descending segment).
*/
function targetGetLowerBoundForField(
target: Target,
fieldPath: FieldPath,
bound: Bound | null
): { value: ProtoValue | undefined; inclusive: boolean } {
let value: ProtoValue | undefined = undefined;
let inclusive = true;

// Process all filters to find a value for the current field segment
for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
let filterValue: ProtoValue | undefined = undefined;
let filterInclusive = true;

switch (fieldFilter.op) {
case Operator.LESS_THAN:
case Operator.LESS_THAN_OR_EQUAL:
filterValue = valuesGetLowerBound(fieldFilter.value);
break;
case Operator.EQUAL:
case Operator.IN:
case Operator.GREATER_THAN_OR_EQUAL:
filterValue = fieldFilter.value;
break;
case Operator.GREATER_THAN:
filterValue = fieldFilter.value;
filterInclusive = false;
break;
case Operator.NOT_EQUAL:
case Operator.NOT_IN:
filterValue = MIN_VALUE;
break;
default:
// Remaining filters cannot be used as lower bounds.
}

// If there is a endAt bound, compare the values against the existing
// boundary to see if we can narrow the scope.
if (target.endAt !== null) {
for (let i = 0; i < target.orderBy.length; ++i) {
const orderBy = target.orderBy[i];
if (orderBy.field.isEqual(segment.fieldPath)) {
const cursorValue = target.endAt.position[i];
if (valuesMin(segmentValue, cursorValue) === cursorValue) {
segmentValue = cursorValue;
segmentInclusive = target.endAt.inclusive;
}
break;
if (valuesMax(value, filterValue) === filterValue) {
value = filterValue;
inclusive = filterInclusive;
}
}

// If there is an additional bound, compare the values against the existing
// range to see if we can narrow the scope.
if (bound !== null) {
for (let i = 0; i < target.orderBy.length; ++i) {
const orderBy = target.orderBy[i];
if (orderBy.field.isEqual(fieldPath)) {
const cursorValue = bound.position[i];
if (valuesMax(value, cursorValue) === cursorValue) {
value = cursorValue;
inclusive = bound.inclusive;
}
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should port this change (adding a break statement) to the Android SDK.

}
}
}

if (segmentValue === undefined) {
// No upper bound exists
return null;
return { value, inclusive };
}

/**
* Returns the value to use as the upper bound for ascending index segment at
* the provided `fieldPath` (or the lower bound for an descending segment).
*/
function targetGetUpperBoundForField(
target: Target,
fieldPath: FieldPath,
bound: Bound | null
): { value: ProtoValue | undefined; inclusive: boolean } {
let value: ProtoValue | undefined = undefined;
let inclusive = true;

// Process all filters to find a value for the current field segment
for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
let filterValue: ProtoValue | undefined = undefined;
let filterInclusive = true;

switch (fieldFilter.op) {
case Operator.GREATER_THAN_OR_EQUAL:
case Operator.GREATER_THAN:
filterValue = valuesGetUpperBound(fieldFilter.value);
filterInclusive = false;
break;
case Operator.EQUAL:
case Operator.IN:
case Operator.LESS_THAN_OR_EQUAL:
filterValue = fieldFilter.value;
break;
case Operator.LESS_THAN:
filterValue = fieldFilter.value;
filterInclusive = false;
break;
case Operator.NOT_EQUAL:
case Operator.NOT_IN:
filterValue = MAX_VALUE;
break;
default:
// Remaining filters cannot be used as upper bounds.
}

if (valuesMin(value, filterValue) === filterValue) {
value = filterValue;
inclusive = filterInclusive;
}
values.push(segmentValue);
inclusive &&= segmentInclusive;
}

return new Bound(values, inclusive);
// If there is an additional bound, compare the values against the existing
// range to see if we can narrow the scope.
if (bound !== null) {
for (let i = 0; i < target.orderBy.length; ++i) {
const orderBy = target.orderBy[i];
if (orderBy.field.isEqual(fieldPath)) {
const cursorValue = bound.position[i];
if (valuesMin(value, cursorValue) === cursorValue) {
value = cursorValue;
inclusive = bound.inclusive;
}
break;
}
}
}

return { value, inclusive };
}

export abstract class Filter {
Expand Down
5 changes: 3 additions & 2 deletions packages/firestore/src/local/index_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
*/

import { Target } from '../core/target';
import { DocumentKeySet, DocumentMap } from '../model/collections';
import { DocumentMap } from '../model/collections';
import { DocumentKey } from '../model/document_key';
import { FieldIndex, IndexOffset } from '../model/field_index';
import { ResourcePath } from '../model/path';

Expand Down Expand Up @@ -108,7 +109,7 @@ export interface IndexManager {
getDocumentsMatchingTarget(
transaction: PersistenceTransaction,
target: Target
): PersistencePromise<DocumentKeySet | null>;
): PersistencePromise<DocumentKey[] | null>;

/**
* Returns the next collection group to update. Returns `null` if no group
Expand Down
Loading