Skip to content

Commit 68aff5b

Browse files
author
Brian Chen
authored
feat: add support for != and NOT_IN queries (#350)
1 parent 107aa05 commit 68aff5b

File tree

3 files changed

+224
-4
lines changed

3 files changed

+224
-4
lines changed

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

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.IN;
2626
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.LESS_THAN;
2727
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.LESS_THAN_OR_EQUAL;
28+
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.NOT_EQUAL;
29+
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.NOT_IN;
2830

2931
import com.google.api.core.ApiFuture;
3032
import com.google.api.core.InternalExtensionOnly;
@@ -492,6 +494,48 @@ public Query whereEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value)
492494
}
493495
}
494496

497+
/**
498+
* Creates and returns a new Query with the additional filter that documents must contain the
499+
* specified field and its value does not equal the specified value.
500+
*
501+
* @param field The name of the field to compare.
502+
* @param value The value for comparison.
503+
* @return The created Query.
504+
*/
505+
@Nonnull
506+
public Query whereNotEqualTo(@Nonnull String field, @Nullable Object value) {
507+
return whereNotEqualTo(FieldPath.fromDotSeparatedString(field), value);
508+
}
509+
510+
/**
511+
* Creates and returns a new Query with the additional filter that documents must contain the
512+
* specified field and the value does not equal the specified value.
513+
*
514+
* @param fieldPath The path of the field to compare.
515+
* @param value The value for comparison.
516+
* @return The created Query.
517+
*/
518+
@Nonnull
519+
public Query whereNotEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) {
520+
Preconditions.checkState(
521+
options.getStartCursor() == null && options.getEndCursor() == null,
522+
"Cannot call whereNotEqualTo() after defining a boundary with startAt(), "
523+
+ "startAfter(), endBefore() or endAt().");
524+
525+
if (isUnaryComparison(value)) {
526+
Builder newOptions = options.toBuilder();
527+
StructuredQuery.UnaryFilter.Operator op =
528+
value == null
529+
? StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL
530+
: StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN;
531+
UnaryFilter newFieldFilter = new UnaryFilter(fieldPath.toProto(), op);
532+
newOptions.setFieldFilters(append(options.getFieldFilters(), newFieldFilter));
533+
return new Query(rpcContext, newOptions.build());
534+
} else {
535+
return whereHelper(fieldPath, NOT_EQUAL, value);
536+
}
537+
}
538+
495539
/**
496540
* Creates and returns a new Query with the additional filter that documents must contain the
497541
* specified field and the value should be less than the specified value.
@@ -617,7 +661,8 @@ public Query whereGreaterThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nonnull Ob
617661
* specified field, the value must be an array, and that the array must contain the provided
618662
* value.
619663
*
620-
* <p>A Query can have only one whereArrayContains() filter.
664+
* <p>A Query can have only one whereArrayContains() filter and it cannot be combined with
665+
* whereArrayContainsAny().
621666
*
622667
* @param field The name of the field containing an array to search
623668
* @param value The value that must be contained in the array
@@ -633,7 +678,8 @@ public Query whereArrayContains(@Nonnull String field, @Nonnull Object value) {
633678
* specified field, the value must be an array, and that the array must contain the provided
634679
* value.
635680
*
636-
* <p>A Query can have only one whereArrayContains() filter.
681+
* <p>A Query can have only one whereArrayContains() filter and it cannot be combined with
682+
* whereArrayContainsAny().
637683
*
638684
* @param fieldPath The path of the field containing an array to search
639685
* @param value The value that must be contained in the array
@@ -732,6 +778,46 @@ public Query whereIn(@Nonnull FieldPath fieldPath, @Nonnull List<? extends Objec
732778
return whereHelper(fieldPath, IN, values);
733779
}
734780

781+
/**
782+
* Creates and returns a new Query with the additional filter that documents must contain the
783+
* specified field and the value does not equal any of the values from the provided list.
784+
*
785+
* <p>A Query can have only one whereNotIn() filter and it cannot be combined with
786+
* whereArrayContains(), whereArrayContainsAny(), whereIn(), or whereNotEqualTo().
787+
*
788+
* @param field The name of the field to search.
789+
* @param values The list that contains the values to match.
790+
* @return The created Query.
791+
*/
792+
@Nonnull
793+
public Query whereNotIn(@Nonnull String field, @Nonnull List<? extends Object> values) {
794+
Preconditions.checkState(
795+
options.getStartCursor() == null && options.getEndCursor() == null,
796+
"Cannot call whereNotIn() after defining a boundary with startAt(), "
797+
+ "startAfter(), endBefore() or endAt().");
798+
return whereHelper(FieldPath.fromDotSeparatedString(field), NOT_IN, values);
799+
}
800+
801+
/**
802+
* Creates and returns a new Query with the additional filter that documents must contain the
803+
* specified field and the value does not equal any of the values from the provided list.
804+
*
805+
* <p>A Query can have only one whereNotIn() filter, and it cannot be combined with
806+
* whereArrayContains(), whereArrayContainsAny(), whereIn(), or whereNotEqualTo().
807+
*
808+
* @param fieldPath The path of the field to search.
809+
* @param values The list that contains the values to match.
810+
* @return The created Query.
811+
*/
812+
@Nonnull
813+
public Query whereNotIn(@Nonnull FieldPath fieldPath, @Nonnull List<? extends Object> values) {
814+
Preconditions.checkState(
815+
options.getStartCursor() == null && options.getEndCursor() == null,
816+
"Cannot call whereNotIn() after defining a boundary with startAt(), "
817+
+ "startAfter(), endBefore() or endAt().");
818+
return whereHelper(fieldPath, NOT_IN, values);
819+
}
820+
735821
private Query whereHelper(
736822
FieldPath fieldPath, StructuredQuery.FieldFilter.Operator operator, Object value) {
737823
Preconditions.checkArgument(
@@ -745,7 +831,7 @@ private Query whereHelper(
745831
String.format(
746832
"Invalid query. You cannot perform '%s' queries on FieldPath.documentId().",
747833
operator.toString()));
748-
} else if (operator == IN) {
834+
} else if (operator == IN | operator == NOT_IN) {
749835
if (!(value instanceof List) || ((List<?>) value).isEmpty()) {
750836
throw new IllegalArgumentException(
751837
String.format(

google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,27 +219,37 @@ public void withFilter() throws Exception {
219219
query.whereEqualTo("foo", null).get().get();
220220
query.whereEqualTo("foo", Double.NaN).get().get();
221221
query.whereEqualTo("foo", Float.NaN).get().get();
222+
query.whereNotEqualTo("foo", "bar").get().get();
223+
query.whereNotEqualTo("foo", null).get().get();
224+
query.whereNotEqualTo("foo", Double.NaN).get().get();
225+
query.whereNotEqualTo("foo", Float.NaN).get().get();
222226
query.whereGreaterThan("foo", "bar").get().get();
223227
query.whereGreaterThanOrEqualTo("foo", "bar").get().get();
224228
query.whereLessThan("foo", "bar").get().get();
225229
query.whereLessThanOrEqualTo("foo", "bar").get().get();
226230
query.whereArrayContains("foo", "bar").get().get();
227231
query.whereIn("foo", Collections.<Object>singletonList("bar"));
228232
query.whereArrayContainsAny("foo", Collections.<Object>singletonList("bar"));
233+
query.whereNotIn("foo", Collections.<Object>singletonList("bar"));
229234

230235
Iterator<RunQueryRequest> expected =
231236
Arrays.asList(
232237
query(filter(StructuredQuery.FieldFilter.Operator.EQUAL)),
233238
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NULL)),
234239
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NAN)),
235240
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NAN)),
241+
query(filter(StructuredQuery.FieldFilter.Operator.NOT_EQUAL)),
242+
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL)),
243+
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN)),
244+
query(unaryFilter(StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN)),
236245
query(filter(StructuredQuery.FieldFilter.Operator.GREATER_THAN)),
237246
query(filter(StructuredQuery.FieldFilter.Operator.GREATER_THAN_OR_EQUAL)),
238247
query(filter(StructuredQuery.FieldFilter.Operator.LESS_THAN)),
239248
query(filter(StructuredQuery.FieldFilter.Operator.LESS_THAN_OR_EQUAL)),
240249
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS)),
241250
query(filter(StructuredQuery.FieldFilter.Operator.IN)),
242-
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS_ANY)))
251+
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS_ANY)),
252+
query(filter(StructuredQuery.FieldFilter.Operator.NOT_IN)))
243253
.iterator();
244254

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

