Skip to content

Commit 514402d

Browse files
authored
Initial support for CompositeFilter class (#3290)
* Add composite filters. * add inequalityField to Filter class. * Address comments. * Return empty filter rather than null from parsing empty composite filters. * Address comments. * Use UnaryFilter. * address comments. * Don't use an index for serving queries with composite filters yet.
1 parent 58c706b commit 514402d

File tree

7 files changed

+365
-107
lines changed

7 files changed

+365
-107
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/Filter.java

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,56 @@
1717
import androidx.annotation.NonNull;
1818
import androidx.annotation.Nullable;
1919
import androidx.annotation.RestrictTo;
20-
import com.google.firebase.firestore.core.FieldFilter;
20+
import com.google.firebase.firestore.core.FieldFilter.Operator;
21+
import com.google.firestore.v1.StructuredQuery;
22+
import java.util.Arrays;
23+
import java.util.List;
2124

2225
/** @hide */
2326
@RestrictTo(RestrictTo.Scope.LIBRARY)
2427
public class Filter {
25-
private final FieldPath field;
26-
private final FieldFilter.Operator operator;
27-
private final Object value;
28+
static class UnaryFilter extends Filter {
29+
private final FieldPath field;
30+
private final Operator operator;
31+
private final Object value;
2832

29-
private Filter(@NonNull FieldPath field, FieldFilter.Operator operator, Object value) {
30-
this.field = field;
31-
this.operator = operator;
32-
this.value = value;
33-
}
33+
public UnaryFilter(FieldPath field, Operator operator, @Nullable Object value) {
34+
this.field = field;
35+
this.operator = operator;
36+
this.value = value;
37+
}
3438

35-
/** @hide */
36-
@RestrictTo(RestrictTo.Scope.LIBRARY)
37-
public FieldPath getField() {
38-
return field;
39-
}
39+
public FieldPath getField() {
40+
return field;
41+
}
42+
43+
public Operator getOperator() {
44+
return operator;
45+
}
4046

41-
/** @hide */
42-
@RestrictTo(RestrictTo.Scope.LIBRARY)
43-
public FieldFilter.Operator getOperator() {
44-
return operator;
47+
@Nullable
48+
public Object getValue() {
49+
return value;
50+
}
4551
}
4652

47-
/** @hide */
48-
@RestrictTo(RestrictTo.Scope.LIBRARY)
49-
public Object getValue() {
50-
return value;
53+
static class CompositeFilter extends Filter {
54+
private final List<Filter> filters;
55+
private final StructuredQuery.CompositeFilter.Operator operator;
56+
57+
public CompositeFilter(
58+
@NonNull List<Filter> filters, StructuredQuery.CompositeFilter.Operator operator) {
59+
this.filters = filters;
60+
this.operator = operator;
61+
}
62+
63+
public List<Filter> getFilters() {
64+
return filters;
65+
}
66+
67+
public StructuredQuery.CompositeFilter.Operator getOperator() {
68+
return operator;
69+
}
5170
}
5271

5372
@NonNull
@@ -57,7 +76,7 @@ public static Filter equalTo(@NonNull String field, @Nullable Object value) {
5776

5877
@NonNull
5978
public static Filter equalTo(@NonNull FieldPath fieldPath, @Nullable Object value) {
60-
return new Filter(fieldPath, FieldFilter.Operator.EQUAL, value);
79+
return new UnaryFilter(fieldPath, Operator.EQUAL, value);
6180
}
6281

6382
@NonNull
@@ -67,7 +86,7 @@ public static Filter notEqualTo(@NonNull String field, @Nullable Object value) {
6786

6887
@NonNull
6988
public static Filter notEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) {
70-
return new Filter(fieldPath, FieldFilter.Operator.NOT_EQUAL, value);
89+
return new UnaryFilter(fieldPath, Operator.NOT_EQUAL, value);
7190
}
7291

7392
@NonNull
@@ -77,7 +96,7 @@ public static Filter greaterThan(@NonNull String field, @Nullable Object value)
7796

7897
@NonNull
7998
public static Filter greaterThan(@NonNull FieldPath fieldPath, @Nullable Object value) {
80-
return new Filter(fieldPath, FieldFilter.Operator.GREATER_THAN, value);
99+
return new UnaryFilter(fieldPath, Operator.GREATER_THAN, value);
81100
}
82101

83102
@NonNull
@@ -87,7 +106,7 @@ public static Filter greaterThanOrEqualTo(@NonNull String field, @Nullable Objec
87106

88107
@NonNull
89108
public static Filter greaterThanOrEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) {
90-
return new Filter(fieldPath, FieldFilter.Operator.GREATER_THAN_OR_EQUAL, value);
109+
return new UnaryFilter(fieldPath, Operator.GREATER_THAN_OR_EQUAL, value);
91110
}
92111

93112
@NonNull
@@ -97,7 +116,7 @@ public static Filter lessThan(@NonNull String field, @Nullable Object value) {
97116

98117
@NonNull
99118
public static Filter lessThan(@NonNull FieldPath fieldPath, @Nullable Object value) {
100-
return new Filter(fieldPath, FieldFilter.Operator.LESS_THAN, value);
119+
return new UnaryFilter(fieldPath, Operator.LESS_THAN, value);
101120
}
102121

103122
@NonNull
@@ -107,7 +126,7 @@ public static Filter lessThanOrEqualTo(@NonNull String field, @Nullable Object v
107126

108127
@NonNull
109128
public static Filter lessThanOrEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) {
110-
return new Filter(fieldPath, FieldFilter.Operator.LESS_THAN_OR_EQUAL, value);
129+
return new UnaryFilter(fieldPath, Operator.LESS_THAN_OR_EQUAL, value);
111130
}
112131

113132
@NonNull
@@ -117,7 +136,7 @@ public static Filter arrayContains(@NonNull String field, @Nullable Object value
117136

118137
@NonNull
119138
public static Filter arrayContains(@NonNull FieldPath fieldPath, @Nullable Object value) {
120-
return new Filter(fieldPath, FieldFilter.Operator.ARRAY_CONTAINS, value);
139+
return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS, value);
121140
}
122141

123142
@NonNull
@@ -127,7 +146,7 @@ public static Filter arrayContainsAny(@NonNull String field, @Nullable Object va
127146

128147
@NonNull
129148
public static Filter arrayContainsAny(@NonNull FieldPath fieldPath, @Nullable Object value) {
130-
return new Filter(fieldPath, FieldFilter.Operator.ARRAY_CONTAINS_ANY, value);
149+
return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS_ANY, value);
131150
}
132151

133152
@NonNull
@@ -137,7 +156,7 @@ public static Filter inArray(@NonNull String field, @Nullable Object value) {
137156

138157
@NonNull
139158
public static Filter inArray(@NonNull FieldPath fieldPath, @Nullable Object value) {
140-
return new Filter(fieldPath, FieldFilter.Operator.IN, value);
159+
return new UnaryFilter(fieldPath, Operator.IN, value);
141160
}
142161

143162
@NonNull
@@ -147,6 +166,19 @@ public static Filter notInArray(@NonNull String field, @Nullable Object value) {
147166

148167
@NonNull
149168
public static Filter notInArray(@NonNull FieldPath fieldPath, @Nullable Object value) {
150-
return new Filter(fieldPath, FieldFilter.Operator.NOT_IN, value);
169+
return new UnaryFilter(fieldPath, Operator.NOT_IN, value);
170+
}
171+
172+
@NonNull
173+
public static Filter or(Filter... filters) {
174+
// TODO(orquery): Change this to Operator.OR once it is available.
175+
return new CompositeFilter(
176+
Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.OPERATOR_UNSPECIFIED);
177+
}
178+
179+
@NonNull
180+
public static Filter and(Filter... filters) {
181+
return new CompositeFilter(
182+
Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.AND);
151183
}
152184
}

firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java

Lines changed: 113 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.firebase.firestore.core.ActivityScope;
2929
import com.google.firebase.firestore.core.AsyncEventListener;
3030
import com.google.firebase.firestore.core.Bound;
31+
import com.google.firebase.firestore.core.CompositeFilter;
3132
import com.google.firebase.firestore.core.EventManager.ListenOptions;
3233
import com.google.firebase.firestore.core.FieldFilter;
3334
import com.google.firebase.firestore.core.FieldFilter.Operator;
@@ -387,15 +388,16 @@ public Query whereNotIn(@NonNull FieldPath fieldPath, @NonNull List<? extends Ob
387388
}
388389

389390
/**
390-
* Parses the given value object and creates a new {@code FieldFilter} with the given field,
391-
* operator, and value. Also performs validation on the filter before retuning it.
391+
* Takes a {@link Filter.UnaryFilter} object, parses the value object and returns a new {@link
392+
* FieldFilter} for the field, operator, and parsed value.
392393
*
393-
* @param fieldPath The field to compare
394-
* @param op The operator
395-
* @param value The value for parsing
396-
* @return The created {@code FieldFilter}.
394+
* @param fieldFilterData The Filter.UnaryFilter object to parse.
395+
* @return The created {@link FieldFilter}.
397396
*/
398-
private FieldFilter parseFieldFilter(@NonNull FieldPath fieldPath, Operator op, Object value) {
397+
private FieldFilter parseFieldFilter(Filter.UnaryFilter fieldFilterData) {
398+
FieldPath fieldPath = fieldFilterData.getField();
399+
Operator op = fieldFilterData.getOperator();
400+
Object value = fieldFilterData.getValue();
399401
checkNotNull(fieldPath, "Provided field path must not be null.");
400402
checkNotNull(op, "Provided op must not be null.");
401403
Value fieldValue;
@@ -426,15 +428,54 @@ private FieldFilter parseFieldFilter(@NonNull FieldPath fieldPath, Operator op,
426428
.parseQueryValue(value, op == Operator.IN || op == Operator.NOT_IN);
427429
}
428430
FieldFilter filter = FieldFilter.create(fieldPath.getInternalPath(), op, fieldValue);
429-
validateNewFilter(filter);
430431
return filter;
431432
}
432433

434+
/**
435+
* Takes a {@link Filter.CompositeFilter} object, parses each of its subfilters, and returns a new
436+
* {@link Filter} that is constructed using the parsed values.
437+
*/
438+
private com.google.firebase.firestore.core.Filter parseCompositeFilter(
439+
Filter.CompositeFilter compositeFilterData) {
440+
List<com.google.firebase.firestore.core.Filter> parsedFilters = new ArrayList<>();
441+
for (Filter filter : compositeFilterData.getFilters()) {
442+
com.google.firebase.firestore.core.Filter parsedFilter = parseFilter(filter);
443+
if (!parsedFilter.getFilters().isEmpty()) {
444+
parsedFilters.add(parsedFilter);
445+
}
446+
}
447+
448+
// For composite filters containing 1 filter, return the only filter.
449+
// For example: AND(FieldFilter1) == FieldFilter1
450+
if (parsedFilters.size() == 1) {
451+
return parsedFilters.get(0);
452+
}
453+
return new CompositeFilter(parsedFilters, compositeFilterData.getOperator());
454+
}
455+
456+
/**
457+
* Takes a filter whose value has not been parsed, parses the value object and returns a
458+
* FieldFilter or CompositeFilter with parsed values.
459+
*/
460+
private com.google.firebase.firestore.core.Filter parseFilter(Filter filter) {
461+
hardAssert(
462+
filter instanceof Filter.UnaryFilter || filter instanceof Filter.CompositeFilter,
463+
"Parsing is only supported for Filter.UnaryFilter and Filter.CompositeFilter.");
464+
if (filter instanceof Filter.UnaryFilter) {
465+
return parseFieldFilter((Filter.UnaryFilter) filter);
466+
}
467+
return parseCompositeFilter((Filter.CompositeFilter) filter);
468+
}
469+
433470
// TODO(orquery): This method will become public API. Change visibility and add documentation.
434471
private Query where(Filter filter) {
435-
return new Query(
436-
query.filter(parseFieldFilter(filter.getField(), filter.getOperator(), filter.getValue())),
437-
firestore);
472+
com.google.firebase.firestore.core.Filter parsedFilter = parseFilter(filter);
473+
if (parsedFilter.getFilters().isEmpty()) {
474+
// Return the existing query if not adding any more filters (e.g. an empty composite filter).
475+
return this;
476+
}
477+
validateNewFilter(parsedFilter);
478+
return new Query(query.filter(parsedFilter), firestore);
438479
}
439480

440481
private void validateOrderByField(com.google.firebase.firestore.model.FieldPath field) {
@@ -553,44 +594,71 @@ private List<Operator> conflictingOps(Operator op) {
553594
}
554595
}
555596

556-
private void validateNewFilter(com.google.firebase.firestore.core.Filter filter) {
557-
if (filter instanceof FieldFilter) {
558-
FieldFilter fieldFilter = (FieldFilter) filter;
559-
Operator filterOp = fieldFilter.getOperator();
560-
if (fieldFilter.isInequality()) {
561-
com.google.firebase.firestore.model.FieldPath existingInequality = query.inequalityField();
562-
com.google.firebase.firestore.model.FieldPath newInequality = fieldFilter.getField();
563-
564-
if (existingInequality != null && !existingInequality.equals(newInequality)) {
565-
throw new IllegalArgumentException(
566-
String.format(
567-
"All where filters with an inequality (notEqualTo, notIn, lessThan, "
568-
+ "lessThanOrEqualTo, greaterThan, or greaterThanOrEqualTo) must be on the "
569-
+ "same field. But you have filters on '%s' and '%s'",
570-
existingInequality.canonicalString(), newInequality.canonicalString()));
571-
}
572-
com.google.firebase.firestore.model.FieldPath firstOrderByField =
573-
query.getFirstOrderByField();
574-
if (firstOrderByField != null) {
575-
validateOrderByFieldMatchesInequality(firstOrderByField, newInequality);
576-
}
597+
/** Checks that adding the given field filter to the given query yields a valid query */
598+
private void validateNewFieldFilter(
599+
com.google.firebase.firestore.core.Query query,
600+
com.google.firebase.firestore.core.FieldFilter fieldFilter) {
601+
Operator filterOp = fieldFilter.getOperator();
602+
if (fieldFilter.isInequality()) {
603+
com.google.firebase.firestore.model.FieldPath existingInequality = query.inequalityField();
604+
com.google.firebase.firestore.model.FieldPath newInequality = fieldFilter.getField();
605+
606+
if (existingInequality != null && !existingInequality.equals(newInequality)) {
607+
throw new IllegalArgumentException(
608+
String.format(
609+
"All where filters with an inequality (notEqualTo, notIn, lessThan, "
610+
+ "lessThanOrEqualTo, greaterThan, or greaterThanOrEqualTo) must be on the "
611+
+ "same field. But you have filters on '%s' and '%s'",
612+
existingInequality.canonicalString(), newInequality.canonicalString()));
577613
}
578-
Operator conflictingOp = query.findFilterOperator(conflictingOps(filterOp));
579-
if (conflictingOp != null) {
580-
// We special case when it's a duplicate op to give a slightly clearer error message.
581-
if (conflictingOp == filterOp) {
582-
throw new IllegalArgumentException(
583-
"Invalid Query. You cannot use more than one '" + filterOp.toString() + "' filter.");
584-
} else {
585-
throw new IllegalArgumentException(
586-
"Invalid Query. You cannot use '"
587-
+ filterOp.toString()
588-
+ "' filters with '"
589-
+ conflictingOp.toString()
590-
+ "' filters.");
614+
com.google.firebase.firestore.model.FieldPath firstOrderByField =
615+
query.getFirstOrderByField();
616+
if (firstOrderByField != null) {
617+
validateOrderByFieldMatchesInequality(firstOrderByField, newInequality);
618+
}
619+
}
620+
Operator conflictingOp = findFilterWithOperator(query.getFilters(), conflictingOps(filterOp));
621+
if (conflictingOp != null) {
622+
// We special case when it's a duplicate op to give a slightly clearer error message.
623+
if (conflictingOp == filterOp) {
624+
throw new IllegalArgumentException(
625+
"Invalid Query. You cannot use more than one '" + filterOp.toString() + "' filter.");
626+
} else {
627+
throw new IllegalArgumentException(
628+
"Invalid Query. You cannot use '"
629+
+ filterOp.toString()
630+
+ "' filters with '"
631+
+ conflictingOp.toString()
632+
+ "' filters.");
633+
}
634+
}
635+
}
636+
637+
/** Checks that adding the given filter to the current query is valid */
638+
private void validateNewFilter(com.google.firebase.firestore.core.Filter filter) {
639+
com.google.firebase.firestore.core.Query testQuery = query;
640+
for (FieldFilter subfilter : filter.getFlattenedFilters()) {
641+
validateNewFieldFilter(testQuery, subfilter);
642+
testQuery = query.filter(subfilter);
643+
}
644+
}
645+
646+
/**
647+
* Checks if any of the provided filter operators are included in the given list of filters and
648+
* returns the first one that is, or null if none are.
649+
*/
650+
@Nullable
651+
private Operator findFilterWithOperator(
652+
List<com.google.firebase.firestore.core.Filter> filters, List<Operator> operators) {
653+
for (com.google.firebase.firestore.core.Filter filter : filters) {
654+
if (filter instanceof FieldFilter) {
655+
Operator filterOp = ((FieldFilter) filter).getOperator();
656+
if (operators.contains(filterOp)) {
657+
return filterOp;
591658
}
592659
}
593660
}
661+
return null;
594662
}
595663

596664
/**

0 commit comments

Comments
 (0)