Skip to content

Commit

Permalink
Merge pull request #762 from couchbase/feature/issue_756_cblis_boolea…
Browse files Browse the repository at this point in the history
…n_predicate

Fix CBLIS storing and querying boolean value

Fixes #756
  • Loading branch information
snej committed Jun 9, 2015
2 parents 9e0ab0e + 4d231e8 commit 9d9f799
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 19 deletions.
6 changes: 6 additions & 0 deletions Source/API/Extras/CBLIncrementalStore.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
74 changes: 61 additions & 13 deletions Source/API/Extras/CBLIncrementalStore.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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.*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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]];
}
}
}

Expand All @@ -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.
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -1865,6 +1909,10 @@ - (void) purgeCachedObjectsForEntityName: (NSString*)entity {
}
}

- (void) invalidateFetchResultCache {
[_fetchRequestResultCache removeAllObjects];
}

#pragma mark - Attachments

- (NSData*) loadDataForAttachmentWithName: (NSString*)name ofDocumentWithID: (NSString*)documentID {
Expand Down
84 changes: 78 additions & 6 deletions Unit-Tests/IncrementalStore_Tests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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],
Expand All @@ -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],
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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"];
Expand Down Expand Up @@ -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"];
Expand All @@ -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);
Expand Down Expand Up @@ -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"];
Expand Down Expand Up @@ -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"];

Expand Down

0 comments on commit 9d9f799

Please sign in to comment.