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