259269
query.whereEqualTo(FieldPath.of("foo"), "bar").get().get();
270+
query.whereNotEqualTo(FieldPath.of("foo"), "bar").get().get();
260271
query.whereGreaterThan(FieldPath.of("foo"), "bar").get().get();
261272
query.whereGreaterThanOrEqualTo(FieldPath.of("foo"), "bar").get().get();
262273
query.whereLessThan(FieldPath.of("foo"), "bar").get().get();
263274
query.whereLessThanOrEqualTo(FieldPath.of("foo"), "bar").get().get();
264275
query.whereArrayContains(FieldPath.of("foo"), "bar").get().get();
265276
query.whereIn(FieldPath.of("foo"), Collections.<Object>singletonList("bar"));
277+
query.whereNotIn(FieldPath.of("foo"), Collections.<Object>singletonList("bar"));
266278
query.whereArrayContainsAny(FieldPath.of("foo"), Collections.<Object>singletonList("bar"));
267279

268280
Iterator<RunQueryRequest> expected =
269281
Arrays.asList(
270282
query(filter(StructuredQuery.FieldFilter.Operator.EQUAL)),
283+
query(filter(StructuredQuery.FieldFilter.Operator.NOT_EQUAL)),
271284
query(filter(StructuredQuery.FieldFilter.Operator.GREATER_THAN)),
272285
query(filter(StructuredQuery.FieldFilter.Operator.GREATER_THAN_OR_EQUAL)),
273286
query(filter(StructuredQuery.FieldFilter.Operator.LESS_THAN)),
274287
query(filter(StructuredQuery.FieldFilter.Operator.LESS_THAN_OR_EQUAL)),
275288
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS)),
276289
query(filter(StructuredQuery.FieldFilter.Operator.IN)),
290+
query(filter(StructuredQuery.FieldFilter.Operator.NOT_IN)),
277291
query(filter(StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS_ANY)))
278292
.iterator();
279293

