Skip to content

Support encoding and decoding Composite Filters. #3339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ public boolean isDisjunction() {
return operator == Operator.OPERATOR_UNSPECIFIED;
}

/**
* Returns true if this filter is a conjunction of field filters only. Returns false otherwise.
*/
public boolean isFlatConjunction() {
if (operator != Operator.AND) {
return false;
}
for (Filter filter : filters) {
if (filter instanceof CompositeFilter) {
return false;
}
}
return true;
}

/**
* Performs a depth-first search to find and return the first FieldFilter in the composite filter
* that satisfies the condition. Returns {@code null} if none of the FieldFilters satisfy the
Expand Down Expand Up @@ -132,4 +147,24 @@ public String getCanonicalId() {
public String toString() {
return getCanonicalId();
}

@Override
public boolean equals(Object o) {
if (o == null || !(o instanceof CompositeFilter)) {
return false;
}
CompositeFilter other = (CompositeFilter) o;
// Note: This comparison requires order of filters in the list to be the same, and it does not
// remove duplicate subfilters from each composite filter. It is therefore way less expensive.
// TODO(orquery): Consider removing duplicates and ignoring order of filters in the list.
return operator == other.operator && filters.equals(other.filters);
}

@Override
public int hashCode() {
int result = 37;
result = 31 * result + operator.hashCode();
result = 31 * result + filters.hashCode();
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
import com.google.firestore.v1.StructuredQuery.CollectionSelector;
import com.google.firestore.v1.StructuredQuery.CompositeFilter;
import com.google.firestore.v1.StructuredQuery.FieldReference;
import com.google.firestore.v1.StructuredQuery.Filter.FilterTypeCase;
import com.google.firestore.v1.StructuredQuery.Order;
import com.google.firestore.v1.StructuredQuery.UnaryFilter;
import com.google.firestore.v1.Target;
Expand Down Expand Up @@ -634,54 +633,39 @@ public com.google.firebase.firestore.core.Target decodeQueryTarget(QueryTarget t
// Filters

private StructuredQuery.Filter encodeFilters(List<Filter> filters) {
List<StructuredQuery.Filter> protos = new ArrayList<>(filters.size());
for (Filter filter : filters) {
if (filter instanceof FieldFilter) {
protos.add(encodeUnaryOrFieldFilter((FieldFilter) filter));
}
}
if (filters.size() == 1) {
return protos.get(0);
} else {
CompositeFilter.Builder composite = CompositeFilter.newBuilder();
composite.setOp(CompositeFilter.Operator.AND);
composite.addAllFilters(protos);
return StructuredQuery.Filter.newBuilder().setCompositeFilter(composite).build();
}
// A target's filter list is implicitly a composite AND filter.
return encodeFilter(
new com.google.firebase.firestore.core.CompositeFilter(
filters, CompositeFilter.Operator.AND));
}

private List<Filter> decodeFilters(StructuredQuery.Filter proto) {
List<StructuredQuery.Filter> filters;
if (proto.getFilterTypeCase() == FilterTypeCase.COMPOSITE_FILTER) {
hardAssert(
proto.getCompositeFilter().getOp() == CompositeFilter.Operator.AND,
"Only AND-type composite filters are supported, got %d",
proto.getCompositeFilter().getOp());
filters = proto.getCompositeFilter().getFiltersList();
} else {
filters = Collections.singletonList(proto);
Filter result = decodeFilter(proto);

// Instead of a singletonList containing AND(F1, F2, ...), we can return a list containing F1,
// F2, ...
// TODO(orquery): Once proper support for composite filters has been completed, we can remove
// this flattening from here.
if (result instanceof com.google.firebase.firestore.core.CompositeFilter) {
com.google.firebase.firestore.core.CompositeFilter compositeFilter =
(com.google.firebase.firestore.core.CompositeFilter) result;
if (compositeFilter.isFlatConjunction()) {
return compositeFilter.getFilters();
}
}

List<Filter> result = new ArrayList<>(filters.size());
for (StructuredQuery.Filter filter : filters) {
switch (filter.getFilterTypeCase()) {
case COMPOSITE_FILTER:
throw fail("Nested composite filters are not supported.");

case FIELD_FILTER:
result.add(decodeFieldFilter(filter.getFieldFilter()));
break;

case UNARY_FILTER:
result.add(decodeUnaryFilter(filter.getUnaryFilter()));
break;
return Collections.singletonList(result);
}

default:
throw fail("Unrecognized Filter.filterType %d", filter.getFilterTypeCase());
}
@VisibleForTesting
StructuredQuery.Filter encodeFilter(com.google.firebase.firestore.core.Filter filter) {
if (filter instanceof FieldFilter) {
return encodeUnaryOrFieldFilter((FieldFilter) filter);
} else if (filter instanceof com.google.firebase.firestore.core.CompositeFilter) {
return encodeCompositeFilter((com.google.firebase.firestore.core.CompositeFilter) filter);
} else {
throw fail("Unrecognized filter type %s", filter.toString());
}

return result;
}

@VisibleForTesting
Expand Down Expand Up @@ -711,6 +695,39 @@ StructuredQuery.Filter encodeUnaryOrFieldFilter(FieldFilter filter) {
return StructuredQuery.Filter.newBuilder().setFieldFilter(proto).build();
}

@VisibleForTesting
StructuredQuery.Filter encodeCompositeFilter(
com.google.firebase.firestore.core.CompositeFilter compositeFilter) {
List<StructuredQuery.Filter> protos = new ArrayList<>(compositeFilter.getFilters().size());
for (Filter filter : compositeFilter.getFilters()) {
protos.add(encodeFilter(filter));
}

// If there's only one filter in the composite filter, use it directly.
if (protos.size() == 1) {
return protos.get(0);
}

CompositeFilter.Builder composite = CompositeFilter.newBuilder();
composite.setOp(compositeFilter.getOperator());
composite.addAllFilters(protos);
return StructuredQuery.Filter.newBuilder().setCompositeFilter(composite).build();
}

@VisibleForTesting
Filter decodeFilter(StructuredQuery.Filter proto) {
switch (proto.getFilterTypeCase()) {
case COMPOSITE_FILTER:
return decodeCompositeFilter(proto.getCompositeFilter());
case FIELD_FILTER:
return decodeFieldFilter(proto.getFieldFilter());
case UNARY_FILTER:
return decodeUnaryFilter(proto.getUnaryFilter());
default:
throw fail("Unrecognized Filter.filterType %d", proto.getFilterTypeCase());
}
}

@VisibleForTesting
FieldFilter decodeFieldFilter(StructuredQuery.FieldFilter proto) {
FieldPath fieldPath = FieldPath.fromServerFormat(proto.getField().getFieldPath());
Expand All @@ -734,6 +751,16 @@ private Filter decodeUnaryFilter(StructuredQuery.UnaryFilter proto) {
}
}

@VisibleForTesting
com.google.firebase.firestore.core.CompositeFilter decodeCompositeFilter(
StructuredQuery.CompositeFilter compositeFilter) {
List<Filter> filters = new ArrayList<>();
for (StructuredQuery.Filter filter : compositeFilter.getFiltersList()) {
filters.add(decodeFilter(filter));
}
return new com.google.firebase.firestore.core.CompositeFilter(filters, compositeFilter.getOp());
}

private FieldReference encodeFieldPath(FieldPath field) {
return FieldReference.newBuilder().setFieldPath(field.canonicalString()).build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.firestore.remote;

import static com.google.firebase.firestore.model.Values.refValue;
import static com.google.firebase.firestore.testutil.TestUtil.andFilters;
import static com.google.firebase.firestore.testutil.TestUtil.bound;
import static com.google.firebase.firestore.testutil.TestUtil.deleteMutation;
import static com.google.firebase.firestore.testutil.TestUtil.deletedDoc;
Expand All @@ -24,6 +25,7 @@
import static com.google.firebase.firestore.testutil.TestUtil.key;
import static com.google.firebase.firestore.testutil.TestUtil.map;
import static com.google.firebase.firestore.testutil.TestUtil.mergeMutation;
import static com.google.firebase.firestore.testutil.TestUtil.orFilters;
import static com.google.firebase.firestore.testutil.TestUtil.orderBy;
import static com.google.firebase.firestore.testutil.TestUtil.patchMutation;
import static com.google.firebase.firestore.testutil.TestUtil.query;
Expand Down Expand Up @@ -680,6 +682,86 @@ public void testEncodesMultipleFiltersOnDeeperCollections() {
serializer.decodeQueryTarget(serializer.encodeQueryTarget(q.toTarget())), q.toTarget());
}

@Test
public void testEncodesCompositeFiltersOnDeeperCollections() {
// (prop < 42) || (author == "ehsann" && tags array-contains "pending")
Query q =
Query.atPath(ResourcePath.fromString("rooms/1/messages/10/attachments"))
.filter(
orFilters(
filter("prop", "<", 42),
andFilters(
filter("author", "==", "ehsann"),
filter("tags", "array-contains", "pending"))));
Target actual = serializer.encodeTarget(wrapTargetData(q));

StructuredQuery.Builder structuredQueryBuilder =
StructuredQuery.newBuilder()
.addFrom(CollectionSelector.newBuilder().setCollectionId("attachments"))
.setWhere(
Filter.newBuilder()
.setCompositeFilter(
StructuredQuery.CompositeFilter.newBuilder()
// TODO(orquery): Replace with Operator.OR once it's available.
.setOp(CompositeFilter.Operator.OPERATOR_UNSPECIFIED)
.addFilters(
Filter.newBuilder()
.setFieldFilter(
StructuredQuery.FieldFilter.newBuilder()
.setField(
FieldReference.newBuilder().setFieldPath("prop"))
.setOp(Operator.LESS_THAN)
.setValue(Value.newBuilder().setIntegerValue(42))))
.addFilters(
Filter.newBuilder()
.setCompositeFilter(
StructuredQuery.CompositeFilter.newBuilder()
.setOp(CompositeFilter.Operator.AND)
.addFilters(
Filter.newBuilder()
.setFieldFilter(
StructuredQuery.FieldFilter.newBuilder()
.setField(
FieldReference.newBuilder()
.setFieldPath("author"))
.setOp(Operator.EQUAL)
.setValue(
Value.newBuilder()
.setStringValue("ehsann"))))
.addFilters(
Filter.newBuilder()
.setFieldFilter(
StructuredQuery.FieldFilter.newBuilder()
.setField(
FieldReference.newBuilder()
.setFieldPath("tags"))
.setOp(Operator.ARRAY_CONTAINS)
.setValue(
Value.newBuilder()
.setStringValue(
"pending"))))))))
.addOrderBy(
Order.newBuilder()
.setField(FieldReference.newBuilder().setFieldPath("prop"))
.setDirection(Direction.ASCENDING))
.addOrderBy(defaultKeyOrder());
QueryTarget.Builder queryBuilder =
QueryTarget.newBuilder()
.setParent("projects/p/databases/d/documents/rooms/1/messages/10")
.setStructuredQuery(structuredQueryBuilder);
Target expected =
Target.newBuilder()
.setQuery(queryBuilder)
.setTargetId(1)
.setResumeToken(ByteString.EMPTY)
.build();

assertEquals(expected, actual);
com.google.firebase.firestore.core.Target roundTripped =
serializer.decodeQueryTarget(serializer.encodeQueryTarget(q.toTarget()));
assertEquals(roundTripped, q.toTarget());
}

@Test
public void testInSerialization() {
FieldFilter inputFilter = filter("field", "in", asList(42));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
import com.google.firebase.firestore.UserDataReader;
import com.google.firebase.firestore.UserDataWriter;
import com.google.firebase.firestore.core.Bound;
import com.google.firebase.firestore.core.CompositeFilter;
import com.google.firebase.firestore.core.FieldFilter;
import com.google.firebase.firestore.core.FieldFilter.Operator;
import com.google.firebase.firestore.core.Filter;
import com.google.firebase.firestore.core.OrderBy;
import com.google.firebase.firestore.core.OrderBy.Direction;
import com.google.firebase.firestore.core.Query;
Expand Down Expand Up @@ -77,6 +79,7 @@
import com.google.firebase.firestore.remote.WatchChange;
import com.google.firebase.firestore.remote.WatchChange.DocumentChange;
import com.google.firebase.firestore.remote.WatchChangeAggregator;
import com.google.firestore.v1.StructuredQuery;
import com.google.firestore.v1.Value;
import com.google.protobuf.ByteString;
import java.io.IOException;
Expand Down Expand Up @@ -255,6 +258,27 @@ public static FieldFilter filter(String key, String operator, Object value) {
return FieldFilter.create(field(key), operatorFromString(operator), wrap(value));
}

public static CompositeFilter andFilters(List<Filter> filters) {
return new CompositeFilter(filters, StructuredQuery.CompositeFilter.Operator.AND);
}

public static CompositeFilter andFilters(Filter... filters) {
return new CompositeFilter(
Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.AND);
}

public static CompositeFilter orFilters(Filter... filters) {
// TODO(orquery): Replace this with Operator.OR once it is available.
return new CompositeFilter(
Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.OPERATOR_UNSPECIFIED);
}

public static CompositeFilter orFilters(List<Filter> filters) {
// TODO(orquery): Replace this with Operator.OR once it is available.
return new CompositeFilter(
filters, StructuredQuery.CompositeFilter.Operator.OPERATOR_UNSPECIFIED);
}

public static Operator operatorFromString(String s) {
if (s.equals("<")) {
return Operator.LESS_THAN;
Expand Down