From fca11ea3853c87faeceb78ce1cd728bdf9204ebb Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Fri, 18 Mar 2016 18:05:22 +0100 Subject: [PATCH 01/27] Implemented case-insensitive, diacritic-insensitive and normalized operations for strings. Two user-defined functions are registered with sqlcipher: ECDSTRINGBETWEEN, which implements the BETWEEN operator, and ECDSTRINGOPERATION which implements all other operators. --- Incremental Store/EncryptedStore.m | 318 +++++++++++++++++++++++++++-- 1 file changed, 296 insertions(+), 22 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 33dfe59..30e3bd6 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -23,6 +23,8 @@ 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"; @@ -666,6 +668,8 @@ - (BOOL)loadMetadata:(NSError **)error { //enable regexp sqlite3_create_function(database, "REGEXP", 2, SQLITE_ANY, 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; @@ -1015,6 +1019,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 = 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 = 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 = 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); + break; + } + + 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 { @@ -2968,9 +3188,75 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request } } else { - query = [@[leftOperand, [operator objectForKey:@"operator"], rightOperand] componentsJoinedByString:@" "]; - if ([[operator objectForKey:@"operator"] isEqualToString:@"LIKE"]) { - query = [query stringByAppendingString:@" ESCAPE '\\'"]; + BOOL isStringOperation = NO; + + if (comparisonPredicate.options) { + 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 (comparisonPredicate.rightExpression.expressionType != NSConstantValueExpressionType || ![comparisonPredicate.rightExpression.constantValue isKindOfClass:[NSString class]]) { + // not an error, this IN is just not a string operation + break; + } + } + + if (comparisonPredicate.predicateOperatorType == NSContainsPredicateOperatorType) { + if (comparisonPredicate.leftExpression.expressionType != NSConstantValueExpressionType || ![comparisonPredicate.leftExpression.constantValue isKindOfClass:[NSString class]]) { + // not an error, this CONTAINS is just not a string operation + break; + } + } + + isStringOperation = YES; + + 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 '\\'"]; + } } } @@ -3264,26 +3550,14 @@ - (void)parseExpression:(NSExpression *)expression } } 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]; } else if ([value isKindOfClass:[NSManagedObject class]] || [value isKindOfClass:[NSManagedObjectID class]]) { NSManagedObjectID * objectId = [value isKindOfClass:[NSManagedObject class]] ? [value objectID]:value; *operand = @"?"; From 15325bc0d275747779085ec16cd6578bb87bd6aa Mon Sep 17 00:00:00 2001 From: Richard Morton Date: Fri, 18 Mar 2016 17:08:13 +0000 Subject: [PATCH 02/27] * Fixed issue where having equivalent models when performing a lightweight migration caused the migration to incorrectly return NO and trigger an assertion without error. - Method now relies on the entity version hashes and does not append the test for equivalence onto the check for old and new models. --- Incremental Store/EncryptedStore.m | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 33dfe59..ac80671 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -704,22 +704,26 @@ - (BOOL)loadMetadata:(NSError **)error { mergedModelFromBundles:bundles forStoreMetadata:metadata]; NSManagedObjectModel *newModel = [[self persistentStoreCoordinator] managedObjectModel]; + + // this is not an error, no migration is needed + if ([[oldModel entityVersionHashesByName] isEqualToDictionary:[newModel entityVersionHashesByName]]) { + return YES; + } + 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; - } + // 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 { From 788db035ac1e7a40231cf3851e7246e9ade86f73 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Fri, 18 Mar 2016 18:05:22 +0100 Subject: [PATCH 03/27] Implemented case-insensitive, diacritic-insensitive and normalized operations for strings. Two user-defined functions are registered with sqlcipher: ECDSTRINGBETWEEN, which implements the BETWEEN operator, and ECDSTRINGOPERATION which implements all other operators. --- Incremental Store/EncryptedStore.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 30e3bd6..4f15274 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"; From 392644a7828f586aafa9a2bc710c5f1666a5404f Mon Sep 17 00:00:00 2001 From: Richard Morton Date: Tue, 29 Mar 2016 09:54:23 +0100 Subject: [PATCH 04/27] #197 addPersistentStoreWithType return nil, but error is nil too - Fixed issue where using NSMigratePersistentStoresAutomaticallyOption without an existing managed object model caused a crash. - Added compatibility check for persistent store coordinator's managed object model. --- Incremental Store/EncryptedStore.m | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index ac80671..d576996 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -698,20 +698,26 @@ - (BOOL)loadMetadata:(NSError **)error { NSDictionary *options = [self options]; if ([[options objectForKey:NSMigratePersistentStoresAutomaticallyOption] boolValue] && [[options objectForKey:NSInferMappingModelAutomaticallyOption] boolValue]) { - NSMutableArray *bundles = [NSMutableArray array]; - [bundles addObject:[NSBundle mainBundle]]; - NSManagedObjectModel *oldModel = [NSManagedObjectModel - mergedModelFromBundles:bundles - forStoreMetadata:metadata]; NSManagedObjectModel *newModel = [[self persistentStoreCoordinator] managedObjectModel]; - // this is not an error, no migration is needed - if ([[oldModel entityVersionHashesByName] isEqualToDictionary:[newModel entityVersionHashesByName]]) { + // 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]; + if (oldModel && newModel) { + // 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; @@ -733,7 +739,7 @@ - (BOOL)loadMetadata:(NSError **)error { *error = [NSError errorWithDomain:EncryptedStoreErrorDomain code:EncryptedStoreErrorMigrationFailed userInfo:userInfo]; } return NO; - } + } } } From 1680237a0c3aab40db56da04021fb6ff45f720ad Mon Sep 17 00:00:00 2001 From: Daniel Broad Date: Fri, 13 May 2016 11:35:08 +0100 Subject: [PATCH 05/27] 3.1 --- EncryptedCoreData.podspec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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' From e9879d79a8dc09ef15332b2a1e834a302e11e653 Mon Sep 17 00:00:00 2001 From: Matan Poreh Date: Thu, 19 May 2016 16:54:57 +0300 Subject: [PATCH 06/27] added fix for bug when doing a lightweight migration where update entity has less relationships than the old entity -bug was first discovered when doing migrate and then calling "addPersistentStoreWithType.." which returned nil and also error nil --- Incremental Store/EncryptedStore.m | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index d576996..7d6bedb 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -1384,13 +1384,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 From f136b1f534f0b8b12e4c6a4692b0cbffa4c43a7a Mon Sep 17 00:00:00 2001 From: Pawel Szot Date: Thu, 2 Jun 2016 12:54:20 +0200 Subject: [PATCH 07/27] implement more aggregate functions --- Incremental Store/EncryptedStore.m | 19 +++++++++++ .../Tests/IncrementalStoreTests.m | 34 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index d576996..a994c55 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3368,6 +3368,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..25aeb77 100644 --- a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m +++ b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m @@ -928,4 +928,38 @@ -(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); +} + @end From 5827b72a1220777c4960ef1ba4e90c7d92e4e926 Mon Sep 17 00:00:00 2001 From: Pawel Szot Date: Thu, 2 Jun 2016 12:24:05 +0200 Subject: [PATCH 08/27] implement string comparision operators --- Incremental Store/EncryptedStore.m | 110 +++++++++++++++--- .../Tests/IncrementalStoreTests.m | 42 +++++++ 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 988ee06..420f459 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -23,6 +23,9 @@ NSString * const EncryptedStoreCacheSize = @"EncryptedStoreCacheSize"; static void dbsqliteRegExp(sqlite3_context *context, int argc, const char **argv); +static void dbsqliteStripCase(sqlite3_context *context, int argc, const char **argv); +static void dbsqliteStripDiacritics(sqlite3_context *context, int argc, const char **argv); +static void dbsqliteStripCaseDiacritics(sqlite3_context *context, int argc, const char **argv); static NSString * const EncryptedStoreMetadataTableName = @"meta"; @@ -664,9 +667,38 @@ - (BOOL)loadMetadata:(NSError **)error { // load metadata BOOL success = [self performInTransaction:^{ + //make 'LIKE' case-sensitive + sqlite3_stmt *setPragma = [self preparedStatementForQuery:@"PRAGMA case_sensitive_like = true;"]; + if (!setPragma || sqlite3_step(setPragma) != SQLITE_DONE || sqlite3_finalize(setPragma) != SQLITE_OK) { + return NO; + } + +#ifdef SQLITE_DETERMINISTIC //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); + //enable case insentitivity + sqlite3_create_function(database, "STRIP_CASE", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripCase, NULL, NULL); + + //enable diacritic insentitivity + sqlite3_create_function(database, "STRIP_DIACRITICS", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripDiacritics, NULL, NULL); + + //enable combined case and diacritic insentitivity + sqlite3_create_function(database, "STRIP_CASE_DIACRITICS", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripCaseDiacritics, NULL, NULL); +#else + //enable regexp + sqlite3_create_function(database, "REGEXP", 2, SQLITE_UTF8, NULL, (void *)dbsqliteRegExp, NULL, NULL); + + //enable case insentitivity + sqlite3_create_function(database, "STRIP_CASE", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripCase, NULL, NULL); + + //enable diacritic insentitivity + sqlite3_create_function(database, "STRIP_DIACRITICS", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripDiacritics, NULL, NULL); + + //enable combined case and diacritic insentitivity + sqlite3_create_function(database, "STRIP_CASE_DIACRITICS", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripCaseDiacritics, NULL, NULL); +#endif + // ask if we have a metadata table BOOL hasTable = NO; if (![self hasMetadataTable:&hasTable error:error]) { return NO; } @@ -1025,6 +1057,51 @@ static void dbsqliteRegExp(sqlite3_context *context, int argc, const char **argv sqlite3_result_int(context, (int)numberOfMatches); } +static void dbsqliteStripCase(sqlite3_context *context, int argc, const char **argv) { + assert(argc == 1); + NSString* string; + const char *aux = (const char *)sqlite3_value_text((sqlite3_value*)argv[0]); + + /*Safeguard against null returns*/ + if (aux) { + string = [NSString stringWithUTF8String:aux]; + string = [string stringByFoldingWithOptions:NSCaseInsensitiveSearch locale:nil]; + sqlite3_result_text(context, [string UTF8String], -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_text(context, nil, -1, NULL); + } +} + +static void dbsqliteStripDiacritics(sqlite3_context *context, int argc, const char **argv) { + assert(argc == 1); + NSString* string; + const char *aux = (const char *)sqlite3_value_text((sqlite3_value*)argv[0]); + + /*Safeguard against null returns*/ + if (aux) { + string = [NSString stringWithUTF8String:aux]; + string = [string stringByFoldingWithOptions:NSDiacriticInsensitiveSearch locale:nil]; + sqlite3_result_text(context, [string UTF8String], -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_text(context, nil, -1, NULL); + } +} + +static void dbsqliteStripCaseDiacritics(sqlite3_context *context, int argc, const char **argv) { + assert(argc == 1); + NSString* string; + const char *aux = (const char *)sqlite3_value_text((sqlite3_value*)argv[0]); + + /*Safeguard against null returns*/ + if (aux) { + string = [NSString stringWithUTF8String:aux]; + string = [string stringByFoldingWithOptions:NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch locale:nil]; + sqlite3_result_text(context, [string UTF8String], -1, SQLITE_TRANSIENT); + } else { + sqlite3_result_text(context, nil, -1, NULL); + } +} + #pragma mark - migration helpers - (BOOL)migrateFromModel:(NSManagedObjectModel *)fromModel toModel:(NSManagedObjectModel *)toModel error:(NSError **)error { @@ -3247,8 +3324,17 @@ - (void)parseExpression:(NSExpression *)expression [self joinedTableNameForComponents:[pathComponents subarrayWithRange:NSMakeRange(0, pathComponents.count -1)] forRelationship:NO], lastComponentName]; } } + NSComparisonPredicateOptions options = [predicate options]; + if ((options & NSCaseInsensitivePredicateOption) && (options & NSDiacriticInsensitivePredicateOption)) { + *operand = [@[@"STRIP_CASE_DIACRITICS(", value, @")"] componentsJoinedByString:@""]; + } else if (options & NSCaseInsensitivePredicateOption) { + *operand = [@[@"STRIP_CASE(", value, @")"] componentsJoinedByString:@""]; + } else if (options & NSDiacriticInsensitivePredicateOption) { + *operand = [@[@"STRIP_DIACRITICS(", value, @")"] componentsJoinedByString:@""]; + } else { *operand = value; } + } else if (type == NSEvaluatedObjectExpressionType) { *operand = @"__objectid"; } @@ -3282,26 +3368,20 @@ - (void)parseExpression:(NSExpression *)expression } } 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]]; + NSComparisonPredicateOptions options = [predicate options]; + if ((options & NSCaseInsensitivePredicateOption) && (options & NSDiacriticInsensitivePredicateOption)) { + value = [value stringByFoldingWithOptions:NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch locale:nil]; + } else if (options & NSCaseInsensitivePredicateOption) { + value = [value stringByFoldingWithOptions:NSCaseInsensitiveSearch locale:nil]; + } else if (options & NSDiacriticInsensitivePredicateOption) { + value = [value stringByFoldingWithOptions:NSDiacriticInsensitiveSearch locale:nil]; } - 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]; - } + *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 = @"?"; diff --git a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m index 25aeb77..1ef2cf3 100644 --- a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m +++ b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m @@ -962,4 +962,46 @@ - (void)test_aggregateFunctions { //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 From 7a20c67ff3605be7fe54ead2e297d932e7c3dd51 Mon Sep 17 00:00:00 2001 From: Nacho Date: Tue, 28 Jun 2016 15:09:45 +0100 Subject: [PATCH 09/27] Fix for issue removing all objects from a many-to-many relationship --- Incremental Store/EncryptedStore.m | 5 --- .../IncrementalStore/Tests/RelationTests.m | 39 ++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 420f459..fa45ff8 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -2094,11 +2094,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; 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 From 2b0cb7902cebf29bee005b38fe493dd4d62cc566 Mon Sep 17 00:00:00 2001 From: Jarek Gajak Date: Fri, 22 Jul 2016 20:27:01 +0200 Subject: [PATCH 10/27] Removed second assignment to firstOrderColumn, changed it to assignment to secondOrderColumn --- Incremental Store/EncryptedStore.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 420f459..610b609 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -1680,7 +1680,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; } From 305b34904b7e8dbd1eda50130179590a2cc1f628 Mon Sep 17 00:00:00 2001 From: Jarek Gajak Date: Mon, 25 Jul 2016 13:19:31 +0200 Subject: [PATCH 11/27] Fixed doubled assignment to firstOrderColumn in previousRelationships method --- Incremental Store/EncryptedStore.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 36d415d..f26d1ed 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -1720,7 +1720,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; } From 9e29ff1fd8d34c8ee04b6d7a759ac2730bd7bcfd Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:37:19 +0200 Subject: [PATCH 12/27] NSPredicateOperatorType and NSComparisonPredicateOptions have underlying type NSUInteger, which is 32 bits wide on 32 bit platforms. Explicitly cast the result value of sqlite2_value_int64 to NSUInteger to fix implicit truncation warnings on 32 bit --- Incremental Store/EncryptedStore.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 4f15274..ff0cfbf 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -1038,7 +1038,7 @@ static void dbsqliteStringBetween(sqlite3_context *context, int argc, sqlite3_va 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 = sqlite3_value_int64(argv[3]); + NSComparisonPredicateOptions options = (NSUInteger)sqlite3_value_int64(argv[3]); NSString *operandString = [[NSString alloc] initWithBytesNoCopy:(void *)operand length:strlen(operand) @@ -1121,10 +1121,10 @@ static void dbsqliteStringOperation(sqlite3_context *context, int argc, sqlite3_ return; } - NSPredicateOperatorType operation = sqlite3_value_int64(argv[0]); + 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 = sqlite3_value_int64(argv[3]); + NSComparisonPredicateOptions options = (NSUInteger)sqlite3_value_int64(argv[3]); NSString *operand1String = [[NSString alloc] initWithBytesNoCopy:(void *)operand1 length:strlen(operand1) From 283efc6707d8f226e78cd5d2ed915492a4a05b70 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:38:13 +0200 Subject: [PATCH 13/27] Fix ECDSTRINGOPERATION's implementation of LIKE --- Incremental Store/EncryptedStore.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index ff0cfbf..113d425 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -1197,7 +1197,7 @@ static void dbsqliteStringOperation(sqlite3_context *context, int argc, sqlite3_ operand2String] evaluateWithObject:operand1String]; sqlite3_result_int(context, !!likeResult); - break; + return; } case NSBeginsWithPredicateOperatorType: { From f76402edacb822d6bcee38318ae7bb2c4a3271f2 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:40:51 +0200 Subject: [PATCH 14/27] Change -parseExpression:inPredicate:inFetchRequest:operator:operand:bindings: into -parseExpression:inPredicate:inFetchRequest:operator:operand:ofType:bindings:: the new argument points to a Class variable where the type of the parsed operand (if it can be determined) is returned --- Incremental Store/EncryptedStore.m | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 113d425..1f87b4e 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3165,22 +3165,26 @@ - (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 @@ -3358,6 +3362,7 @@ - (void)parseExpression:(NSExpression *)expression inFetchRequest:(NSFetchRequest *)request operator:(NSDictionary *)operator operand:(id *)operand + ofType:(Class *)operandType bindings:(id *)bindings { NSExpressionType type = [expression expressionType]; @@ -3401,8 +3406,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]; @@ -3466,6 +3475,7 @@ - (void)parseExpression:(NSExpression *)expression } } } + *operandType = [NSNumber class]; value = [NSString stringWithFormat:@"LENGTH(%@)", [[pathComponents subarrayWithRange:NSMakeRange(0, pathComponents.count - 1)] componentsJoinedByString:@"."]]; foundPredicate = YES; } @@ -3490,6 +3500,7 @@ - (void)parseExpression:(NSExpression *)expression rel.destinationEntity.typeHash]]; } value = [value stringByAppendingString:@")"]; + *operandType = [NSNumber class]; foundPredicate = YES; } @@ -3505,7 +3516,6 @@ - (void)parseExpression:(NSExpression *)expression NSEntityDescription * entity = [property destinationEntity]; subProperties = entity.propertiesByName; } else { - property = nil; *stop = YES; } }]; @@ -3513,6 +3523,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:@"%@.%@", @@ -3535,10 +3549,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) { @@ -3552,6 +3568,7 @@ - (void)parseExpression:(NSExpression *)expression [operator objectForKey:@"format"], [parameters componentsJoinedByString:@", "]]; } + *operandType = [value class]; } else if ([value isKindOfClass:[NSString class]]) { *operand = @"?"; @@ -3562,6 +3579,7 @@ - (void)parseExpression:(NSExpression *)expression *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 = @"?"; @@ -3572,14 +3590,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]; } } From 9bcfba178250ba23f93124a63eaa637e69329c3d Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:45:19 +0200 Subject: [PATCH 15/27] LIKE, CONTAINS, ENDSWITH, STARTSWITH etc. are now implemented by ECDSTRINGOPERATION, which doesn't need LIKE patterns to implement CONTAINS, ENDSWITH and STARTSWITH, and doesn't need LIKE patterns to be translated from NSPredicate to SQL syntax: if a comparison predicate is implemented with ECDSTRINGOPERATION, parse both expressions again, without any formatting, escaping etc. of the operands --- Incremental Store/EncryptedStore.m | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 1f87b4e..bbd015d 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3228,6 +3228,33 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request 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, From 784b9a3d3bef1732abb5ed7bdeb9354ac52af006 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:46:05 +0200 Subject: [PATCH 16/27] Better detection of string vs aggregate form of IN and CONTAINS operators --- Incremental Store/EncryptedStore.m | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index bbd015d..ad662d0 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3090,6 +3090,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 { @@ -3213,14 +3218,14 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request case NSInPredicateOperatorType: case NSContainsPredicateOperatorType: { if (comparisonPredicate.predicateOperatorType == NSInPredicateOperatorType) { - if (comparisonPredicate.rightExpression.expressionType != NSConstantValueExpressionType || ![comparisonPredicate.rightExpression.constantValue isKindOfClass:[NSString class]]) { + if (rightOperandClass == Nil || isCollection(rightOperandClass)) { // not an error, this IN is just not a string operation break; } } if (comparisonPredicate.predicateOperatorType == NSContainsPredicateOperatorType) { - if (comparisonPredicate.leftExpression.expressionType != NSConstantValueExpressionType || ![comparisonPredicate.leftExpression.constantValue isKindOfClass:[NSString class]]) { + if (leftOperandClass == Nil || isCollection(leftOperandClass)) { // not an error, this CONTAINS is just not a string operation break; } From 5c5580cd0e48d78c886fe09da110d94b7ff71e02 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:56:08 +0200 Subject: [PATCH 17/27] The previous code to parse " == NULL" to translate into " IS NULL" would check for an operand without bindings to detect NULL, mistaking key path expressions for NULLs. Instead, check that the right operand is, in fact, NULL. Also support expressions of the form "NULL == " (which translate to " IS NULL") --- Incremental Store/EncryptedStore.m | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index ad662d0..e90ba42 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3193,11 +3193,18 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request 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 { From 24a4d5fc085639d3080c6def71648379bca4a07c Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 18:22:43 +0200 Subject: [PATCH 18/27] Merge branch 'master' of github.com:project-imas/encrypted-core-data --- Incremental Store/EncryptedStore.m | 366 +++++++++++++++++++++++------ 1 file changed, 288 insertions(+), 78 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index f26d1ed..18f4f45 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -23,9 +23,9 @@ NSString * const EncryptedStoreCacheSize = @"EncryptedStoreCacheSize"; static void dbsqliteRegExp(sqlite3_context *context, int argc, const char **argv); -static void dbsqliteStripCase(sqlite3_context *context, int argc, const char **argv); -static void dbsqliteStripDiacritics(sqlite3_context *context, int argc, const char **argv); -static void dbsqliteStripCaseDiacritics(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"; @@ -673,32 +673,11 @@ - (BOOL)loadMetadata:(NSError **)error { return NO; } -#ifdef SQLITE_DETERMINISTIC //enable regexp 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); - //enable case insentitivity - sqlite3_create_function(database, "STRIP_CASE", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripCase, NULL, NULL); - - //enable diacritic insentitivity - sqlite3_create_function(database, "STRIP_DIACRITICS", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripDiacritics, NULL, NULL); - - //enable combined case and diacritic insentitivity - sqlite3_create_function(database, "STRIP_CASE_DIACRITICS", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripCaseDiacritics, NULL, NULL); -#else - //enable regexp - sqlite3_create_function(database, "REGEXP", 2, SQLITE_UTF8, NULL, (void *)dbsqliteRegExp, NULL, NULL); - - //enable case insentitivity - sqlite3_create_function(database, "STRIP_CASE", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripCase, NULL, NULL); - - //enable diacritic insentitivity - sqlite3_create_function(database, "STRIP_DIACRITICS", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripDiacritics, NULL, NULL); - - //enable combined case and diacritic insentitivity - sqlite3_create_function(database, "STRIP_CASE_DIACRITICS", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripCaseDiacritics, NULL, NULL); -#endif - // ask if we have a metadata table BOOL hasTable = NO; if (![self hasMetadataTable:&hasTable error:error]) { return NO; } @@ -1057,48 +1036,219 @@ static void dbsqliteRegExp(sqlite3_context *context, int argc, const char **argv sqlite3_result_int(context, (int)numberOfMatches); } -static void dbsqliteStripCase(sqlite3_context *context, int argc, const char **argv) { - assert(argc == 1); - NSString* string; - const char *aux = (const char *)sqlite3_value_text((sqlite3_value*)argv[0]); - - /*Safeguard against null returns*/ - if (aux) { - string = [NSString stringWithUTF8String:aux]; - string = [string stringByFoldingWithOptions:NSCaseInsensitiveSearch locale:nil]; - sqlite3_result_text(context, [string UTF8String], -1, SQLITE_TRANSIENT); - } else { - sqlite3_result_text(context, nil, -1, NULL); +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 = 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 void dbsqliteStripDiacritics(sqlite3_context *context, int argc, const char **argv) { - assert(argc == 1); - NSString* string; - const char *aux = (const char *)sqlite3_value_text((sqlite3_value*)argv[0]); - - /*Safeguard against null returns*/ - if (aux) { - string = [NSString stringWithUTF8String:aux]; - string = [string stringByFoldingWithOptions:NSDiacriticInsensitiveSearch locale:nil]; - sqlite3_result_text(context, [string UTF8String], -1, SQLITE_TRANSIENT); - } else { - sqlite3_result_text(context, nil, -1, NULL); +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 dbsqliteStripCaseDiacritics(sqlite3_context *context, int argc, const char **argv) { - assert(argc == 1); - NSString* string; - const char *aux = (const char *)sqlite3_value_text((sqlite3_value*)argv[0]); +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 = 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 = 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); + break; + } + + 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]; - /*Safeguard against null returns*/ - if (aux) { - string = [NSString stringWithUTF8String:aux]; - string = [string stringByFoldingWithOptions:NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch locale:nil]; - sqlite3_result_text(context, [string UTF8String], -1, SQLITE_TRANSIENT); - } else { - sqlite3_result_text(context, nil, -1, NULL); + 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); } } @@ -3058,9 +3208,75 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request } } else { - query = [@[leftOperand, [operator objectForKey:@"operator"], rightOperand] componentsJoinedByString:@" "]; - if ([[operator objectForKey:@"operator"] isEqualToString:@"LIKE"]) { - query = [query stringByAppendingString:@" ESCAPE '\\'"]; + BOOL isStringOperation = NO; + + if (comparisonPredicate.options) { + 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 (comparisonPredicate.rightExpression.expressionType != NSConstantValueExpressionType || ![comparisonPredicate.rightExpression.constantValue isKindOfClass:[NSString class]]) { + // not an error, this IN is just not a string operation + break; + } + } + + if (comparisonPredicate.predicateOperatorType == NSContainsPredicateOperatorType) { + if (comparisonPredicate.leftExpression.expressionType != NSConstantValueExpressionType || ![comparisonPredicate.leftExpression.constantValue isKindOfClass:[NSString class]]) { + // not an error, this CONTAINS is just not a string operation + break; + } + } + + isStringOperation = YES; + + 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 '\\'"]; + } } } @@ -3363,20 +3579,14 @@ - (void)parseExpression:(NSExpression *)expression } } else if ([value isKindOfClass:[NSString class]]) { - if ([[operator objectForKey:@"operator"] isEqualToString:@"LIKE"]) { - BOOL isLike = predicate.predicateOperatorType == NSLikePredicateOperatorType; - value = [self escapedString:value allowWildcards:isLike]; - } - NSComparisonPredicateOptions options = [predicate options]; - if ((options & NSCaseInsensitivePredicateOption) && (options & NSDiacriticInsensitivePredicateOption)) { - value = [value stringByFoldingWithOptions:NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch locale:nil]; - } else if (options & NSCaseInsensitivePredicateOption) { - value = [value stringByFoldingWithOptions:NSCaseInsensitiveSearch locale:nil]; - } else if (options & NSDiacriticInsensitivePredicateOption) { - value = [value stringByFoldingWithOptions:NSDiacriticInsensitiveSearch locale:nil]; - } - *operand = @"?"; - *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]; } else if ([value isKindOfClass:[NSManagedObject class]] || [value isKindOfClass:[NSManagedObjectID class]]) { NSManagedObjectID * objectId = [value isKindOfClass:[NSManagedObject class]] ? [value objectID]:value; *operand = @"?"; From b95f367c1baa9253c0f133417890dd075eec79ec Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Fri, 18 Mar 2016 18:05:22 +0100 Subject: [PATCH 19/27] Implemented case-insensitive, diacritic-insensitive and normalized operations for strings. Two user-defined functions are registered with sqlcipher: ECDSTRINGBETWEEN, which implements the BETWEEN operator, and ECDSTRINGOPERATION which implements all other operators. --- Incremental Store/EncryptedStore.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 18f4f45..fdd6e7f 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"; From 78e55042f71b90a4c3faf580e6c83f07328cfd58 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:37:19 +0200 Subject: [PATCH 20/27] NSPredicateOperatorType and NSComparisonPredicateOptions have underlying type NSUInteger, which is 32 bits wide on 32 bit platforms. Explicitly cast the result value of sqlite2_value_int64 to NSUInteger to fix implicit truncation warnings on 32 bit --- Incremental Store/EncryptedStore.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index fdd6e7f..0e8eac2 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -1055,7 +1055,7 @@ static void dbsqliteStringBetween(sqlite3_context *context, int argc, sqlite3_va 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 = sqlite3_value_int64(argv[3]); + NSComparisonPredicateOptions options = (NSUInteger)sqlite3_value_int64(argv[3]); NSString *operandString = [[NSString alloc] initWithBytesNoCopy:(void *)operand length:strlen(operand) @@ -1138,10 +1138,10 @@ static void dbsqliteStringOperation(sqlite3_context *context, int argc, sqlite3_ return; } - NSPredicateOperatorType operation = sqlite3_value_int64(argv[0]); + 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 = sqlite3_value_int64(argv[3]); + NSComparisonPredicateOptions options = (NSUInteger)sqlite3_value_int64(argv[3]); NSString *operand1String = [[NSString alloc] initWithBytesNoCopy:(void *)operand1 length:strlen(operand1) From 4d5be7e4cdad6ecd7b8754e67219bb62d16309e7 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:38:13 +0200 Subject: [PATCH 21/27] Fix ECDSTRINGOPERATION's implementation of LIKE --- Incremental Store/EncryptedStore.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 0e8eac2..8d1d49c 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -1214,7 +1214,7 @@ static void dbsqliteStringOperation(sqlite3_context *context, int argc, sqlite3_ operand2String] evaluateWithObject:operand1String]; sqlite3_result_int(context, !!likeResult); - break; + return; } case NSBeginsWithPredicateOperatorType: { From 8dfc1953017ef41bbf3f4059e573ca69b1b4d9bd Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:40:51 +0200 Subject: [PATCH 22/27] Change -parseExpression:inPredicate:inFetchRequest:operator:operand:bindings: into -parseExpression:inPredicate:inFetchRequest:operator:operand:ofType:bindings:: the new argument points to a Class variable where the type of the parsed operand (if it can be determined) is returned --- Incremental Store/EncryptedStore.m | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 8d1d49c..11450e5 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3185,22 +3185,26 @@ - (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 @@ -3378,6 +3382,7 @@ - (void)parseExpression:(NSExpression *)expression inFetchRequest:(NSFetchRequest *)request operator:(NSDictionary *)operator operand:(id *)operand + ofType:(Class *)operandType bindings:(id *)bindings { NSExpressionType type = [expression expressionType]; @@ -3421,8 +3426,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]; @@ -3486,6 +3495,7 @@ - (void)parseExpression:(NSExpression *)expression } } } + *operandType = [NSNumber class]; value = [NSString stringWithFormat:@"LENGTH(%@)", [[pathComponents subarrayWithRange:NSMakeRange(0, pathComponents.count - 1)] componentsJoinedByString:@"."]]; foundPredicate = YES; } @@ -3510,6 +3520,7 @@ - (void)parseExpression:(NSExpression *)expression rel.destinationEntity.typeHash]]; } value = [value stringByAppendingString:@")"]; + *operandType = [NSNumber class]; foundPredicate = YES; } @@ -3525,7 +3536,6 @@ - (void)parseExpression:(NSExpression *)expression NSEntityDescription * entity = [property destinationEntity]; subProperties = entity.propertiesByName; } else { - property = nil; *stop = YES; } }]; @@ -3533,6 +3543,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:@"%@.%@", @@ -3564,10 +3578,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) { @@ -3581,6 +3597,7 @@ - (void)parseExpression:(NSExpression *)expression [operator objectForKey:@"format"], [parameters componentsJoinedByString:@", "]]; } + *operandType = [value class]; } else if ([value isKindOfClass:[NSString class]]) { *operand = @"?"; @@ -3591,6 +3608,7 @@ - (void)parseExpression:(NSExpression *)expression *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 = @"?"; @@ -3601,14 +3619,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]; } } From ad9d9e5dca9d0f16e3cff80b88825292f395672c Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:45:19 +0200 Subject: [PATCH 23/27] LIKE, CONTAINS, ENDSWITH, STARTSWITH etc. are now implemented by ECDSTRINGOPERATION, which doesn't need LIKE patterns to implement CONTAINS, ENDSWITH and STARTSWITH, and doesn't need LIKE patterns to be translated from NSPredicate to SQL syntax: if a comparison predicate is implemented with ECDSTRINGOPERATION, parse both expressions again, without any formatting, escaping etc. of the operands --- Incremental Store/EncryptedStore.m | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 11450e5..c116034 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3248,6 +3248,33 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request 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, From 4d8c473bda332147638cafc54fded39d5997b64f Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:46:05 +0200 Subject: [PATCH 24/27] Better detection of string vs aggregate form of IN and CONTAINS operators --- Incremental Store/EncryptedStore.m | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index c116034..9d7d812 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3110,6 +3110,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 { @@ -3233,14 +3238,14 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request case NSInPredicateOperatorType: case NSContainsPredicateOperatorType: { if (comparisonPredicate.predicateOperatorType == NSInPredicateOperatorType) { - if (comparisonPredicate.rightExpression.expressionType != NSConstantValueExpressionType || ![comparisonPredicate.rightExpression.constantValue isKindOfClass:[NSString class]]) { + if (rightOperandClass == Nil || isCollection(rightOperandClass)) { // not an error, this IN is just not a string operation break; } } if (comparisonPredicate.predicateOperatorType == NSContainsPredicateOperatorType) { - if (comparisonPredicate.leftExpression.expressionType != NSConstantValueExpressionType || ![comparisonPredicate.leftExpression.constantValue isKindOfClass:[NSString class]]) { + if (leftOperandClass == Nil || isCollection(leftOperandClass)) { // not an error, this CONTAINS is just not a string operation break; } From 32de611380b4bf20641f4f7bbdfaa2fe2d496fa7 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 17:56:08 +0200 Subject: [PATCH 25/27] The previous code to parse " == NULL" to translate into " IS NULL" would check for an operand without bindings to detect NULL, mistaking key path expressions for NULLs. Instead, check that the right operand is, in fact, NULL. Also support expressions of the form "NULL == " (which translate to " IS NULL") --- Incremental Store/EncryptedStore.m | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 9d7d812..fa8d881 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3213,11 +3213,18 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request 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 { From 9716c5ad4bebfe5ffb2b5564fcc8fd6903b96920 Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 18:37:37 +0200 Subject: [PATCH 26/27] Forgot to undo an upstream change --- Incremental Store/EncryptedStore.m | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index fa8d881..019a68e 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -3592,17 +3592,8 @@ - (void)parseExpression:(NSExpression *)expression [self joinedTableNameForComponents:[pathComponents subarrayWithRange:NSMakeRange(0, pathComponents.count -1)] forRelationship:NO], lastComponentName]; } } - NSComparisonPredicateOptions options = [predicate options]; - if ((options & NSCaseInsensitivePredicateOption) && (options & NSDiacriticInsensitivePredicateOption)) { - *operand = [@[@"STRIP_CASE_DIACRITICS(", value, @")"] componentsJoinedByString:@""]; - } else if (options & NSCaseInsensitivePredicateOption) { - *operand = [@[@"STRIP_CASE(", value, @")"] componentsJoinedByString:@""]; - } else if (options & NSDiacriticInsensitivePredicateOption) { - *operand = [@[@"STRIP_DIACRITICS(", value, @")"] componentsJoinedByString:@""]; - } else { *operand = value; } - } else if (type == NSEvaluatedObjectExpressionType) { *operand = @"__objectid"; } From 83d31819b8b12b39e0eedf19e8bb8904195af75b Mon Sep 17 00:00:00 2001 From: Michele Cicciotti Date: Mon, 1 Aug 2016 18:59:02 +0200 Subject: [PATCH 27/27] No need to force SQLite to use a case-sensitive LIKE with PRAGMA case_sensitive_like: we can use ECDSTRINGOPERATION --- Incremental Store/EncryptedStore.m | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index 019a68e..0f392d0 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -671,12 +671,6 @@ - (BOOL)loadMetadata:(NSError **)error { // load metadata BOOL success = [self performInTransaction:^{ - //make 'LIKE' case-sensitive - sqlite3_stmt *setPragma = [self preparedStatementForQuery:@"PRAGMA case_sensitive_like = true;"]; - if (!setPragma || sqlite3_step(setPragma) != SQLITE_DONE || sqlite3_finalize(setPragma) != SQLITE_OK) { - return NO; - } - //enable regexp 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); @@ -3230,7 +3224,8 @@ - (NSDictionary *)recursiveWhereClauseWithFetchRequest:(NSFetchRequest *)request else { BOOL isStringOperation = NO; - if (comparisonPredicate.options) { + // 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: