Skip to content

Commit

Permalink
Merge pull request #265 from patrickhartling/adjust_self_referential_…
Browse files Browse the repository at this point in the history
…many_to_many_relationships

Handle self-referential asymmetric many-to-many relationships
  • Loading branch information
DanielBroad authored Mar 22, 2017
2 parents 4d24a78 + bb2272f commit baa4998
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 13 deletions.
28 changes: 20 additions & 8 deletions Incremental Store/EncryptedStore.m
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ - (BOOL)checkDatabaseStatusWithError:(NSError *__autoreleasing*)error {
*error = [NSError errorWithDomain:EncryptedStoreErrorDomain code:EncryptedStoreErrorIncorrectPasscode userInfo:userInfo];
}
}
return result && (*error == nil);
return result && (error == NULL || *error == nil);
}

- (BOOL)changeDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing*)error {
Expand Down Expand Up @@ -1194,7 +1194,7 @@ - (BOOL)setDatabasePassphrase:(NSString *)passphrase error:(NSError *__autorelea
result = [self checkDatabaseStatusWithError:error];
}

return result && (*error == nil);
return result && (error == NULL || *error == nil);
}

- (BOOL)validateDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing*)error {
Expand Down Expand Up @@ -1541,7 +1541,8 @@ - (BOOL)migrateFromModel:(NSManagedObjectModel *)fromModel toModel:(NSManagedObj
- (BOOL)initializeDatabase:(NSError**)error {
BOOL __block success = YES;
NSMutableSet *manytomanys = [NSMutableSet set];

NSMutableSet *tableNames = [NSMutableSet new];

if (success) {
NSArray *entities = [self storeEntities];
[entities enumerateObjectsUsingBlock:^(NSEntityDescription *entity, NSUInteger idx, BOOL *stop) {
Expand All @@ -1555,8 +1556,12 @@ - (BOOL)initializeDatabase:(NSError**)error {
NSRelationshipDescription *relation = [relations objectForKey:key];
NSRelationshipDescription *inverse = [relation inverseRelationship];
if (relation.transient || inverse.transient) continue;
if ([relation isToMany] && [inverse isToMany] && ![manytomanys containsObject:inverse]) {
[manytomanys addObject:relation];
if ([relation isToMany] && [inverse isToMany]) {
NSString *const tableName = [self tableNameForRelationship:relation];
if (! [tableNames containsObject:tableName]) {
[manytomanys addObject:relation];
[tableNames addObject:tableName];
}
}
}
}];
Expand Down Expand Up @@ -2074,7 +2079,7 @@ -(NSComparator)fixedLocaleCaseInsensitiveComparator

-(NSString *)tableNameForRelationship:(NSRelationshipDescription *)relationship {
NSRelationshipDescription *inverse = [relationship inverseRelationship];
NSArray *names = [@[[relationship name],[inverse name]] sortedArrayUsingComparator:[self fixedLocaleCaseInsensitiveComparator]];
NSArray *names = @[[relationship name],[inverse name]];
return [NSString stringWithFormat:@"ecd_%@",[names componentsJoinedByString:@"_"]];
}

Expand Down Expand Up @@ -2838,9 +2843,16 @@ - (BOOL)handleDeletedRelationInSaveRequest:(NSManagedObject *)object error:(NSEr
NSRelationshipDescription *desc = (NSRelationshipDescription *)prop;
NSRelationshipDescription *inverse = [desc inverseRelationship];
if ([desc isToMany] && [inverse isToMany]) {

NSEntityDescription *rootSourceEntity = [self rootForEntity:desc.entity];
NSEntityDescription *rootDestinationEntity = [self rootForEntity:inverse.entity];
NSString *entityName = [rootSourceEntity name];

if ([rootSourceEntity isEqual:rootDestinationEntity]) {
entityName = [entityName stringByAppendingString:@"_1"];
}

NSString *string = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@__objectid=?;",
[self tableNameForRelationship:desc],[[self rootForEntity:[desc entity]] name]];
[self tableNameForRelationship:desc],entityName];
sqlite3_stmt *statement = [self preparedStatementForQuery:string];
NSNumber *number = [self referenceObjectForObjectID:[object objectID]];
sqlite3_bind_int64(statement, 1, [number unsignedLongLongValue]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="9525" systemVersion="15C50" minimumToolsVersion="Xcode 4.3">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="11759" systemVersion="16D32" minimumToolsVersion="Xcode 4.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<entity name="Account" syncable="YES">
<attribute name="accountID" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="transferFromAccounts" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Account" inverseName="transferToAccounts" inverseEntity="Account" syncable="YES"/>
<relationship name="transferToAccounts" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Account" inverseName="transferFromAccounts" inverseEntity="Account" syncable="YES"/>
</entity>
<entity name="Comment" parentEntity="Post" syncable="YES">
<relationship name="parent" optional="YES" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="Post" inverseName="comments" inverseEntity="Post" syncable="YES"/>
</entity>
Expand All @@ -10,7 +15,7 @@
<entity name="Post" syncable="YES">
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="tags" optional="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="timestamp" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="comments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Comment" inverseName="parent" inverseEntity="Comment" syncable="YES"/>
<relationship name="user" optional="YES" minCount="1" maxCount="1" deletionRule="Cascade" destinationEntity="User" inverseName="posts" inverseEntity="User" syncable="YES"/>
Expand All @@ -21,19 +26,20 @@
<relationship name="hasUsers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="User" inverseName="hasTags" inverseEntity="User" syncable="YES"/>
</entity>
<entity name="User" syncable="YES">
<attribute name="admin" optional="YES" attributeType="Boolean" syncable="YES"/>
<attribute name="age" optional="YES" attributeType="Integer 64" defaultValueString="0" syncable="YES"/>
<attribute name="admin" optional="YES" attributeType="Boolean" usesScalarValueType="NO" syncable="YES"/>
<attribute name="age" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="hasTags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="hasUsers" inverseEntity="Tag" syncable="YES"/>
<relationship name="nicknames" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Nickname" inverseName="user" inverseEntity="Nickname" syncable="YES"/>
<relationship name="posts" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Post" inverseName="user" inverseEntity="Post" syncable="YES"/>
</entity>
<elements>
<element name="Comment" positionX="97" positionY="-13" width="128" height="58"/>
<element name="Nickname" positionX="-153" positionY="99" width="128" height="75"/>
<element name="Post" positionX="-144" positionY="-45" width="128" height="133"/>
<element name="RootTag" positionX="-99" positionY="117" width="128" height="45"/>
<element name="Tag" positionX="-99" positionY="197" width="128" height="73"/>
<element name="User" positionX="-360" positionY="54" width="128" height="135"/>
<element name="Nickname" positionX="-153" positionY="99" width="128" height="75"/>
<element name="Account" positionX="-153" positionY="99" width="128" height="90"/>
</elements>
</model>
89 changes: 89 additions & 0 deletions exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1049,4 +1049,93 @@ - (void)test_stringComparision {
XCTAssertEqualObjects(query(@"TRUEPREDICATE", nil), [NSSet setWithArray:expected]);
}

- (void)test_fetchAccountsByManyToManyRelationship {
// Create three Account entities with transfer relationships set up this way:
//
// - account0 can transfer to account1 and account2
// - account1 can transfer to account2
//
// The fetch request is set up to fetch those entities that can send to at least one other Account AND
// can receive from at least one other Account.

NSString *const entityName = @"Account";
NSManagedObject *const account0 = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:context];
NSManagedObject *const account1 = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:context];
NSManagedObject *const account2 = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:context];

NSString *const accountID0 = @"account0";
NSString *const accountID1 = @"account1";
NSString *const accountID2 = @"account2";
NSString *const idKey = @"accountID";
NSString *const transferFromAccountsKey = @"transferFromAccounts";
NSString *const transferToAccountsKey = @"transferToAccounts";

[account0 setValue:accountID0 forKey:idKey];
[account1 setValue:accountID1 forKey:idKey];
[account2 setValue:accountID2 forKey:idKey];

NSMutableSet *const transferToAccounts0 = [account0 mutableSetValueForKey:transferToAccountsKey];
[transferToAccounts0 addObject:account1];
[transferToAccounts0 addObject:account2];

NSMutableSet *const transferToAccounts1 = [account1 mutableSetValueForKey:transferToAccountsKey];
[transferToAccounts1 addObject:account2];

NSSet *transferFromAccounts;
NSSet *transferToAccounts;

transferFromAccounts = [account0 valueForKey:transferFromAccountsKey];
transferToAccounts = [account0 valueForKey:transferToAccountsKey];
XCTAssertEqual(transferFromAccounts.count, 0,
@"The %@ entity with ID %@ should have an empty set for %@", entityName, accountID0,
transferFromAccountsKey);
XCTAssertEqual(transferToAccounts.count, 2,
@"The %@ entity with ID %@ should be able to send to two %@ entities", entityName,
accountID0, entityName);

transferFromAccounts = [account1 valueForKey:transferFromAccountsKey];
transferToAccounts = [account1 valueForKey:transferToAccountsKey];
XCTAssertEqual(transferFromAccounts.count, 1,
@"The %@ entity with ID %@ should be able to receive from one other %@", entityName,
accountID1, entityName);
XCTAssertTrue([transferFromAccounts containsObject:account0],
@"The %@ entity with ID %@ should have %@ in its transfer-from set", entityName,
accountID1, accountID0);
XCTAssertEqual(transferToAccounts.count, 1,
@"The %@ entity with ID %@ should be able to send to one other %@", entityName,
accountID1, entityName);

transferFromAccounts = [account2 valueForKey:transferFromAccountsKey];
transferToAccounts = [account2 valueForKey:transferToAccountsKey];
XCTAssertEqual(transferFromAccounts.count, 2,
@"The %@ entity with ID %@ should be able to receive from two other %@ entities",
entityName, accountID1, entityName);
XCTAssertTrue([transferFromAccounts containsObject:account0],
@"The %@ entity with ID %@ should have %@ in its transfer-from set", entityName,
accountID2, accountID0);
XCTAssertTrue([transferFromAccounts containsObject:account1],
@"The %@ entity with ID %@ should have %@ in its transfer-from set", entityName,
accountID2, accountID1);
XCTAssertEqual(transferToAccounts.count, 0,
@"The %@ entity with ID %@ should have an empty set for %@", entityName, accountID2,
transferToAccountsKey);

[context save:nil];

NSFetchRequest *const fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName];
NSPredicate *const linkageCountPredicate = [NSPredicate predicateWithFormat:@"%K.@count > 0 AND %K.@count > 0",
transferToAccountsKey, transferFromAccountsKey];
fetchRequest.predicate = linkageCountPredicate;

NSError * __autoreleasing error;
NSArray *fetchedAccounts = [context executeFetchRequest:fetchRequest error:&error];
XCTAssertNotNil(fetchedAccounts, @"Failed to fetch %@ entities: %@", entityName, error);
XCTAssertEqual(fetchedAccounts.count, 1, @"Should have only fetched one %@ (the one with ID %@)",
entityName, accountID1);
XCTAssertTrue([fetchedAccounts containsObject:account1], @"Expected %@ to be in %@", account1, fetchedAccounts);
}

@end

0 comments on commit baa4998

Please sign in to comment.