Skip to content

Commit 9fecaa6

Browse files
author
Brian Chen
committed
Add != and notIn support
1 parent fa04212 commit 9fecaa6

23 files changed

+841
-95
lines changed

Firestore/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
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 documents
5+
where a specified field's value does not equal the specified value.
26

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

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

Lines changed: 133 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,74 @@ - (void)testQueriesCanUseInFiltersWithDocIds {
441506
XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ testDocs[@"aa"], testDocs[@"ab"] ]));
442507
}
443508

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

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

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ - (void)testQueryInequalityFieldMustMatchFirstOrderByField {
602602

603603
NSString *reason =
604604
@"Invalid query. You have a where filter with "
605-
"an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) "
605+
"an inequality (notEqual, lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) "
606606
"on field 'x' and so you must also use 'x' as your first queryOrderedBy field, "
607607
"but your first queryOrderedBy is currently on field 'y' instead.";
608608
FSTAssertThrows([base queryOrderedByField:@"y"], reason);
@@ -611,6 +611,7 @@ - (void)testQueryInequalityFieldMustMatchFirstOrderByField {
611611
FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x"
612612
isGreaterThan:@32],
613613
reason);
614+
FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isNotEqualTo:@32], reason);
614615

615616
XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"],
616617
@"Same inequality fields work");
@@ -638,6 +639,20 @@ - (void)testQueryInequalityFieldMustMatchFirstOrderByField {
638639
@"array_contains different than orderBy works.");
639640
}
640641

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

