Skip to content

Commit 679c0c4

Browse files
Port Filter/Target changes
1 parent 4983f4d commit 679c0c4

File tree

15 files changed

+902
-50
lines changed

15 files changed

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

770+
// TODO(indexing): Change Bound.before to "inclusive"
516771
/**
517772
* Represents a bound of a query.
518773
*

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';
@@ -270,7 +270,7 @@ export function toDbTarget(
270270
targetData.lastLimboFreeSnapshotVersion
271271
);
272272
let queryProto: DbQuery;
273-
if (isDocumentTarget(targetData.target)) {
273+
if (targetIsDocumentTarget(targetData.target)) {
274274
queryProto = toDocumentsTarget(
275275
localSerializer.remoteSerializer,
276276
targetData.target

packages/firestore/src/model/document_key.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export class DocumentKey {
3939
return new DocumentKey(ResourcePath.fromString(name).popFirst(5));
4040
}
4141

42+
static empty(): DocumentKey {
43+
return new DocumentKey(ResourcePath.emptyPath());
44+
}
45+
4246
/** Returns true if the document is in the specified collectionId. */
4347
hasCollectionId(collectionId: string): boolean {
4448
return (

packages/firestore/src/model/field_index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ export class FieldIndex {
4545
) {}
4646
}
4747

48+
/** Returns the ArrayContains/ArrayContainsAny segment for this index. */
49+
export function fieldIndexGetArraySegment(
50+
fieldIndex: FieldIndex
51+
): Segment | undefined {
52+
return fieldIndex.segments.find(s => s.kind === Kind.CONTAINS);
53+
}
54+
55+
/** Returns all directional (ascending/descending) segments for this index. */
56+
export function fieldIndexGetDirectionalSegments(
57+
fieldIndex: FieldIndex
58+
): Segment[] {
59+
return fieldIndex.segments.filter(s => s.kind !== Kind.CONTAINS);
60+
}
61+
4862
/** The type of the index, e.g. for which type of query it can be used. */
4963
export const enum Kind {
5064
/**

0 commit comments

Comments
 (0)