diff --git a/EncryptedCoreData.podspec b/EncryptedCoreData.podspec index 0aa4ca6..33ce9c9 100644 --- a/EncryptedCoreData.podspec +++ b/EncryptedCoreData.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'EncryptedCoreData' - s.version = '3.0' + s.version = '3.1' s.license = 'Apache-2.0' s.summary = 'iOS Core Data encrypted SQLite store using SQLCipher' @@ -12,17 +12,17 @@ Pod::Spec.new do |s| 'MITRE' => 'imas-proj-list@lists.mitre.org' } - s.source = { :git => 'https://github.com/project-imas/encrypted-core-data.git', :tag => '3.0' } + s.source = { :git => 'https://github.com/project-imas/encrypted-core-data.git', :tag => '3.1' } s.frameworks = ['CoreData', 'Security'] s.requires_arc = true - s.ios.deployment_target = '6.0' - s.osx.deployment_target = '10.8' + s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.10' s.source_files = 'Incremental Store/**/*.{h,m}' s.public_header_files = 'Incremental Store/EncryptedStore.h' - s.dependency 'SQLCipher', '~> 3.3.0' + s.dependency 'SQLCipher', '~> 3.4.0' s.xcconfig = { 'OTHER_CFLAGS' => '$(inherited) -DSQLITE_HAS_CODEC -DSQLCIPHER_CRYPTO_CC' diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 33dfe59..0f392d0 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -15,6 +15,10 @@ typedef sqlite3_stmt sqlite3_statement; +#ifndef SQLITE_DETERMINISTIC +#define SQLITE_DETERMINISTIC 0 +#endif + NSString * const EncryptedStoreType = @"EncryptedStore"; NSString * const EncryptedStorePassphraseKey = @"EncryptedStorePassphrase"; NSString * const EncryptedStoreErrorDomain = @"EncryptedStoreErrorDomain"; @@ -23,6 +27,9 @@ NSString * const EncryptedStoreCacheSize = @"EncryptedStoreCacheSize"; static void dbsqliteRegExp(sqlite3_context *context, int argc, const char **argv); +static void dbsqliteStringBetween(sqlite3_context *context, int argc, sqlite3_value **argv); +static void dbsqliteStringOperation(sqlite3_context *context, int argc, sqlite3_value **argv); + static NSString * const EncryptedStoreMetadataTableName = @"meta"; @@ -665,7 +672,9 @@ - (BOOL)loadMetadata:(NSError **)error { BOOL success = [self performInTransaction:^{ //enable regexp - sqlite3_create_function(database, "REGEXP", 2, SQLITE_ANY, NULL, (void *)dbsqliteRegExp, NULL, NULL); + sqlite3_create_function(database, "REGEXP", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteRegExp, NULL, NULL); + sqlite3_create_function(database, "ECDSTRINGOPERATION", 4, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, dbsqliteStringOperation, NULL, NULL); + sqlite3_create_function(database, "ECDSTRINGBETWEEN", 4, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, dbsqliteStringBetween, NULL, NULL); // ask if we have a metadata table BOOL hasTable = NO; @@ -698,28 +707,38 @@ - (BOOL)loadMetadata:(NSError **)error { NSDictionary *options = [self options]; if ([[options objectForKey:NSMigratePersistentStoresAutomaticallyOption] boolValue] && [[options objectForKey:NSInferMappingModelAutomaticallyOption] boolValue]) { + NSManagedObjectModel *newModel = [[self persistentStoreCoordinator] managedObjectModel]; + + // check that a migration is required first: + if ([newModel isConfiguration:nil compatibleWithStoreMetadata:metadata]){ + return YES; + } + + // load the old model: NSMutableArray *bundles = [NSMutableArray array]; [bundles addObject:[NSBundle mainBundle]]; - NSManagedObjectModel *oldModel = [NSManagedObjectModel - mergedModelFromBundles:bundles - forStoreMetadata:metadata]; - NSManagedObjectModel *newModel = [[self persistentStoreCoordinator] managedObjectModel]; + NSManagedObjectModel *oldModel = [NSManagedObjectModel mergedModelFromBundles:bundles + forStoreMetadata:metadata]; + if (oldModel && newModel) { - if (![oldModel isEqual:newModel]) { - // run migrations - if (![self migrateFromModel:oldModel toModel:newModel error:error]) { - return NO; - } - - // update metadata - NSMutableDictionary *mutableMetadata = [metadata mutableCopy]; - [mutableMetadata setObject:[newModel entityVersionHashesByName] forKey:NSStoreModelVersionHashesKey]; - [self setMetadata:mutableMetadata]; - if (![self saveMetadata]) { - if (error) { *error = [self databaseError]; } - return NO; - } + // no migration is needed if the old and new models are identical: + if ([[oldModel entityVersionHashesByName] isEqualToDictionary:[newModel entityVersionHashesByName]]) { + return YES; + } + + // run migrations + if (![self migrateFromModel:oldModel toModel:newModel error:error]) { + return NO; + } + + // update metadata + NSMutableDictionary *mutableMetadata = [metadata mutableCopy]; + [mutableMetadata setObject:[newModel entityVersionHashesByName] forKey:NSStoreModelVersionHashesKey]; + [self setMetadata:mutableMetadata]; + if (![self saveMetadata]) { + if (error) { *error = [self databaseError]; } + return NO; } } else { @@ -729,7 +748,7 @@ - (BOOL)loadMetadata:(NSError **)error { *error = [NSError errorWithDomain:EncryptedStoreErrorDomain code:EncryptedStoreErrorMigrationFailed userInfo:userInfo]; } return NO; - } + } } } @@ -1015,6 +1034,222 @@ static void dbsqliteRegExp(sqlite3_context *context, int argc, const char **argv sqlite3_result_int(context, (int)numberOfMatches); } +static void dbsqliteStringBetween(sqlite3_context *context, int argc, sqlite3_value **argv) { + @autoreleasepool { + assert(argc == 4); + + // if any of the operands is NULL, return NULL + if (sqlite3_value_type(argv[0]) == SQLITE_NULL || + sqlite3_value_type(argv[1]) == SQLITE_NULL || + sqlite3_value_type(argv[2]) == SQLITE_NULL) { + sqlite3_result_null(context); + return; + } + + const char *operand = (const char *)sqlite3_value_text(argv[1]); + const char *rangeLow = (const char *)sqlite3_value_text(argv[2]); + const char *rangeHigh = (const char *)sqlite3_value_text(argv[2]); + NSComparisonPredicateOptions options = (NSUInteger)sqlite3_value_int64(argv[3]); + + NSString *operandString = [[NSString alloc] initWithBytesNoCopy:(void *)operand + length:strlen(operand) + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + + NSString *rangeLowString = [[NSString alloc] initWithBytesNoCopy:(void *)rangeLow + length:strlen(rangeLow) + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + + NSString *rangeHighString = [[NSString alloc] initWithBytesNoCopy:(void *)rangeHigh + length:strlen(rangeHigh) + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + + NSStringCompareOptions comparisonOptions = 0; + + if (options & NSCaseInsensitivePredicateOption) { + comparisonOptions |= NSCaseInsensitiveSearch; + } + + if (options & NSDiacriticInsensitivePredicateOption) { + comparisonOptions |= NSDiacriticInsensitiveSearch; + } + + BOOL result = + [operandString compare:rangeLowString options:comparisonOptions] != NSOrderedAscending && + [rangeHighString compare:operandString options:comparisonOptions] != NSOrderedAscending; + + sqlite3_result_int(context, result); + } +} + +static NSString *formatComparisonPredicateOptions(NSComparisonPredicateOptions options) { + // special-case the most common options + if (options == 0) { + return @""; + } + + if (options == NSCaseInsensitivePredicateOption) { + return @"[c]"; + } + + // the general case + NSMutableString *optionsString = [NSMutableString stringWithCapacity:5]; + + [optionsString appendString:@"["]; + + if (options & NSCaseInsensitivePredicateOption) { + [optionsString appendString:@"c"]; + } + + if (options & NSDiacriticInsensitivePredicateOption) { + [optionsString appendString:@"d"]; + } + + if (options & NSNormalizedPredicateOption) { + [optionsString appendString:@"n"]; + } + + [optionsString appendString:@"]"]; + + return [optionsString copy]; +} + +static void dbsqliteStringOperation(sqlite3_context *context, int argc, sqlite3_value **argv) { + @autoreleasepool { + assert(argc == 4); + + // must have an operator + if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { + sqlite3_result_error(context, "NULL operator passed to ECDSTRINGOPERATION", -1); + return; + } + + // if any of the two operands is NULL, return NULL + if (sqlite3_value_type(argv[1]) == SQLITE_NULL || sqlite3_value_type(argv[2]) == SQLITE_NULL) { + sqlite3_result_null(context); + return; + } + + NSPredicateOperatorType operation = (NSUInteger)sqlite3_value_int64(argv[0]); + const char *operand1 = (const char *)sqlite3_value_text(argv[1]); + const char *operand2 = (const char *)sqlite3_value_text(argv[2]); + NSComparisonPredicateOptions options = (NSUInteger)sqlite3_value_int64(argv[3]); + + NSString *operand1String = [[NSString alloc] initWithBytesNoCopy:(void *)operand1 + length:strlen(operand1) + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + + NSString *operand2String = [[NSString alloc] initWithBytesNoCopy:(void *)operand2 + length:strlen(operand2) + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + + NSStringCompareOptions comparisonOptions = 0; + + if (options & NSNormalizedPredicateOption) { + comparisonOptions = NSLiteralSearch; + } + else { + if (options & NSCaseInsensitivePredicateOption) { + comparisonOptions |= NSCaseInsensitiveSearch; + } + + if (options & NSDiacriticInsensitivePredicateOption) { + comparisonOptions |= NSDiacriticInsensitiveSearch; + } + } + + switch (operation) { + case NSLessThanPredicateOperatorType: + sqlite3_result_int(context, [operand1String compare:operand2String options:comparisonOptions] == NSOrderedAscending); + return; + + case NSLessThanOrEqualToPredicateOperatorType: + sqlite3_result_int(context, [operand1String compare:operand2String options:comparisonOptions] != NSOrderedDescending); + return; + + case NSGreaterThanPredicateOperatorType: + sqlite3_result_int(context, [operand1String compare:operand2String options:comparisonOptions] == NSOrderedDescending); + return; + + case NSGreaterThanOrEqualToPredicateOperatorType: + sqlite3_result_int(context, [operand1String compare:operand2String options:comparisonOptions] != NSOrderedAscending); + return; + + case NSEqualToPredicateOperatorType: + sqlite3_result_int(context, [operand1String compare:operand2String options:comparisonOptions] == NSOrderedSame); + return; + + case NSNotEqualToPredicateOperatorType: + sqlite3_result_int(context, [operand1String compare:operand2String options:comparisonOptions] != NSOrderedSame); + return; + + case NSMatchesPredicateOperatorType: { + // TODO: we should probably memoize the predicate + NSString *matchesPredicateFormat = [NSString stringWithFormat:@"SELF MATCHES%@ %%@", + formatComparisonPredicateOptions(options)]; + + BOOL matchesResult = [[NSPredicate predicateWithFormat:matchesPredicateFormat, + operand2String] evaluateWithObject:operand1String]; + + sqlite3_result_int(context, !!matchesResult); + return; + } + + case NSLikePredicateOperatorType: { + // TODO: we should probably memoize the predicate + NSString *likePredicateFormat = [NSString stringWithFormat:@"SELF LIKE%@ %%@", + formatComparisonPredicateOptions(options)]; + + BOOL likeResult = [[NSPredicate predicateWithFormat:likePredicateFormat, + operand2String] evaluateWithObject:operand1String]; + + sqlite3_result_int(context, !!likeResult); + return; + } + + case NSBeginsWithPredicateOperatorType: { + NSRange range = [operand1String rangeOfString:operand2String + options:NSAnchoredSearch | comparisonOptions]; + + sqlite3_result_int(context, range.location != NSNotFound); + return; + } + + case NSEndsWithPredicateOperatorType: { + NSRange range = [operand1String rangeOfString:operand2String + options:NSAnchoredSearch | NSBackwardsSearch | comparisonOptions]; + + sqlite3_result_int(context, range.location != NSNotFound); + return; + } + + case NSInPredicateOperatorType: { + NSRange range = [operand2String rangeOfString:operand1String options:comparisonOptions]; + sqlite3_result_int(context, range.location != NSNotFound); + return; + } + + case NSContainsPredicateOperatorType: { + NSRange range = [operand1String rangeOfString:operand2String options:comparisonOptions]; + sqlite3_result_int(context, range.location != NSNotFound); + return; + } + + default: + break; + } + + NSString *errorString = [NSString stringWithFormat:@"unsupported operator type %lu for ECDSTRINGOPERATION", + (unsigned long)operation]; + + sqlite3_result_error(context, errorString.UTF8String, -1); + } +} + #pragma mark - migration helpers - (BOOL)migrateFromModel:(NSManagedObjectModel *)fromModel toModel:(NSManagedObjectModel *)toModel error:(NSError **)error { @@ -1374,13 +1609,21 @@ - (BOOL)alterTableForSourceEntity:(NSEntityDescription *)sourceEntity }]; // ensure we copy any relationships for sub entities that aren't included in the mapping - NSDictionary *allRelationships = [self relationshipsForEntity:rootSourceEntity]; - [allRelationships enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSRelationshipDescription *relationship, BOOL *stop) { - NSString *foreignKeyColumn = [self foreignKeyColumnForRelationshipName:relationship.name]; - if (![relationship isToMany] && ![sourceColumns containsObject:foreignKeyColumn]) { - [sourceColumns addObject:foreignKeyColumn]; - [destinationColumns addObject:foreignKeyColumn]; - } + // also make sure that the destination entity actually has such a relationship before the copy + NSDictionary *sourceRelationships = [self relationshipsForEntity:rootSourceEntity]; + NSDictionary *destinationRelationships = [self relationshipsForEntity:destinationEntity]; + + [sourceRelationships enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSRelationshipDescription *relationship, BOOL *stop) { + NSString *foreignKeyColumn = [self foreignKeyColumnForRelationshipName:relationship.name]; + if (![relationship isToMany] && ![sourceColumns containsObject:foreignKeyColumn]) { + + if ([destinationRelationships objectForKey:key]) { + [sourceColumns addObject:foreignKeyColumn]; + [destinationColumns addObject:foreignKeyColumn]; + } + + + } }]; // copy entity types for sub entity @@ -1585,7 +1828,7 @@ -(BOOL)relationships:(NSRelationshipDescription *)relationship firstIDColumn:(NS *firstIDColumn = [NSString stringWithFormat:format, [rootSourceEntity.name stringByAppendingString:@"_1"]]; *secondIDColumn = [NSString stringWithFormat:format, [rootDestinationEntity.name stringByAppendingString:@"_2"]]; *firstOrderColumn = [NSString stringWithFormat:orderFormat, [rootSourceEntity.name stringByAppendingString:@"_1"]]; - *firstOrderColumn = [NSString stringWithFormat:orderFormat, [rootDestinationEntity.name stringByAppendingString:@"_2"]]; + *secondOrderColumn = [NSString stringWithFormat:orderFormat, [rootDestinationEntity.name stringByAppendingString:@"_2"]]; return YES; } @@ -1625,7 +1868,7 @@ -(BOOL)previousRelationships:(NSRelationshipDescription *)relationship firstIDCo *firstIDColumn = [NSString stringWithFormat:format, [rootSourceEntity.name stringByAppendingString:@"_1"]]; *secondIDColumn = [NSString stringWithFormat:format, [rootDestinationEntity.name stringByAppendingString:@"_2"]]; *firstOrderColumn = [NSString stringWithFormat:orderFormat, [rootSourceEntity.name stringByAppendingString:@"_1"]]; - *firstOrderColumn = [NSString stringWithFormat:orderFormat, [rootDestinationEntity.name stringByAppendingString:@"_2"]]; + *secondOrderColumn = [NSString stringWithFormat:orderFormat, [rootDestinationEntity.name stringByAppendingString:@"_2"]]; return YES; } @@ -1999,11 +2242,6 @@ - (BOOL)handleUpdatedRelationInSaveRequest:(NSRelationshipDescription *)relation // Inverse NSSet *inverseObjects = [object valueForKey:[relationship name]]; - if ([inverseObjects count] == 0) { - // No objects to add so finish - return YES; - } - NSString *tableName = [self tableNameForRelationship:relationship]; NSString *firstIDColumn, *secondIDColumn, *firstOrderColumn, *secondOrderColumn; @@ -2866,6 +3104,11 @@ - (NSDictionary *)whereClauseWithFetchRequest:(NSFetchRequest *)request { return result; } +static BOOL isCollection(Class class) +{ + return [class conformsToProtocol:@protocol(NSFastEnumeration)]; +} + - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request predicate:(NSPredicate *)predicate { // enum { @@ -2941,36 +3184,141 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request // left expression id leftOperand = nil; + Class leftOperandClass = Nil; id leftBindings = nil; [self parseExpression:comparisonPredicate.leftExpression inPredicate:comparisonPredicate inFetchRequest:request operator:operator operand:&leftOperand + ofType:&leftOperandClass bindings:&leftBindings]; // right expression id rightOperand = nil; + Class rightOperandClass = Nil; id rightBindings = nil; [self parseExpression:comparisonPredicate.rightExpression inPredicate:comparisonPredicate inFetchRequest:request operator:operator operand:&rightOperand + ofType:&rightOperandClass bindings:&rightBindings]; // build result and return - if (rightOperand && !rightBindings) { - if([[operator objectForKey:@"operator"] isEqualToString:@"!="]) { - query = [@[leftOperand, @"IS NOT", rightOperand] componentsJoinedByString:@" "]; + if ([rightOperandClass isEqual:[NSNull class]] && (comparisonPredicate.predicateOperatorType == NSEqualToPredicateOperatorType || comparisonPredicate.predicateOperatorType == NSNotEqualToPredicateOperatorType)) { + if(comparisonPredicate.predicateOperatorType == NSNotEqualToPredicateOperatorType) { + query = [@[leftOperand, @"IS NOT NULL"] componentsJoinedByString:@" "]; } else { - query = [@[leftOperand, @"IS", rightOperand] componentsJoinedByString:@" "]; + query = [@[leftOperand, @"IS NULL"] componentsJoinedByString:@" "]; + } + } + else if ([leftOperandClass isEqual:[NSNull class]] && (comparisonPredicate.predicateOperatorType == NSEqualToPredicateOperatorType || comparisonPredicate.predicateOperatorType == NSNotEqualToPredicateOperatorType)) { + if(comparisonPredicate.predicateOperatorType == NSNotEqualToPredicateOperatorType) { + query = [@[rightOperand, @"IS NOT NULL"] componentsJoinedByString:@" "]; + } else { + query = [@[rightOperand, @"IS NULL"] componentsJoinedByString:@" "]; } } else { - query = [@[leftOperand, [operator objectForKey:@"operator"], rightOperand] componentsJoinedByString:@" "]; - if ([[operator objectForKey:@"operator"] isEqualToString:@"LIKE"]) { - query = [query stringByAppendingString:@" ESCAPE '\\'"]; + BOOL isStringOperation = NO; + + // Note: SQLite's LIKE is case-insensitive, so if we want a case-sensitive LIKE, we have to use ECDSTRINGOPERATION + if (comparisonPredicate.options || comparisonPredicate.predicateOperatorType == NSLikePredicateOperatorType) { + switch (comparisonPredicate.predicateOperatorType) { + case NSLessThanPredicateOperatorType: + case NSLessThanOrEqualToPredicateOperatorType: + case NSGreaterThanPredicateOperatorType: + case NSGreaterThanOrEqualToPredicateOperatorType: + case NSEqualToPredicateOperatorType: + case NSNotEqualToPredicateOperatorType: + case NSMatchesPredicateOperatorType: + case NSLikePredicateOperatorType: + case NSBeginsWithPredicateOperatorType: + case NSEndsWithPredicateOperatorType: + case NSInPredicateOperatorType: + case NSContainsPredicateOperatorType: { + if (comparisonPredicate.predicateOperatorType == NSInPredicateOperatorType) { + if (rightOperandClass == Nil || isCollection(rightOperandClass)) { + // not an error, this IN is just not a string operation + break; + } + } + + if (comparisonPredicate.predicateOperatorType == NSContainsPredicateOperatorType) { + if (leftOperandClass == Nil || isCollection(leftOperandClass)) { + // not an error, this CONTAINS is just not a string operation + break; + } + } + + isStringOperation = YES; + + // Re-parse the expressions with a dummy operator that formats the operand/bindings verbatim + operator = @{ @"operator" : @"ECDSTRINGOPERATION", @"format" : @"%@" }; + + // left expression + leftOperand = nil; + leftOperandClass = Nil; + leftBindings = nil; + [self parseExpression:comparisonPredicate.leftExpression + inPredicate:comparisonPredicate + inFetchRequest:request + operator:operator + operand:&leftOperand + ofType:&leftOperandClass + bindings:&leftBindings]; + + // right expression + rightOperand = nil; + rightOperandClass = Nil; + rightBindings = nil; + [self parseExpression:comparisonPredicate.rightExpression + inPredicate:comparisonPredicate + inFetchRequest:request + operator:operator + operand:&rightOperand + ofType:&rightOperandClass + bindings:&rightBindings]; + + query = [NSString stringWithFormat:@"ECDSTRINGOPERATION(%@, %@, %@, %@)", + @(comparisonPredicate.predicateOperatorType), + leftOperand, + rightOperand, + @(comparisonPredicate.options)]; + + break; + } + + case NSBetweenPredicateOperatorType: { + if (comparisonPredicate.rightExpression.expressionType != NSConstantValueExpressionType || + ![comparisonPredicate.rightExpression.constantValue isKindOfClass:[NSArray class]]) { + // TODO: we should emit a warning if we can't handle the operand types + break; + } + + NSArray *range = comparisonPredicate.rightExpression.constantValue; + rightBindings = @[range[0], range[1]]; + isStringOperation = YES; + + query = [NSString stringWithFormat:@"ECDSTRINGBETWEEN(%@, ?, ?, %@)", + leftOperand, + @(comparisonPredicate.options)]; + + break; + } + + default: + break; + } + } + + if (!isStringOperation) { + query = [@[leftOperand, [operator objectForKey:@"operator"], rightOperand] componentsJoinedByString:@" "]; + if ([[operator objectForKey:@"operator"] isEqualToString:@"LIKE"]) { + query = [query stringByAppendingString:@" ESCAPE '\\'"]; + } } } @@ -3068,6 +3416,7 @@ - (void)parseExpression:(NSExpression *)expression inFetchRequest:(NSFetchRequest *)request operator:(NSDictionary *)operator operand:(id *)operand + ofType:(Class *)operandType bindings:(id *)bindings { NSExpressionType type = [expression expressionType]; @@ -3111,8 +3460,12 @@ - (void)parseExpression:(NSExpression *)expression [self tableNameForEntity:entity], [self foreignKeyColumnForRelationship:property]]; } + *operandType = [NSManagedObjectID class]; } else if (property != nil) { + if ([property isKindOfClass:[NSAttributeDescription class]]) { + *operandType = NSClassFromString(((NSAttributeDescription *)property).attributeValueClassName); + } value = [NSString stringWithFormat:@"%@.%@", [self tableNameForEntity:entity], value]; @@ -3176,6 +3529,7 @@ - (void)parseExpression:(NSExpression *)expression } } } + *operandType = [NSNumber class]; value = [NSString stringWithFormat:@"LENGTH(%@)", [[pathComponents subarrayWithRange:NSMakeRange(0, pathComponents.count - 1)] componentsJoinedByString:@"."]]; foundPredicate = YES; } @@ -3200,6 +3554,7 @@ - (void)parseExpression:(NSExpression *)expression rel.destinationEntity.typeHash]]; } value = [value stringByAppendingString:@")"]; + *operandType = [NSNumber class]; foundPredicate = YES; } @@ -3215,7 +3570,6 @@ - (void)parseExpression:(NSExpression *)expression NSEntityDescription * entity = [property destinationEntity]; subProperties = entity.propertiesByName; } else { - property = nil; *stop = YES; } }]; @@ -3223,6 +3577,10 @@ - (void)parseExpression:(NSExpression *)expression if ([property isKindOfClass:[NSRelationshipDescription class]]) { [request setReturnsDistinctResults:YES]; lastComponentName = @"__objectID"; + *operandType = [NSManagedObjectID class]; + } + else if ([property isKindOfClass:[NSAttributeDescription class]]) { + *operandType = NSClassFromString(((NSAttributeDescription *)property).attributeValueClassName); } value = [NSString stringWithFormat:@"%@.%@", @@ -3245,10 +3603,12 @@ - (void)parseExpression:(NSExpression *)expression *operand = [NSString stringWithFormat: [operator objectForKey:@"format"], [parameters componentsJoinedByString:@", "]]; + *operandType = [value class]; } else if ([value isKindOfClass:[NSDate class]]) { *bindings = value; *operand = @"?"; + *operandType = [value class]; } else if ([value isKindOfClass:[NSArray class]]) { if (predicate.predicateOperatorType == NSBetweenPredicateOperatorType) { @@ -3262,28 +3622,18 @@ - (void)parseExpression:(NSExpression *)expression [operator objectForKey:@"format"], [parameters componentsJoinedByString:@", "]]; } + *operandType = [value class]; } else if ([value isKindOfClass:[NSString class]]) { - if ([predicate options] & NSCaseInsensitivePredicateOption) { - *operand = @"UPPER(?)"; - if ([[operator objectForKey:@"operator"] isEqualToString:@"LIKE"]) { - BOOL isLike = predicate.predicateOperatorType == NSLikePredicateOperatorType; - value = [self escapedString:value allowWildcards:isLike]; - } - *bindings = [NSString stringWithFormat: - [operator objectForKey:@"format"], - [value uppercaseString]]; - } - else { - *operand = @"?"; - if ([[operator objectForKey:@"operator"] isEqualToString:@"LIKE"]) { - BOOL isLike = predicate.predicateOperatorType == NSLikePredicateOperatorType; - value = [self escapedString:value allowWildcards:isLike]; - } - *bindings = [NSString stringWithFormat: - [operator objectForKey:@"format"], - value]; + *operand = @"?"; + if ([[operator objectForKey:@"operator"] isEqualToString:@"LIKE"]) { + BOOL isLike = predicate.predicateOperatorType == NSLikePredicateOperatorType; + value = [self escapedString:value allowWildcards:isLike]; } + *bindings = [NSString stringWithFormat: + [operator objectForKey:@"format"], + value]; + *operandType = [NSString class]; } else if ([value isKindOfClass:[NSManagedObject class]] || [value isKindOfClass:[NSManagedObjectID class]]) { NSManagedObjectID * objectId = [value isKindOfClass:[NSManagedObject class]] ? [value objectID]:value; *operand = @"?"; @@ -3294,14 +3644,17 @@ - (void)parseExpression:(NSExpression *)expression } else { *bindings = value; } + *operandType = [NSManagedObjectID class]; } else if (!value || value == [NSNull null]) { *bindings = nil; *operand = @"NULL"; + *operandType = [NSNull class]; } else { *bindings = value; *operand = @"?"; + *operandType = [value class]; } } @@ -3358,6 +3711,25 @@ - (NSString*) columnForFunctionExpressionDescription: (NSExpressionDescription*) NSArray *arguments = expression.arguments; return [NSString stringWithFormat:@"sum(%@)",[arguments componentsJoinedByString:@","]]; } + else if([function isEqualToString:@"average:"]) { + NSArray *arguments = expression.arguments; + return [NSString stringWithFormat:@"avg(%@)",[arguments componentsJoinedByString:@","]]; + } + + else if([function isEqualToString:@"count:"]) { + NSArray *arguments = expression.arguments; + return [NSString stringWithFormat:@"count(%@)",[arguments componentsJoinedByString:@","]]; + } + + else if([function isEqualToString:@"min:"]) { + NSArray *arguments = expression.arguments; + return [NSString stringWithFormat:@"min(%@)",[arguments componentsJoinedByString:@","]]; + } + + else if([function isEqualToString:@"max:"]) { + NSArray *arguments = expression.arguments; + return [NSString stringWithFormat:@"max(%@)",[arguments componentsJoinedByString:@","]]; + } return nil; diff --git a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m index c68e3d3..1ef2cf3 100644 --- a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m +++ b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m @@ -928,4 +928,80 @@ -(void)test_predicateCompound }]; } +- (void)test_aggregateFunctions { + NSArray *data = @[@1, @2, @3, @4, @7]; + + for (NSNumber *obj in data) { + NSManagedObject *add = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:context]; + [add setValue:obj forKey:@"age"]; + } + [context save:nil]; + + NSSet*(^query)(NSString*) = ^(NSString *function){ + NSExpressionDescription *expressionDescription = [NSExpressionDescription new]; + expressionDescription.name = @"age"; + expressionDescription.expression = [NSExpression expressionForFunction:function arguments:@[[NSExpression expressionForKeyPath:@"age"]]]; + expressionDescription.expressionResultType = NSDoubleAttributeType; + NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName:@"User"]; + req.propertiesToFetch = @[expressionDescription]; + req.resultType = NSDictionaryResultType; + + NSDictionary * result = [context executeFetchRequest:req error:nil].firstObject; + return result[@"age"]; + }; + + XCTAssertEqualObjects(query(@"sum:"), @17); + XCTAssertEqualObjects(query(@"count:"), @5); + XCTAssertEqualObjects(query(@"min:"), @1); + XCTAssertEqualObjects(query(@"max:"), @7); + XCTAssertEqualObjects(query(@"average:"), @3.4); + + //unsupported in default sqlite store + //XCTAssertEqualObjects(query(@"median:"), @0); + //XCTAssertEqualObjects(query(@"mode:"), @0); + //XCTAssertEqualObjects(query(@"stddev:"), @0); +} + +- (void)test_stringComparision { + NSArray *data = @[@"testa", @"testą", @"TESTĄ", @"TESTA"]; + + for (NSString *obj in data) { + NSManagedObject *add = [NSEntityDescription insertNewObjectForEntityForName:@"Post" inManagedObjectContext:context]; + [add setValue:obj forKey:@"title"]; + } + [context save:nil]; + + NSSet*(^query)(NSString*, NSString*) = ^(NSString *query, NSString *value){ + NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName:@"Post"]; + req.predicate = [NSPredicate predicateWithFormat:query, value]; + NSArray *queryResult = [context executeFetchRequest:req error:nil]; + NSMutableSet *result = [NSMutableSet new]; + + for (NSManagedObject *obj in queryResult) { + [result addObject:[obj valueForKey:@"title"]]; + } + + return result; + }; + NSArray* expected; + + expected = @[@"testa"]; + XCTAssertEqualObjects(query(@"title like %@", @"testa"), [NSSet setWithArray:expected]); + + expected = @[@"testa", @"TESTA"]; + XCTAssertEqualObjects(query(@"title like[c] %@", @"testa"), [NSSet setWithArray:expected]); + + expected = @[@"testa", @"testą"]; + XCTAssertEqualObjects(query(@"title like[d] %@", @"testa"), [NSSet setWithArray:expected]); + + expected = data; + XCTAssertEqualObjects(query(@"title like[cd] %@", @"testa"), [NSSet setWithArray:expected]); + + expected = @[@"testą"]; + XCTAssertEqualObjects(query(@"title like %@", @"testą"), [NSSet setWithArray:expected]); + + expected = data; + XCTAssertEqualObjects(query(@"TRUEPREDICATE", nil), [NSSet setWithArray:expected]); +} + @end diff --git a/exampleProjects/IncrementalStore/Tests/RelationTests.m b/exampleProjects/IncrementalStore/Tests/RelationTests.m index 0555fb3..e371d9a 100644 --- a/exampleProjects/IncrementalStore/Tests/RelationTests.m +++ b/exampleProjects/IncrementalStore/Tests/RelationTests.m @@ -254,15 +254,25 @@ -(void)createObjectGraph } -(ISDRoot *)fetchRootObject +{ + return [self fetchRootObjectByName:@"root"]; +} + +-(ISDRoot *)fetchManyRootObject +{ + return [self fetchRootObjectByName:@"manyRoot"]; +} + +-(ISDRoot *)fetchRootObjectByName:(NSString *)name { NSError *error = nil; NSFetchRequest *request = [ISDRoot fetchRequest]; - request.predicate = [NSPredicate predicateWithFormat:@"name == %@", @"root"]; + request.predicate = [NSPredicate predicateWithFormat:@"name == %@", name]; NSArray *results = [context executeFetchRequest:request error:&error]; XCTAssertNotNil(results, @"Could not execute fetch request."); XCTAssertEqual([results count], (NSUInteger)1, @"The number of root objects is wrong."); ISDRoot *root = [results firstObject]; - XCTAssertEqualObjects(root.name, @"root", @"The name of the root object is wrong."); + XCTAssertEqualObjects(root.name, name, @"The name of the root object is wrong. It should be '%@' but it is '%@'", name, [root.name copy]); return root; } @@ -337,6 +347,31 @@ -(void)testFetchingManyToManyFromDatabase [self checkManyToManyWithChildACount:2 childBCount:3]; } +- (void)testDeleteAllManyToManyFromDatabase +{ + // Remove all many to many relationships + ISDRoot *fetchedRoot = [self fetchRootObject]; + [fetchedRoot removeManyToMany:fetchedRoot.manyToMany]; + ISDRoot *fetchedManyRoot = [self fetchManyRootObject]; + [fetchedManyRoot removeManyToMany:fetchedManyRoot.manyToMany]; + + XCTAssertEqual(0, fetchedRoot.manyToMany.count + fetchedManyRoot.manyToMany.count, @"The total number of many-to-many relationships after removing all of them in memory should be 0"); + + // Save into database + NSError *error = nil; + BOOL save = [context save:&error]; + XCTAssertTrue(save, @"Unable to perform save.\n%@", error); + + // Invalidate any existing NSManagedObject + [context reset]; + fetchedRoot = nil; + fetchedManyRoot = nil; + + // Verify that relationships have been removed from database + fetchedRoot = [self fetchRootObject]; + fetchedManyRoot = [self fetchManyRootObject]; + XCTAssertEqual(0, fetchedRoot.manyToMany.count + fetchedManyRoot.manyToMany.count, @"The total number of relationships when fetching from database after removing all of them & saving should be 0"); +} /** Multiple one-to-many is designed to test the case where one entity (Root) has two one-to-many