diff --git a/Parse/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m b/Parse/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m index 6c4fc743a..3a2613766 100644 --- a/Parse/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m +++ b/Parse/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m @@ -329,6 +329,19 @@ + (BOOL)matchesValue:(id)value containsAllObjectsInArray:(id)constraints { return YES; } +/** + Matches $containedBy constraints. + */ ++ (BOOL)matchesValue:(NSArray *)values + containedBy:(NSArray *)constraints { + for (id value in values) { + if (![self matchesValue:value containedIn:constraints]) { + return NO; + } + } + return YES; +} + /** Matches $regex constraints. */ @@ -446,6 +459,8 @@ + (BOOL)matchesValue:(id)value return [self matchesValue:value notContainedIn:constraint]; } else if ([operator isEqualToString:PFQueryKeyContainsAll]) { return [self matchesValue:value containsAllObjectsInArray:constraint]; + } else if ([operator isEqualToString:PFQueryKeyContainedBy]) { + return [self matchesValue:value containedBy:constraint]; } else if ([operator isEqualToString:PFQueryKeyRegex]) { return [self matchesValue:value regex:constraint withOptions:allKeyConstraints[PFQueryOptionKeyRegexOptions]]; } else if ([operator isEqualToString:PFQueryOptionKeyRegexOptions]) { diff --git a/Parse/Parse/Internal/Query/PFQueryConstants.h b/Parse/Parse/Internal/Query/PFQueryConstants.h index 8e7ee2d55..3ba8afb6f 100644 --- a/Parse/Parse/Internal/Query/PFQueryConstants.h +++ b/Parse/Parse/Internal/Query/PFQueryConstants.h @@ -19,6 +19,7 @@ extern NSString *const PFQueryKeyGreaterThanOrEqualTo; extern NSString *const PFQueryKeyContainedIn; extern NSString *const PFQueryKeyNotContainedIn; extern NSString *const PFQueryKeyContainsAll; +extern NSString *const PFQueryKeyContainedBy; extern NSString *const PFQueryKeyNearSphere; extern NSString *const PFQueryKeyWithin; extern NSString *const PFQueryKeyGeoWithin; diff --git a/Parse/Parse/Internal/Query/PFQueryConstants.m b/Parse/Parse/Internal/Query/PFQueryConstants.m index 81297d7af..97ba404cd 100644 --- a/Parse/Parse/Internal/Query/PFQueryConstants.m +++ b/Parse/Parse/Internal/Query/PFQueryConstants.m @@ -17,6 +17,7 @@ NSString *const PFQueryKeyContainedIn = @"$in"; NSString *const PFQueryKeyNotContainedIn = @"$nin"; NSString *const PFQueryKeyContainsAll = @"$all"; +NSString *const PFQueryKeyContainedBy = @"$containedBy"; NSString *const PFQueryKeyNearSphere = @"$nearSphere"; NSString *const PFQueryKeyWithin = @"$within"; NSString *const PFQueryKeyGeoWithin = @"$geoWithin"; diff --git a/Parse/Parse/Source/PFQuery.h b/Parse/Parse/Source/PFQuery.h index 699a80e99..ae844c066 100644 --- a/Parse/Parse/Source/PFQuery.h +++ b/Parse/Parse/Source/PFQuery.h @@ -273,6 +273,17 @@ typedef void (^PFQueryArrayResultBlock)(NSArray *_Nullable obje */ - (instancetype)whereKey:(NSString *)key containsAllObjectsInArray:(NSArray *)array; +/** + Adds a constraint to the query that requires a particular key's value to + be contained by the provided list of values. Get objects where all array elements match + + @param key The key to be constrained. + @param array The array of values to search for. + + @return The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key containedBy:(NSArray *)array; + ///-------------------------------------- #pragma mark - Adding Location Constraints ///-------------------------------------- diff --git a/Parse/Parse/Source/PFQuery.m b/Parse/Parse/Source/PFQuery.m index bd9455289..105e1d6ea 100644 --- a/Parse/Parse/Source/PFQuery.m +++ b/Parse/Parse/Source/PFQuery.m @@ -303,6 +303,10 @@ - (instancetype)whereKey:(NSString *)key containsAllObjectsInArray:(NSArray *)ar return [self whereKey:key condition:PFQueryKeyContainsAll object:array]; } +- (instancetype)whereKey:(NSString *)key containedBy:(NSArray *)inArray { + return [self whereKey:key condition:PFQueryKeyContainedBy object:inArray]; +} + - (instancetype)whereKey:(NSString *)key nearGeoPoint:(PFGeoPoint *)geopoint { return [self whereKey:key condition:PFQueryKeyNearSphere object:geopoint]; } diff --git a/Parse/Tests/Unit/OfflineQueryLogicUnitTests.m b/Parse/Tests/Unit/OfflineQueryLogicUnitTests.m index 6585305f3..130c8014e 100644 --- a/Parse/Tests/Unit/OfflineQueryLogicUnitTests.m +++ b/Parse/Tests/Unit/OfflineQueryLogicUnitTests.m @@ -1097,6 +1097,42 @@ - (void)testQueryAll { [task waitUntilFinished]; } +- (void)testQueryContainedBy { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"numbers"] = @[@0, @2]; + object[@"letters"] = @[@"b", @"c", @"d"]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"numbers" containedBy:@[@1, @2, @3, @4]]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"letters" containedBy:@[@"a", @"b", @"c", @"d", @"e"]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + - (void)testQueryRegex { PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; diff --git a/Parse/Tests/Unit/QueryUnitTests.m b/Parse/Tests/Unit/QueryUnitTests.m index 134de6db5..0fe42af81 100644 --- a/Parse/Tests/Unit/QueryUnitTests.m +++ b/Parse/Tests/Unit/QueryUnitTests.m @@ -426,6 +426,12 @@ - (void)testWhereContainsAllObjectsInArray { XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$all" : @[ @"yarr" ]} }); } +- (void)testWhereContainedBy { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" containedBy:@[ @"yarr" ]]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$containedBy" : @[ @"yarr" ]} }); +} + - (void)testWhereKeyNearGeoPoint { PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0];