Skip to content

Remove some query restrictions and add integration tests #4231

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 12 commits into from
Feb 15, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.google.firebase.firestore.Filter.equalTo;
import static com.google.firebase.firestore.Filter.greaterThan;
import static com.google.firebase.firestore.Filter.inArray;
import static com.google.firebase.firestore.Filter.notEqualTo;
import static com.google.firebase.firestore.Filter.notInArray;
import static com.google.firebase.firestore.Filter.or;
import static com.google.firebase.firestore.testutil.Assert.assertThrows;
Expand Down Expand Up @@ -693,20 +694,31 @@ public void queriesInAndArrayContainsAnyArrayRules() {
"Invalid Query. A non-empty array is required for 'array_contains_any' filters.");

expectError(
// The 10 element max includes duplicates.
() -> testCollection().whereIn("bar", asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9)),
"Invalid Query. 'in' filters support a maximum of 10 elements in the value array.");
// The 30 element max includes duplicates.
() ->
testCollection()
.whereIn(
"bar",
asList(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31)),
"Invalid Query. 'in' filters support a maximum of 30 elements in the value array.");

expectError(
// The 10 element max includes duplicates.
() -> testCollection().whereNotIn("bar", asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9)),
// The 30 element max includes duplicates.
() -> testCollection().whereNotIn("bar", asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)),
"Invalid Query. 'not_in' filters support a maximum of 10 elements in the value array.");

expectError(
// The 10 element max includes duplicates.
() ->
testCollection().whereArrayContainsAny("bar", asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9)),
"Invalid Query. 'array_contains_any' filters support a maximum of 10 elements in the value array.");
testCollection()
.whereArrayContainsAny(
"bar",
asList(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31)),
"Invalid Query. 'array_contains_any' filters support a maximum of 30 elements in the value array.");
}

@Test
Expand Down Expand Up @@ -864,6 +876,54 @@ public void testInvalidQueryFilters() {
reason);
}

@Test
public void testDisjunctionWithNotInFilter() {
CollectionReference collection = testCollection();
String reason = "The 'notIn' filter should not be used in a query that contains a disjunction.";
expectError(
() -> collection.where(or(equalTo("a", "b"), notInArray("c", asList(1, 2)))), reason);
expectError(
() ->
collection.where(
and(equalTo("x", "y"), or(equalTo("a", "b"), notInArray("c", asList(1, 2))))),
reason);
expectError(
() ->
collection.where(
or(equalTo("x", "y"), and(equalTo("a", "b"), notInArray("c", asList(1, 2))))),
reason);
}

@Test
public void testOrderByEqualityWithDisjunctions() {
CollectionReference collection = testCollection();
String reason =
"Performing an equality ('in' and '==') and an inequality (notEqualTo, notIn, "
+ "lessThan, lessThanOrEqualTo, greaterThan, or greaterThanOrEqualTo) on the same field "
+ "is not yet allowed in disjunctions.";
// Using ==
expectError(() -> collection.where(or(equalTo("a", "b"), notEqualTo("a", "c"))), reason);
expectError(
() -> collection.where(and(greaterThan("a", 5), or(equalTo("a", 6), equalTo("b", "c")))),
reason);
expectError(
() -> collection.where(or(greaterThan("a", 5), and(equalTo("a", 6), equalTo("b", "c")))),
reason);

// Using 'in'
expectError(() -> collection.where(or(inArray("a", asList(1, 2)), notEqualTo("a", 5))), reason);
expectError(
() ->
collection.where(
and(greaterThan("a", 5), or(inArray("a", asList(1, 2)), equalTo("b", "c")))),
reason);
expectError(
() ->
collection.where(
or(greaterThan("a", 5), and(inArray("a", asList(1, 2)), equalTo("b", "c")))),
reason);
}

// Helpers

