diff --git a/Source/API/Extras/CBLIncrementalStore.h b/Source/API/Extras/CBLIncrementalStore.h index f5594a19a..e31b59d74 100644 --- a/Source/API/Extras/CBLIncrementalStore.h +++ b/Source/API/Extras/CBLIncrementalStore.h @@ -14,6 +14,12 @@ extern NSString* const kCBLISObjectHasBeenChangedInStoreNotification; /** Maximum number of relationship loading allowed in a fetch request. Default value is 3. */ extern NSString* const kCBLISCustomPropertyMaxRelationshipLoadDepth; +/** Enable query boolean value with number. After CBL 1.1.0, CBLIS stores a boolean value as + * a JSON boolean value instead of a number value of 1 or 0. If the application + * uses CBLIS and has stored boolean values since CBL 1.1.0 or below, setting this option to + * YES is required for quering data with boolean value predicates. Default value is NO. */ +extern NSString* const kCBLISCustomPropertyQueryBooleanWithNumber; + /** Error codes for CBLIncrementalStore. */ typedef enum { diff --git a/Source/API/Extras/CBLIncrementalStore.m b/Source/API/Extras/CBLIncrementalStore.m index 8883ba6e2..c410e6a02 100644 --- a/Source/API/Extras/CBLIncrementalStore.m +++ b/Source/API/Extras/CBLIncrementalStore.m @@ -33,13 +33,16 @@ NSString* const kCBLISErrorDomain = @"CBLISErrorDomain"; NSString* const kCBLISObjectHasBeenChangedInStoreNotification = @"CBLISObjectHasBeenChangedInStoreNotification"; NSString* const kCBLISCustomPropertyMaxRelationshipLoadDepth = @"CBLISCustomPropertyMaxRelationshipLoadDepth"; +NSString* const kCBLISCustomPropertyQueryBooleanWithNumber = @"CBLISCustomPropertyQueryBooleanWithNumber"; + +static NSString* const kCBLISMetadataDocumentID = @"CBLIS_metadata"; +static NSString* const kCBLISMetadata_DefaultTypeKey = @"type_key"; static NSString* const kCBLISDefaultTypeKey = @"type"; static NSString* const kCBLISOldDefaultTypeKey = @"CBLIS_type"; -static NSString* const kCBLISMetadata_DefaultTypeKey = @"type_key"; + static NSString* const kCBLISCurrentRevisionAttributeName = @"CBLIS_Rev"; static NSString* const kCBLISManagedObjectIDPrefix = @"CBL"; -static NSString* const kCBLISMetadataDocumentID = @"CBLIS_metadata"; static NSString* const kCBLISToManyViewNameFormat = @"CBLIS/%@_%@_%@"; // Utility functions @@ -370,7 +373,7 @@ - (NSDictionary*) metadataDocument: (NSError **)outError { withID: kCBLISMetadataDocumentID error: &error]) { if (outError) { - NSString* errorDesc = @"Could not store metadata in database"; + NSString* errorDesc = @"Could not save metadata in database"; *outError = CBLISError(CBLIncrementalStoreErrorStoringMetadataFailed, errorDesc, error); } } @@ -652,12 +655,22 @@ - (NSString*)documentTypeKey { #pragma mark - Custom properties +- (void)setCustomProperties: (NSDictionary*)customProperties { + _customProperties = customProperties; + [self invalidateFetchResultCache]; +} + - (NSUInteger) maxRelationshipLoadDepth { id maxDepth = _customProperties[kCBLISCustomPropertyMaxRelationshipLoadDepth]; NSUInteger maxDepthValue = [maxDepth unsignedIntegerValue]; return maxDepthValue > 0 ? maxDepthValue : kDefaultMaxRelationshipLoadDepth; } +- (BOOL) queryBooleanValueWithNumber { + id queryBoolNum = _customProperties[kCBLISCustomPropertyQueryBooleanWithNumber]; + return [queryBoolNum boolValue]; // Default value is NO +} + #pragma mark - Views /** Initializes the views needed for querying objects by type and for to-many relationships.*/ @@ -973,6 +986,7 @@ - (NSPredicate*) scanPredicate: (NSPredicate*)predicate @[rhs, lhs] : @[lhs, rhs]; BOOL hasError = NO; NSString* keyPath = nil; + NSExpression* boolNumberExp = nil; for (NSExpression* expression in expressions) { if (expression.expressionType == NSKeyPathExpressionType) { BOOL needJoins; @@ -1033,15 +1047,26 @@ - (NSPredicate*) scanPredicate: (NSPredicate*)predicate [outTemplateVars setObject: values forKey: varName]; } } else { - NSString* varName = [self variableForKeyPath: keyPath - suffix: nil - current: outTemplateVars]; - newExpression = [NSExpression expressionForVariable: varName]; - - id exValue = [self scanConstantValue: constantValue]; - if (exValue) - [outTemplateVars setObject: exValue forKey: varName]; - else + id expValue = [self scanConstantValue: constantValue]; + if (expValue) { + id exValue = [self scanConstantValue: constantValue]; + if ([self isBooleanConstantValue: exValue] && [self queryBooleanValueWithNumber]) { + // Workaround for #756: + // Need to be able to query both JSON boolean value (true,false) and + // number boolean value(1,0). + // 1. Not templating the original boolean value expression. + // 2. Create a pair boolean number expression used for creating an + // OR-compound predicate with the original boolean value expression + id boolNumberValue = [exValue boolValue] ? @(1) : @(0); + boolNumberExp = [NSExpression expressionForConstantValue: boolNumberValue]; + } else { + NSString* varName = [self variableForKeyPath: keyPath + suffix: nil + current: outTemplateVars]; + newExpression = [NSExpression expressionForVariable: varName]; + [outTemplateVars setObject: expValue forKey: varName]; + } + } else break; // Not templating nil predicate: if (comparison.predicateOperatorType == NSContainsPredicateOperatorType) { @@ -1068,6 +1093,19 @@ - (NSPredicate*) scanPredicate: (NSPredicate*)predicate options: comparison.options]; else output = comparison; + + if (boolNumberExp) { + // Workaround for #756: + // Create a pair boolean number expression used for creating an + // OR-compound predicate with the original boolean value expression. + NSPredicate* boolNumberPredicate = + [NSComparisonPredicate predicateWithLeftExpression: lhs + rightExpression: boolNumberExp + modifier: comparison.comparisonPredicateModifier + type: comparison.predicateOperatorType + options: comparison.options]; + output = [NSCompoundPredicate orPredicateWithSubpredicates: @[output, boolNumberPredicate]]; + } } } @@ -1084,6 +1122,12 @@ - (NSPredicate*) scanPredicate: (NSPredicate*)predicate return output; } + +- (BOOL) isBooleanConstantValue: (id)value { + return (value == (id)@(YES) || value == (id)@(NO)); +} + + /* * Scan an expression, detect if the expression is a joins query, and validate if * the keypath is valid. @@ -1523,7 +1567,7 @@ - (id) convertCoreDataValue: (id)value toCouchbaseLiteValueOfType: (NSAttributeT result = CBLISIsNull(value) ? @"" : value; break; case NSBooleanAttributeType: - result = CBLISIsNull(value) ? @(NO) : value; + result = CBLISIsNull(value) ? @(NO) : [value boolValue] ? @(YES) : @(NO); break; case NSDateAttributeType: result = CBLISIsNull(value) ? nil : [CBLJSON JSONObjectWithDate: value]; @@ -1865,6 +1909,10 @@ - (void) purgeCachedObjectsForEntityName: (NSString*)entity { } } +- (void) invalidateFetchResultCache { + [_fetchRequestResultCache removeAllObjects]; +} + #pragma mark - Attachments - (NSData*) loadDataForAttachmentWithName: (NSString*)name ofDocumentWithID: (NSString*)documentID { diff --git a/Unit-Tests/IncrementalStore_Tests.m b/Unit-Tests/IncrementalStore_Tests.m index 7f4546137..5197c43f1 100644 --- a/Unit-Tests/IncrementalStore_Tests.m +++ b/Unit-Tests/IncrementalStore_Tests.m @@ -817,6 +817,7 @@ - (void) test_FetchWithPredicates { NSDictionary *entry1 = @{ @"created_at": [NSDate new], + @"check": @YES, @"text": @"This is a test for predicates. Möhre.", @"text2": @"This is text2.", @"number": [NSNumber numberWithInt:10], @@ -825,6 +826,7 @@ - (void) test_FetchWithPredicates { }; NSDictionary *entry2 = @{ @"created_at": [[NSDate new] dateByAddingTimeInterval:-60], + @"check": @YES, @"text": @"Entry number 2. touché.", @"text2": @"Text 2 by Entry number 2", @"number": [NSNumber numberWithInt:20], @@ -833,6 +835,7 @@ - (void) test_FetchWithPredicates { }; NSDictionary *entry3 = @{ @"created_at": [[NSDate new] dateByAddingTimeInterval:60], + @"check": @NO, @"text": @"Entry number 3", @"text2": @"Text 2 by Entry number 3", @"number": [NSNumber numberWithInt:30], @@ -1078,6 +1081,71 @@ - (void) test_FetchWithDate { }]; } +- (void)test_FetchBooleanValue { + NSError *error; + + NSDictionary *entry1 = @{ + @"created_at": [NSDate new], + @"check": @YES, + @"text": @"This is a test for predicates. Möhre.", + @"text2": @"This is text2.", + @"number": [NSNumber numberWithInt:10], + @"decimalNumber": [NSDecimalNumber decimalNumberWithString:@"10.10"], + @"doubleNumber": [NSNumber numberWithDouble:42.23] + }; + NSDictionary *entry2 = @{ + @"created_at": [[NSDate new] dateByAddingTimeInterval:-60], + @"check": @YES, + @"text": @"Entry number 2. touché.", + @"text2": @"Text 2 by Entry number 2", + @"number": [NSNumber numberWithInt:20], + @"decimalNumber": [NSDecimalNumber decimalNumberWithString:@"20.20"], + @"doubleNumber": [NSNumber numberWithDouble:12.45] + }; + NSDictionary *entry3 = @{ + @"created_at": [[NSDate new] dateByAddingTimeInterval:60], + @"check": @NO, + @"text": @"Entry number 3", + @"text2": @"Text 2 by Entry number 3", + @"number": [NSNumber numberWithInt:30], + @"decimalNumber": [NSDecimalNumber decimalNumberWithString:@"30.30"], + @"doubleNumber": [NSNumber numberWithDouble:98.76] + }; + + CBLISTestInsertEntriesWithProperties(context, @[entry1, entry2, entry3]); + + BOOL success = [context save:&error]; + Assert(success, @"Could not save context: %@", error); + + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entry"]; + + //// == + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"check == YES"]; + [self assertFetchRequest: fetchRequest block: ^(NSArray *result, NSFetchRequestResultType resultType) { + AssertEq((int)result.count, 2); + }]; + + //// == + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"check == NO"]; + [self assertFetchRequest: fetchRequest block: ^(NSArray *result, NSFetchRequestResultType resultType) { + AssertEq((int)result.count, 1); + }]; + + [store setCustomProperties:@{kCBLISCustomPropertyQueryBooleanWithNumber: @(YES)}]; + + //// == + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"check == YES"]; + [self assertFetchRequest: fetchRequest block: ^(NSArray *result, NSFetchRequestResultType resultType) { + AssertEq((int)result.count, 2); + }]; + + //// == + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"check == NO"]; + [self assertFetchRequest: fetchRequest block: ^(NSArray *result, NSFetchRequestResultType resultType) { + AssertEq((int)result.count, 1); + }]; +} + - (void)test_FetchWithRelationship { NSError *error; @@ -1149,7 +1217,7 @@ - (void)test_FetchWithRelationship { BOOL success = [context save:&error]; Assert(success, @"Could not save context: %@", error); - // Tear down the database to refresh cache + // Reset context and cache: [self reCreateCoreDataContext]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Entry"]; @@ -1285,7 +1353,7 @@ - (void)test_FetchWithNestedRelationship { BOOL success = [context save:&error]; Assert(success, @"Could not save context: %@", error); - // Tear down the database to refresh cache + // Reset context and cache: [self reCreateCoreDataContext]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Subentry"]; @@ -1302,6 +1370,11 @@ - (void)test_FetchWithNestedRelationship { AssertEq((int)result.count, 3); }]; + fetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[[NSPredicate predicateWithFormat:@"entry == %@", entry1], [NSPredicate predicateWithFormat:@"number == 10"]]]; + [self assertFetchRequest: fetchRequest block: ^(NSArray *result, NSFetchRequestResultType resultType) { + AssertEq((int)result.count, 1); + }]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"entry.user.name like %@", user1.name]; [self assertFetchRequest: fetchRequest block: ^(NSArray *result, NSFetchRequestResultType resultType) { AssertEq((int)result.count, 3); @@ -1373,7 +1446,7 @@ - (void)test_FetchWithNestedRelationshipAndSort { BOOL success = [context save:&error]; Assert(success, @"Could not save context: %@", error); - // Tear down the database to refresh cache + // Reset context and cache: [self reCreateCoreDataContext]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Subentry"]; @@ -1459,9 +1532,8 @@ - (void)test_FetchWithRelationshipNil { BOOL success = [context save:&error]; Assert(success, @"Could not save context: %@", error); - // Tear down the database to refresh cache - context = [CBLIncrementalStore createManagedObjectContextWithModel:model - databaseName:db.name error:&error]; + // Reset context and cache: + [self reCreateCoreDataContext]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Subentry"];