673+
- (void)testQueriesWithNotEqualAndNotInFiltersFail {
674+
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
675+
676+
FSTAssertThrows([[coll queryWhereField:@"foo" notIn:@[ @1 ]] queryWhereField:@"foo"
677+
isNotEqualTo:@2],
678+
@"Invalid Query. You cannot use 'notEqual' filters with 'notIn' filters.");
679+
680+
FSTAssertThrows([[coll queryWhereField:@"foo" isNotEqualTo:@2] queryWhereField:@"foo"
681+
notIn:@[ @1 ]],
682+
@"Invalid Query. You cannot use 'notIn' filters with 'notEqual' filters.");
683+
}
684+
658685
- (void)testQueriesWithMultipleDisjunctiveFiltersFail {
659686
FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
660687
FSTAssertThrows([[coll queryWhereField:@"foo" in:@[ @1 ]] queryWhereField:@"foo" in:@[ @2 ]],
@@ -664,6 +691,10 @@ - (void)testQueriesWithMultipleDisjunctiveFiltersFail {
664691
arrayContainsAny:@[ @2 ]],
665692
@"Invalid Query. You cannot use more than one 'arrayContainsAny' filter.");
666693

694+
FSTAssertThrows([[coll queryWhereField:@"foo" notIn:@[ @1 ]] queryWhereField:@"foo"
695+
notIn:@[ @2 ]],
696+
@"Invalid Query. You cannot use more than one 'notIn' filter.");
697+
667698
FSTAssertThrows([[coll queryWhereField:@"foo" arrayContainsAny:@[ @1 ]] queryWhereField:@"foo"
668699
in:@[ @2 ]],
669700
@"Invalid Query. You cannot use 'in' filters with 'arrayContainsAny' filters.");
@@ -672,18 +703,44 @@ - (void)testQueriesWithMultipleDisjunctiveFiltersFail {
672703
arrayContainsAny:@[ @2 ]],
673704
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");
674705

706+
FSTAssertThrows(
707+
[[coll queryWhereField:@"foo" arrayContainsAny:@[ @1 ]] queryWhereField:@"foo" notIn:@[ @2 ]],
708+
@"Invalid Query. You cannot use 'notIn' filters with 'arrayContainsAny' filters.");
709+
710+
FSTAssertThrows(
711+
[[coll queryWhereField:@"foo" notIn:@[ @1 ]] queryWhereField:@"foo" arrayContainsAny:@[ @2 ]],
712+
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'notIn' filters.");
713+
714+
FSTAssertThrows([[coll queryWhereField:@"foo" in:@[ @1 ]] queryWhereField:@"foo" notIn:@[ @2 ]],
715+
@"Invalid Query. You cannot use 'notIn' filters with 'in' filters.");
716+
717+
FSTAssertThrows([[coll queryWhereField:@"foo" notIn:@[ @1 ]] queryWhereField:@"foo" in:@[ @2 ]],
718+
@"Invalid Query. You cannot use 'in' filters with 'notIn' filters.");
719+
675720
// This is redundant with the above tests, but makes sure our validation doesn't get confused.
676721
FSTAssertThrows([[[coll queryWhereField:@"foo"
677722
in:@[ @1 ]] queryWhereField:@"foo"
678723
arrayContains:@2] queryWhereField:@"foo"
679724
arrayContainsAny:@[ @2 ]],
680725
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");
681726

727+
FSTAssertThrows(
728+
[[[coll queryWhereField:@"foo"
729+
arrayContains:@1] queryWhereField:@"foo" in:@[ @2 ]] queryWhereField:@"foo"
730+
arrayContainsAny:@[ @2 ]],
731+
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'arrayContains' filters.");
732+
733+
FSTAssertThrows([[[coll queryWhereField:@"foo"
734+
notIn:@[ @1 ]] queryWhereField:@"foo"
735+
arrayContains:@2] queryWhereField:@"foo"
736+
arrayContainsAny:@[ @2 ]],
737+
@"Invalid Query. You cannot use 'arrayContains' filters with 'notIn' filters.");
738+
682739
FSTAssertThrows([[[coll queryWhereField:@"foo"
683740
arrayContains:@1] queryWhereField:@"foo"
684741
in:@[ @2 ]] queryWhereField:@"foo"
685-
arrayContainsAny:@[ @2 ]],
686-
@"Invalid Query. You cannot use 'arrayContainsAny' filters with 'in' filters.");
742+
notIn:@[ @2 ]],
743+
@"Invalid Query. You cannot use 'notIn' filters with 'arrayContains' filters.");
687744
}
688745

689746
- (void)testQueriesCanUseInWithArrayContain {
@@ -715,6 +772,9 @@ - (void)testQueriesInAndArrayContainsAnyArrayRules {
715772
FSTAssertThrows([coll queryWhereField:@"foo" in:@[]],
716773
@"Invalid Query. A non-empty array is required for 'in' filters.");
717774

775+
FSTAssertThrows([coll queryWhereField:@"foo" notIn:@[]],
776+
@"Invalid Query. A non-empty array is required for 'notIn' filters.");
777+
718778
FSTAssertThrows([coll queryWhereField:@"foo" arrayContainsAny:@[]],
719779
@"Invalid Query. A non-empty array is required for 'arrayContainsAny' filters.");
720780

@@ -726,6 +786,9 @@ - (void)testQueriesInAndArrayContainsAnyArrayRules {
726786
FSTAssertThrows([coll queryWhereField:@"foo" arrayContainsAny:values],
727787
@"Invalid Query. 'arrayContainsAny' filters support a maximum of 10 elements"
728788
" in the value array.");
789+
FSTAssertThrows(
790+
[coll queryWhereField:@"foo" notIn:values],
791+
@"Invalid Query. 'notIn' filters support a maximum of 10 elements in the value array.");
729792

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

Firestore/Source/API/FIRQuery.mm

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,16 @@ - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value {
215215
return [self queryWithFilterOperator:Filter::Operator::Equal path:path.internalValue value:value];
216216
}
217217

218+
- (FIRQuery *)queryWhereField:(NSString *)field isNotEqualTo:(id)value {
219+
return [self queryWithFilterOperator:Filter::Operator::NotEqual field:field value:value];
220+
}
221+
222+
- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isNotEqualTo:(id)value {
223+
return [self queryWithFilterOperator:Filter::Operator::NotEqual
224+
path:path.internalValue
225+
value:value];
226+
}
227+
218228
- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value {
219229
return [self queryWithFilterOperator:Filter::Operator::LessThan field:field value:value];
220230
}
@@ -285,6 +295,16 @@ - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path in:(NSArray<id> *)values
285295
return [self queryWithFilterOperator:Filter::Operator::In path:path.internalValue value:values];
286296
}
287297

298+
- (FIRQuery *)queryWhereField:(NSString *)field notIn:(NSArray<id> *)values {
299+
return [self queryWithFilterOperator:Filter::Operator::NotIn field:field value:values];
300+
}
301+
302+
- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path notIn:(NSArray<id> *)values {
303+
return [self queryWithFilterOperator:Filter::Operator::NotIn
304+
path:path.internalValue
305+
value:values];
306+
}
307+
288308
- (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate {
289309
NSComparisonPredicate *comparison = (NSComparisonPredicate *)predicate;
290310
if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) {
@@ -307,6 +327,8 @@ - (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate {
307327
return [self queryWhereField:path isGreaterThan:value];
308328
case NSGreaterThanOrEqualToPredicateOperatorType:
309329
return [self queryWhereField:path isGreaterThanOrEqualTo:value];
330+
case NSNotEqualToPredicateOperatorType:
331+
return [self queryWhereField:path isNotEqualTo:value];
310332
default:; // Fallback below to throw assertion.
311333
}
312334
} else if ([comparison.leftExpression expressionType] == NSConstantValueExpressionType &&
@@ -324,6 +346,8 @@ - (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate {
324346
return [self queryWhereField:path isLessThan:value];
325347
case NSGreaterThanOrEqualToPredicateOperatorType:
326348
return [self queryWhereField:path isLessThanOrEqualTo:value];
349+
case NSNotEqualToPredicateOperatorType:
350+
return [self queryWhereField:path isNotEqualTo:value];
327351
default:; // Fallback below to throw assertion.
328352
}
329353
} else {
@@ -483,7 +507,8 @@ - (FIRQuery *)queryWithFilterOperator:(Filter::Operator)filterOperator
483507
path:(const FieldPath &)fieldPath
484508
value:(id)value {
485509
FieldValue fieldValue = [self parsedQueryValue:value
486-
allowArrays:filterOperator == Filter::Operator::In];
510+
allowArrays:filterOperator == Filter::Operator::In ||
511+
filterOperator == Filter::Operator::NotIn];
487512
auto describer = [value] { return MakeString(NSStringFromClass([value class])); };
488513
return Wrap(_query.Filter(fieldPath, filterOperator, std::move(fieldValue), describer));
489514
}

0 commit comments

Comments
 (0)