diff --git a/Parse/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m b/Parse/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m index 310c1d0dd..6c4fc743a 100644 --- a/Parse/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m +++ b/Parse/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m @@ -587,6 +587,31 @@ - (PFConstraintMatcherBlock)createOrMatcherForQueries:(NSArray *)queries user:(P }; } +/** + Handles $and queries. + */ +- (PFConstraintMatcherBlock)createAndMatcherForQueries:(NSArray *)queries user:(PFUser *)user { + NSMutableArray *matchers = [NSMutableArray array]; + for (PFQuery *query in queries) { + PFConstraintMatcherBlock matcher = [self createMatcherWithQueryConstraints:query.state.conditions user:user]; + [matchers addObject:matcher]; + } + + // Now AND together the constraints for each query. + return ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + BFTask *task = [BFTask taskWithResult:@YES]; + for (PFConstraintMatcherBlock matcher in matchers) { + task = [task continueWithSuccessBlock:^id(BFTask *task) { + if (![task.result boolValue]) { + return task; + } + return matcher(object, database); + }]; + } + return task; + }; +} + /** Returns a PFConstraintMatcherBlock that return true iff the object matches queryConstraints. This takes in a SQLiteDatabase connection because SQLite is finicky about nesting connections, so we @@ -599,6 +624,10 @@ - (PFConstraintMatcherBlock)createMatcherWithQueryConstraints:(NSDictionary *)qu // A set of queries to be OR-ed together PFConstraintMatcherBlock matcher = [self createOrMatcherForQueries:queryConstraintValue user:user]; [matchers addObject:matcher]; + } else if ([key isEqualToString:PFQueryKeyAnd]) { + // A set of queries to be AND-ed together + PFConstraintMatcherBlock matcher = [self createAndMatcherForQueries:queryConstraintValue user:user]; + [matchers addObject:matcher]; } else if ([key isEqualToString:PFQueryKeyRelatedTo]) { PFConstraintMatcherBlock matcher = ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { PFObject *parent = queryConstraintValue[PFQueryKeyObject]; diff --git a/Parse/Parse/Internal/Query/PFQueryConstants.h b/Parse/Parse/Internal/Query/PFQueryConstants.h index c75301081..8e7ee2d55 100644 --- a/Parse/Parse/Internal/Query/PFQueryConstants.h +++ b/Parse/Parse/Internal/Query/PFQueryConstants.h @@ -32,6 +32,7 @@ extern NSString *const PFQueryKeySelect; extern NSString *const PFQueryKeyDontSelect; extern NSString *const PFQueryKeyRelatedTo; extern NSString *const PFQueryKeyOr; +extern NSString *const PFQueryKeyAnd; extern NSString *const PFQueryKeyQuery; extern NSString *const PFQueryKeyKey; extern NSString *const PFQueryKeyObject; diff --git a/Parse/Parse/Internal/Query/PFQueryConstants.m b/Parse/Parse/Internal/Query/PFQueryConstants.m index df6af9b74..81297d7af 100644 --- a/Parse/Parse/Internal/Query/PFQueryConstants.m +++ b/Parse/Parse/Internal/Query/PFQueryConstants.m @@ -30,6 +30,7 @@ NSString *const PFQueryKeyDontSelect = @"$dontSelect"; NSString *const PFQueryKeyRelatedTo = @"$relatedTo"; NSString *const PFQueryKeyOr = @"$or"; +NSString *const PFQueryKeyAnd = @"$and"; NSString *const PFQueryKeyQuery = @"query"; NSString *const PFQueryKeyKey = @"key"; NSString *const PFQueryKeyObject = @"object"; diff --git a/Parse/Parse/Source/PFQuery.h b/Parse/Parse/Source/PFQuery.h index 3235997b9..933b56297 100644 --- a/Parse/Parse/Source/PFQuery.h +++ b/Parse/Parse/Source/PFQuery.h @@ -454,6 +454,15 @@ typedef void (^PFQueryArrayResultBlock)(NSArray *_Nullable obje */ + (instancetype)orQueryWithSubqueries:(NSArray *)queries; +/** + Returns a `PFQuery` that is the `and` of the passed in queries. + + @param queries The list of queries to and together. + + @return An instance of `PFQuery` that is the `and` of the passed in queries. + */ ++ (instancetype)andQueryWithSubqueries:(NSArray *)queries; + /** Adds a constraint that requires that a key's value matches a value in another key in objects returned by a sub query. diff --git a/Parse/Parse/Source/PFQuery.m b/Parse/Parse/Source/PFQuery.m index 4b4f4d625..41291e1f7 100644 --- a/Parse/Parse/Source/PFQuery.m +++ b/Parse/Parse/Source/PFQuery.m @@ -683,23 +683,31 @@ + (instancetype)queryWithClassName:(NSString *)className predicate:(NSPredicate return [self queryWithClassName:className normalizedPredicate:normalizedPredicate]; } -+ (instancetype)orQueryWithSubqueries:(NSArray *)queries { - PFParameterAssert(queries.count, @"Can't create an `or` query from no subqueries."); ++ (instancetype)queryForSubqueries:(NSArray *)queries forKey:(NSString *)key { + PFParameterAssert(queries.count, @"Can't create an `%@` query from no subqueries.", key); NSMutableArray *array = [NSMutableArray arrayWithCapacity:queries.count]; NSString *className = queries.firstObject.parseClassName; for (PFQuery *query in queries) { PFParameterAssert([query isKindOfClass:[PFQuery class]], @"All elements should be instances of `PFQuery` class."); PFParameterAssert([query.parseClassName isEqualToString:className], - @"All sub queries of an `or` query should be on the same class."); + @"All sub queries of an `%@` query should be on the same class.", key); [array addObject:query]; } PFQuery *query = [self queryWithClassName:className]; - [query.state setEqualityConditionWithObject:array forKey:PFQueryKeyOr]; + [query.state setEqualityConditionWithObject:array forKey:key]; return query; } ++ (instancetype)orQueryWithSubqueries:(NSArray *)queries { + return [self queryForSubqueries:queries forKey:PFQueryKeyOr]; +} + ++ (instancetype)andQueryWithSubqueries:(NSArray *)queries { + return [self queryForSubqueries:queries forKey:PFQueryKeyAnd]; +} + ///-------------------------------------- #pragma mark - Get with objectId ///-------------------------------------- diff --git a/Parse/Tests/Unit/OfflineQueryLogicUnitTests.m b/Parse/Tests/Unit/OfflineQueryLogicUnitTests.m index 03b712344..6585305f3 100644 --- a/Parse/Tests/Unit/OfflineQueryLogicUnitTests.m +++ b/Parse/Tests/Unit/OfflineQueryLogicUnitTests.m @@ -1468,6 +1468,35 @@ - (void)testQueryOr { [task waitUntilFinished]; } +- (void)testQueryAnd { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"ArrezTheGodOfWar"] = @[@"bar", @1337]; + PFQuery *query = nil; + BFTask *task = [BFTask taskWithResult:nil]; + + PFQuery *query1 = [PFQuery queryWithClassName:@"Object"]; + [query1 whereKey:@"foo" equalTo:@"bar"]; + PFQuery *query2 = [PFQuery queryWithClassName:@"Object"]; + [query2 whereKey:@"sum" equalTo:@1337]; + query = [PFQuery andQueryWithSubqueries:@[query1, query2]]; + PFConstraintMatcherBlock 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)testSortDate { PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; diff --git a/Parse/Tests/Unit/QueryUnitTests.m b/Parse/Tests/Unit/QueryUnitTests.m index 2b2b0b257..134de6db5 100644 --- a/Parse/Tests/Unit/QueryUnitTests.m +++ b/Parse/Tests/Unit/QueryUnitTests.m @@ -120,6 +120,14 @@ - (void)testOrQuery { XCTAssertEqualObjects(query.state.conditions[@"$or"], (@[ query1, query2 ])); } +- (void)testAndQuery { + PFQuery *query1 = [PFQuery queryWithClassName:@"Yolo"]; + PFQuery *query2 = [PFQuery queryWithClassName:@"Yolo"]; + + PFQuery *query = [PFQuery andQueryWithSubqueries:@[ query1, query2 ]]; + XCTAssertEqualObjects(query.state.conditions[@"$and"], (@[ query1, query2 ])); +} + #pragma mark Pagination - (void)testLimit {