Open shorbenko opened 7 years ago
Seems likely this might be because the underlying API does not decrypt the unique IDs before comparing them. If it’s an Apppe private API doing it, swizzling that could get you in trouble on the App Store but not for Enterprise deploys. Hopefully it’s not.
That can be fixed by adding table-constraint while create table
(BOOL)createTableForEntity:(NSEntityDescription *)entity error:(NSError**)error { // Skip sub-entities since the super-entities should handle // creating columns for all their children. if (entity.superentity) { return YES; }
// prepare columns NSMutableArray *columns = [NSMutableArray arrayWithObject:@"'objectid' integer primary key"]; if (entity.subentities.count > 0) { // NOTE: Will use '-[NSString hash]' to determine the entity type so we can use // faster integer-indexed queries. Any string greater than 96-chars is // not guaranteed to produce a unique hash value, but for entity names that // shouldn't be a problem. [columns addObject:@"'entityType' integer"]; }
[columns addObjectsFromArray:[self columnNamesForEntity:entity indexedOnly:NO quotedNames:YES]];
NSArray<NSString > constraints = [self constraintsQueryFragmentForEntity:entity error:error];
if (constraints == nil) // error return NO;
NSString *string = nil;
if (constraints.count) string = [NSString stringWithFormat: @"CREATE TABLE %@ (%@, %@);", [self tableNameForEntity:entity], [columns componentsJoinedByString:@", "], [constraints componentsJoinedByString:@", "]]; else string = [NSString stringWithFormat: @"CREATE TABLE %@ (%@);", [self tableNameForEntity:entity], [columns componentsJoinedByString:@", "]];
NSLog(@"create table query: %@", string);
sqlite3_stmt *statement = [self preparedStatementForQuery:string]; sqlite3_step(statement);
BOOL result = (statement != NULL && sqlite3_finalize(statement) == SQLITE_OK); if (!result && error) { *error = [self databaseError]; return result; }
return [self createIndicesForEntity:entity error:error]; }
(BOOL) reportUniquenessConstraintsMalformed:(NSEntityDescription *)entity error:(NSError *)error { if (error) error = [NSError errorWithDomain: [EncryptedStoreOptionsKeys optionErrorDomain] code:EncryptedStoreErrorUniquenessConstraintsMalformed userInfo:@{[EncryptedStoreOptionsKeys optionErrorMessageKey] :@"cannot parse uniquenessConstraints"}]; return NO; }
(NSString ) constraintsQueryFragmentForUniqConstraint:(NSArray )contsraintColumns tableName:(NSString *)tableName error:(NSError *)error { NSMutableArray<NSString > constraintElems = [[NSMutableArray<NSString > alloc] init]; for (id element in contsraintColumns) { if ( [element isKindOfClass:[NSString class]]) { [constraintElems addObject:(NSString *)element]; } else return nil; }
NSString joined = [constraintElems componentsJoinedByString:@"_"]; NSString escaped = [NSString stringWithFormat:@"'%@'", [constraintElems componentsJoinedByString:@"','"]];
return [NSString stringWithFormat: @"CONSTRAINT %@_%@ UNIQUE (%@)", tableName, joined, escaped]; }
(NSArray<NSString > ) constraintsQueryFragmentForEntity:(NSEntityDescription *)entity error:(NSError **)error { // в случае ошибки возвращает nil, возврат пустого массива означает, что нет констраинтов
NSMutableArray<NSString > parts = [[NSMutableArray<NSString *> alloc] init];
if (entity.superentity) return parts;
NSArray<NSArray
for (id constraint in uniquenessConstraints ) { if ([constraint isKindOfClass:[NSArray class]]) { NSString part = [self constraintsQueryFragmentForUniqConstraint:(NSArray )constraint tableName:[self tableNameForEntity:entity] error:error]; if (part == nil) { [self reportUniquenessConstraintsMalformed:entity error:error]; return nil; } [parts addObject:part]; } else { [self reportUniquenessConstraintsMalformed:entity error:error]; return nil; } } return parts; }
Would that work to add uniqueness constraints as part of a managed object model migration?
As far as I can see migration calls createTableForEntity so it should work. But
To help anyone wanting to try this out, here's the code from @Izulle in an easier way to copy. This wasn't enough for me, as this does not cause Core Data to merge the conflicting objects.
typedef NS_ENUM(NSInteger, EncryptedStoreError) {
EncryptedStoreErrorIncorrectPasscode = 6000,
EncryptedStoreErrorMigrationFailed,
EncryptedStoreErrorUniquenessConstraintsMalformed,
};
- (BOOL)createTableForEntity:(NSEntityDescription *)entity error:(NSError **)error {
// Skip sub-entities since the super-entities should handle
// creating columns for all their children.
if (entity.superentity) {
return YES;
}
// prepare columns
NSMutableArray *columns = [NSMutableArray arrayWithObject:@"'__objectid' integer primary key"];
if (entity.subentities.count > 0) {
// NOTE: Will use '-[NSString hash]' to determine the entity type so we can use
// faster integer-indexed queries. Any string greater than 96-chars is
// not guaranteed to produce a unique hash value, but for entity names that
// shouldn't be a problem.
[columns addObject:@"'__entityType' integer"];
}
[columns addObjectsFromArray:[self columnNamesForEntity:entity indexedOnly:NO quotedNames:YES]];
NSArray<NSString *> *constraints = [self constraintsQueryFragmentForEntity:entity error:error];
if (constraints == nil) {
return NO;
}
// create table
NSString *string = nil;
if (constraints.count) {
string = [NSString stringWithFormat:
@"CREATE TABLE %@ (%@, %@);",
[self tableNameForEntity:entity],
[columns componentsJoinedByString:@", "],
[constraints componentsJoinedByString:@", "]];
} else {
string = [NSString stringWithFormat:
@"CREATE TABLE %@ (%@);",
[self tableNameForEntity:entity],
[columns componentsJoinedByString:@", "]];
}
NSLog(@"[VUZEDA-DEBUG] Create table query: %@", string);
sqlite3_stmt *statement = [self preparedStatementForQuery:string];
sqlite3_step(statement);
BOOL result = (statement != NULL && sqlite3_finalize(statement) == SQLITE_OK);
if (!result && error) {
*error = [self databaseError];
return result;
}
return [self createIndicesForEntity:entity error:error];
}
- (void)reportUniquenessConstraintsMalformed:(NSEntityDescription *)entity error:(NSError **)error {
if (error) {
*error = [NSError errorWithDomain:EncryptedStoreErrorDomain
code:EncryptedStoreErrorUniquenessConstraintsMalformed
userInfo:@{EncryptedStoreErrorMessageKey: [NSString stringWithFormat:@"Cannot parse uniqueness constraints for entity %@", entity.name]}];
}
}
- (NSString *)constraintsQueryFragmentForUniquenessConstraint:(NSArray *)contsraintColumns tableName:(NSString *)tableName error:(NSError **)error {
NSMutableArray<NSString *> *constraintElems = [[NSMutableArray<NSString *> alloc] init];
for (id element in contsraintColumns) {
if ([element isKindOfClass:[NSString class]]) {
[constraintElems addObject:(NSString *)element];
} else {
return nil;
}
}
NSString *joined = [constraintElems componentsJoinedByString:@"_"];
NSString *escaped = [NSString stringWithFormat:@"'%@'", [constraintElems componentsJoinedByString:@"','"]];
return [NSString stringWithFormat:@"CONSTRAINT %@_%@ UNIQUE (%@)", tableName, joined, escaped];
}
- (NSArray<NSString *> *)constraintsQueryFragmentForEntity:(NSEntityDescription *)entity error:(NSError **)error {
// Returns nil on error, returning an empty array means that there are no constraints
NSMutableArray<NSString *> *parts = [[NSMutableArray<NSString *> alloc] init];
if (entity.superentity) {
return parts;
}
NSArray<NSArray *> *uniquenessConstraints = [entity uniquenessConstraints];
if (!uniquenessConstraints.count) {
return parts;
}
for (id constraint in uniquenessConstraints) {
if ([constraint isKindOfClass:[NSArray class]]) {
NSString *part = [self constraintsQueryFragmentForUniquenessConstraint:(NSArray *)constraint tableName:[self tableNameForEntity:entity] error:error];
if (part == nil) {
[self reportUniquenessConstraintsMalformed:entity error:error];
return nil;
}
[parts addObject:part];
} else {
[self reportUniquenessConstraintsMalformed:entity error:error];
return nil;
}
}
return parts;
}
Uniqueness Constraints https://developer.apple.com/documentation/coredata/nsentitydescription/1425095-uniquenessconstraints?language=objc do not work with Encrypted Core Data SQLite Store. Instead of replacing the record with the same identifier for example I have multiple objects with the same identifier in my tables.