@@ -365,6 +379,56 @@ public void validatesInQueries() {
365379
}
366380
}
367381

382+
@Test
383+
public void notInQueriesWithReferenceArray() throws Exception {
384+
doAnswer(queryResponse())
385+
.when(firestoreMock)
386+
.streamRequest(
387+
runQuery.capture(),
388+
streamObserverCapture.capture(),
389+
Matchers.<ServerStreamingCallable>any());
390+
391+
query
392+
.whereNotIn(
393+
FieldPath.documentId(),
394+
Arrays.<Object>asList("doc", firestoreMock.document("coll/doc")))
395+
.get()
396+
.get();
397+
398+
Value value =
399+
Value.newBuilder()
400+
.setArrayValue(
401+
ArrayValue.newBuilder()
402+
.addValues(reference(DOCUMENT_NAME))
403+
.addValues(reference(DOCUMENT_NAME))
404+
.build())
405+
.build();
406+
RunQueryRequest expectedRequest = query(filter(Operator.NOT_IN, "__name__", value));
407+
408+
assertEquals(expectedRequest, runQuery.getValue());
409+
}
410+
411+
@Test
412+
public void validatesNotInQueries() {
413+
try {
414+
query.whereNotIn(FieldPath.documentId(), Arrays.<Object>asList("foo", 42)).get();
415+
fail();
416+
} catch (IllegalArgumentException e) {
417+
assertEquals(
418+
"The corresponding value for FieldPath.documentId() must be a String or a "
419+
+ "DocumentReference, but was: 42.",
420+
e.getMessage());
421+
}
422+
423+
try {
424+
query.whereNotIn(FieldPath.documentId(), Arrays.<Object>asList()).get();
425+
fail();
426+
} catch (IllegalArgumentException e) {
427+
assertEquals(
428+
"Invalid Query. A non-empty array is required for 'NOT_IN' filters.", e.getMessage());
429+
}
430+
}
431+
368432
@Test
369433
public void validatesQueryOperatorForFieldPathDocumentId() {
370434
try {

google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,39 @@ public void inQueries() throws Exception {
12181218
assertEquals(asList("a", "c"), querySnapshotToIds(querySnapshot));
12191219
}
12201220

1221+
@Test
1222+
public void notEqualQueries() throws Exception {
1223+
setDocument("a", map("zip", Double.NaN));
1224+
setDocument("b", map("zip", 91102));
1225+
setDocument("c", map("zip", 98101));
1226+
setDocument("d", map("zip", 98103));
1227+
setDocument("e", map("zip", asList(98101)));
1228+
setDocument("f", map("zip", asList("98101", map("zip", 98101))));
1229+
setDocument("g", map("zip", map("zip", 98101)));
1230+
setDocument("h", map("zip", null));
1231+
1232+
QuerySnapshot querySnapshot = randomColl.whereNotEqualTo("zip", 98101).get().get();
1233+
assertEquals(asList("a", "b", "d", "e", "f", "g"), querySnapshotToIds(querySnapshot));
1234+
1235+
querySnapshot = randomColl.whereNotEqualTo("zip", Double.NaN).get().get();
1236+
assertEquals(asList("b", "c", "d", "e", "f", "g"), querySnapshotToIds(querySnapshot));
1237+
1238+
querySnapshot = randomColl.whereNotEqualTo("zip", null).get().get();
1239+
assertEquals(asList("a", "b", "c", "d", "e", "f", "g"), querySnapshotToIds(querySnapshot));
1240+
}
1241+
1242+
@Test
1243+
public void notEqualQueriesWithDocumentId() throws Exception {
1244+
DocumentReference doc1 = setDocument("a", map("count", 1));
1245+
DocumentReference doc2 = setDocument("b", map("count", 2));
1246+
setDocument("c", map("count", 3));
1247+
1248+
QuerySnapshot querySnapshot =
1249+
randomColl.whereNotEqualTo(FieldPath.documentId(), doc1.getId()).get().get();
1250+
1251+
assertEquals(asList("b", "c"), querySnapshotToIds(querySnapshot));
1252+
}
1253+
12211254
@Test
12221255
public void inQueriesWithDocumentId() throws Exception {
12231256
DocumentReference doc1 = setDocument("a", map("count", 1));
@@ -1233,6 +1266,43 @@ public void inQueriesWithDocumentId() throws Exception {
12331266
assertEquals(asList("a", "b"), querySnapshotToIds(querySnapshot));
12341267
}
12351268

1269+
@Test
1270+
public void notInQueries() throws Exception {
1271+
setDocument("a", map("zip", 98101));
1272+
setDocument("b", map("zip", 91102));
1273+
setDocument("c", map("zip", 98103));
1274+
setDocument("d", map("zip", asList(98101)));
1275+
setDocument("e", map("zip", asList("98101", map("zip", 98101))));
1276+
setDocument("f", map("zip", map("code", 500)));
1277+
1278+
QuerySnapshot querySnapshot =
1279+
randomColl.whereNotIn("zip", Arrays.<Object>asList(98101, 98103)).get().get();
1280+
assertEquals(asList("b", "d", "e", "f"), querySnapshotToIds(querySnapshot));
1281+
1282+
querySnapshot = randomColl.whereNotIn("zip", Arrays.<Object>asList(Double.NaN)).get().get();
1283+
assertEquals(asList("b", "a", "c", "d", "e", "f"), querySnapshotToIds(querySnapshot));
1284+
1285+
List<Object> nullArray = new ArrayList<>();
1286+
nullArray.add(null);
1287+
querySnapshot = randomColl.whereNotIn("zip", nullArray).get().get();
1288+
assertEquals(new ArrayList<>(), querySnapshotToIds(querySnapshot));
1289+
}
1290+
1291+
@Test
1292+
public void notInQueriesWithDocumentId() throws Exception {
1293+
DocumentReference doc1 = setDocument("a", map("count", 1));
1294+
DocumentReference doc2 = setDocument("b", map("count", 2));
1295+
setDocument("c", map("count", 3));
1296+
1297+
QuerySnapshot querySnapshot =
1298+
randomColl
1299+
.whereNotIn(FieldPath.documentId(), Arrays.<Object>asList(doc1.getId(), doc2))
1300+
.get()
1301+
.get();
1302+
1303+
assertEquals(asList("c"), querySnapshotToIds(querySnapshot));
1304+
}
1305+
12361306
@Test
12371307
public void arrayContainsAnyQueries() throws Exception {
12381308
setDocument("a", map("array", asList(42)));

0 commit comments

Comments
 (0)