Skip to content

Commit 21921e9

Browse files
author
Brian Chen
authored
Add != and notIn support (#6418)
1 parent ed91583 commit 21921e9

File tree

22 files changed

+866
-123
lines changed

22 files changed

+866
-123
lines changed

Firestore/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
# Unreleased
2+
- [feature] Added `whereField(_:notIn:)` and `whereField(_:isNotEqualTo:)` query
3+
operators. `whereField(_:notIn:)` finds documents where a specified field’s
4+
value is not in a specified array. `whereField(_:isNotEqualTo:)` finds
5+
documents where a specified field's value does not equal the specified value.
6+
Neither query operator will match documents where the specified field is not
7+
present.
28

39
# v1.17.1
410
- [fixed] Fix gRPC documentation warning surfaced in Xcode (#6340).

Firestore/Example/Tests/Integration/API/FIRQueryTests.mm

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,71 @@ - (void)testCanHaveMultipleMutationsWhileOffline {
382382
]));
383383
}
384384

385+
- (void)testQueriesCanUseNotEqualFilters {
386+
// These documents are ordered by value in "zip" since notEquals filter is an inequality, which
387+
// results in documents being sorted by value.
388+
NSDictionary *testDocs = @{
389+
@"a" : @{@"zip" : @(NAN)},
390+
@"b" : @{@"zip" : @91102},
391+
@"c" : @{@"zip" : @98101},
392+
@"d" : @{@"zip" : @98103},
393+
@"e" : @{@"zip" : @[ @98101 ]},
394+
@"f" : @{@"zip" : @[ @98101, @98102 ]},
395+
@"g" : @{@"zip" : @[ @"98101", @{@"zip" : @98101} ]},
396+
@"h" : @{@"zip" : @{@"code" : @500}},
397+
@"i" : @{@"zip" : [NSNull null]},
398+
@"j" : @{@"code" : @500},
399+
};
400+
FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
401+
402+
// Search for zips not matching 98101.
403+
FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip"
404+
isNotEqualTo:@98101]];
405+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
406+
testDocs[@"a"], testDocs[@"b"], testDocs[@"d"], testDocs[@"e"],
407+
testDocs[@"f"], testDocs[@"g"], testDocs[@"h"]
408+
]));
409+
410+
// With objects.
411+
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip"
412+
isNotEqualTo:@{@"code" : @500}]];
413+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
414+
testDocs[@"a"], testDocs[@"b"], testDocs[@"c"], testDocs[@"d"],
415+
testDocs[@"e"], testDocs[@"f"], testDocs[@"g"]
416+
]));
417+
418+
// With null.
419+
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip"
420+
isNotEqualTo:@[ [NSNull null] ]]];
421+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
422+
testDocs[@"a"], testDocs[@"b"], testDocs[@"c"], testDocs[@"d"],
423+
testDocs[@"e"], testDocs[@"f"], testDocs[@"g"], testDocs[@"h"]
424+
]));
425+
426+
// With NAN.
427+
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip" isNotEqualTo:@(NAN)]];
428+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
429+
testDocs[@"b"], testDocs[@"c"], testDocs[@"d"], testDocs[@"e"],
430+
testDocs[@"f"], testDocs[@"g"], testDocs[@"h"]
431+
]));
432+
}
433+
434+
- (void)testQueriesCanUseNotEqualFiltersWithDocIds {
435+
NSDictionary *testDocs = @{
436+
@"aa" : @{@"key" : @"aa"},
437+
@"ab" : @{@"key" : @"ab"},
438+
@"ba" : @{@"key" : @"ba"},
439+
@"bb" : @{@"key" : @"bb"},
440+
};
441+
FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
442+
443+
FIRQuerySnapshot *snapshot =
444+
[self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
445+
isNotEqualTo:@"aa"]];
446+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot),
447+
(@[ testDocs[@"ab"], testDocs[@"ba"], testDocs[@"bb"] ]));
448+
}
449+
385450
- (void)testQueriesCanUseArrayContainsFilters {
386451
NSDictionary *testDocs = @{
387452
@"a" : @{@"array" : @[ @42 ]},
@@ -441,6 +506,75 @@ - (void)testQueriesCanUseInFiltersWithDocIds {
441506
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ testDocs[@"aa"], testDocs[@"ab"] ]));
442507
}
443508

