Skip to content

Instantly share code, notes, and snippets.

@jchris
Created December 13, 2013 22:56
Show Gist options
  • Save jchris/7952874 to your computer and use it in GitHub Desktop.
Save jchris/7952874 to your computer and use it in GitHub Desktop.

Revisions

  1. jchris created this gist Dec 13, 2013.
    1,608 changes: 1,608 additions & 0 deletions cblis.m
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1608 @@
    //
    // 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;
    }
    }