/** Performs a write using each write API and makes sure it succeeds. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static com.google.firebase.firestore.util.Assert.fail;
import static com.google.firebase.firestore.util.Assert.hardAssert;
import static com.google.firebase.firestore.util.Preconditions.checkNotNull;
import static java.util.Collections.singletonList;

import android.app.Activity;
import androidx.annotation.NonNull;
Expand Down Expand Up @@ -540,12 +541,19 @@ private void validateDisjunctiveFilterElements(Object value, Operator op) {
throw new IllegalArgumentException(
"Invalid Query. A non-empty array is required for '" + op.toString() + "' filters.");
}
if (((List) value).size() > 10) {
if (op.equals(Operator.NOT_IN) && ((List) value).size() > 10) {
throw new IllegalArgumentException(
"Invalid Query. '"
+ op.toString()
+ "' filters support a maximum of 10 elements in the value array.");
}
if ((op.equals(Operator.IN) || op.equals(Operator.ARRAY_CONTAINS_ANY))
&& ((List) value).size() > 30) {
throw new IllegalArgumentException(
"Invalid Query. '"
+ op.toString()
+ "' filters support a maximum of 30 elements in the value array.");
}
}

private void validateOrderByFieldMatchesInequality(
Expand Down Expand Up @@ -646,6 +654,35 @@ private void validateNewFilter(com.google.firebase.firestore.core.Filter filter)
validateNewFieldFilter(testQuery, subfilter);
testQuery = testQuery.filter(subfilter);
}

CompositeFilter existingQueryFilter =
new CompositeFilter(query.getFilters(), CompositeFilter.Operator.AND);
CompositeFilter newQueryFilter =
new CompositeFilter(
Arrays.asList(existingQueryFilter, filter), CompositeFilter.Operator.AND);
if (newQueryFilter.containsDisjunction()) {
// The NOT_IN FieldFilter operator may not be used in a query that contains a composite filter
// with a disjunction operator.
if (findOpInsideFilters(singletonList(newQueryFilter), singletonList(Operator.NOT_IN))
!= null) {
throw new IllegalArgumentException(
"The 'notIn' filter should not be used in a query that " + "contains a disjunction.");
}

// The Early Preview Release of OR Queries does not support order-by-equality.
com.google.firebase.firestore.model.FieldPath inequalityField =
newQueryFilter.getFirstInequalityField();
for (FieldFilter fieldFilter : newQueryFilter.getFlattenedFilters()) {
if ((fieldFilter.getOperator().equals(Operator.IN)
|| fieldFilter.getOperator().equals(Operator.EQUAL))
&& fieldFilter.getField().equals(inequalityField)) {
throw new IllegalArgumentException(
"Performing an equality ('in' and '==') and an inequality (notEqualTo, notIn, "
+ "lessThan, lessThanOrEqualTo, greaterThan, or greaterThanOrEqualTo) on the same "
+ "field is not yet allowed in disjunctions.");
}
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public String toString() {
// contained in this composite filter.
private List<FieldFilter> memoizedFlattenedFilters;

// Memoized boolean indicating whether any disjunction operations exist in the tree of this
// filter.
private Boolean memoizedContainsDisjunction;

public CompositeFilter(List<Filter> filters, Operator operator) {
this.filters = new ArrayList<>(filters);
this.operator = operator;
Expand All @@ -74,6 +78,26 @@ public List<FieldFilter> getFlattenedFilters() {
return Collections.unmodifiableList(memoizedFlattenedFilters);
}

@Override
public boolean containsDisjunction() {
if (memoizedContainsDisjunction != null) {
return memoizedContainsDisjunction;
}

if (isDisjunction()) {
memoizedContainsDisjunction = true;
return true;
}
for (Filter subfilter : filters) {
if (subfilter.containsDisjunction()) {
memoizedContainsDisjunction = true;
return true;
}
}
memoizedContainsDisjunction = false;
return false;
}

/**
* Returns the first inequality filter contained within this composite filter. Returns {@code
* null} if it does not contain any inequalities.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ public List<Filter> getFilters() {
return Collections.singletonList(this);
}

@Override
public boolean containsDisjunction() {
return false;
}

@Override
public FieldPath getFirstInequalityField() {
if (isInequality()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public abstract class Filter {
/** Returns a list of all filters that are contained within this filter */
public abstract List<Filter> getFilters();

/** Returns true if there exists any disjunction operation in the tree of this filter */
public abstract boolean containsDisjunction();

/** Returns the field of the first filter that's an inequality, or null if none. */
@Nullable
public abstract FieldPath getFirstInequalityField();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,16 +507,6 @@ public Target toTarget() {
return this.memoizedTarget;
}

/** Returns true if the query contains any composite filters (AND/OR). Returns false otherwise. */
public boolean containsCompositeFilters() {
for (Filter filter : filters) {
if (filter instanceof CompositeFilter) {
return true;
}
}
return false;
}

/**
* Returns a canonical string representing this query. This should match the iOS and Android
* canonical ids for a query exactly.
Expand Down