509+
- (void)testQueriesCanUseNotInFilters {
510+
// These documents are ordered by value in "zip" since the NOT_IN filter is an inequality, which
511+
// results in documents being sorted by value.
512+
NSDictionary *testDocs = @{
513+
@"a" : @{@"zip" : @(NAN)},
514+
@"b" : @{@"zip" : @91102},
515+
@"c" : @{@"zip" : @98101},
516+
@"d" : @{@"zip" : @98103},
517+
@"e" : @{@"zip" : @[ @98101 ]},
518+
@"f" : @{@"zip" : @[ @98101, @98102 ]},
519+
@"g" : @{@"zip" : @[ @"98101", @{@"zip" : @98101} ]},
520+
@"h" : @{@"zip" : @{@"code" : @500}},
521+
@"i" : @{@"zip" : [NSNull null]},
522+
@"j" : @{@"code" : @500},
523+
};
524+
FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
525+
526+
// Search for zips not matching 98101, 98103, and [98101, 98102].
527+
FIRQuerySnapshot *snapshot = [self
528+
readDocumentSetForRef:[collection queryWhereField:@"zip"
529+
notIn:@[ @98101, @98103, @[ @98101, @98102 ] ]]];
530+
XCTAssertEqualObjects(
531+
FIRQuerySnapshotGetData(snapshot),
532+
(@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"e"], testDocs[@"g"], testDocs[@"h"] ]));
533+
534+
// With objects.
535+
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip"
536+
notIn:@[ @{@"code" : @500} ]]];
537+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
538+
testDocs[@"a"], testDocs[@"b"], testDocs[@"c"], testDocs[@"d"],
539+
testDocs[@"e"], testDocs[@"f"], testDocs[@"g"]
540+
]));
541+
542+
// With null.
543+
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip"
544+
notIn:@[ [NSNull null] ]]];
545+
XCTAssertTrue(snapshot.isEmpty);
546+
547+
// With NAN.
548+
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip" notIn:@[ @(NAN) ]]];
549+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
550+
testDocs[@"b"], testDocs[@"c"], testDocs[@"d"], testDocs[@"e"],
551+
testDocs[@"f"], testDocs[@"g"], testDocs[@"h"]
552+
]));
553+
554+
// With NAN and a number.
555+
snapshot = [self readDocumentSetForRef:[collection queryWhereField:@"zip"
556+
notIn:@[ @(NAN), @98101 ]]];
557+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
558+
testDocs[@"b"], testDocs[@"d"], testDocs[@"e"], testDocs[@"f"],
559+
testDocs[@"g"], testDocs[@"h"]
560+
]));
561+
}
562+
563+
- (void)testQueriesCanUseNotInFiltersWithDocIds {
564+
NSDictionary *testDocs = @{
565+
@"aa" : @{@"key" : @"aa"},
566+
@"ab" : @{@"key" : @"ab"},
567+
@"ba" : @{@"key" : @"ba"},
568+
@"bb" : @{@"key" : @"bb"},
569+
};
570+
FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
571+
572+
FIRQuerySnapshot *snapshot =
573+
[self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
574+
notIn:@[ @"aa", @"ab" ]]];
575+
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ testDocs[@"ba"], testDocs[@"bb"] ]));
576+
}
577+
444578
- (void)testQueriesCanUseArrayContainsAnyFilters {
445579
NSDictionary *testDocs = @{
446580
@"a" : @{@"array" : @[ @42 ]},

Firestore/Example/Tests/Integration/API/FIRValidationTests.mm

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -416,19 +416,17 @@ - (void)testQueryWithNonPositiveLimitFails {
416416
}
417417

418418
- (void)testNonEqualityQueriesOnNullOrNaNFail {
419-
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil],
420-
@"Invalid Query. Null supports only equality comparisons.");
419+
NSString *expected = @"Invalid Query. Null supports only 'equalTo' and 'notEqualTo' comparisons.";
420+
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil], expected);
421421
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:[NSNull null]],
422-
@"Invalid Query. Null supports only equality comparisons.");
423-
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" arrayContains:nil],
424-
@"Invalid Query. Null supports only equality comparisons.");
422+
expected);
423+
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" arrayContains:nil], expected);
425424
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" arrayContains:[NSNull null]],
426-
@"Invalid Query. Null supports only equality comparisons.");
425+
expected);
427426

