Skip to content

Commit 53ff022

Browse files
authored
Support encoding and decoding Composite Filters. (#3339)
* Support encoding and decoding Composite Filters. * Add encode/decode test for composite filters. * Address comments.
1 parent ccd1cb6 commit 53ff022

File tree

4 files changed

+210
-42
lines changed

4 files changed

+210
-42
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/core/CompositeFilter.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ public boolean isDisjunction() {
7474
return operator == Operator.OPERATOR_UNSPECIFIED;
7575
}
7676

77+
/**
78+
* Returns true if this filter is a conjunction of field filters only. Returns false otherwise.
79+
*/
80+
public boolean isFlatConjunction() {
81+
if (operator != Operator.AND) {
82+
return false;
83+
}
84+
for (Filter filter : filters) {
85+
if (filter instanceof CompositeFilter) {
86+
return false;
87+
}
88+
}
89+
return true;
90+
}
91+
7792
/**
7893
* Performs a depth-first search to find and return the first FieldFilter in the composite filter
7994
* that satisfies the condition. Returns {@code null} if none of the FieldFilters satisfy the
@@ -132,4 +147,24 @@ public String getCanonicalId() {
132147
public String toString() {
133148
return getCanonicalId();
134149
}
150+
151+
@Override
152+
public boolean equals(Object o) {
153+
if (o == null || !(o instanceof CompositeFilter)) {
154+
return false;
155+
}
156+
CompositeFilter other = (CompositeFilter) o;
157+
// Note: This comparison requires order of filters in the list to be the same, and it does not
158+
// remove duplicate subfilters from each composite filter. It is therefore way less expensive.
159+
// TODO(orquery): Consider removing duplicates and ignoring order of filters in the list.
160+
return operator == other.operator && filters.equals(other.filters);
161+
}
162+
163+
@Override
164+
public int hashCode() {
165+
int result = 37;
166+
result = 31 * result + operator.hashCode();
167+
result = 31 * result + filters.hashCode();
168+
return result;
169+
}
135170
}

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java

Lines changed: 69 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
import com.google.firestore.v1.StructuredQuery.CollectionSelector;
6969
import com.google.firestore.v1.StructuredQuery.CompositeFilter;
7070
import com.google.firestore.v1.StructuredQuery.FieldReference;
71-
import com.google.firestore.v1.StructuredQuery.Filter.FilterTypeCase;
7271
import com.google.firestore.v1.StructuredQuery.Order;
7372
import com.google.firestore.v1.StructuredQuery.UnaryFilter;
7473
import com.google.firestore.v1.Target;
@@ -634,54 +633,39 @@ public com.google.firebase.firestore.core.Target decodeQueryTarget(QueryTarget t
634633
// Filters
635634

636635
private StructuredQuery.Filter encodeFilters(List<Filter> filters) {
637-
List<StructuredQuery.Filter> protos = new ArrayList<>(filters.size());
638-
for (Filter filter : filters) {
639-
if (filter instanceof FieldFilter) {
640-
protos.add(encodeUnaryOrFieldFilter((FieldFilter) filter));
641-
}
642-
}
643-
if (filters.size() == 1) {
644-
return protos.get(0);
645-
} else {
646-
CompositeFilter.Builder composite = CompositeFilter.newBuilder();
647-
composite.setOp(CompositeFilter.Operator.AND);
648-
composite.addAllFilters(protos);
649-
return StructuredQuery.Filter.newBuilder().setCompositeFilter(composite).build();
650-
}
636+
// A target's filter list is implicitly a composite AND filter.
637+
return encodeFilter(
638+
new com.google.firebase.firestore.core.CompositeFilter(
639+
filters, CompositeFilter.Operator.AND));
651640
}
652641

653642
private List<Filter> decodeFilters(StructuredQuery.Filter proto) {
654-
List<StructuredQuery.Filter> filters;
655-
if (proto.getFilterTypeCase() == FilterTypeCase.COMPOSITE_FILTER) {
656-
hardAssert(
657-
proto.getCompositeFilter().getOp() == CompositeFilter.Operator.AND,
658-
"Only AND-type composite filters are supported, got %d",
659-
proto.getCompositeFilter().getOp());
660-
filters = proto.getCompositeFilter().getFiltersList();
661-
} else {
662-
filters = Collections.singletonList(proto);
643+
Filter result = decodeFilter(proto);
644+
645+
// Instead of a singletonList containing AND(F1, F2, ...), we can return a list containing F1,
646+
// F2, ...
647+
// TODO(orquery): Once proper support for composite filters has been completed, we can remove
648+
// this flattening from here.
649+
if (result instanceof com.google.firebase.firestore.core.CompositeFilter) {
650+
com.google.firebase.firestore.core.CompositeFilter compositeFilter =
651+
(com.google.firebase.firestore.core.CompositeFilter) result;
652+
if (compositeFilter.isFlatConjunction()) {
653+
return compositeFilter.getFilters();
654+
}
663655
}
664656

665-
List<Filter> result = new ArrayList<>(filters.size());
666-
for (StructuredQuery.Filter filter : filters) {
667-
switch (filter.getFilterTypeCase()) {
668-
case COMPOSITE_FILTER:
669-
throw fail("Nested composite filters are not supported.");
670-
671-
case FIELD_FILTER:
672-
result.add(decodeFieldFilter(filter.getFieldFilter()));
673-
break;
674-
675-
case UNARY_FILTER:
676-
result.add(decodeUnaryFilter(filter.getUnaryFilter()));
677-
break;
657+
return Collections.singletonList(result);
658+
}
678659

679-
default:
680-
throw fail("Unrecognized Filter.filterType %d", filter.getFilterTypeCase());
681-
}
660+
@VisibleForTesting
661+
StructuredQuery.Filter encodeFilter(com.google.firebase.firestore.core.Filter filter) {
662+
if (filter instanceof FieldFilter) {
663+
return encodeUnaryOrFieldFilter((FieldFilter) filter);
664+
} else if (filter instanceof com.google.firebase.firestore.core.CompositeFilter) {
665+
return encodeCompositeFilter((com.google.firebase.firestore.core.CompositeFilter) filter);
666+
} else {
667+
throw fail("Unrecognized filter type %s", filter.toString());
682668
}
683-
684-
return result;
685669
}
686670

687671
@VisibleForTesting
@@ -711,6 +695,39 @@ StructuredQuery.Filter encodeUnaryOrFieldFilter(FieldFilter filter) {
711695
return StructuredQuery.Filter.newBuilder().setFieldFilter(proto).build();
712696
}
713697

698+
@VisibleForTesting
699+
StructuredQuery.Filter encodeCompositeFilter(
700+
com.google.firebase.firestore.core.CompositeFilter compositeFilter) {
701+
List<StructuredQuery.Filter> protos = new ArrayList<>(compositeFilter.getFilters().size());
702+
for (Filter filter : compositeFilter.getFilters()) {
703+
protos.add(encodeFilter(filter));
704+
}
705+
706+
// If there's only one filter in the composite filter, use it directly.
707+
if (protos.size() == 1) {
708+
return protos.get(0);
709+
}
710+
711+
CompositeFilter.Builder composite = CompositeFilter.newBuilder();
712+
composite.setOp(compositeFilter.getOperator());
713+
composite.addAllFilters(protos);
714+
return StructuredQuery.Filter.newBuilder().setCompositeFilter(composite).build();
715+
}
716+
717+
@VisibleForTesting
718+
Filter decodeFilter(StructuredQuery.Filter proto) {
719+
switch (proto.getFilterTypeCase()) {
720+
case COMPOSITE_FILTER:
721+
return decodeCompositeFilter(proto.getCompositeFilter());
722+
case FIELD_FILTER:
723+
return decodeFieldFilter(proto.getFieldFilter());
724+
case UNARY_FILTER:
725+
return decodeUnaryFilter(proto.getUnaryFilter());
726+
default:
727+
throw fail("Unrecognized Filter.filterType %d", proto.getFilterTypeCase());
728+
}
729+
}
730+
714731
@VisibleForTesting
715732
FieldFilter decodeFieldFilter(StructuredQuery.FieldFilter proto) {
716733
FieldPath fieldPath = FieldPath.fromServerFormat(proto.getField().getFieldPath());
@@ -734,6 +751,16 @@ private Filter decodeUnaryFilter(StructuredQuery.UnaryFilter proto) {
734751
}
735752
}
736753

754+
@VisibleForTesting
755+
com.google.firebase.firestore.core.CompositeFilter decodeCompositeFilter(
756+
StructuredQuery.CompositeFilter compositeFilter) {
757+
List<Filter> filters = new ArrayList<>();
758+
for (StructuredQuery.Filter filter : compositeFilter.getFiltersList()) {
759+
filters.add(decodeFilter(filter));
760+
}
761+
return new com.google.firebase.firestore.core.CompositeFilter(filters, compositeFilter.getOp());
762+
}
763+
737764
private FieldReference encodeFieldPath(FieldPath field) {
738765
return FieldReference.newBuilder().setFieldPath(field.canonicalString()).build();
739766
}

firebase-firestore/src/test/java/com/google/firebase/firestore/remote/RemoteSerializerTest.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.firestore.remote;
1616

1717
import static com.google.firebase.firestore.model.Values.refValue;
18+
import static com.google.firebase.firestore.testutil.TestUtil.andFilters;
1819
import static com.google.firebase.firestore.testutil.TestUtil.bound;
1920
import static com.google.firebase.firestore.testutil.TestUtil.deleteMutation;
2021
import static com.google.firebase.firestore.testutil.TestUtil.deletedDoc;
@@ -24,6 +25,7 @@
2425
import static com.google.firebase.firestore.testutil.TestUtil.key;
2526
import static com.google.firebase.firestore.testutil.TestUtil.map;
2627
import static com.google.firebase.firestore.testutil.TestUtil.mergeMutation;
28+
import static com.google.firebase.firestore.testutil.TestUtil.orFilters;
2729
import static com.google.firebase.firestore.testutil.TestUtil.orderBy;
2830
import static com.google.firebase.firestore.testutil.TestUtil.patchMutation;
2931
import static com.google.firebase.firestore.testutil.TestUtil.query;
@@ -680,6 +682,86 @@ public void testEncodesMultipleFiltersOnDeeperCollections() {
680682
serializer.decodeQueryTarget(serializer.encodeQueryTarget(q.toTarget())), q.toTarget());
681683
}
682684

685+
@Test
686+
public void testEncodesCompositeFiltersOnDeeperCollections() {
687+
// (prop < 42) || (author == "ehsann" && tags array-contains "pending")
688+
Query q =
689+
Query.atPath(ResourcePath.fromString("rooms/1/messages/10/attachments"))
690+
.filter(
691+
orFilters(
692+
filter("prop", "<", 42),
693+
andFilters(
694+
filter("author", "==", "ehsann"),
695+
filter("tags", "array-contains", "pending"))));
696+
Target actual = serializer.encodeTarget(wrapTargetData(q));
697+
698+
StructuredQuery.Builder structuredQueryBuilder =
699+
StructuredQuery.newBuilder()
700+
.addFrom(CollectionSelector.newBuilder().setCollectionId("attachments"))
701+
.setWhere(
702+
Filter.newBuilder()
703+
.setCompositeFilter(
704+
StructuredQuery.CompositeFilter.newBuilder()
705+
// TODO(orquery): Replace with Operator.OR once it's available.
706+
.setOp(CompositeFilter.Operator.OPERATOR_UNSPECIFIED)
707+
.addFilters(
708+
Filter.newBuilder()
709+
.setFieldFilter(
710+
StructuredQuery.FieldFilter.newBuilder()
711+
.setField(
712+
FieldReference.newBuilder().setFieldPath("prop"))
713+
.setOp(Operator.LESS_THAN)
714+
.setValue(Value.newBuilder().setIntegerValue(42))))
715+
.addFilters(
716+
Filter.newBuilder()
717+
.setCompositeFilter(
718+
StructuredQuery.CompositeFilter.newBuilder()
719+
.setOp(CompositeFilter.Operator.AND)
720+
.addFilters(
721+
Filter.newBuilder()
722+
.setFieldFilter(
723+
StructuredQuery.FieldFilter.newBuilder()
724+
.setField(
725+
FieldReference.newBuilder()
726+
.setFieldPath("author"))
727+
.setOp(Operator.EQUAL)
728+
.setValue(
729+
Value.newBuilder()
730+
.setStringValue("ehsann"))))
731+
.addFilters(
732+
Filter.newBuilder()
733+
.setFieldFilter(
734+
StructuredQuery.FieldFilter.newBuilder()
735+
.setField(
736+
FieldReference.newBuilder()
737+
.setFieldPath("tags"))
738+
.setOp(Operator.ARRAY_CONTAINS)
739+
.setValue(
740+
Value.newBuilder()
741+
.setStringValue(
742+
"pending"))))))))
743+
.addOrderBy(
744+
Order.newBuilder()
745+
.setField(FieldReference.newBuilder().setFieldPath("prop"))
746+
.setDirection(Direction.ASCENDING))
747+
.addOrderBy(defaultKeyOrder());
748+
QueryTarget.Builder queryBuilder =
749+
QueryTarget.newBuilder()
750+
.setParent("projects/p/databases/d/documents/rooms/1/messages/10")
751+
.setStructuredQuery(structuredQueryBuilder);
752+
Target expected =
753+
Target.newBuilder()
754+
.setQuery(queryBuilder)
755+
.setTargetId(1)
756+
.setResumeToken(ByteString.EMPTY)
757+
.build();
758+
759+
assertEquals(expected, actual);
760+
com.google.firebase.firestore.core.Target roundTripped =
761+
serializer.decodeQueryTarget(serializer.encodeQueryTarget(q.toTarget()));
762+
assertEquals(roundTripped, q.toTarget());
763+
}
764+
683765
@Test
684766
public void testInSerialization() {
685767
FieldFilter inputFilter = filter("field", "in", asList(42));

firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@
4141
import com.google.firebase.firestore.UserDataReader;
4242
import com.google.firebase.firestore.UserDataWriter;
4343
import com.google.firebase.firestore.core.Bound;
44+
import com.google.firebase.firestore.core.CompositeFilter;
4445
import com.google.firebase.firestore.core.FieldFilter;
4546
import com.google.firebase.firestore.core.FieldFilter.Operator;
47+
import com.google.firebase.firestore.core.Filter;
4648
import com.google.firebase.firestore.core.OrderBy;
4749
import com.google.firebase.firestore.core.OrderBy.Direction;
4850
import com.google.firebase.firestore.core.Query;
@@ -77,6 +79,7 @@
7779
import com.google.firebase.firestore.remote.WatchChange;
7880
import com.google.firebase.firestore.remote.WatchChange.DocumentChange;
7981
import com.google.firebase.firestore.remote.WatchChangeAggregator;
82+
import com.google.firestore.v1.StructuredQuery;
8083
import com.google.firestore.v1.Value;
8184
import com.google.protobuf.ByteString;
8285
import java.io.IOException;
@@ -256,6 +259,27 @@ public static FieldFilter filter(String key, String operator, Object value) {
256259
return FieldFilter.create(field(key), operatorFromString(operator), wrap(value));
257260
}
258261

262+
public static CompositeFilter andFilters(List<Filter> filters) {
263+
return new CompositeFilter(filters, StructuredQuery.CompositeFilter.Operator.AND);
264+
}
265+
266+
public static CompositeFilter andFilters(Filter... filters) {
267+
return new CompositeFilter(
268+
Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.AND);
269+
}
270+
271+
public static CompositeFilter orFilters(Filter... filters) {
272+
// TODO(orquery): Replace this with Operator.OR once it is available.
273+
return new CompositeFilter(
274+
Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.OPERATOR_UNSPECIFIED);
275+
}
276+
277+
public static CompositeFilter orFilters(List<Filter> filters) {
278+
// TODO(orquery): Replace this with Operator.OR once it is available.
279+
return new CompositeFilter(
280+
filters, StructuredQuery.CompositeFilter.Operator.OPERATOR_UNSPECIFIED);
281+
}
282+
259283
public static Operator operatorFromString(String s) {
260284
if (s.equals("<")) {
261285
return Operator.LESS_THAN;

0 commit comments

Comments
 (0)