Skip to content

Commit b58224b

Browse files
Merge ed6a600 into 8260149
2 parents 8260149 + ed6a600 commit b58224b

File tree

6 files changed

+316
-72
lines changed

6 files changed

+316
-72
lines changed

common/api-review/firestore-lite.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export abstract class QueryConstraint {
230230
}
231231

232232
// @public
233-
export type QueryConstraintType = 'where' | 'orderBy' | 'limit' | 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore';
233+
export type QueryConstraintType = 'where' | 'orderBy' | 'limit' | 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore' | 'and' | 'or';
234234

235235
// @public
236236
export class QueryDocumentSnapshot<T = DocumentData> extends DocumentSnapshot<T> {

common/api-review/firestore.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export abstract class QueryConstraint {
359359
}
360360

361361
// @public
362-
export type QueryConstraintType = 'where' | 'orderBy' | 'limit' | 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore';
362+
export type QueryConstraintType = 'where' | 'orderBy' | 'limit' | 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore' | 'and' | 'or';
363363

364364
// @public
365365
export class QueryDocumentSnapshot<T = DocumentData> extends DocumentSnapshot<T> {

packages/firestore/src/core/query.ts

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,15 @@ import {
2424
Bound,
2525
canonifyTarget,
2626
Direction,
27-
FieldFilter,
2827
Filter,
2928
newTarget,
30-
Operator,
3129
OrderBy,
3230
boundSortsBeforeDocument,
3331
stringifyTarget,
3432
Target,
3533
targetEquals,
36-
boundSortsAfterDocument
34+
boundSortsAfterDocument,
35+
CompositeFilter
3736
} from './target';
3837

3938
export const enum LimitType {
@@ -166,6 +165,13 @@ export function queryMatchesAllDocuments(query: Query): boolean {
166165
);
167166
}
168167

168+
export function queryContainsCompositeFilters(query: Query): boolean {
169+
return (
170+
query.filters.find(filter => filter instanceof CompositeFilter) !==
171+
undefined
172+
);
173+
}
174+
169175
export function getFirstOrderByField(query: Query): FieldPath | null {
170176
return query.explicitOrderBy.length > 0
171177
? query.explicitOrderBy[0].field
@@ -174,34 +180,12 @@ export function getFirstOrderByField(query: Query): FieldPath | null {
174180

175181
export function getInequalityFilterField(query: Query): FieldPath | null {
176182
for (const filter of query.filters) {
177-
debugAssert(
178-
filter instanceof FieldFilter,
179-
'Only FieldFilters are supported'
180-
);
181-
if (filter.isInequality()) {
182-
return filter.field;
183+
const result = filter.getFirstInequalityField();
184+
if (result !== null) {
185+
return result;
183186
}
184187
}
185-
return null;
186-
}
187188

188-
/**
189-
* Checks if any of the provided Operators are included in the query and
190-
* returns the first one that is, or null if none are.
191-
*/
192-
export function findFilterOperator(
193-
query: Query,
194-
operators: Operator[]
195-
): Operator | null {
196-
for (const filter of query.filters) {
197-
debugAssert(
198-
filter instanceof FieldFilter,
199-
'Only FieldFilters are supported'
200-
);
201-
if (operators.indexOf(filter.op) >= 0) {
202-
return filter.op;
203-
}
204-
}
205189
return null;
206190
}
207191

@@ -337,11 +321,13 @@ export function queryToTarget(query: Query): Target {
337321
}
338322

339323
export function queryWithAddedFilter(query: Query, filter: Filter): Query {
324+
const newInequalityField = filter.getFirstInequalityField();
325+
const queryInequalityField = getInequalityFilterField(query);
326+
340327
debugAssert(
341-
getInequalityFilterField(query) == null ||
342-
!(filter instanceof FieldFilter) ||
343-
!filter.isInequality() ||
344-
filter.field.isEqual(getInequalityFilterField(query)!),
328+
queryInequalityField == null ||
329+
newInequalityField == null ||
330+
newInequalityField.isEqual(queryInequalityField),
345331
'Query must only have one inequality field.'
346332
);
347333

packages/firestore/src/core/target.ts

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,12 @@ export function targetGetSegmentCount(target: Target): number {
536536

537537
export abstract class Filter {
538538
abstract matches(doc: Document): boolean;
539+
540+
abstract getFlattenedFilters(): readonly FieldFilter[];
541+
542+
abstract getFilters(): readonly Filter[];
543+
544+
abstract getFirstInequalityField(): FieldPath | null;
539545
}
540546

541547
export const enum Operator {
@@ -551,6 +557,11 @@ export const enum Operator {
551557
ARRAY_CONTAINS_ANY = 'array-contains-any'
552558
}
553559

560+
export const enum CompositeOperator {
561+
OR = 'or',
562+
AND = 'and'
563+
}
564+
554565
/**
555566
* The direction of sorting in an order by.
556567
*/
@@ -559,11 +570,12 @@ export const enum Direction {
559570
DESCENDING = 'desc'
560571
}
561572

573+
// TODO(orquery) move Filter classes to a new file, e.g. filter.ts
562574
export class FieldFilter extends Filter {
563575
protected constructor(
564-
public field: FieldPath,
565-
public op: Operator,
566-
public value: ProtoValue
576+
public readonly field: FieldPath,
577+
public readonly op: Operator,
578+
public readonly value: ProtoValue
567579
) {
568580
super();
569581
}
@@ -685,21 +697,117 @@ export class FieldFilter extends Filter {
685697
].indexOf(this.op) >= 0
686698
);
687699
}
700+
701+
getFlattenedFilters(): readonly FieldFilter[] {
702+
return [this];
703+
}
704+
705+
getFilters(): readonly Filter[] {
706+
return [this];
707+
}
708+
709+
getFirstInequalityField(): FieldPath | null {
710+
if (this.isInequality()) {
711+
return this.field;
712+
}
713+
return null;
714+
}
715+
}
716+
717+
export class CompositeFilter extends Filter {
718+
private memoizedFlattenedFilters: FieldFilter[] | null = null;
719+
720+
protected constructor(
721+
public readonly filters: readonly Filter[],
722+
public readonly op: CompositeOperator
723+
) {
724+
super();
725+
}
726+
727+
/**
728+
* Creates a filter based on the provided arguments.
729+
*/
730+
static create(filters: Filter[], op: CompositeOperator): CompositeFilter {
731+
return new CompositeFilter(filters, op);
732+
}
733+
734+
matches(doc: Document): boolean {
735+
if (this.isConjunction()) {
736+
// For conjunctions, all filters must match, so return false if any filter doesn't match.
737+
return this.filters.find(filter => !filter.matches(doc)) === undefined;
738+
} else {
739+
// For disjunctions, at least one filter should match.
740+
return this.filters.find(filter => filter.matches(doc)) !== undefined;
741+
}
742+
}
743+
744+
getFlattenedFilters(): readonly FieldFilter[] {
745+
if (this.memoizedFlattenedFilters !== null) {
746+
return this.memoizedFlattenedFilters;
747+
}
748+
749+
this.memoizedFlattenedFilters = this.filters.reduce((result, subfilter) => {
750+
return result.concat(subfilter.getFlattenedFilters());
751+
}, [] as FieldFilter[]);
752+
753+
return this.memoizedFlattenedFilters;
754+
}
755+
756+
getFilters(): readonly Filter[] {
757+
return this.filters;
758+
}
759+
760+
getFirstInequalityField(): FieldPath | null {
761+
const found = this.findFirstMatchingFilter(filter => filter.isInequality());
762+
763+
if (found !== null) {
764+
return found.field;
765+
}
766+
return null;
767+
}
768+
769+
// Performs a depth-first search to find and return the first FieldFilter in the composite filter
770+
// that satisfies the predicate. Returns `null` if none of the FieldFilters satisfy the
771+
// predicate.
772+
private findFirstMatchingFilter(
773+
predicate: (filter: FieldFilter) => boolean
774+
): FieldFilter | null {
775+
for (const fieldFilter of this.getFlattenedFilters()) {
776+
if (predicate(fieldFilter)) {
777+
return fieldFilter;
778+
}
779+
}
780+
781+
return null;
782+
}
783+
784+
isConjunction(): boolean {
785+
return this.op === CompositeOperator.AND;
786+
}
688787
}
689788

690789
export function canonifyFilter(filter: Filter): string {
691790
debugAssert(
692-
filter instanceof FieldFilter,
693-
'canonifyFilter() only supports FieldFilters'
694-
);
695-
// TODO(b/29183165): Technically, this won't be unique if two values have
696-
// the same description, such as the int 3 and the string "3". So we should
697-
// add the types in here somehow, too.
698-
return (
699-
filter.field.canonicalString() +
700-
filter.op.toString() +
701-
canonicalId(filter.value)
791+
filter instanceof FieldFilter || filter instanceof CompositeFilter,
792+
'canonifyFilter() only supports FieldFilters and CompositeFilters'
702793
);
794+
795+
if (filter instanceof FieldFilter) {
796+
// TODO(b/29183165): Technically, this won't be unique if two values have
797+
// the same description, such as the int 3 and the string "3". So we should
798+
// add the types in here somehow, too.
799+
return (
800+
filter.field.canonicalString() +
801+
filter.op.toString() +
802+
canonicalId(filter.value)
803+
);
804+
} else {
805+
// filter instanceof CompositeFilter
806+
const canonicalIdsString = filter.filters
807+
.map(filter => canonifyFilter(filter))
808+
.join(',');
809+
return `${filter.op}(${canonicalIdsString})`;
810+
}
703811
}
704812

705813
export function filterEquals(f1: Filter, f2: Filter): boolean {

0 commit comments

Comments
 (0)