428-
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)],
429-
@"Invalid Query. NaN supports only equality comparisons.");
430-
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" arrayContains:@(NAN)],
431-
@"Invalid Query. NaN supports only equality comparisons.");
427+
expected = @"Invalid Query. NaN supports only 'equalTo' and 'notEqualTo' comparisons.";
428+
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)], expected);
429+
FSTAssertThrows([[self collectionRef] queryWhereField:@"a" arrayContains:@(NAN)], expected);
432430
}
433431

434432
- (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues {
@@ -596,13 +594,13 @@ - (void)testQueryInequalityFieldMustMatchFirstOrderByField {
596594
FIRQuery *base = [coll queryWhereField:@"x" isGreaterThanOrEqualTo:@32];
597595

598596
FSTAssertThrows([base queryWhereField:@"y" isLessThan:@"cat"],
599-
@"Invalid Query. All where filters with an inequality (lessThan, "
597+
@"Invalid Query. All where filters with an inequality (notEqual, lessThan, "
600598
"lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same "
601599
"field. But you have inequality filters on 'x' and 'y'");
602600

603601
NSString *reason =
604602
@"Invalid query. You have a where filter with "
605-
"an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) "
603+
"an inequality (notEqual, lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) "
606604
"on field 'x' and so you must also use 'x' as your first queryOrderedBy field, "
607605
"but your first queryOrderedBy is currently on field 'y' instead.";
608606
FSTAssertThrows([base queryOrderedByField:@"y"], reason);
@@ -611,6 +609,7 @@ - (void)testQueryInequalityFieldMustMatchFirstOrderByField {
611609
FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x"
612610
isGreaterThan:@32],
613611
reason);
612+
FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isNotEqualTo:@32], reason);
614613

615614
XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"],
616615
@"Same inequality fields work");
@@ -638,6 +637,20 @@ - (void)testQueryInequalityFieldMustMatchFirstOrderByField {
638637
@"array_contains different than orderBy works.");
639638
}
640639

