Skip to content

Commit

Permalink
feat: add support for != and NOT_IN queries (#350)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Chen committed Sep 8, 2020
1 parent 107aa05 commit 68aff5b
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 4 deletions.
Expand Up @@ -25,6 +25,8 @@
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.IN;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.LESS_THAN;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.LESS_THAN_OR_EQUAL;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.NOT_EQUAL;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.NOT_IN;

import com.google.api.core.ApiFuture;
import com.google.api.core.InternalExtensionOnly;
Expand Down Expand Up @@ -492,6 +494,48 @@ public Query whereEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value)
}
}

/**
* Creates and returns a new Query with the additional filter that documents must contain the
* specified field and its value does not equal the specified value.
*
* @param field The name of the field to compare.
* @param value The value for comparison.
* @return The created Query.
*/
@Nonnull
public Query whereNotEqualTo(@Nonnull String field, @Nullable Object value) {
return whereNotEqualTo(FieldPath.fromDotSeparatedString(field), value);
}

/**
* Creates and returns a new Query with the additional filter that documents must contain the
* specified field and the value does not equal the specified value.
*
* @param fieldPath The path of the field to compare.
* @param value The value for comparison.
* @return The created Query.
*/
@Nonnull
public Query whereNotEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) {
Preconditions.checkState(
options.getStartCursor() == null && options.getEndCursor() == null,
"Cannot call whereNotEqualTo() after defining a boundary with startAt(), "
+ "startAfter(), endBefore() or endAt().");

if (isUnaryComparison(value)) {
Builder newOptions = options.toBuilder();
StructuredQuery.UnaryFilter.Operator op =
value == null
? StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL
: StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN;
UnaryFilter newFieldFilter = new UnaryFilter(fieldPath.toProto(), op);
newOptions.setFieldFilters(append(options.getFieldFilters(), newFieldFilter));
return new Query(rpcContext, newOptions.build());
} else {
return whereHelper(fieldPath, NOT_EQUAL, value);
}
}

/**
* Creates and returns a new Query with the additional filter that documents must contain the
* specified field and the value should be less than the specified value.
Expand Down Expand Up @@ -617,7 +661,8 @@ public Query whereGreaterThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nonnull Ob
* specified field, the value must be an array, and that the array must contain the provided
* value.
*
* <p>A Query can have only one whereArrayContains() filter.
* <p>A Query can have only one whereArrayContains() filter and it cannot be combined with
* whereArrayContainsAny().
*
* @param field The name of the field containing an array to search
* @param value The value that must be contained in the array
Expand All @@ -633,7 +678,8 @@ public Query whereArrayContains(@Nonnull String field, @Nonnull Object value) {
* specified field, the value must be an array, and that the array must contain the provided
* value.
*
* <p>A Query can have only one whereArrayContains() filter.
* <p>A Query can have only one whereArrayContains() filter and it cannot be combined with
* whereArrayContainsAny().
*
* @param fieldPath The path of the field containing an array to search
* @param value The value that must be contained in the array
Expand Down Expand Up @@ -732,6 +778,46 @@ public Query whereIn(@Nonnull FieldPath fieldPath, @Nonnull List<? extends Objec
return whereHelper(fieldPath, IN, values);
}

/**
* Creates and returns a new Query with the additional filter that documents must contain the
* specified field and the value does not equal any of the values from the provided list.
*
* <p>A Query can have only one whereNotIn() filter and it cannot be combined with
* whereArrayContains(), whereArrayContainsAny(), whereIn(), or whereNotEqualTo().
*
* @param field The name of the field to search.
* @param values The list that contains the values to match.
* @return The created Query.
*/
@Nonnull
public Query whereNotIn(@Nonnull String field, @Nonnull List<? extends Object> values) {
Preconditions.checkState(
options.getStartCursor() == null && options.getEndCursor() == null,
"Cannot call whereNotIn() after defining a boundary with startAt(), "
+ "startAfter(), endBefore() or endAt().");
return whereHelper(FieldPath.fromDotSeparatedString(field), NOT_IN, values);
}

/**
* Creates and returns a new Query with the additional filter that documents must contain the
* specified field and the value does not equal any of the values from the provided list.
*
* <p>A Query can have only one whereNotIn() filter, and it cannot be combined with
* whereArrayContains(), whereArrayContainsAny(), whereIn(), or whereNotEqualTo().
*
* @param fieldPath The path of the field to search.
* @param values The list that contains the values to match.
* @return The created Query.
*/
@Nonnull
public Query whereNotIn(@Nonnull FieldPath fieldPath, @Nonnull List<? extends Object> values) {
Preconditions.checkState(
options.getStartCursor() == null && options.getEndCursor() == null,
"Cannot call whereNotIn() after defining a boundary with startAt(), "
+ "startAfter(), endBefore() or endAt().");
return whereHelper(fieldPath, NOT_IN, values);
}

