From 583d2662c05b871bfda75cf6e44608e903b544a2 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 10 Mar 2023 09:19:52 -0600 Subject: [PATCH] feat: Add support for `PFQuery.explain` and `PFQuery.hint` (#1723) --- .../Internal/Commands/PFRESTQueryCommand.h | 4 ++ .../Internal/Commands/PFRESTQueryCommand.m | 20 ++++++++- .../Query/Controller/PFQueryController.m | 3 ++ .../Query/State/PFMutableQueryState.h | 3 ++ .../Query/State/PFMutableQueryState.m | 2 + .../Parse/Internal/Query/State/PFQueryState.h | 3 ++ .../Parse/Internal/Query/State/PFQueryState.m | 6 ++- .../Query/State/PFQueryState_Private.h | 6 +++ Parse/Parse/Source/PFQuery.h | 17 ++++++++ Parse/Parse/Source/PFQuery.m | 32 ++++++++++++++ Parse/Tests/Unit/QueryStateUnitTests.m | 4 ++ Parse/Tests/Unit/QueryUnitTests.m | 43 ++++++++++++++++++- 12 files changed, 139 insertions(+), 4 deletions(-) diff --git a/Parse/Parse/Internal/Commands/PFRESTQueryCommand.h b/Parse/Parse/Internal/Commands/PFRESTQueryCommand.h index da25010f8..b8f75782b 100644 --- a/Parse/Parse/Internal/Commands/PFRESTQueryCommand.h +++ b/Parse/Parse/Internal/Commands/PFRESTQueryCommand.h @@ -30,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN includedKeys:(nullable NSSet *)includedKeys limit:(NSInteger)limit skip:(NSInteger)skip + explain:(BOOL)explain + hint:(NSString *)hint extraOptions:(nullable NSDictionary *)extraOptions tracingEnabled:(BOOL)trace sessionToken:(nullable NSString *)sessionToken @@ -54,6 +56,8 @@ NS_ASSUME_NONNULL_BEGIN includedKeys:(nullable NSSet *)includedKeys limit:(NSInteger)limit skip:(NSInteger)skip + explain:(BOOL)explain + hint:(NSString *)hint extraOptions:(nullable NSDictionary *)extraOptions tracingEnabled:(BOOL)trace error:(NSError **)error; diff --git a/Parse/Parse/Internal/Commands/PFRESTQueryCommand.m b/Parse/Parse/Internal/Commands/PFRESTQueryCommand.m index d6f6fbb8a..06c0206bb 100644 --- a/Parse/Parse/Internal/Commands/PFRESTQueryCommand.m +++ b/Parse/Parse/Internal/Commands/PFRESTQueryCommand.m @@ -38,6 +38,8 @@ + (nullable instancetype)findCommandForClassWithName:(NSString *)className includedKeys:(NSSet *)includedKeys limit:(NSInteger)limit skip:(NSInteger)skip + explain:(BOOL)explain + hint:(NSString *)hint extraOptions:(NSDictionary *)extraOptions tracingEnabled:(BOOL)trace sessionToken:(NSString *)sessionToken @@ -48,6 +50,8 @@ + (nullable instancetype)findCommandForClassWithName:(NSString *)className includedKeys:includedKeys limit:limit skip:skip + explain:explain + hint:hint extraOptions:extraOptions tracingEnabled:trace error:error]; @@ -100,6 +104,8 @@ + (nullable NSDictionary *)findCommandParametersForQueryState:(PFQueryState *)qu includedKeys:queryState.includedKeys limit:queryState.limit skip:queryState.skip + explain:queryState.explain + hint:queryState.hint extraOptions:queryState.extraOptions tracingEnabled:queryState.trace error:error]; @@ -111,6 +117,8 @@ + (nullable NSDictionary *)findCommandParametersWithOrder:(NSString *)order includedKeys:(NSSet *)includedKeys limit:(NSInteger)limit skip:(NSInteger)skip + explain:(BOOL)explain + hint:(NSString *)hint extraOptions:(NSDictionary *)extraOptions tracingEnabled:(BOOL)trace error:(NSError **)error { @@ -139,6 +147,12 @@ + (nullable NSDictionary *)findCommandParametersWithOrder:(NSString *)order // TODO: (nlutsenko) Double check that tracing still works. Maybe create test for it. parameters[@"trace"] = @"1"; } + if (explain) { + parameters[@"explain"] = @"1"; + } + if (hint != nil) { + parameters[@"hint"] = hint; + } [extraOptions enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { parameters[key] = obj; }]; @@ -165,6 +179,8 @@ + (nullable NSDictionary *)findCommandParametersWithOrder:(NSString *)order includedKeys:subquery.state.includedKeys limit:subquery.state.limit skip:subquery.state.skip + explain:subquery.state.explain + hint:subquery.state.hint extraOptions:nil tracingEnabled:NO error:&encodingError]; @@ -224,8 +240,10 @@ + (nullable id)_encodeSubqueryIfNeeded:(id)object error:(NSError * __autoreleasi conditions:subquery.state.conditions selectedKeys:subquery.state.selectedKeys includedKeys:subquery.state.includedKeys - limit:subquery.state.limit + limit:subquery.state.limit skip:subquery.state.skip + explain:subquery.state.explain + hint:subquery.state.hint extraOptions:subquery.state.extraOptions tracingEnabled:NO error:&encodingError]; diff --git a/Parse/Parse/Internal/Query/Controller/PFQueryController.m b/Parse/Parse/Internal/Query/Controller/PFQueryController.m index 3afb7ab78..49b03be33 100644 --- a/Parse/Parse/Internal/Query/Controller/PFQueryController.m +++ b/Parse/Parse/Internal/Query/Controller/PFQueryController.m @@ -77,6 +77,9 @@ - (BFTask *)findObjectsAsyncForQueryState:(PFQueryState *)queryState PFCommandResult *result = task.result; NSDate *queryReceived = (queryState.trace ? [NSDate date] : nil); + if (queryState.explain) { + return result.result[@"results"]; + } NSArray *resultObjects = result.result[@"results"]; NSMutableArray *foundObjects = [NSMutableArray arrayWithCapacity:resultObjects.count]; if (resultObjects != nil) { diff --git a/Parse/Parse/Internal/Query/State/PFMutableQueryState.h b/Parse/Parse/Internal/Query/State/PFMutableQueryState.h index 33d8d31dd..78e3ba447 100644 --- a/Parse/Parse/Internal/Query/State/PFMutableQueryState.h +++ b/Parse/Parse/Internal/Query/State/PFMutableQueryState.h @@ -16,6 +16,9 @@ @property (nonatomic, assign, readwrite) NSInteger limit; @property (nonatomic, assign, readwrite) NSInteger skip; +@property (nonatomic, assign, readwrite) BOOL explain; +@property (nonatomic, copy, readwrite) NSString *hint; + ///-------------------------------------- #pragma mark - Remote + Caching Options ///-------------------------------------- diff --git a/Parse/Parse/Internal/Query/State/PFMutableQueryState.m b/Parse/Parse/Internal/Query/State/PFMutableQueryState.m index 169ffa283..3031c0e8f 100644 --- a/Parse/Parse/Internal/Query/State/PFMutableQueryState.m +++ b/Parse/Parse/Internal/Query/State/PFMutableQueryState.m @@ -36,6 +36,8 @@ @implementation PFMutableQueryState @dynamic cachePolicy; @dynamic maxCacheAge; @dynamic trace; +@dynamic explain; +@dynamic hint; @dynamic shouldIgnoreACLs; @dynamic shouldIncludeDeletingEventually; @dynamic queriesLocalDatastore; diff --git a/Parse/Parse/Internal/Query/State/PFQueryState.h b/Parse/Parse/Internal/Query/State/PFQueryState.h index eb69af826..5e541b292 100644 --- a/Parse/Parse/Internal/Query/State/PFQueryState.h +++ b/Parse/Parse/Internal/Query/State/PFQueryState.h @@ -29,6 +29,9 @@ @property (nonatomic, assign, readonly) NSInteger limit; @property (nonatomic, assign, readonly) NSInteger skip; +@property (nonatomic, assign, readonly) BOOL explain; +@property (nonatomic, copy, readonly) NSString *hint; + ///-------------------------------------- #pragma mark - Remote + Caching Options ///-------------------------------------- diff --git a/Parse/Parse/Internal/Query/State/PFQueryState.m b/Parse/Parse/Internal/Query/State/PFQueryState.m index 4978b8aa3..58707f308 100644 --- a/Parse/Parse/Internal/Query/State/PFQueryState.m +++ b/Parse/Parse/Internal/Query/State/PFQueryState.m @@ -38,8 +38,10 @@ + (NSDictionary *)propertyAttributes { PFQueryStatePropertyName(shouldIgnoreACLs): [PFPropertyAttributes attributes], PFQueryStatePropertyName(shouldIncludeDeletingEventually): [PFPropertyAttributes attributes], PFQueryStatePropertyName(queriesLocalDatastore): [PFPropertyAttributes attributes], - - PFQueryStatePropertyName(localDatastorePinName): [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy] + PFQueryStatePropertyName(localDatastorePinName): [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + + PFQueryStatePropertyName(explain): [PFPropertyAttributes attributes], + PFQueryStatePropertyName(hint): [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy] }; } diff --git a/Parse/Parse/Internal/Query/State/PFQueryState_Private.h b/Parse/Parse/Internal/Query/State/PFQueryState_Private.h index 48e322f5e..f3a4e2e0d 100644 --- a/Parse/Parse/Internal/Query/State/PFQueryState_Private.h +++ b/Parse/Parse/Internal/Query/State/PFQueryState_Private.h @@ -39,6 +39,9 @@ NSTimeInterval _maxCacheAge; BOOL _trace; + + BOOL _explain; + NSString *_hint; BOOL _shouldIgnoreACLs; BOOL _shouldIncludeDeletingEventually; @@ -51,6 +54,9 @@ @property (nonatomic, assign, readwrite) NSInteger limit; @property (nonatomic, assign, readwrite) NSInteger skip; +@property (nonatomic, assign, readwrite) BOOL explain; +@property (nonatomic, copy, readwrite) NSString *hint; + ///-------------------------------------- #pragma mark - Remote + Caching Options ///-------------------------------------- diff --git a/Parse/Parse/Source/PFQuery.h b/Parse/Parse/Source/PFQuery.h index a65a73647..7dfbdfcdc 100644 --- a/Parse/Parse/Source/PFQuery.h +++ b/Parse/Parse/Source/PFQuery.h @@ -762,6 +762,23 @@ typedef void (^PFQueryArrayResultBlock)(NSArray *_Nullable obje */ - (instancetype)ignoreACLs; +/** + Change the source of this query to investigate the query execution plan. Useful for optimizing queries. + + @param explain Used to toggle the information on the query plan. + */ +- (void)setExplain:(BOOL)explain; + +/** + Change the source of this query to force an index selection. + + @param hint The index name that should be used when executing query. + */ +- (void)setHint:(nullable NSString *)hint; + +@property (nonatomic, copy) NSString *hint; +@property (nonatomic, assign) BOOL explain; + ///-------------------------------------- #pragma mark - Advanced Settings ///-------------------------------------- diff --git a/Parse/Parse/Source/PFQuery.m b/Parse/Parse/Source/PFQuery.m index 240cf7a68..cdf2e00f7 100644 --- a/Parse/Parse/Source/PFQuery.m +++ b/Parse/Parse/Source/PFQuery.m @@ -183,6 +183,26 @@ - (BOOL)trace { return self.state.trace; } +#pragma mark Hint + +- (void)setHint:(NSString *)hint { + self.state.hint = hint; +} + +- (NSString *)hint { + return self.state.hint; +} + +#pragma mark Explain + +- (void)setExplain:(BOOL)explain { + self.state.explain = explain; +} + +- (BOOL)explain { + return self.state.explain; +} + ///-------------------------------------- #pragma mark - Order ///-------------------------------------- @@ -990,6 +1010,18 @@ - (instancetype)ignoreACLs { return self; } +- (instancetype)explain:(BOOL)explain { + [self checkIfCommandIsRunning]; + self.state.explain = explain; + return self; +} + +- (instancetype)hint:(NSString *)indexName { + [self checkIfCommandIsRunning]; + self.state.hint = [indexName copy]; + return self; +} + ///-------------------------------------- #pragma mark - Query State ///-------------------------------------- diff --git a/Parse/Tests/Unit/QueryStateUnitTests.m b/Parse/Tests/Unit/QueryStateUnitTests.m index 59dad10c8..76a0fb093 100644 --- a/Parse/Tests/Unit/QueryStateUnitTests.m +++ b/Parse/Tests/Unit/QueryStateUnitTests.m @@ -28,6 +28,8 @@ - (PFQueryState *)sampleQueryState { state.cachePolicy = kPFCachePolicyCacheOnly; state.maxCacheAge = 100500.0; state.trace = YES; + state.hint = @"_id_"; + state.explain = YES; state.shouldIgnoreACLs = YES; state.shouldIncludeDeletingEventually = YES; state.queriesLocalDatastore = YES; @@ -64,6 +66,8 @@ - (void)assertQueryState:(PFQueryState *)state equalToState:(PFQueryState *)diff XCTAssertEqualObjects(state.extraOptions, differentState.extraOptions); XCTAssertEqualObjects(state.localDatastorePinName, differentState.localDatastorePinName); + XCTAssertEqualObjects(state.hint, differentState.hint); + XCTAssertEqual(state.explain, differentState.explain); } ///-------------------------------------- diff --git a/Parse/Tests/Unit/QueryUnitTests.m b/Parse/Tests/Unit/QueryUnitTests.m index a9ccda784..93cdc2f97 100644 --- a/Parse/Tests/Unit/QueryUnitTests.m +++ b/Parse/Tests/Unit/QueryUnitTests.m @@ -213,6 +213,43 @@ - (void)testTrace { XCTAssertTrue(query.state.trace); } +- (void)testExplain { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.explain = YES; + XCTAssertTrue(query.explain); + XCTAssertTrue(query.state.explain); + + [query setExplain:NO]; + XCTAssertFalse(query.explain); + XCTAssertFalse(query.state.explain); +} + +- (void)testFindWithExplain { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query setExplain:YES]; + + [self mockQueryControllerFindObjectsForQueryState:query.state withResult:@[ @"yolo" ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[query findObjectsInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @[ @"yolo" ]); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testHint { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.hint = @"_id_"; + XCTAssertEqualObjects(query.hint, @"_id_"); + XCTAssertEqualObjects(query.state.hint, @"_id_"); + + [query setHint:nil]; + XCTAssertNil(query.hint); + XCTAssertNil(query.state.hint); +} + - (void)testIncludeKey { PFQuery *query = [PFQuery queryWithClassName:@"a"]; [query includeKey:@"yolo"]; @@ -1287,7 +1324,8 @@ - (void)testNSCopying { query.limit = 10; query.skip = 20; - + query.explain = YES; + query.hint = @"_id_"; query.cachePolicy = kPFCachePolicyIgnoreCache; query.maxCacheAge = 30.0; @@ -1311,6 +1349,9 @@ - (void)testNSCopying { XCTAssertEqual(queryCopy.maxCacheAge, query.maxCacheAge); XCTAssertEqual(queryCopy.trace, query.trace); + + XCTAssertEqual(queryCopy.explain, query.explain); + XCTAssertEqualObjects(queryCopy.hint, query.hint); } #pragma mark Predicates