640+
- (void)testQueriesWithMultipleNotEqualAndInequalitiesFail {
641+
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
642+
643+
FSTAssertThrows([[coll queryWhereField:@"x" isNotEqualTo:@1] queryWhereField:@"x"
644+
isNotEqualTo:@2],
645+
@"Invalid Query. You cannot use more than one 'notEqual' filter.");
646+
647+
FSTAssertThrows([[coll queryWhereField:@"x" isNotEqualTo:@1] queryWhereField:@"y"
648+
isNotEqualTo:@2],
649+
@"Invalid Query. All where filters with an inequality (notEqual, lessThan, "
650+
"lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on "
651+
"the same field. But you have inequality filters on 'x' and 'y'");
652+
}
653+
641654
- (void)testQueriesWithMultipleArrayFiltersFail {
642655
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
643656
FSTAssertThrows([[coll queryWhereField:@"foo" arrayContains:@1] queryWhereField:@"foo"
@@ -655,6 +668,18 @@ - (void)testQueriesWithMultipleArrayFiltersFail {
655668
@"Invalid Query. You cannot use 'arrayContains' filters with 'arrayContainsAny' filters.");
656669
}
657670

671+
- (void)testQueriesWithNotEqualAndNotInFiltersFail {
672+
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
673+
674+
FSTAssertThrows([[coll queryWhereField:@"foo" notIn:@[ @1 ]] queryWhereField:@"foo"
675+
isNotEqualTo:@2],
676+
@"Invalid Query. You cannot use 'notEqual' filters with 'notIn' filters.");
677+
678+
FSTAssertThrows([[coll queryWhereField:@"foo" isNotEqualTo:@2] queryWhereField:@"foo"
679+
notIn:@[ @1 ]],
680+
@"Invalid Query. You cannot use 'notIn' filters with 'notEqual' filters.");
681+
}
682+
658683
- (void)testQueriesWithMultipleDisjunctiveFiltersFail {
659684
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
660685
FSTAssertThrows([[coll queryWhereField:@"foo" in:@[ @1 ]] queryWhereField:@"foo" in:@[ @2 ]],
@@ -664,6 +689,10 @@ - (void)testQueriesWithMultipleDisjunctiveFiltersFail {
664689
arrayContainsAny:@[ @2 ]],
665690
@"Invalid Query. You cannot use more than one 'arrayContainsAny' filter.");
666691

692+
FSTAssertThrows([[coll queryWhereField:@"foo" notIn:@[ @1 ]] queryWhereField:@"foo"
693+
notIn:@[ @2 ]],
694+
@"Invalid Query. You cannot use more than one 'notIn' filter.");
695+
667696
FSTAssertThrows([[coll queryWhereField:@"foo" arrayContainsAny:@[ @1 ]] queryWhereField:@"foo"
668697
in:@[ @2 ]],
669698
@"Invalid Query. You cannot use 'in' filters with 'arrayContainsAny' filters.");
@@ -672,18 +701,44 @@ - (void)testQueriesWithMultipleDisjunctiveFiltersFail {
672701
arrayContainsAny:@[ @2 ]],
673702
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");
674703

704+
FSTAssertThrows(
705+
[[coll queryWhereField:@"foo" arrayContainsAny:@[ @1 ]] queryWhereField:@"foo" notIn:@[ @2 ]],
706+
@"Invalid Query. You cannot use 'notIn' filters with 'arrayContainsAny' filters.");
707+
708+
FSTAssertThrows(
709+
[[coll queryWhereField:@"foo" notIn:@[ @1 ]] queryWhereField:@"foo" arrayContainsAny:@[ @2 ]],
710+
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'notIn' filters.");
711+
712+
FSTAssertThrows([[coll queryWhereField:@"foo" in:@[ @1 ]] queryWhereField:@"foo" notIn:@[ @2 ]],
713+
@"Invalid Query. You cannot use 'notIn' filters with 'in' filters.");
714+
715+
FSTAssertThrows([[coll queryWhereField:@"foo" notIn:@[ @1 ]] queryWhereField:@"foo" in:@[ @2 ]],
716+
@"Invalid Query. You cannot use 'in' filters with 'notIn' filters.");
717+
675718
// This is redundant with the above tests, but makes sure our validation doesn't get confused.
676719
FSTAssertThrows([[[coll queryWhereField:@"foo"
677720
in:@[ @1 ]] queryWhereField:@"foo"
678721
arrayContains:@2] queryWhereField:@"foo"
679722
arrayContainsAny:@[ @2 ]],
680723
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");
681724

725+
FSTAssertThrows(
726+
[[[coll queryWhereField:@"foo"
727+
arrayContains:@1] queryWhereField:@"foo" in:@[ @2 ]] queryWhereField:@"foo"
728+
arrayContainsAny:@[ @2 ]],
729+
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'arrayContains' filters.");
730+
731+
FSTAssertThrows([[[coll queryWhereField:@"foo"
732+
notIn:@[ @1 ]] queryWhereField:@"foo"
733+
arrayContains:@2] queryWhereField:@"foo"
734+
arrayContainsAny:@[ @2 ]],
735+
@"Invalid Query. You cannot use 'arrayContains' filters with 'notIn' filters.");
736+
682737
FSTAssertThrows([[[coll queryWhereField:@"foo"
683738
arrayContains:@1] queryWhereField:@"foo"
684739
in:@[ @2 ]] queryWhereField:@"foo"
685-
arrayContainsAny:@[ @2 ]],
686-
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");
740+
notIn:@[ @2 ]],
741+
@"Invalid Query. You cannot use 'notIn' filters with 'arrayContains' filters.");
687742
}
688743

689744
- (void)testQueriesCanUseInWithArrayContain {
@@ -715,6 +770,9 @@ - (void)testQueriesInAndArrayContainsAnyArrayRules {
715770
FSTAssertThrows([coll queryWhereField:@"foo" in:@[]],
716771
@"Invalid Query. A non-empty array is required for 'in' filters.");
717772

773+
FSTAssertThrows([coll queryWhereField:@"foo" notIn:@[]],
774+
@"Invalid Query. A non-empty array is required for 'notIn' filters.");
775+
718776
FSTAssertThrows([coll queryWhereField:@"foo" arrayContainsAny:@[]],
719777
@"Invalid Query. A non-empty array is required for 'arrayContainsAny' filters.");
720778

@@ -726,6 +784,9 @@ - (void)testQueriesInAndArrayContainsAnyArrayRules {
726784
FSTAssertThrows([coll queryWhereField:@"foo" arrayContainsAny:values],
727785
@"Invalid Query. 'arrayContainsAny' filters support a maximum of 10 elements"
728786
" in the value array.");
787+
FSTAssertThrows(
788+
[coll queryWhereField:@"foo" notIn:values],
789+
@"Invalid Query. 'notIn' filters support a maximum of 10 elements in the value array.");
729790

730791
NSArray *withNullValues = @[ @1, [NSNull null] ];
731792
FSTAssertThrows([coll queryWhereField:@"foo" in:withNullValues],

0 commit comments

Comments
 (0)