From b49f99e3cdc45ba97766062ab5852571c9e0adca Mon Sep 17 00:00:00 2001 From: Evan Grim Date: Tue, 17 Nov 2020 15:51:10 -0700 Subject: [PATCH 1/3] Test fetches with unpersisted objects --- .../Tests/IncrementalStoreTests.m | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m index 434520a..db7ecf3 100644 --- a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m +++ b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m @@ -851,6 +851,42 @@ -(void)test_predicateEqualityComparisonUsingDates }]; } +-(void)test_predicateForSelfInComparisonWithUnpersistedObjects { + NSArray *users = @[ + [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:context], + [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:context] + ]; + + NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"User"]; + [request setPredicate:[NSPredicate predicateWithFormat:@"SELF IN %@", users]]; + + NSError *error; + NSArray *results = [context executeFetchRequest:request error:&error]; + XCTAssertNotNil(results); + XCTAssertEqual([results count], 2); +} + +-(void)test_batchFetchWithNestedContextsAndUnpersistedObjects { + NSManagedObjectContext *parentContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; + [parentContext setPersistentStoreCoordinator: coordinator]; + + NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; + [childContext setParentContext:parentContext]; + NSManagedObject *user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:childContext]; + + NSError *error; + BOOL saved = [childContext save:&error]; + XCTAssertTrue(saved, @"Failed to save child context: %@", error); + + NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"User"]; + [request setFetchBatchSize:20]; + + NSArray *results = [childContext executeFetchRequest:request error:&error]; + XCTAssertNotNil(results, @"Failed to fetch saved user from parent context: %@", error); + XCTAssertEqual([results count], 1); + XCTAssertEqualObjects([results[0] objectID], [user objectID]); +} + -(void)test_sumExpression { [self createUsers:10]; From 6e40484a0219d29df34f3d0d181335fa9a308f60 Mon Sep 17 00:00:00 2001 From: Evan Grim Date: Tue, 17 Nov 2020 16:18:50 -0700 Subject: [PATCH 2/3] Fix predicates with comparison to collection of unpersisted objects --- Incremental Store/EncryptedStore.m | 31 ++++++++++-------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index aa7a382..aa68f61 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3707,7 +3707,16 @@ - (void)bindWhereClause:(NSDictionary *)clause toStatement:(sqlite3_statement *) NSArray *bindings = [clause objectForKey:@"bindings"]; [bindings enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + // downconvert objects and objectIDs to concrete values + if ([obj isKindOfClass:[NSManagedObject class]]) { + obj = [obj objectID]; + } + if ([obj isKindOfClass:[NSManagedObjectID class]]) { + // objects with temporary ids haven't been inserted yet, so look for an id we know will never match + obj = [obj isTemporaryID] ? @"-1" : [self referenceObjectForObjectID:obj]; + } + // string if ([obj isKindOfClass:[NSString class]]) { const char* str = [obj UTF8String]; @@ -3732,19 +3741,6 @@ - (void)bindWhereClause:(NSDictionary *)clause toStatement:(sqlite3_statement *) } } - // managed object id - else if ([obj isKindOfClass:[NSManagedObjectID class]]) { - id referenceObject = [self referenceObjectForObjectID:obj]; - sqlite3_bind_int64(statement, ((int)idx + 1), [referenceObject unsignedLongLongValue]); - } - - // managed object - else if ([obj isKindOfClass:[NSManagedObject class]]) { - NSManagedObjectID *objectID = [obj objectID]; - id referenceObject = [self referenceObjectForObjectID:objectID]; - sqlite3_bind_int64(statement, ((int)idx + 1), [referenceObject unsignedLongLongValue]); - } - // date else if ([obj isKindOfClass:[NSDate class]]) { sqlite3_bind_double(statement, ((int)idx + 1), [obj timeIntervalSince1970]); @@ -4145,15 +4141,8 @@ - (void)parseExpression:(NSExpression *)expression *operand = @"?"; *bindings = [NSString stringWithFormat:[operator objectForKey:@"format"], value]; } else if ([value isKindOfClass:[NSManagedObject class]] || [value isKindOfClass:[NSManagedObjectID class]]) { - NSManagedObjectID * objectId = [value isKindOfClass:[NSManagedObject class]] ? [value objectID]:value; *operand = @"?"; - // We're not going to be able to look up an object with a temporary id, it hasn't been inserted yet - if ([objectId isTemporaryID]) { - // Just look for an id we know will never match - *bindings = @"-1"; - } else { - *bindings = value; - } + *bindings = value; } else if (!value || value == [NSNull null]) { *bindings = nil; From f37540be216a4dae0e41e0102af9fb97ea199248 Mon Sep 17 00:00:00 2001 From: Evan Grim Date: Tue, 17 Nov 2020 16:22:37 -0700 Subject: [PATCH 3/3] Fix fetch on fault with unsubstituted variables --- Incremental Store/EncryptedStore.m | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index aa68f61..d8545e6 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3530,7 +3530,19 @@ -(id)expressionDescriptionTypeValue:(NSExpressionDescription *)expressionDescrip */ - (NSDictionary *)whereClauseWithFetchRequest:(NSFetchRequest *)request { - NSDictionary *result = [self recursiveWhereClauseWithFetchRequest:request predicate:[request predicate]]; + NSPredicate *predicate = [request predicate]; + + // Perform substitution in predicate if substitutionVariables are available + SEL substitutionVariablesSelector = NSSelectorFromString(@"substitutionVariables"); + if ([request respondsToSelector:substitutionVariablesSelector]) { + // See https://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown for explanation of this pattern + IMP imp = [request methodForSelector:substitutionVariablesSelector]; + NSDictionary *(*func)(id, SEL) = (void *)imp; + NSDictionary *substitutionVariables = func(request, substitutionVariablesSelector); + predicate = [predicate predicateWithSubstitutionVariables:substitutionVariables]; + } + + NSDictionary *result = [self recursiveWhereClauseWithFetchRequest:request predicate:predicate]; NSString *query = result[@"query"]; if ([self entityNeedsEntityTypeColumn:request.entity]) {