private Query whereHelper(
FieldPath fieldPath, StructuredQuery.FieldFilter.Operator operator, Object value) {
Preconditions.checkArgument(
Expand All @@ -745,7 +831,7 @@ private Query whereHelper(
String.format(
"Invalid query. You cannot perform '%s' queries on FieldPath.documentId().",
operator.toString()));
} else if (operator == IN) {
} else if (operator == IN | operator == NOT_IN) {
if (!(value instanceof List) || ((List<?>) value).isEmpty()) {
throw new IllegalArgumentException(
String.format(
Expand Down
Expand Up @@ -219,27 +219,37 @@ public void withFilter() throws Exception {
query.whereEqualTo("foo", null).get().get();
query.whereEqualTo("foo", Double.NaN).get().get();
query.whereEqualTo("foo", Float.NaN).get().get();
query.whereNotEqualTo("foo", "bar").get().get();
query.whereNotEqualTo("foo", null).get().get();
query.whereNotEqualTo("foo", Double.NaN).get().get();
query.whereNotEqualTo("foo", Float.NaN).get().get();
query.whereGreaterThan("foo", "bar").get().get();
query.whereGreaterThanOrEqualTo("foo", "bar").get().get();
query.whereLessThan("foo", "bar").get().get();
query.whereLessThanOrEqualTo("foo", "bar").get().get();
query.whereArrayContains("foo", "bar").get().get();
query.whereIn("foo", Collections.<Object>singletonList("bar"));
query.whereArrayContainsAny("foo", Collections.<Object>singletonList("bar"));
query.whereNotIn("foo", Collections.<Object>singletonList("bar"));

Iterator<RunQueryRequest> expected =
Arrays.asList(
query(filter(StructuredQuery.FieldFilter.Operator.EQUAL)),
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NULL)),
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NAN)),
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NAN)),
query(filter(StructuredQuery.FieldFilter.Operator.NOT_EQUAL)),
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL)),
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN)),
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN)),
query(filter(StructuredQuery.FieldFilter.Operator.GREATER_THAN)),
query(filter(StructuredQuery.FieldFilter.Operator.GREATER_THAN_OR_EQUAL)),
query(filter(StructuredQuery.FieldFilter.Operator.LESS_THAN)),
query(filter(StructuredQuery.FieldFilter.Operator.LESS_THAN_OR_EQUAL)),
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS)),
query(filter(StructuredQuery.FieldFilter.Operator.IN)),
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS_ANY)))
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS_ANY)),
query(filter(StructuredQuery.FieldFilter.Operator.NOT_IN)))
.iterator();

for (RunQueryRequest actual : runQuery.getAllValues()) {
Expand All @@ -257,23 +267,27 @@ public void withFieldPathFilter() throws Exception {
Matchers.<ServerStreamingCallable>any());

query.whereEqualTo(FieldPath.of("foo"), "bar").get().get();
query.whereNotEqualTo(FieldPath.of("foo"), "bar").get().get();
query.whereGreaterThan(FieldPath.of("foo"), "bar").get().get();
query.whereGreaterThanOrEqualTo(FieldPath.of("foo"), "bar").get().get();
query.whereLessThan(FieldPath.of("foo"), "bar").get().get();
query.whereLessThanOrEqualTo(FieldPath.of("foo"), "bar").get().get();
query.whereArrayContains(FieldPath.of("foo"), "bar").get().get();
query.whereIn(FieldPath.of("foo"), Collections.<Object>singletonList("bar"));
query.whereNotIn(FieldPath.of("foo"), Collections.<Object>singletonList("bar"));
query.whereArrayContainsAny(FieldPath.of("foo"), Collections.<Object>singletonList("bar"));

Iterator<RunQueryRequest> expected =
Arrays.asList(
query(filter(StructuredQuery.FieldFilter.Operator.EQUAL)),
query(filter(StructuredQuery.FieldFilter.Operator.NOT_EQUAL)),
query(filter(StructuredQuery.FieldFilter.Operator.GREATER_THAN)),
query(filter(StructuredQuery.FieldFilter.Operator.GREATER_THAN_OR_EQUAL)),
query(filter(StructuredQuery.FieldFilter.Operator.LESS_THAN)),
query(filter(StructuredQuery.FieldFilter.Operator.LESS_THAN_OR_EQUAL)),
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS)),
query(filter(StructuredQuery.FieldFilter.Operator.IN)),
query(filter(StructuredQuery.FieldFilter.Operator.NOT_IN)),
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS_ANY)))
.iterator();

Expand Down Expand Up @@ -365,6 +379,56 @@ public void validatesInQueries() {
}
}

@Test
public void notInQueriesWithReferenceArray() throws Exception {
doAnswer(queryResponse())
.when(firestoreMock)
.streamRequest(
runQuery.capture(),
streamObserverCapture.capture(),
Matchers.<ServerStreamingCallable>any());

query
.whereNotIn(
FieldPath.documentId(),
Arrays.<Object>asList("doc", firestoreMock.document("coll/doc")))
.get()
.get();

Value value =
Value.newBuilder()
.setArrayValue(
ArrayValue.newBuilder()
.addValues(reference(DOCUMENT_NAME))
.addValues(reference(DOCUMENT_NAME))
.build())
.build();
RunQueryRequest expectedRequest = query(filter(Operator.NOT_IN, "__name__", value));

assertEquals(expectedRequest, runQuery.getValue());
}

