Created
December 13, 2013 22:56
-
-
Save jchris/7952874 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CBLIncrementalStore.m | |
// CBLIncrementalStore | |
// | |
// Created by Christian Beer on 21.11.13. | |
// Copyright (c) 2013 Christian Beer. All rights reserved. | |
// | |
#import "CBLIncrementalStore.h" | |
#import <CouchbaseLite/CouchbaseLite.h> | |
#if !__has_feature(objc_arc) | |
# error This class requires ARC! | |
#endif | |
NSString * const kCBLIncrementalStoreErrorDomain = @"CBLISIncrementalStoreErrorDomain"; | |
NSString * const kCBLISObjectHasBeenChangedInStoreNotification = @"kCBLISObjectHasBeenChangedInStoreNotification"; | |
static NSString * const kCBLISTypeKey = @"CBLIS_type"; | |
static NSString * const kCBLISCurrentRevisionAttributeName = @"CBLIS_Rev"; | |
static NSString * const kCBLISManagedObjectIDPrefix = @"CBL"; | |
static NSString * const kCBLISMetadataDocumentID = @"CBLIS_metadata"; | |
static NSString * const kCBLISAllByTypeViewName = @"CBLIS/allByType"; | |
static NSString * const kCBLISFetchEntityByPropertyViewNameFormat = @"CBLIS/fetch_%@_by_%@"; | |
static NSString * const kCBLISFetchEntityToManyViewNameFormat = @"CBLIS/%@_tomany_%@"; | |
// utility functions | |
static BOOL CBLISIsNull(id value); | |
static NSString *CBLISToManyViewNameForRelationship(NSRelationshipDescription *relationship); | |
static NSString *CBLISResultTypeName(NSFetchRequestResultType resultType); | |
@interface NSManagedObjectID (CBLIncrementalStore) | |
- (NSString*) couchbaseLiteIDRepresentation; | |
@end | |
@interface CBLIncrementalStore () | |
// TODO: check if there is a better way to not hold strong references on these MOCs | |
@property (nonatomic, strong) NSMutableArray *observingManagedObjectContexts; | |
@property (nonatomic, strong, readwrite) CBLDatabase *database; | |
@end | |
@implementation CBLIncrementalStore | |
{ | |
NSMutableArray *_coalescedChanges; | |
NSMutableDictionary *_fetchRequestResultCache; | |
NSMutableDictionary *_entityAndPropertyToFetchViewName; | |
CBLLiveQuery *_conflictsQuery; | |
} | |
@synthesize database = _database; | |
@synthesize conflictHandler = _conflictHandler; | |
@synthesize observingManagedObjectContexts = _observingManagedObjectContexts; | |
#pragma mark - Convenience Method | |
+ (NSManagedObjectContext*) createManagedObjectContextWithModel:(NSManagedObjectModel*)managedObjectModel | |
databaseName:(NSString*)databaseName | |
error:(NSError**)outError | |
{ | |
return [self createManagedObjectContextWithModel:managedObjectModel databaseName:databaseName | |
importingDatabaseAtURL:nil importType:nil error:outError]; | |
} | |
+ (NSManagedObjectContext*) createManagedObjectContextWithModel:(NSManagedObjectModel*)managedObjectModel | |
databaseName:(NSString*)databaseName | |
importingDatabaseAtURL:(NSURL*)importUrl | |
importType:(NSString*)importType | |
error:(NSError**)outError | |
{ | |
NSManagedObjectModel *model = [managedObjectModel mutableCopy]; | |
[self updateManagedObjectModel:model]; | |
NSError *error; | |
NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; | |
NSDictionary *options = @{ | |
NSMigratePersistentStoresAutomaticallyOption : @YES, | |
NSInferMappingModelAutomaticallyOption : @YES | |
}; | |
CBLIncrementalStore *store = nil; | |
if (importUrl) { | |
NSPersistentStore *oldStore = [persistentStoreCoordinator addPersistentStoreWithType:importType configuration:nil | |
URL:importUrl options:options error:&error]; | |
if (!oldStore) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorMigrationOfStoreFailed | |
userInfo:@{ | |
NSLocalizedDescriptionKey: @"Couldn't open store to import", | |
NSUnderlyingErrorKey: error | |
}]; | |
return nil; | |
} | |
store = (CBLIncrementalStore*)[persistentStoreCoordinator migratePersistentStore:oldStore | |
toURL:[NSURL URLWithString:databaseName] options:options | |
withType:[self type] error:&error]; | |
if (!store) { | |
NSString *errorDescription = [NSString stringWithFormat:@"Migration of store at URL %@ failed: %@", importUrl, error.description]; | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorMigrationOfStoreFailed | |
userInfo:@{ | |
NSLocalizedDescriptionKey: errorDescription, | |
NSUnderlyingErrorKey: error | |
}]; | |
return nil; | |
} | |
} else { | |
store = (CBLIncrementalStore*)[persistentStoreCoordinator addPersistentStoreWithType:[self type] | |
configuration:nil URL:[NSURL URLWithString:databaseName] | |
options:options error:&error]; | |
if (!store) { | |
NSString *errorDescription = [NSString stringWithFormat:@"Initialization of store failed: %@", error.description]; | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCreatingStoreFailed | |
userInfo:@{ | |
NSLocalizedDescriptionKey: errorDescription, | |
NSUnderlyingErrorKey: error | |
}]; | |
return nil; | |
} | |
} | |
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; | |
[managedObjectContext setPersistentStoreCoordinator:persistentStoreCoordinator]; | |
[store addObservingManagedObjectContext:managedObjectContext]; | |
return managedObjectContext; | |
} | |
#pragma mark - | |
+ (void)initialize | |
{ | |
if ([[self class] isEqual:[CBLIncrementalStore class]]) { | |
[NSPersistentStoreCoordinator registerStoreClass:self | |
forStoreType:[self type]]; | |
} | |
} | |
- (void)dealloc | |
{ | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | |
} | |
/** | |
* This method has to be called once, before the NSManagedObjectModel is used by a NSPersistentStoreCoordinator. This method updates | |
* the entities in the managedObjectModel and adds some required properties. | |
* | |
* @param managedObjectModel the managedObjectModel to use with this store | |
*/ | |
+ (void) updateManagedObjectModel:(NSManagedObjectModel*)managedObjectModel | |
{ | |
NSArray *entites = managedObjectModel.entities; | |
for (NSEntityDescription *entity in entites) { | |
if (entity.superentity) { // only add to super-entities, not the sub-entities | |
continue; | |
} | |
NSMutableArray *properties = [entity.properties mutableCopy]; | |
for (NSPropertyDescription *prop in properties) { | |
if ([prop.name isEqual:kCBLISCurrentRevisionAttributeName]) { | |
return; | |
} | |
} | |
NSAttributeDescription *revAttribute = [NSAttributeDescription new]; | |
revAttribute.name = kCBLISCurrentRevisionAttributeName; | |
revAttribute.attributeType = NSStringAttributeType; | |
revAttribute.optional = YES; | |
revAttribute.indexed = YES; | |
[properties addObject:revAttribute]; | |
entity.properties = properties; | |
} | |
} | |
#pragma mark - NSPersistentStore | |
+ (NSString *)type | |
{ | |
return @"CBLIncrementalStore"; | |
} | |
- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)root | |
configurationName:(NSString *)name | |
URL:(NSURL *)url options:(NSDictionary *)options | |
{ | |
self = [super initWithPersistentStoreCoordinator:root configurationName:name URL:url options:options]; | |
if (!self) return nil; | |
_coalescedChanges = [[NSMutableArray alloc] init]; | |
_fetchRequestResultCache = [[NSMutableDictionary alloc] init]; | |
_entityAndPropertyToFetchViewName = [[NSMutableDictionary alloc] init]; | |
self.conflictHandler = [self _defaultConflictHandler]; | |
return self; | |
} | |
#pragma mark - NSIncrementalStore | |
-(BOOL)loadMetadata:(NSError **)outError | |
{ | |
// check data model if compatible with this store | |
NSArray *entites = self.persistentStoreCoordinator.managedObjectModel.entities; | |
for (NSEntityDescription *entity in entites) { | |
NSDictionary *attributesByName = [entity attributesByName]; | |
if (![attributesByName objectForKey:kCBLISCurrentRevisionAttributeName]) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorDatabaseModelIncompatible | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Database Model not compatible. You need to call +[updateManagedObjectModel:]." | |
}]; | |
return NO; | |
} | |
} | |
NSError *error; | |
NSString *databaseName = [self.URL lastPathComponent]; | |
CBLManager *manager = [CBLManager sharedInstance]; | |
if (!manager) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCBLManagerSharedInstanceMissing | |
userInfo:@{ | |
NSLocalizedDescriptionKey: @"No CBLManager shared instance available" | |
}]; | |
return NO; | |
} | |
self.database = [manager databaseNamed:databaseName error:&error]; | |
if (!self.database) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCreatingDatabaseFailed | |
userInfo:@{ | |
NSLocalizedDescriptionKey: @"Could not create database", | |
NSUnderlyingErrorKey: error | |
}]; | |
return NO; | |
} | |
[self _initializeViews]; | |
[[NSNotificationCenter defaultCenter] addObserverForName:kCBLDatabaseChangeNotification | |
object:self.database queue:nil | |
usingBlock:^(NSNotification *note) { | |
NSArray *changes = note.userInfo[@"changes"]; | |
[self _couchDocumentsChanged:changes]; | |
}]; | |
CBLDocument *doc = [self.database documentWithID:kCBLISMetadataDocumentID]; | |
BOOL success = NO; | |
NSDictionary *metaData = doc.properties; | |
if (![metaData objectForKey:NSStoreUUIDKey]) { | |
metaData = @{ | |
NSStoreUUIDKey: [[NSProcessInfo processInfo] globallyUniqueString], | |
NSStoreTypeKey: [[self class] type] | |
}; | |
[self setMetadata:metaData]; | |
NSError *error; | |
success = [doc putProperties:metaData error:&error] != nil; | |
if (!success) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorStoringMetadataFailed | |
userInfo:@{ | |
NSLocalizedDescriptionKey: @"Could not store metadata in database", | |
NSUnderlyingErrorKey: error | |
}]; | |
return NO; | |
} | |
} else { | |
[self setMetadata:doc.properties]; | |
success = YES; | |
} | |
if (success) { | |
// create a live-query for conflicting documents | |
CBLQuery* query = [self.database createAllDocumentsQuery]; | |
query.allDocsMode = kCBLOnlyConflicts; | |
CBLLiveQuery *liveQuery = query.asLiveQuery; | |
[liveQuery addObserver:self forKeyPath:@"rows" options:NSKeyValueObservingOptionNew context:nil]; | |
[liveQuery start]; | |
_conflictsQuery = liveQuery; | |
} | |
return success; | |
} | |
- (id)executeRequest:(NSPersistentStoreRequest *)request withContext:(NSManagedObjectContext*)context error:(NSError **)outError | |
{ | |
if (request.requestType == NSSaveRequestType) { | |
NSSaveChangesRequest *save = (NSSaveChangesRequest*)request; | |
#ifdef DEBUG_DETAILS | |
NSLog(@"[tdis] save request: ---------------- \n" | |
"[tdis] inserted:%@\n" | |
"[tdis] updated:%@\n" | |
"[tdis] deleted:%@\n" | |
"[tdis] locked:%@\n" | |
"[tids]---------------- ", [save insertedObjects], [save updatedObjects], [save deletedObjects], [save lockedObjects]); | |
#endif | |
// TODO: Check if using the CouchbaseLite transaction mechanism makes sense here. | |
NSError *error; | |
NSMutableSet *changedEntities = [NSMutableSet setWithCapacity:[save insertedObjects].count]; | |
// Objects that were inserted... | |
for (NSManagedObject *object in [save insertedObjects]) { | |
NSDictionary *contents = [self _couchbaseLiteRepresentationOfManagedObject:object withCouchbaseLiteID:YES]; | |
CBLDocument *doc = [self.database documentWithID:[object.objectID couchbaseLiteIDRepresentation]]; | |
if ([doc putProperties:contents error:&error]) { | |
[changedEntities addObject:object.entity.name]; | |
[object willChangeValueForKey:kCBLISCurrentRevisionAttributeName]; | |
[object setPrimitiveValue:doc.currentRevisionID forKey:kCBLISCurrentRevisionAttributeName]; | |
[object didChangeValueForKey:kCBLISCurrentRevisionAttributeName]; | |
[object willChangeValueForKey:@"objectID"]; | |
[context obtainPermanentIDsForObjects:@[object] error:nil]; | |
[object didChangeValueForKey:@"objectID"]; | |
[context refreshObject:object mergeChanges:YES]; | |
} else { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorPersistingInsertedObjectsFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error persisting inserted objects", | |
NSUnderlyingErrorKey:error | |
}]; | |
} | |
} | |
// Objects that were updated... | |
for (NSManagedObject *object in [save updatedObjects]) { | |
NSDictionary *contents = [self _couchbaseLiteRepresentationOfManagedObject:object withCouchbaseLiteID:YES]; | |
CBLDocument *doc = [self.database documentWithID:[object.objectID couchbaseLiteIDRepresentation]]; | |
if ([doc putProperties:contents error:&error]) { | |
[changedEntities addObject:object.entity.name]; | |
[object willChangeValueForKey:kCBLISCurrentRevisionAttributeName]; | |
[object setPrimitiveValue:doc.currentRevisionID forKey:kCBLISCurrentRevisionAttributeName]; | |
[object didChangeValueForKey:kCBLISCurrentRevisionAttributeName]; | |
[context refreshObject:object mergeChanges:YES]; | |
} else { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorPersistingUpdatedObjectsFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error persisting updated object", | |
NSUnderlyingErrorKey:error | |
}]; | |
} | |
} | |
// Objects that were deleted from the calling context... | |
for (NSManagedObject *object in [save deletedObjects]) { | |
// doesn't delete the document the normal way, but marks it as deleted to keep the type field needed for notifying Core Data. | |
CBLDocument *doc = [self.database documentWithID:[object.objectID couchbaseLiteIDRepresentation]]; | |
NSDictionary *contents = [self _propertiesForDeletingDocument:doc]; | |
if (![doc putProperties:contents error:&error]) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorPersistingDeletedObjectsFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error deleting object", | |
NSUnderlyingErrorKey:error | |
}]; | |
} | |
} | |
// clear cache for entities to get changes | |
for (NSString *entityName in changedEntities) { | |
[self _purgeCacheForEntityName:entityName]; | |
} | |
return @[]; | |
} else if (request.requestType == NSFetchRequestType) { | |
NSFetchRequest *fetch = (NSFetchRequest*)request; | |
NSFetchRequestResultType resultType = fetch.resultType; | |
id result = nil; | |
NSEntityDescription *entity = fetch.entity; | |
NSString *entityName = entity.name; | |
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); | |
// Docs: "note that it is not necessary to populate the managed object with attribute or relationship values at this point" | |
// -> you'll need them for predicates, though ;) | |
switch (resultType) { | |
case NSManagedObjectResultType: | |
case NSManagedObjectIDResultType: { | |
result = [self _queryObjectsOfEntity:entity byFetchRequest:fetch inContext:context error:outError]; | |
if (!result) return nil; | |
if (fetch.sortDescriptors) { | |
result = [result sortedArrayUsingDescriptors:fetch.sortDescriptors]; | |
} | |
if (resultType == NSManagedObjectIDResultType) { | |
NSMutableArray *objectIDs = [NSMutableArray arrayWithCapacity:[result count]]; | |
for (NSManagedObject *obj in result) { | |
[objectIDs addObject:[obj objectID]]; | |
} | |
result = objectIDs; | |
} | |
} | |
break; | |
case NSDictionaryResultType: { | |
CBLView *view = [self.database existingViewNamed:kCBLISAllByTypeViewName]; | |
CBLQuery* query = [view createQuery]; | |
query.keys = @[ entityName ]; | |
query.prefetch = YES; | |
CBLQueryEnumerator *rows = [self _queryEnumeratorForQuery:query error:outError]; | |
if (!rows) return nil; | |
NSMutableArray *array = [NSMutableArray arrayWithCapacity:rows.count]; | |
for (CBLQueryRow *row in rows) { | |
NSDictionary *properties = row.documentProperties; | |
if (!fetch.predicate || [fetch.predicate evaluateWithObject:properties]) { | |
if (fetch.propertiesToFetch) { | |
[array addObject:[properties dictionaryWithValuesForKeys:fetch.propertiesToFetch]]; | |
} else { | |
[array addObject:properties]; | |
} | |
} | |
} | |
result = array; | |
} | |
break; | |
case NSCountResultType: { | |
NSUInteger count = 0; | |
if (fetch.predicate) { | |
NSArray *array = [self _queryObjectsOfEntity:entity byFetchRequest:fetch inContext:context error:outError]; | |
if (!array) return nil; | |
count = array.count; | |
} else { | |
CBLView *view = [self.database existingViewNamed:kCBLISAllByTypeViewName]; | |
CBLQuery* query = [view createQuery]; | |
query.keys = @[ entityName ]; | |
query.prefetch = NO; | |
CBLQueryEnumerator *rows = [self _queryEnumeratorForQuery:query error:outError]; | |
if (!rows) return nil; | |
count = rows.count; | |
} | |
result = @[@(count)]; | |
} | |
break; | |
default: | |
break; | |
} | |
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent(); | |
#ifndef PROFILE | |
if (end - start > 1) { | |
#endif | |
NSLog(@"[tdis] fetch request ---------------- \n" | |
"[tdis] entity-name:%@\n" | |
"[tdis] resultType:%@\n" | |
"[tdis] fetchPredicate: %@\n" | |
"[tdis] --> took %f seconds\n" | |
"[tids]---------------- ", | |
entityName, CBLISResultTypeName(resultType), fetch.predicate, end - start); | |
#ifndef PROFILE | |
} | |
#endif | |
return result; | |
} else { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorUnsupportedRequestType | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"Unsupported requestType: %d", request.requestType] | |
}]; | |
return nil; | |
} | |
} | |
- (NSIncrementalStoreNode *)newValuesForObjectWithID:(NSManagedObjectID*)objectID withContext:(NSManagedObjectContext*)context error:(NSError**)outError | |
{ | |
CBLDocument* doc = [self.database documentWithID:[objectID couchbaseLiteIDRepresentation]]; | |
NSEntityDescription *entity = objectID.entity; | |
if (![entity.name isEqual:[doc propertyForKey:kCBLISTypeKey]]) { | |
entity = [NSEntityDescription entityForName:[doc propertyForKey:kCBLISTypeKey] | |
inManagedObjectContext:context]; | |
} | |
NSDictionary *values = [self _coreDataPropertiesOfDocumentWithID:doc.documentID properties:doc.properties withEntity:entity inContext:context]; | |
NSIncrementalStoreNode *node = [[NSIncrementalStoreNode alloc] initWithObjectID:objectID | |
withValues:values | |
version:1]; | |
return node; | |
} | |
- (id)newValueForRelationship:(NSRelationshipDescription*)relationship forObjectWithID:(NSManagedObjectID*)objectID withContext:(NSManagedObjectContext *)context error:(NSError **)outError | |
{ | |
if ([relationship isToMany]) { | |
CBLView *view = [self.database existingViewNamed:CBLISToManyViewNameForRelationship(relationship)]; | |
CBLQuery* query = [view createQuery]; | |
query.keys = @[ [objectID couchbaseLiteIDRepresentation] ]; | |
CBLQueryEnumerator *rows = [self _queryEnumeratorForQuery:query error:outError]; | |
if (!rows) return nil; | |
NSMutableArray *result = [NSMutableArray arrayWithCapacity:rows.count]; | |
for (CBLQueryRow* row in rows) { | |
[result addObject:[self _newObjectIDForEntity:relationship.destinationEntity | |
managedObjectContext:context couchID:row.documentID]]; | |
} | |
return result; | |
} else { | |
CBLDocument* doc = [self.database documentWithID:[objectID couchbaseLiteIDRepresentation]]; | |
NSString *destinationID = [doc propertyForKey:relationship.name]; | |
if (destinationID) { | |
return [self newObjectIDForEntity:relationship.destinationEntity referenceObject:destinationID]; | |
} else { | |
return [NSNull null]; | |
} | |
} | |
} | |
- (NSArray *)obtainPermanentIDsForObjects:(NSArray *)array error:(NSError **)outError | |
{ | |
NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; | |
for (NSManagedObject *object in array) { | |
// if you call -[NSManagedObjectContext obtainPermanentIDsForObjects:error:] yourself, | |
// this can get called with already permanent ids which leads to mismatch between store. | |
if (![object.objectID isTemporaryID]) { | |
[result addObject:object.objectID]; | |
} else { | |
NSString *uuid = [[NSProcessInfo processInfo] globallyUniqueString]; | |
NSManagedObjectID *objectID = [self newObjectIDForEntity:object.entity | |
referenceObject:uuid]; | |
[result addObject:objectID]; | |
} | |
} | |
return result; | |
} | |
- (NSManagedObjectID *)newObjectIDForEntity:(NSEntityDescription *)entity referenceObject:(id)data | |
{ | |
NSString *referenceObject = data; | |
if ([referenceObject hasPrefix:@"p"]) { | |
referenceObject = [referenceObject substringFromIndex:1]; | |
} | |
// we need to prefix the refernceObject with a non-numeric prefix, because of a bug where | |
// referenceObjects starting with a digit will only use the first digit part. As described here: | |
// https://github.com/AFNetworking/AFIncrementalStore/issues/82 | |
referenceObject = [kCBLISManagedObjectIDPrefix stringByAppendingString:referenceObject]; | |
NSManagedObjectID *objectID = [super newObjectIDForEntity:entity referenceObject:referenceObject]; | |
return objectID; | |
} | |
#pragma mark - Views | |
/** Initializes the views needed for querying objects by type and for to-many relationships. */ | |
- (void) _initializeViews | |
{ | |
NSMutableDictionary *subentitiesToSuperentities = [NSMutableDictionary dictionary]; | |
// Create a view for each to-many relationship | |
NSArray *entites = self.persistentStoreCoordinator.managedObjectModel.entities; | |
for (NSEntityDescription *entity in entites) { | |
NSArray *properties = [entity properties]; | |
for (NSPropertyDescription *property in properties) { | |
if ([property isKindOfClass:[NSRelationshipDescription class]]) { | |
NSRelationshipDescription *rel = (NSRelationshipDescription*)property; | |
if (rel.isToMany) { | |
NSMutableArray *entityNames = nil; | |
if (rel.destinationEntity.subentities.count > 0) { | |
entityNames = [NSMutableArray arrayWithCapacity:3]; | |
for (NSEntityDescription *subentity in rel.destinationEntity.subentities) { | |
[entityNames addObject:subentity.name]; | |
} | |
} | |
NSString *viewName = CBLISToManyViewNameForRelationship(rel); | |
NSString *destEntityName = rel.destinationEntity.name; | |
NSString *inverseRelNameLower = [rel.inverseRelationship.name lowercaseString]; | |
if (entityNames.count == 0) { | |
CBLView *view = [self.database viewNamed:viewName]; | |
[view setMapBlock:^(NSDictionary *doc, CBLMapEmitBlock emit) { | |
if ([[doc objectForKey:kCBLISTypeKey] isEqual:destEntityName] && [doc objectForKey:inverseRelNameLower]) { | |
emit([doc objectForKey:inverseRelNameLower], nil); | |
} | |
} | |
version:@"1.0"]; | |
[self _setViewName:viewName forFetchingProperty:inverseRelNameLower fromEntity:destEntityName]; | |
} else { | |
CBLView *view = [self.database viewNamed:viewName]; | |
[view setMapBlock:^(NSDictionary *doc, CBLMapEmitBlock emit) { | |
if ([entityNames containsObject:[doc objectForKey:kCBLISTypeKey]] && [doc objectForKey:inverseRelNameLower]) { | |
emit([doc objectForKey:inverseRelNameLower], nil); | |
} | |
} | |
version:@"1.0"]; | |
// remember view for mapping super-entity and all sub-entities | |
[self _setViewName:viewName forFetchingProperty:inverseRelNameLower fromEntity:rel.destinationEntity.name]; | |
for (NSString *entityName in entityNames) { | |
[self _setViewName:viewName forFetchingProperty:inverseRelNameLower fromEntity:entityName]; | |
} | |
} | |
} | |
} | |
} | |
if (entity.subentities.count > 0) { | |
for (NSEntityDescription *subentity in entity.subentities) { | |
[subentitiesToSuperentities setObject:entity.name forKey:subentity.name]; | |
} | |
} | |
} | |
// Create a view that maps entity names to instances | |
CBLView *view = [self.database viewNamed:kCBLISAllByTypeViewName]; | |
[view setMapBlock:^(NSDictionary *doc, CBLMapEmitBlock emit) { | |
NSString *ident = [doc valueForKey:@"_id"]; | |
if ([ident hasPrefix:@"cblis_"]) return; | |
NSString* type = [doc objectForKey: kCBLISTypeKey]; | |
if (type) emit(type, nil); | |
NSString *superentity = [subentitiesToSuperentities objectForKey:type]; | |
if (superentity) { | |
emit(superentity, nil); | |
} | |
} | |
version:@"1.0"]; | |
} | |
/** Creates a view for fetching entities by a property name. Can speed up fetching this entity by this property. */ | |
- (void) defineFetchViewForEntity:(NSString*)entityName | |
byProperty:(NSString*)propertyName | |
{ | |
NSString *viewName = [self _createViewNameForFetchingFromEntity:entityName byProperty:propertyName]; | |
CBLView *view = [self.database viewNamed:viewName]; | |
[view setMapBlock:^(NSDictionary *doc, CBLMapEmitBlock emit) { | |
NSString* type = [doc objectForKey:kCBLISTypeKey]; | |
if ([type isEqual:entityName] && [doc objectForKey:propertyName]) { | |
emit([doc objectForKey:propertyName], nil); | |
} | |
} | |
version:@"1.0"]; | |
[self _setViewName:viewName forFetchingProperty:propertyName fromEntity:entityName]; | |
} | |
#pragma mark - Querying | |
/** Queries the database by a given fetch request. Checks the cache for the result first. */ | |
- (NSArray*) _queryObjectsOfEntity:(NSEntityDescription*)entity byFetchRequest:(NSFetchRequest*)fetch inContext:(NSManagedObjectContext*)context error:(NSError**)outError | |
{ | |
id cached = [self _cachedQueryResultsForEntity:entity.name predicate:fetch.predicate]; | |
if (cached) { | |
return cached; | |
} | |
CBLQuery* query = [self _queryForFetchRequest:fetch onEntity:entity error:nil]; | |
if (!query) { | |
CBLView *view = [self.database existingViewNamed:kCBLISAllByTypeViewName]; | |
query = [view createQuery]; | |
query.keys = @[ entity.name ]; | |
query.prefetch = fetch.predicate != nil; | |
} | |
NSArray *result = [self _filterObjectsOfEntity:entity fromQuery:query byFetchRequest:fetch | |
inContext:context error:outError]; | |
return result; | |
} | |
/** Filters a query by a given fetch request. Checks the cache for the result first. */ | |
- (NSArray*) _filterObjectsOfEntity:(NSEntityDescription*)entity fromQuery:(CBLQuery*)query byFetchRequest:(NSFetchRequest*)fetch inContext:(NSManagedObjectContext*)context error:(NSError**)outError | |
{ | |
id cached = [self _cachedQueryResultsForEntity:entity.name predicate:fetch.predicate]; | |
if (cached) { | |
return cached; | |
} | |
CBLQueryEnumerator *rows = [self _queryEnumeratorForQuery:query error:outError]; | |
if (!rows) return nil; | |
NSMutableArray *array = [NSMutableArray arrayWithCapacity:rows.count]; | |
for (CBLQueryRow *row in rows) { | |
if (!fetch.predicate || [self _evaluatePredicate:fetch.predicate withEntity:entity properties:row.documentProperties]) { | |
NSManagedObjectID *objectID = [self _newObjectIDForEntity:entity managedObjectContext:context | |
couchID:row.documentID]; | |
NSManagedObject *object = [context objectWithID:objectID]; | |
[array addObject:object]; | |
} | |
} | |
[self _setCacheResults:array forEntity:entity.name predicate:fetch.predicate]; | |
return array; | |
} | |
/** Creates a query for fetching the data for an entity filtered by a NSFetchRequest. Only takes a NSComparisonPredicate that references | |
* the requested entity into account. | |
*/ | |
- (CBLQuery*) _queryForFetchRequest:(NSFetchRequest*)fetch onEntity:(NSEntityDescription*)entity error:(NSError**)outError | |
{ | |
NSPredicate *predicate = fetch.predicate; | |
if (!predicate) return nil; | |
// Check if the query is a compound query. | |
if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { | |
if (((NSCompoundPredicate*)predicate).subpredicates.count == 1 || | |
((NSCompoundPredicate*)predicate).compoundPredicateType == NSAndPredicateType) { | |
predicate = ((NSCompoundPredicate*)predicate).subpredicates[0]; | |
} else { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCreatingQueryFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error creating query: unsupported NSCompoundPredicate." | |
}]; | |
return nil; | |
} | |
} | |
if (![predicate isKindOfClass:[NSComparisonPredicate class]]) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCreatingQueryFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error creating query: unsupported predicate: only comparison predicate supported" | |
}]; | |
return nil; | |
} | |
NSComparisonPredicate *comparisonPredicate = (NSComparisonPredicate*)predicate; | |
if (comparisonPredicate.predicateOperatorType != NSEqualToPredicateOperatorType && | |
comparisonPredicate.predicateOperatorType != NSNotEqualToPredicateOperatorType && | |
comparisonPredicate.predicateOperatorType != NSInPredicateOperatorType) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCreatingQueryFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error creating query: unsupported predicate: only equal, not equal or IN supported" | |
}]; | |
return nil; | |
} | |
if (comparisonPredicate.leftExpression.expressionType != NSKeyPathExpressionType) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCreatingQueryFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error creating query: unsupported predicate: left expression invalid" | |
}]; | |
return nil; | |
} | |
if (comparisonPredicate.rightExpression.expressionType != NSConstantValueExpressionType) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCreatingQueryFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error creating query: unsupported predicate: right expression invalid" | |
}]; | |
return nil; | |
} | |
if (![self _hasViewForFetchingFromEntity:entity.name byProperty:comparisonPredicate.leftExpression.keyPath]) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorCreatingQueryFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error creating query: no view for that entity found" | |
}]; | |
return nil; | |
} | |
NSString *viewName = [self _viewNameForFetchingFromEntity:entity.name byProperty:comparisonPredicate.leftExpression.keyPath]; | |
if (!viewName) { | |
return nil; | |
} | |
CBLView *view = [self.database existingViewNamed:viewName]; | |
CBLQuery *query = [view createQuery]; | |
if (comparisonPredicate.predicateOperatorType == NSEqualToPredicateOperatorType) { | |
id rightValue = [comparisonPredicate.rightExpression constantValue]; | |
if ([rightValue isKindOfClass:[NSManagedObjectID class]]) { | |
rightValue = [rightValue couchbaseLiteIDRepresentation]; | |
} else if ([rightValue isKindOfClass:[NSManagedObject class]]) { | |
rightValue = [[rightValue objectID] couchbaseLiteIDRepresentation]; | |
} | |
query.keys = @[ rightValue ]; | |
} else if (comparisonPredicate.predicateOperatorType == NSInPredicateOperatorType) { | |
id rightValue = [comparisonPredicate.rightExpression constantValue]; | |
if ([rightValue isKindOfClass:[NSSet class]]) { | |
rightValue = [[self _replaceManagedObjectsWithCouchIDInSet:rightValue] allObjects]; | |
} else if ([rightValue isKindOfClass:[NSArray class]]) { | |
rightValue = [self _replaceManagedObjectsWithCouchIDInArray:rightValue]; | |
} else if (rightValue != nil) { | |
NSAssert(NO, @"Wrong value in IN predicate rhv"); | |
} | |
query.keys = rightValue; | |
} | |
query.prefetch = YES; | |
return query; | |
} | |
- (NSArray*) _replaceManagedObjectsWithCouchIDInArray:(NSArray*)array | |
{ | |
NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; | |
for (id value in array) { | |
if ([value isKindOfClass:[NSManagedObject class]]) { | |
[result addObject:[[value objectID] couchbaseLiteIDRepresentation]]; | |
} else if ([value isKindOfClass:[NSManagedObjectID class]]) { | |
[result addObject:[value couchbaseLiteIDRepresentation]]; | |
} else { | |
[result addObject:value]; | |
} | |
} | |
return result; | |
} | |
- (NSSet*) _replaceManagedObjectsWithCouchIDInSet:(NSSet*)set | |
{ | |
NSMutableSet *result = [NSMutableSet setWithCapacity:set.count]; | |
for (id value in set) { | |
if ([value isKindOfClass:[NSManagedObject class]]) { | |
[result addObject:[[value objectID] couchbaseLiteIDRepresentation]]; | |
} else if ([value isKindOfClass:[NSManagedObjectID class]]) { | |
[result addObject:[value couchbaseLiteIDRepresentation]]; | |
} else { | |
[result addObject:value]; | |
} | |
} | |
return result; | |
} | |
- (NSManagedObjectID *)_newObjectIDForEntity:(NSEntityDescription *)entity managedObjectContext:(NSManagedObjectContext*)context | |
couchID:(NSString*)couchID | |
{ | |
NSManagedObjectID *objectID = [self newObjectIDForEntity:entity referenceObject:couchID]; | |
return objectID; | |
} | |
- (id) _couchbaseLiteRepresentationOfManagedObject:(NSManagedObject*)object | |
{ | |
return [self _couchbaseLiteRepresentationOfManagedObject:object withCouchbaseLiteID:NO]; | |
} | |
- (id) _couchbaseLiteRepresentationOfManagedObject:(NSManagedObject*)object withCouchbaseLiteID:(BOOL)withID | |
{ | |
NSEntityDescription *desc = object.entity; | |
NSDictionary *propertyDesc = [desc propertiesByName]; | |
NSMutableDictionary *proxy = [NSMutableDictionary dictionary]; | |
[proxy setObject:desc.name | |
forKey:kCBLISTypeKey]; | |
if ([propertyDesc objectForKey:kCBLISCurrentRevisionAttributeName]) { | |
id rev = [object valueForKey:kCBLISCurrentRevisionAttributeName]; | |
if (!CBLISIsNull(rev)) { | |
[proxy setObject:rev forKey:@"_rev"]; | |
} | |
} | |
if (withID) { | |
[proxy setObject:[object.objectID couchbaseLiteIDRepresentation] forKey:@"_id"]; | |
} | |
NSMutableArray *dataAttributes = nil; | |
for (NSString *property in propertyDesc) { | |
if ([kCBLISCurrentRevisionAttributeName isEqual:property]) continue; | |
id desc = [propertyDesc objectForKey:property]; | |
if ([desc isKindOfClass:[NSAttributeDescription class]]) { | |
NSAttributeDescription *attr = desc; | |
if ([attr isTransient]) { | |
continue; | |
} | |
// handle binary attributes to not load them into memory here | |
if ([attr attributeType] == NSBinaryDataAttributeType) { | |
if (!dataAttributes) { | |
dataAttributes = [NSMutableArray array]; | |
} | |
[dataAttributes addObject:attr]; | |
continue; | |
} | |
id value = [object valueForKey:property]; | |
if (value) { | |
NSAttributeType attributeType = [attr attributeType]; | |
if (attr.valueTransformerName) { | |
NSValueTransformer *transformer = [NSValueTransformer valueTransformerForName:attr.valueTransformerName]; | |
if (!transformer) { | |
NSLog(@"[info] value transformer for attribute %@ with name %@ not found", attr.name, attr.valueTransformerName); | |
continue; | |
} | |
value = [transformer transformedValue:value]; | |
Class transformedClass = [[transformer class] transformedValueClass]; | |
if (transformedClass == [NSString class]) { | |
attributeType = NSStringAttributeType; | |
} else if (transformedClass == [NSData class]) { | |
value = [value base64EncodedStringWithOptions:0]; | |
attributeType = NSStringAttributeType; | |
} else { | |
NSLog(@"[info] unsupported value transformer transformedValueClass: %@", NSStringFromClass(transformedClass)); | |
continue; | |
} | |
} | |
switch (attributeType) { | |
case NSInteger16AttributeType: | |
case NSInteger32AttributeType: | |
case NSInteger64AttributeType: | |
value = [NSNumber numberWithLong:CBLISIsNull(value) ? 0 : [value longValue]]; | |
break; | |
case NSDecimalAttributeType: | |
case NSDoubleAttributeType: | |
case NSFloatAttributeType: | |
value = [NSNumber numberWithDouble:CBLISIsNull(value) ? 0.0 : [value doubleValue]]; | |
break; | |
case NSStringAttributeType: | |
value = CBLISIsNull(value) ? @"" : value; | |
break; | |
case NSBooleanAttributeType: | |
value = [NSNumber numberWithBool:CBLISIsNull(value) ? NO : [value boolValue]]; | |
break; | |
case NSDateAttributeType: | |
value = CBLISIsNull(value) ? nil : [CBLJSON JSONObjectWithDate:value]; | |
break; | |
// case NSBinaryDataAttributeType: // handled above | |
case NSUndefinedAttributeType: | |
// intentionally do nothing | |
break; | |
default: | |
NSLog(@"[info] unsupported attribute %@, type: %@ (%d)", attr.name, attr, (int)[attr attributeType]); | |
break; | |
} | |
if (value) { | |
[proxy setObject:value forKey:property]; | |
} | |
} | |
} else if ([desc isKindOfClass:[NSRelationshipDescription class]]) { | |
NSRelationshipDescription *rel = desc; | |
id relationshipDestination = [object valueForKey:property]; | |
if (relationshipDestination) { | |
if (![rel isToMany]) { | |
NSManagedObjectID *objectID = [relationshipDestination valueForKey:@"objectID"]; | |
[proxy setObject:[objectID couchbaseLiteIDRepresentation] forKey:property]; | |
} | |
} | |
} | |
} | |
// add binary data attributes as attachment | |
if (dataAttributes) { | |
NSMutableDictionary *attachments = [NSMutableDictionary dictionary]; | |
for (NSAttributeDescription *attribute in dataAttributes) { | |
NSData *data = [object valueForKey:attribute.name]; | |
if (!data) continue; | |
[attachments setObject:@{ | |
@"data": [data base64EncodedStringWithOptions:0], | |
@"length": @(data.length), | |
@"content_type": @"application/binary" | |
} forKey:attribute.name]; | |
} | |
if (attachments.count > 0) { | |
[proxy setObject:attachments forKey:@"_attachments"]; | |
} | |
} | |
return proxy; | |
} | |
- (NSDictionary*) _coreDataPropertiesOfDocumentWithID:(NSString*)documentID properties:(NSDictionary*)properties withEntity:(NSEntityDescription*)entity inContext:(NSManagedObjectContext*)context | |
{ | |
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:properties.count]; | |
NSDictionary *propertyDesc = [entity propertiesByName]; | |
for (NSString *property in propertyDesc) { | |
id desc = [propertyDesc objectForKey:property]; | |
if ([desc isKindOfClass:[NSAttributeDescription class]]) { | |
NSAttributeDescription *attr = desc; | |
if ([attr isTransient]) { | |
continue; | |
} | |
// handle binary attributes specially | |
if ([attr attributeType] == NSBinaryDataAttributeType) { | |
NSDictionary *attachments = [properties objectForKey:@"_attachments"]; | |
NSDictionary *attachment = [attachments objectForKey:property]; | |
if (!attachment) continue; | |
id value = [self _loadDataForAttachmentWithName:property ofDocumentWithID:documentID metadata:attachment]; | |
if (value) { | |
[result setObject:value forKey:property]; | |
} | |
continue; | |
} | |
id value = nil; | |
if ([kCBLISCurrentRevisionAttributeName isEqual:property]) { | |
value = [properties objectForKey:@"_rev"]; | |
} else { | |
value = [properties objectForKey:property]; | |
} | |
if (value) { | |
NSAttributeType attributeType = [attr attributeType]; | |
if (attr.valueTransformerName) { | |
NSValueTransformer *transformer = [NSValueTransformer valueTransformerForName:attr.valueTransformerName]; | |
Class transformedClass = [[transformer class] transformedValueClass]; | |
if (transformedClass == [NSString class]) { | |
value = [transformer reverseTransformedValue:value]; | |
} else if (transformedClass == [NSData class]) { | |
value = [transformer reverseTransformedValue:[[NSData alloc]initWithBase64EncodedString:value options:0]]; | |
} else { | |
NSLog(@"[info] unsupported value transformer transformedValueClass: %@", NSStringFromClass(transformedClass)); | |
continue; | |
} | |
} | |
switch (attributeType) { | |
case NSInteger16AttributeType: | |
case NSInteger32AttributeType: | |
case NSInteger64AttributeType: | |
value = [NSNumber numberWithLong:CBLISIsNull(value) ? 0 : [value longValue]]; | |
break; | |
case NSDecimalAttributeType: | |
case NSDoubleAttributeType: | |
case NSFloatAttributeType: | |
value = [NSNumber numberWithDouble:CBLISIsNull(value) ? 0.0 : [value doubleValue]]; | |
break; | |
case NSStringAttributeType: | |
value = CBLISIsNull(value) ? @"" : value; | |
break; | |
case NSBooleanAttributeType: | |
value = [NSNumber numberWithBool:CBLISIsNull(value) ? NO : [value boolValue]]; | |
break; | |
case NSDateAttributeType: | |
value = CBLISIsNull(value) ? nil : [CBLJSON dateWithJSONObject:value]; | |
break; | |
case NSTransformableAttributeType: | |
// intentionally do nothing | |
break; | |
/* | |
default: | |
//NSAssert(NO, @"Unsupported attribute type"); | |
//break; | |
NSLog(@"ii unsupported attribute %@, type: %@ (%d)", attribute, attr, [attr attributeType]); | |
*/ | |
} | |
if (value) { | |
[result setObject:value forKey:property]; | |
} | |
} | |
} else if ([desc isKindOfClass:[NSRelationshipDescription class]]) { | |
NSRelationshipDescription *rel = desc; | |
if (![rel isToMany]) { // only handle to-one relationships | |
id value = [properties objectForKey:property]; | |
if (!CBLISIsNull(value)) { | |
NSManagedObjectID *destination = [self newObjectIDForEntity:rel.destinationEntity | |
referenceObject:value]; | |
[result setObject:destination forKey:property]; | |
} | |
} | |
} | |
} | |
return result; | |
} | |
/** Convenience method to execute a CouchbaseLite query and build a telling NSError if it fails. */ | |
- (CBLQueryEnumerator*) _queryEnumeratorForQuery:(CBLQuery*)query error:(NSError**)outError | |
{ | |
NSError *error; | |
CBLQueryEnumerator *rows = [query run:&error]; | |
if (!rows) { | |
if (outError) *outError = [NSError errorWithDomain:kCBLIncrementalStoreErrorDomain | |
code:CBLIncrementalStoreErrorQueryingCouchbaseLiteFailed | |
userInfo:@{ | |
NSLocalizedFailureReasonErrorKey: @"Error querying CouchbaseLite", | |
NSUnderlyingErrorKey: error | |
}]; | |
return nil; | |
} | |
return rows; | |
} | |
#pragma mark - Caching | |
- (void) _setCacheResults:(NSArray*)array forEntity:(NSString*)entityName predicate:(NSPredicate*)predicate | |
{ | |
NSString *cacheKey = [NSString stringWithFormat:@"%@_%@", entityName, predicate]; | |
[_fetchRequestResultCache setObject:array forKey:cacheKey]; | |
} | |
- (NSArray*) _cachedQueryResultsForEntity:(NSString*)entityName predicate:(NSPredicate*)predicate | |
{ | |
NSString *cacheKey = [NSString stringWithFormat:@"%@_%@", entityName, predicate]; | |
return [_fetchRequestResultCache objectForKey:cacheKey]; | |
} | |
- (void) _purgeCacheForEntityName:(NSString*)type | |
{ | |
for (NSString *key in [_fetchRequestResultCache allKeys]) { | |
if ([key hasPrefix:type]) { | |
[_fetchRequestResultCache removeObjectForKey:key]; | |
} | |
} | |
} | |
#pragma mark - NSPredicate | |
- (BOOL) _evaluatePredicate:(NSPredicate*)predicate withEntity:(NSEntityDescription*)entity properties:(NSDictionary*)properties | |
{ | |
if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { | |
NSCompoundPredicate *compoundPredicate = (NSCompoundPredicate*)predicate; | |
NSCompoundPredicateType type = compoundPredicate.compoundPredicateType; | |
if (compoundPredicate.subpredicates.count == 0) { | |
switch (type) { | |
case NSAndPredicateType: | |
return YES; | |
break; | |
case NSOrPredicateType: | |
return NO; | |
break; | |
default: | |
return NO; | |
break; | |
} | |
} | |
BOOL compoundResult = NO; | |
for (NSPredicate *subpredicate in compoundPredicate.subpredicates) { | |
BOOL result = [self _evaluatePredicate:subpredicate withEntity:entity properties:properties]; | |
switch (type) { | |
case NSAndPredicateType: | |
if (!result) return NO; | |
compoundResult = YES; | |
break; | |
case NSOrPredicateType: | |
if (result) return YES; | |
break; | |
case NSNotPredicateType: | |
return !result; | |
break; | |
default: | |
break; | |
} | |
} | |
return compoundResult; | |
} else if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { | |
NSComparisonPredicate *comparisonPredicate = (NSComparisonPredicate*)predicate; | |
id leftValue = [self _evaluateExpression:comparisonPredicate.leftExpression withEntity:entity properties:properties]; | |
id rightValue = [self _evaluateExpression:comparisonPredicate.rightExpression withEntity:entity properties:properties]; | |
NSExpression *leftExpression = [NSExpression expressionForConstantValue:leftValue]; | |
NSExpression *rightExpression = [NSExpression expressionForConstantValue:rightValue]; | |
NSPredicate *comp = [NSComparisonPredicate predicateWithLeftExpression:leftExpression rightExpression:rightExpression | |
modifier:comparisonPredicate.comparisonPredicateModifier | |
type:comparisonPredicate.predicateOperatorType | |
options:comparisonPredicate.options]; | |
BOOL result = [comp evaluateWithObject:nil]; | |
return result; | |
} | |
return NO; | |
} | |
- (id) _evaluateExpression:(NSExpression*)expression withEntity:(NSEntityDescription*)entity properties:(NSDictionary*)properties | |
{ | |
id value = nil; | |
switch (expression.expressionType) { | |
case NSConstantValueExpressionType: | |
value = [expression constantValue]; | |
break; | |
case NSEvaluatedObjectExpressionType: | |
value = properties; | |
break; | |
case NSKeyPathExpressionType: { | |
value = [properties objectForKey:expression.keyPath]; | |
if (!value) return nil; | |
NSPropertyDescription *property = [entity.propertiesByName objectForKey:expression.keyPath]; | |
if ([property isKindOfClass:[NSRelationshipDescription class]]) { | |
// if it's a relationship it should be a MOCID | |
NSRelationshipDescription *rel = (NSRelationshipDescription*)property; | |
value = [self newObjectIDForEntity:rel.destinationEntity referenceObject:value]; | |
} | |
} | |
break; | |
default: | |
NSAssert(NO, @"[devel] Expression Type not yet supported: %@", expression); | |
break; | |
} | |
// not supported yet: | |
// NSFunctionExpressionType, | |
// NSAggregateExpressionType, | |
// NSSubqueryExpressionType = 13, | |
// NSUnionSetExpressionType, | |
// NSIntersectSetExpressionType, | |
// NSMinusSetExpressionType, | |
// NSBlockExpressionType = 19 | |
return value; | |
} | |
#pragma mark - Views | |
- (NSString*) _createViewNameForFetchingFromEntity:(NSString*)entityName | |
byProperty:(NSString*)propertyName | |
{ | |
NSString *viewName = [NSString stringWithFormat:kCBLISFetchEntityByPropertyViewNameFormat, [entityName lowercaseString], propertyName]; | |
return viewName; | |
} | |
- (BOOL) _hasViewForFetchingFromEntity:(NSString*)entityName | |
byProperty:(NSString*)propertyName | |
{ | |
return [self _viewNameForFetchingFromEntity:entityName byProperty:propertyName] != nil; | |
} | |
- (NSString*) _viewNameForFetchingFromEntity:(NSString*)entityName | |
byProperty:(NSString*)propertyName | |
{ | |
return [_entityAndPropertyToFetchViewName objectForKey:[NSString stringWithFormat:@"%@_%@", entityName, propertyName]]; | |
} | |
- (void) _setViewName:(NSString*)viewName forFetchingProperty:(NSString*)propertyName fromEntity:(NSString*)entity | |
{ | |
[_entityAndPropertyToFetchViewName setObject:viewName | |
forKey:[NSString stringWithFormat:@"%@_%@", entity, propertyName]]; | |
} | |
#pragma mark - Attachments | |
- (NSData*) _loadDataForAttachmentWithName:(NSString*)name ofDocumentWithID:(NSString*)documentID metadata:(NSDictionary*)metadata | |
{ | |
CBLDocument *document = [self.database documentWithID:documentID]; | |
CBLAttachment *attachment = [document.currentRevision attachmentNamed:name]; | |
return attachment.content; | |
} | |
#pragma mark - Change Handling | |
- (void) addObservingManagedObjectContext:(NSManagedObjectContext*)context | |
{ | |
if (!_observingManagedObjectContexts) { | |
_observingManagedObjectContexts = [[NSMutableArray alloc] init]; | |
} | |
[_observingManagedObjectContexts addObject:context]; | |
} | |
- (void) removeObservingManagedObjectContext:(NSManagedObjectContext*)context | |
{ | |
[_observingManagedObjectContexts removeObject:context]; | |
} | |
- (void) _informManagedObjectContext:(NSManagedObjectContext*)context updatedIDs:(NSArray*)updatedIDs deletedIDs:(NSArray*)deletedIDs | |
{ | |
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:3]; | |
if (updatedIDs.count > 0) { | |
NSMutableArray *updated = [NSMutableArray arrayWithCapacity:updatedIDs.count]; | |
NSMutableArray *inserted = [NSMutableArray arrayWithCapacity:updatedIDs.count]; | |
for (NSManagedObjectID *mocid in updatedIDs) { | |
NSManagedObject *moc = [context objectRegisteredForID:mocid]; | |
if (!moc) { | |
moc = [context objectWithID:mocid]; | |
[inserted addObject:moc]; | |
} else { | |
[context refreshObject:moc mergeChanges:YES]; | |
[updated addObject:moc]; | |
} | |
} | |
[userInfo setObject:updated forKey:NSUpdatedObjectsKey]; | |
if (inserted.count > 0) { | |
[userInfo setObject:inserted forKey:NSInsertedObjectsKey]; | |
} | |
} | |
if (deletedIDs.count > 0) { | |
NSMutableArray *deleted = [NSMutableArray arrayWithCapacity:deletedIDs.count]; | |
for (NSManagedObjectID *mocid in deletedIDs) { | |
NSManagedObject *moc = [context objectWithID:mocid]; | |
[context deleteObject:moc]; | |
// load object again to get a fault | |
[deleted addObject:[context objectWithID:mocid]]; | |
} | |
[userInfo setObject:deleted forKey:NSDeletedObjectsKey]; | |
} | |
NSNotification *didUpdateNote = [NSNotification notificationWithName:NSManagedObjectContextObjectsDidChangeNotification | |
object:context userInfo:userInfo]; | |
[context mergeChangesFromContextDidSaveNotification:didUpdateNote]; | |
} | |
- (void) _informObservingManagedObjectContextsAboutUpdatedIDs:(NSArray*)updatedIDs deletedIDs:(NSArray*)deletedIDs | |
{ | |
for (NSManagedObjectContext *context in self.observingManagedObjectContexts) { | |
[self _informManagedObjectContext:context updatedIDs:updatedIDs deletedIDs:deletedIDs]; | |
} | |
} | |
- (void) _couchDocumentsChanged:(NSArray*)changes | |
{ | |
#if CBLIS_NO_CHANGE_COALESCING | |
[_coalescedChanges addObjectsFromArray:changes]; | |
[self _processCouchbaseLiteChanges]; | |
#else | |
[NSThread cancelPreviousPerformRequestsWithTarget:self selector:@selector(_processCouchbaseLiteChanges) object:nil]; | |
@synchronized(self) { | |
[_coalescedChanges addObjectsFromArray:changes]; | |
} | |
[self performSelector:@selector(_processCouchbaseLiteChanges) withObject:nil afterDelay:0.1]; | |
#endif | |
} | |
- (void) _processCouchbaseLiteChanges | |
{ | |
NSArray *changes = nil; | |
@synchronized(self) { | |
changes = _coalescedChanges; | |
_coalescedChanges = [[NSMutableArray alloc] initWithCapacity:20]; | |
} | |
NSMutableSet *changedEntitites = [NSMutableSet setWithCapacity:changes.count]; | |
NSMutableArray *deletedObjectIDs = [NSMutableArray array]; | |
NSMutableArray *updatedObjectIDs = [NSMutableArray array]; | |
for (CBLDatabaseChange *change in changes) { | |
CBLRevision *rev = [[_database documentWithID:change.documentID] revisionWithID:change.revisionID]; | |
NSString *ident = change.documentID; | |
BOOL deleted = rev.isDeletion; | |
if ([ident hasPrefix:@"CBLIS"]) { | |
continue; | |
} | |
NSDictionary *properties = [rev properties]; | |
NSString *type = [properties objectForKey:kCBLISTypeKey]; | |
NSString *reference = ident; | |
[changedEntitites addObject:type]; | |
NSEntityDescription *entity = [self.persistentStoreCoordinator.managedObjectModel.entitiesByName objectForKey:type]; | |
NSManagedObjectID *objectID = [self newObjectIDForEntity:entity referenceObject:reference]; | |
if (deleted) { | |
[deletedObjectIDs addObject:objectID]; | |
} else { | |
[updatedObjectIDs addObject:objectID]; | |
} | |
} | |
[self _informObservingManagedObjectContextsAboutUpdatedIDs:updatedObjectIDs deletedIDs:deletedObjectIDs]; | |
NSDictionary *userInfo = @{ | |
NSDeletedObjectsKey: deletedObjectIDs, | |
NSUpdatedObjectsKey: updatedObjectIDs | |
}; | |
[[NSNotificationCenter defaultCenter] postNotificationName:kCBLISObjectHasBeenChangedInStoreNotification | |
object:self userInfo:userInfo]; | |
} | |
#pragma mark - Conflicts handling | |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context | |
{ | |
if ([@"rows" isEqualToString:keyPath]) { | |
CBLLiveQuery *query = object; | |
NSError *error; | |
CBLQueryEnumerator *enumerator = [query run:&error]; | |
if (enumerator.count == 0) return; | |
[self _resolveConflicts:enumerator]; | |
} | |
} | |
- (void) _resolveConflicts:(CBLQueryEnumerator*)enumerator | |
{ | |
// resolve conflicts | |
for (CBLQueryRow *row in enumerator) { | |
if ([kCBLISMetadataDocumentID isEqual:row.documentID]) { | |
// TODO: what to do here? | |
continue; | |
} | |
if (self.conflictHandler) self.conflictHandler(row.conflictingRevisions); | |
} | |
} | |
- (CBLISConflictHandler) _defaultConflictHandler | |
{ | |
CBLISConflictHandler handler = ^(NSArray *conflictingRevisions) { | |
// merges changes by | |
// - taking the winning revision | |
// - adding missing values from other revisions (starting with biggest version) | |
CBLRevision *winning = conflictingRevisions[0]; | |
NSMutableDictionary *properties = [winning.properties mutableCopy]; | |
NSRange otherRevisionsRange = NSMakeRange(1, conflictingRevisions.count - 1); | |
NSArray *otherRevisions = [conflictingRevisions subarrayWithRange:otherRevisionsRange]; | |
NSArray *desc = @[[NSSortDescriptor sortDescriptorWithKey:@"revisionID" | |
ascending:NO]]; | |
NSArray *sortedRevisions = [otherRevisions sortedArrayUsingDescriptors:desc]; | |
// this solution merges missing keys from other conflicting revisions to not loose any values | |
for (CBLRevision *rev in sortedRevisions) { | |
for (NSString *key in rev.properties) { | |
if ([key hasPrefix:@"_"]) continue; | |
if (![properties objectForKey:key]) { | |
[properties setObject:[rev propertyForKey:key] forKey:key]; | |
} | |
} | |
} | |
// TODO: Attachments | |
CBLUnsavedRevision *newRevision = [winning.document newRevision]; | |
[newRevision setProperties:properties]; | |
NSError *error; | |
[newRevision save:&error]; | |
}; | |
return handler; | |
} | |
#pragma mark - | |
/** Returns the properties that are stored for deleting a document. Must contain at least "_rev" and "_deleted" = true for | |
* CouchbaseLite and kCBLISTypeKey for this store. Can be overridden if you need more for filtered syncing, for example. | |
*/ | |
- (NSDictionary*) _propertiesForDeletingDocument:(CBLDocument*)doc | |
{ | |
NSDictionary *contents = @{ | |
@"_deleted": @YES, | |
@"_rev": [doc propertyForKey:@"_rev"], | |
kCBLISTypeKey: [doc propertyForKey:kCBLISTypeKey] | |
}; | |
return contents; | |
} | |
@end | |
@implementation NSManagedObjectID (CBLIncrementalStore) | |
/** Returns an internal representation of this objectID that is used as _id in Couchbase. */ | |
- (NSString*) couchbaseLiteIDRepresentation | |
{ | |
// +1 because of "p" prefix in managed object IDs | |
NSString *uuid = [[self.URIRepresentation lastPathComponent] substringFromIndex:kCBLISManagedObjectIDPrefix.length + 1]; | |
return uuid; | |
} | |
@end | |
//// utility methods | |
/** Checks if value is nil or NSNull. */ | |
BOOL CBLISIsNull(id value) | |
{ | |
return value == nil || [value isKindOfClass:[NSNull class]]; | |
} | |
/** returns name of a view that returns objectIDs for all destination entities of a to-many relationship. */ | |
NSString *CBLISToManyViewNameForRelationship(NSRelationshipDescription *relationship) | |
{ | |
NSString *entityName = relationship.entity.name; | |
NSString *destinationName = relationship.destinationEntity.name; | |
return [NSString stringWithFormat:kCBLISFetchEntityToManyViewNameFormat, entityName, destinationName]; | |
} | |
/** Returns a readable name for a NSFetchRequestResultType */ | |
NSString *CBLISResultTypeName(NSFetchRequestResultType resultType) | |
{ | |
switch (resultType) { | |
case NSManagedObjectResultType: | |
return @"NSManagedObjectResultType"; | |
case NSManagedObjectIDResultType: | |
return @"NSManagedObjectIDResultType"; | |
case NSDictionaryResultType: | |
return @"NSDictionaryResultType"; | |
case NSCountResultType: | |
return @"NSCountResultType"; | |
default: | |
return @"Unknown"; | |
break; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment