From 68aff5b406fb2732951750f3d5f9768df6ee12b5 Mon Sep 17 00:00:00 2001 From: Brian Chen Date: Tue, 8 Sep 2020 11:09:11 -0500 Subject: [PATCH] feat: add support for != and NOT_IN queries (#350) --- .../com/google/cloud/firestore/Query.java | 92 ++++++++++++++++++- .../com/google/cloud/firestore/QueryTest.java | 66 ++++++++++++- .../cloud/firestore/it/ITSystemTest.java | 70 ++++++++++++++ 3 files changed, 224 insertions(+), 4 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index 6b856abb0..035074461 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -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; @@ -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. @@ -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. * - *

A Query can have only one whereArrayContains() filter. + *

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 @@ -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. * - *

A Query can have only one whereArrayContains() filter. + *

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 @@ -732,6 +778,46 @@ public Query whereIn(@Nonnull FieldPath fieldPath, @Nonnull ListA 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 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. + * + *

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 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( @@ -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( diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java index c71297a65..a7b6b5716 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java @@ -219,6 +219,10 @@ 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(); @@ -226,6 +230,7 @@ public void withFilter() throws Exception { query.whereArrayContains("foo", "bar").get().get(); query.whereIn("foo", Collections.singletonList("bar")); query.whereArrayContainsAny("foo", Collections.singletonList("bar")); + query.whereNotIn("foo", Collections.singletonList("bar")); Iterator expected = Arrays.asList( @@ -233,13 +238,18 @@ public void withFilter() throws Exception { 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()) { @@ -257,23 +267,27 @@ public void withFieldPathFilter() throws Exception { Matchers.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.singletonList("bar")); + query.whereNotIn(FieldPath.of("foo"), Collections.singletonList("bar")); query.whereArrayContainsAny(FieldPath.of("foo"), Collections.singletonList("bar")); Iterator 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(); @@ -365,6 +379,56 @@ public void validatesInQueries() { } } + @Test + public void notInQueriesWithReferenceArray() throws Exception { + doAnswer(queryResponse()) + .when(firestoreMock) + .streamRequest( + runQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + query + .whereNotIn( + FieldPath.documentId(), + Arrays.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.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.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 { diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java index 8036bc71b..5b86510cb 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java @@ -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)); @@ -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.asList(98101, 98103)).get().get(); + assertEquals(asList("b", "d", "e", "f"), querySnapshotToIds(querySnapshot)); + + querySnapshot = randomColl.whereNotIn("zip", Arrays.asList(Double.NaN)).get().get(); + assertEquals(asList("b", "a", "c", "d", "e", "f"), querySnapshotToIds(querySnapshot)); + + List 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.asList(doc1.getId(), doc2)) + .get() + .get(); + + assertEquals(asList("c"), querySnapshotToIds(querySnapshot)); + } + @Test public void arrayContainsAnyQueries() throws Exception { setDocument("a", map("array", asList(42)));