Skip to content

Commit

Permalink
Fix CBLIS storing and querying boolean value
Browse files Browse the repository at this point in the history
- Store boolean value in CBL database as JSON boolean value instead of number boolean value

- Provide kCBLISCustomPropertyQueryBooleanWithNumber (Default is NO) to support querying the existing boolean number value.

- If setting the kCBLISCustomPropertyQueryBooleanWithNumber property value to YES, when scanning the predicate for CBLQueryBuilder, CBLIS will replcate the original boolean predicate with an OR-compound predicate of the original predicate and the improvised number boolean predicate without parameterizing. For example, if the predicate is 'checked == YES', CBLIS will replace the predicate with 'checked == YES or checked == 1.

#756
  • Loading branch information
pasin committed Jun 9, 2015
1 parent 6e412fc commit 4d231e8
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 31 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
88 changes: 70 additions & 18 deletions Unit-Tests/IncrementalStore_Tests.m
Original file line number Diff line number Diff line change
Expand Up @@ -850,18 +850,6 @@ - (void) test_FetchWithPredicates {

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);
}];

//// ==
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"text == %@", entry1[@"text"]];
[self assertFetchRequest: fetchRequest block: ^(NSArray *result, NSFetchRequestResultType resultType) {
Expand Down Expand Up @@ -1093,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 @@ -1164,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 @@ -1300,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 Down Expand Up @@ -1393,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 @@ -1479,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 4d231e8

Please sign in to comment.