Skip to content

Commit d04b608

Browse files
Port Filter/Target changes (#5929)
1 parent dd86989 commit d04b608

File tree

14 files changed

+884
-50
lines changed

14 files changed

+884
-50
lines changed

packages/firestore/src/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export {
8585
QueryConstraintType,
8686
OrderByDirection,
8787
WhereFilterOp
88-
} from './api/query';
88+
} from './api/filter';
8989

9090
export { Unsubscribe, SnapshotListenOptions } from './api/reference_impl';
9191

packages/firestore/src/core/database_info.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export class DatabaseId {
5858
this.database = database ? database : DEFAULT_DATABASE_NAME;
5959
}
6060

61+
static empty(): DatabaseId {
62+
return new DatabaseId('', '');
63+
}
64+
6165
get isDefaultDatabase(): boolean {
6266
return this.database === DEFAULT_DATABASE_NAME;
6367
}

packages/firestore/src/core/target.ts

Lines changed: 262 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,26 @@
1717

1818
import { Document } from '../model/document';
1919
import { DocumentKey } from '../model/document_key';
20+
import {
21+
FieldIndex,
22+
fieldIndexGetArraySegment,
23+
fieldIndexGetDirectionalSegments
24+
} from '../model/field_index';
2025
import { FieldPath, ResourcePath } from '../model/path';
2126
import {
2227
arrayValueContains,
2328
canonicalId,
2429
isArray,
2530
isReferenceValue,
31+
MAX_VALUE,
32+
MIN_VALUE,
2633
typeOrder,
2734
valueCompare,
28-
valueEquals
35+
valueEquals,
36+
valuesGetLowerBound,
37+
valuesGetUpperBound,
38+
valuesMax,
39+
valuesMin
2940
} from '../model/values';
3041
import { Value as ProtoValue } from '../protos/firestore_proto_api';
3142
import { debugAssert, debugCast, fail } from '../util/assert';
@@ -187,14 +198,262 @@ export function targetEquals(left: Target, right: Target): boolean {
187198
return boundEquals(left.endAt, right.endAt);
188199
}
189200

