Skip to content

Commit 1dc2837

Browse files
author
Brian Chen
authored
Allow null and NaN values in filters (#2115)
1 parent 3932951 commit 1dc2837

File tree

7 files changed

+70
-94
lines changed

7 files changed

+70
-94
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
# Unreleased (22.0.0)
1+
# Unreleased
2+
- [fixed] Removed excess validation of null and NaN values in query filters.
3+
This more closely aligns the SDK with the Firestore backend, which has always
4+
accepted null and NaN for all operators, even though this isn't necessarily
5+
useful.
6+
7+
# (22.0.0)
28
- [changed] Removed the deprecated `timestampsInSnapshotsEnabled` setting.
39
Any timestamps in Firestore documents are now returned as `Timestamps`. To
410
convert `Timestamp` classed to `java.util.Date`, use `Timestamp.toDate()`.

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.firestore;
1616

17+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.nullList;
1718
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.querySnapshotToIds;
1819
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.querySnapshotToValues;
1920
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollection;
@@ -562,15 +563,20 @@ public void testQueriesCanUseArrayContainsFilters() {
562563
Map<String, Object> docB = map("array", asList("a", 42L, "c"));
563564
Map<String, Object> docC = map("array", asList(41.999, "42", map("a", asList(42))));
564565
Map<String, Object> docD = map("array", asList(42L), "array2", asList("bingo"));
566+
Map<String, Object> docE = map("array", nullList());
567+
Map<String, Object> docF = map("array", asList(Double.NaN));
565568
CollectionReference collection =
566-
testCollectionWithDocs(map("a", docA, "b", docB, "c", docC, "d", docD));
569+
testCollectionWithDocs(
570+
map("a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF));
567571

568572
// Search for "array" to contain 42
569573
QuerySnapshot snapshot = waitFor(collection.whereArrayContains("array", 42L).get());
570574
assertEquals(asList(docA, docB, docD), querySnapshotToValues(snapshot));
571575

572-
// NOTE: The backend doesn't currently support null, NaN, objects, or arrays, so there isn't
573-
// much of anything else interesting to test.
576+
// Note: whereArrayContains() requires a non-null value parameter, so no null test is needed.
577+
// With NaN.
578+
snapshot = waitFor(collection.whereArrayContains("array", Double.NaN).get());
579+
assertEquals(new ArrayList<>(), querySnapshotToValues(snapshot));
574580
}
575581

576582
@Test
@@ -582,9 +588,14 @@ public void testQueriesCanUseInFilters() {
582588
Map<String, Object> docE = map("zip", asList("98101", map("zip", 98101L)));
583589
Map<String, Object> docF = map("zip", map("code", 500L));
584590
Map<String, Object> docG = map("zip", asList(98101L, 98102L));
591+
Map<String, Object> docH = map("zip", null);
592+
Map<String, Object> docI = map("zip", Double.NaN);
593+
585594
CollectionReference collection =
586595
testCollectionWithDocs(
587-
map("a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG));
596+
map(
597+
"a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h",
598+
docH, "i", docI));
588599

589600
// Search for zips matching 98101, 98103, or [98101, 98102].
590601
QuerySnapshot snapshot =
@@ -594,6 +605,24 @@ public void testQueriesCanUseInFilters() {
594605
// With objects.
595606
snapshot = waitFor(collection.whereIn("zip", asList(map("code", 500L))).get());
596607
assertEquals(asList(docF), querySnapshotToValues(snapshot));
608+
609+
// With null.
610+
snapshot = waitFor(collection.whereIn("zip", nullList()).get());
611+
assertEquals(new ArrayList<>(), querySnapshotToValues(snapshot));
612+
613+
// With null and a value.
614+
List<Object> inputList = nullList();
615+
inputList.add(98101L);
616+
snapshot = waitFor(collection.whereIn("zip", inputList).get());
617+
assertEquals(asList(docA), querySnapshotToValues(snapshot));
618+
619+
// With NaN.
620+
snapshot = waitFor(collection.whereIn("zip", asList(Double.NaN)).get());
621+
assertEquals(new ArrayList<>(), querySnapshotToValues(snapshot));
622+
623+
// With NaN and a value.
624+
snapshot = waitFor(collection.whereIn("zip", asList(Double.NaN, 98101L)).get());
625+
assertEquals(asList(docA), querySnapshotToValues(snapshot));
597626
}
598627

599628
@Test
@@ -656,9 +685,7 @@ public void testQueriesCanUseNotInFilters() {
656685
assertEquals(Lists.newArrayList(expectedDocsMap.values()), querySnapshotToValues(snapshot));
657686

658687
// With Null.
659-
List<Object> nullArray = new ArrayList<>();
660-
nullArray.add(null);
661-
snapshot = waitFor(collection.whereNotIn("zip", nullArray).get());
688+
snapshot = waitFor(collection.whereNotIn("zip", nullList()).get());
662689
assertEquals(new ArrayList<>(), querySnapshotToValues(snapshot));
663690

664691
// With NaN.
@@ -706,10 +733,14 @@ public void testQueriesCanUseArrayContainsAnyFilters() {
706733
Map<String, Object> docE = map("array", asList(43L));
707734
Map<String, Object> docF = map("array", asList(map("a", 42L)));
708735
Map<String, Object> docG = map("array", 42L);
736+
Map<String, Object> docH = map("array", nullList());
737+
Map<String, Object> docI = map("array", asList(Double.NaN));
709738

710739
CollectionReference collection =
711740
testCollectionWithDocs(
712-
map("a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG));
741+
map(
742+
"a", docA, "b", docB, "c", docC, "d", docD, "e", docE, "f", docF, "g", docG, "h",
743+
docH, "i", docI));
713744

714745
// Search for "array" to contain [42, 43].
715746
QuerySnapshot snapshot =
@@ -719,6 +750,24 @@ public void testQueriesCanUseArrayContainsAnyFilters() {
719750
// With objects.
720751
snapshot = waitFor(collection.whereArrayContainsAny("array", asList(map("a", 42L))).get());
721752
assertEquals(asList(docF), querySnapshotToValues(snapshot));
753+
754+
// With null.
755+
snapshot = waitFor(collection.whereArrayContainsAny("array", nullList()).get());
756+
assertEquals(new ArrayList<>(), querySnapshotToValues(snapshot));
757+
758+
// With null and a value.
759+
List<Object> inputList = nullList();
760+
inputList.add(43L);
761+
snapshot = waitFor(collection.whereArrayContainsAny("array", inputList).get());
762+
assertEquals(asList(docE), querySnapshotToValues(snapshot));
763+
764+
// With NaN.
765+
snapshot = waitFor(collection.whereArrayContainsAny("array", asList(Double.NaN)).get());
766+
assertEquals(new ArrayList<>(), querySnapshotToValues(snapshot));
767+
768+
// With NaN and a value.
769+
snapshot = waitFor(collection.whereArrayContainsAny("array", asList(Double.NaN, 43L)).get());
770+
assertEquals(asList(docE), querySnapshotToValues(snapshot));
722771
}
723772

724773
@Test

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -409,37 +409,6 @@ public void queriesWithNonPositiveLimitFail() {
409409
"Invalid Query. Query limitToLast (-1) is invalid. Limit must be positive.");
410410
}
411411

412-
@Test
413-
public void queriesWithNullOrNaNFiltersOtherThanEqualityFail() {
414-
CollectionReference collection = testCollection();
415-
expectError(
416-
() -> collection.whereGreaterThan("a", null),
417-
"Invalid Query. Null only supports comparisons via "
418-
+ "whereEqualTo() and whereNotEqualTo().");
419-
expectError(
420-
() -> collection.whereArrayContains("a", null),
421-
"Invalid Query. Null only supports comparisons via "
422-
+ "whereEqualTo() and whereNotEqualTo().");
423-
expectError(
424-
() -> collection.whereArrayContainsAny("a", null),
425-
"Invalid Query. A non-empty array is required for 'array_contains_any' filters.");
426-
expectError(
427-
() -> collection.whereIn("a", null),
428-
"Invalid Query. A non-empty array is required for 'in' filters.");
429-
expectError(
430-
() -> collection.whereNotIn("a", null),
431-
"Invalid Query. A non-empty array is required for 'not_in' filters.");
432-
433-
expectError(
434-
() -> collection.whereGreaterThan("a", Double.NaN),
435-
"Invalid Query. NaN only supports comparisons via "
436-
+ "whereEqualTo() and whereNotEqualTo().");
437-
expectError(
438-
() -> collection.whereArrayContains("a", Double.NaN),
439-
"Invalid Query. NaN only supports comparisons via "
440-
+ "whereEqualTo() and whereNotEqualTo().");
441-
}
442-
443412
@Test
444413
public void queriesCannotBeCreatedFromDocumentsMissingSortValues() {
445414
CollectionReference collection = testCollectionWithDocs(map("f", map("k", "f", "nosort", 1.0)));
@@ -757,22 +726,6 @@ public void queriesInAndArrayContainsAnyArrayRules() {
757726
() ->
758727
testCollection().whereArrayContainsAny("bar", asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9)),
759728
"Invalid Query. 'array_contains_any' filters support a maximum of 10 elements in the value array.");
760-
761-
expectError(
762-
() -> testCollection().whereIn("bar", asList("foo", null)),
763-
"Invalid Query. 'in' filters cannot contain 'null' in the value array.");
764-
765-
expectError(
766-
() -> testCollection().whereArrayContainsAny("bar", asList("foo", null)),
767-
"Invalid Query. 'array_contains_any' filters cannot contain 'null' in the value array.");
768-
769-
expectError(
770-
() -> testCollection().whereIn("bar", asList("foo", Double.NaN)),
771-
"Invalid Query. 'in' filters cannot contain 'NaN' in the value array.");
772-
773-
expectError(
774-
() -> testCollection().whereArrayContainsAny("bar", asList("foo", Float.NaN)),
775-
"Invalid Query. 'array_contains_any' filters cannot contain 'NaN' in the value array.");
776729
}
777730

778731
@Test

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,4 +445,10 @@ public static boolean isRunningAgainstEmulator() {
445445
public static void testChangeUserTo(User user) {
446446
MockCredentialsProvider.instance().changeUserTo(user);
447447
}
448+
449+
public static List<Object> nullList() {
450+
List<Object> nullArray = new ArrayList<>();
451+
nullArray.add(null);
452+
return nullArray;
453+
}
448454
}

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -493,20 +493,6 @@ private void validateDisjunctiveFilterElements(Object value, Operator op) {
493493
+ op.toString()
494494
+ "' filters support a maximum of 10 elements in the value array.");
495495
}
496-
if (op == Operator.IN || op == Operator.ARRAY_CONTAINS_ANY) {
497-
if (((List) value).contains(null)) {
498-
throw new IllegalArgumentException(
499-
"Invalid Query. '"
500-
+ op.toString()
501-
+ "' filters cannot contain 'null' in the value array.");
502-
}
503-
if (((List) value).contains(Double.NaN) || ((List) value).contains(Float.NaN)) {
504-
throw new IllegalArgumentException(
505-
"Invalid Query. '"
506-
+ op.toString()
507-
+ "' filters cannot contain 'NaN' in the value array.");
508-
}
509-
}
510496
}
511497

512498
private void validateOrderByFieldMatchesInequality(

firebase-firestore/src/main/java/com/google/firebase/firestore/core/FieldFilter.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,6 @@ public static FieldFilter create(FieldPath path, Operator operator, Value value)
7272
operator.toString() + "queries don't make sense on document keys");
7373
return new KeyFieldFilter(path, operator, value);
7474
}
75-
} else if (Values.isNullValue(value)) {
76-
if (operator != Operator.EQUAL && operator != Operator.NOT_EQUAL) {
77-
throw new IllegalArgumentException(
78-
"Invalid Query. Null only supports comparisons via "
79-
+ "whereEqualTo() and whereNotEqualTo().");
80-
}
81-
return new FieldFilter(path, operator, value);
82-
} else if (Values.isNanValue(value)) {
83-
if (operator != Operator.EQUAL && operator != Operator.NOT_EQUAL) {
84-
throw new IllegalArgumentException(
85-
"Invalid Query. NaN only supports comparisons via "
86-
+ "whereEqualTo() and whereNotEqualTo().");
87-
}
88-
return new FieldFilter(path, operator, value);
8975
} else if (operator == Operator.ARRAY_CONTAINS) {
9076
return new ArrayContainsFilter(path, value);
9177
} else if (operator == Operator.IN) {

firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package com.google.firebase.firestore.core;
1616

1717
import static com.google.firebase.firestore.model.DocumentKey.KEY_FIELD_NAME;
18-
import static com.google.firebase.firestore.testutil.Assert.assertThrows;
1918
import static com.google.firebase.firestore.testutil.TestUtil.doc;
2019
import static com.google.firebase.firestore.testutil.TestUtil.filter;
2120
import static com.google.firebase.firestore.testutil.TestUtil.map;
@@ -352,15 +351,6 @@ public void testNullFilter() {
352351
assertTrue(query.matches(doc6));
353352
}
354353

355-
@Test
356-
public void testOnlySupportsEqualsForNull() {
357-
List<String> invalidOps = asList("<", "<=", ">", ">=");
358-
Query query = Query.atPath(ResourcePath.fromString("collection"));
359-
for (String op : invalidOps) {
360-
assertThrows(IllegalArgumentException.class, () -> query.filter(filter("sort", op, null)));
361-
}
362-
}
363-
364354
@Test
365355
public void testComplexObjectFilters() {
366356
Query query1 =

0 commit comments

Comments
 (0)