@Test
public void validatesNotInQueries() {
try {
query.whereNotIn(FieldPath.documentId(), Arrays.<Object>asList("foo", 42)).get();
fail();
} catch (IllegalArgumentException e) {
assertEquals(
"The corresponding value for FieldPath.documentId() must be a String or a "
+ "DocumentReference, but was: 42.",
e.getMessage());
}

try {
query.whereNotIn(FieldPath.documentId(), Arrays.<Object>asList()).get();
fail();
} catch (IllegalArgumentException e) {
assertEquals(
"Invalid Query. A non-empty array is required for 'NOT_IN' filters.", e.getMessage());
}
}

@Test
public void validatesQueryOperatorForFieldPathDocumentId() {
try {
Expand Down
Expand Up @@ -1218,6 +1218,39 @@ public void inQueries() throws Exception {
assertEquals(asList("a", "c"), querySnapshotToIds(querySnapshot));
}

@Test
public void notEqualQueries() throws Exception {
setDocument("a", map("zip", Double.NaN));
setDocument("b", map("zip", 91102));
setDocument("c", map("zip", 98101));
setDocument("d", map("zip", 98103));
setDocument("e", map("zip", asList(98101)));
setDocument("f", map("zip", asList("98101", map("zip", 98101))));
setDocument("g", map("zip", map("zip", 98101)));
setDocument("h", map("zip", null));

QuerySnapshot querySnapshot = randomColl.whereNotEqualTo("zip", 98101).get().get();
assertEquals(asList("a", "b", "d", "e", "f", "g"), querySnapshotToIds(querySnapshot));

querySnapshot = randomColl.whereNotEqualTo("zip", Double.NaN).get().get();
assertEquals(asList("b", "c", "d", "e", "f", "g"), querySnapshotToIds(querySnapshot));

querySnapshot = randomColl.whereNotEqualTo("zip", null).get().get();
assertEquals(asList("a", "b", "c", "d", "e", "f", "g"), querySnapshotToIds(querySnapshot));
}

@Test
public void notEqualQueriesWithDocumentId() throws Exception {
DocumentReference doc1 = setDocument("a", map("count", 1));
DocumentReference doc2 = setDocument("b", map("count", 2));
setDocument("c", map("count", 3));

QuerySnapshot querySnapshot =
randomColl.whereNotEqualTo(FieldPath.documentId(), doc1.getId()).get().get();

assertEquals(asList("b", "c"), querySnapshotToIds(querySnapshot));
}

@Test
public void inQueriesWithDocumentId() throws Exception {
DocumentReference doc1 = setDocument("a", map("count", 1));
Expand All @@ -1233,6 +1266,43 @@ public void inQueriesWithDocumentId() throws Exception {
assertEquals(asList("a", "b"), querySnapshotToIds(querySnapshot));
}

@Test
public void notInQueries() throws Exception {
setDocument("a", map("zip", 98101));
setDocument("b", map("zip", 91102));
setDocument("c", map("zip", 98103));
setDocument("d", map("zip", asList(98101)));
setDocument("e", map("zip", asList("98101", map("zip", 98101))));
setDocument("f", map("zip", map("code", 500)));

QuerySnapshot querySnapshot =
randomColl.whereNotIn("zip", Arrays.<Object>asList(98101, 98103)).get().get();
assertEquals(asList("b", "d", "e", "f"), querySnapshotToIds(querySnapshot));

querySnapshot = randomColl.whereNotIn("zip", Arrays.<Object>asList(Double.NaN)).get().get();
assertEquals(asList("b", "a", "c", "d", "e", "f"), querySnapshotToIds(querySnapshot));

List<Object> nullArray = new ArrayList<>();
nullArray.add(null);
querySnapshot = randomColl.whereNotIn("zip", nullArray).get().get();
assertEquals(new ArrayList<>(), querySnapshotToIds(querySnapshot));
}

@Test
public void notInQueriesWithDocumentId() throws Exception {
DocumentReference doc1 = setDocument("a", map("count", 1));
DocumentReference doc2 = setDocument("b", map("count", 2));
setDocument("c", map("count", 3));

QuerySnapshot querySnapshot =
randomColl
.whereNotIn(FieldPath.documentId(), Arrays.<Object>asList(doc1.getId(), doc2))
.get()
.get();

assertEquals(asList("c"), querySnapshotToIds(querySnapshot));
}

@Test
public void arrayContainsAnyQueries() throws Exception {
setDocument("a", map("array", asList(42)));
Expand Down

0 comments on commit 68aff5b

Please sign in to comment.