190-
export function isDocumentTarget(target: Target): boolean {
201+
export function targetIsDocumentTarget(target: Target): boolean {
191202
return (
192203
DocumentKey.isDocumentKey(target.path) &&
193204
target.collectionGroup === null &&
194205
target.filters.length === 0
195206
);
196207
}
197208

209+
/** Returns the field filters that target the given field path. */
210+
export function targetGetFieldFiltersForPath(
211+
target: Target,
212+
path: FieldPath
213+
): FieldFilter[] {
214+
return target.filters.filter(
215+
f => f instanceof FieldFilter && f.field.isEqual(path)
216+
) as FieldFilter[];
217+
}
218+
219+
/**
220+
* Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
221+
* filters. Returns `null` if there are no such filters.
222+
*/
223+
export function targetGetArrayValues(
224+
target: Target,
225+
fieldIndex: FieldIndex
226+
): ProtoValue[] | null {
227+
const segment = fieldIndexGetArraySegment(fieldIndex);
228+
if (segment === undefined) {
229+
return null;
230+
}
231+
232+
for (const fieldFilter of targetGetFieldFiltersForPath(
233+
target,
234+
segment.fieldPath
235+
)) {
236+
switch (fieldFilter.op) {
237+
case Operator.ARRAY_CONTAINS_ANY:
238+
return fieldFilter.value.arrayValue!.values || [];
239+
case Operator.ARRAY_CONTAINS:
240+
return [fieldFilter.value];
241+
default:
242+
// Remaining filters are not array filters.
243+
}
244+
}
245+
return null;
246+
}
247+
248+
/**
249+
* Returns the list of values that are used in != or NOT_IN filters. Returns
250+
* `null` if there are no such filters.
251+
*/
252+
export function targetGetNotInValues(
253+
target: Target,
254+
fieldIndex: FieldIndex
255+
): ProtoValue[] | null {
256+
const values: ProtoValue[] = [];
257+
258+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
259+
for (const fieldFilter of targetGetFieldFiltersForPath(
260+
target,
261+
segment.fieldPath
262+
)) {
263+
switch (fieldFilter.op) {
264+
case Operator.EQUAL:
265+
case Operator.IN:
266+
// Encode equality prefix, which is encoded in the index value before
267+
// the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
268+
// `value != 'ab'`).
269+
values.push(fieldFilter.value);
270+
break;
271+
case Operator.NOT_IN:
272+
case Operator.NOT_EQUAL:
273+
// NotIn/NotEqual is always a suffix. There cannot be any remaining
274+
// segments and hence we can return early here.
275+
values.push(fieldFilter.value);
276+
return values;
277+
default:
278+
// Remaining filters cannot be used as notIn bounds.
279+
}
280+
}
281+
}
282+
283+
return null;
284+
}
285+
286+
/**
287+
* Returns a lower bound of field values that can be used as a starting point to
288+
* scan the index defined by `fieldIndex`. Returns `null` if no lower bound
289+
* exists.
290+
*/
291+
export function targetGetLowerBound(
292+
target: Target,
293+
fieldIndex: FieldIndex
294+
): Bound | null {
295+
const values: ProtoValue[] = [];
296+
let inclusive = true;
297+
298+
// For each segment, retrieve a lower bound if there is a suitable filter or
299+
// startAt.
300+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
301+
let segmentValue: ProtoValue | undefined = undefined;
302+
let segmentInclusive = true;
303+
304+
// Process all filters to find a value for the current field segment
305+
for (const fieldFilter of targetGetFieldFiltersForPath(
306+
target,
307+
segment.fieldPath
308+
)) {
309+
let filterValue: ProtoValue | undefined = undefined;
310+
let filterInclusive = true;
311+
312+
switch (fieldFilter.op) {
313+
case Operator.LESS_THAN:
314+
case Operator.LESS_THAN_OR_EQUAL:
315+
filterValue = valuesGetLowerBound(fieldFilter.value);
316+
break;
317+
case Operator.EQUAL:
318+
case Operator.IN:
319+
case Operator.GREATER_THAN_OR_EQUAL:
320+
filterValue = fieldFilter.value;
321+
break;
322+
case Operator.GREATER_THAN:
323+
filterValue = fieldFilter.value;
324+
filterInclusive = false;
325+
break;
326+
case Operator.NOT_EQUAL:
327+
filterValue = MIN_VALUE;
328+
break;
329+
case Operator.NOT_IN:
330+
const length = (fieldFilter.value.arrayValue!.values || []).length;
331+
filterValue = {
332+
arrayValue: { values: new Array(length).fill(MIN_VALUE) }
333+
};
334+
break;
335+
default:
336+
// Remaining filters cannot be used as lower bounds.
337+
}
338+
339+
if (valuesMax(segmentValue, filterValue) === filterValue) {
340+
segmentValue = filterValue;
341+
segmentInclusive = filterInclusive;
342+
}
343+
}
344+
345+
// If there is a startAt bound, compare the values against the existing
346+
// boundary to see if we can narrow the scope.
347+
if (target.startAt !== null) {
348+
for (let i = 0; i < target.orderBy.length; ++i) {
349+
const orderBy = target.orderBy[i];
350+
if (orderBy.field.isEqual(segment.fieldPath)) {
351+
const cursorValue = target.startAt.position[i];
352+
if (valuesMax(segmentValue, cursorValue) === cursorValue) {
353+
segmentValue = cursorValue;
354+
segmentInclusive = !target.startAt.before;
355+
}
356+
break;
357+
}
358+
}
359+
}
360+
361+
if (segmentValue === undefined) {
362+
// No lower bound exists
363+
return null;
364+
}
365+
values.push(segmentValue);
366+
inclusive &&= segmentInclusive;
367+
}
368+
return new Bound(values, !inclusive);
369+
}
370+
/**
371+
* Returns an upper bound of field values that can be used as an ending point
372+
* when scanning the index defined by `fieldIndex`. Returns `null` if no
373+
* upper bound exists.
374+
*/
375+
export function targetGetUpperBound(
376+
target: Target,
377+
fieldIndex: FieldIndex
378+
): Bound | null {
379+
const values: ProtoValue[] = [];
380+
let inclusive = true;
381+
382+
// For each segment, retrieve an upper bound if there is a suitable filter or
383+
// endAt.
384+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
385+
let segmentValue: ProtoValue | undefined = undefined;
386+
let segmentInclusive = true;
387+
388+
// Process all filters to find a value for the current field segment
389+
for (const fieldFilter of targetGetFieldFiltersForPath(
390+
target,
391+
segment.fieldPath
392+
)) {
393+
let filterValue: ProtoValue | undefined = undefined;
394+
let filterInclusive = true;
395+
396+
switch (fieldFilter.op) {
397+
case Operator.GREATER_THAN_OR_EQUAL:
398+
case Operator.GREATER_THAN:
399+
filterValue = valuesGetUpperBound(fieldFilter.value);
400+
filterInclusive = false;
401+
break;
402+
case Operator.EQUAL:
403+
case Operator.IN:
404+
case Operator.LESS_THAN_OR_EQUAL:
405+
filterValue = fieldFilter.value;
406+
break;
407+
case Operator.LESS_THAN:
408+
filterValue = fieldFilter.value;
409+
filterInclusive = false;
410+
break;
411+
case Operator.NOT_EQUAL:
412+
filterValue = MAX_VALUE;
413+
break;
414+
case Operator.NOT_IN:
415+
const length = (fieldFilter.value.arrayValue!.values || []).length;
416+
filterValue = {
417+
arrayValue: { values: new Array(length).fill(MIN_VALUE) }
418+
};
419+
break;
420+
default:
421+
// Remaining filters cannot be used as upper bounds.
422+
}
423+
424+
if (valuesMin(segmentValue, filterValue) === filterValue) {
425+
segmentValue = filterValue;
426+
segmentInclusive = filterInclusive;
427+
}
428+
}
429+
430+
// If there is a endAt bound, compare the values against the existing
431+
// boundary to see if we can narrow the scope.
432+
if (target.endAt !== null) {
433+
for (let i = 0; i < target.orderBy.length; ++i) {
434+
const orderBy = target.orderBy[i];
435+
if (orderBy.field.isEqual(segment.fieldPath)) {
436+
const cursorValue = target.endAt.position[i];
437+
if (valuesMin(segmentValue, cursorValue) === cursorValue) {
438+
segmentValue = cursorValue;
439+
segmentInclusive = !target.endAt.before;
440+
}
441+
break;
442+
}
443+
}
444+
}
445+
446+
if (segmentValue === undefined) {
447+
// No lower bound exists
448+
return null;
449+
}
450+
values.push(segmentValue);
451+
inclusive &&= segmentInclusive;
452+
}
453+
454+
return new Bound(values, !inclusive);
455+
}
456+
198457
export abstract class Filter {
199458
abstract matches(doc: Document): boolean;
200459
}
@@ -513,6 +772,7 @@ export class ArrayContainsAnyFilter extends FieldFilter {
513772
}
514773
}
515774

775+
// TODO(indexing): Change Bound.before to "inclusive"
516776
/**
517777
* Represents a bound of a query.
518778
*

packages/firestore/src/local/local_serializer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Timestamp } from '../api/timestamp';
1919
import { BundleMetadata, NamedQuery } from '../core/bundle';
2020
import { LimitType, Query, queryWithLimit } from '../core/query';
2121
import { SnapshotVersion } from '../core/snapshot_version';
22-
import { canonifyTarget, isDocumentTarget, Target } from '../core/target';
22+
import { canonifyTarget, targetIsDocumentTarget, Target } from '../core/target';
2323
import { MutableDocument } from '../model/document';
2424
import { DocumentKey } from '../model/document_key';
2525
import { MutationBatch } from '../model/mutation_batch';
@@ -276,7 +276,7 @@ export function toDbTarget(
276276
targetData.lastLimboFreeSnapshotVersion
277277
);
278278
let queryProto: DbQuery;
279-
if (isDocumentTarget(targetData.target)) {
279+
if (targetIsDocumentTarget(targetData.target)) {
280280
queryProto = toDocumentsTarget(
281281
localSerializer.remoteSerializer,
282282
targetData.target

packages/firestore/src/model/field_index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ export class FieldIndex {
6565
) {}
6666
}
6767

68+
/** Returns the ArrayContains/ArrayContainsAny segment for this index. */
69+
export function fieldIndexGetArraySegment(
70+
fieldIndex: FieldIndex
71+
): IndexSegment | undefined {
72+
return fieldIndex.segments.find(s => s.kind === IndexKind.CONTAINS);
73+
}
74+
75+
/** Returns all directional (ascending/descending) segments for this index. */
76+
export function fieldIndexGetDirectionalSegments(
77+
fieldIndex: FieldIndex
78+
): IndexSegment[] {
79+
return fieldIndex.segments.filter(s => s.kind !== IndexKind.CONTAINS);
80+
}
81+
6882
/**
6983
* Compares indexes by collection group and segments. Ignores update time and
7084
* index ID.

0 commit comments

Comments
 (0)