Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Data "The Database appears corrupt" -- What causes this error?

I'm banging my head against the wall here, I'm using Core Data for a SQLLite DB, and I am able to successfully save to the database (I've checked the contents in an offline SQLLite browser), but after saving the first query I attempt to run comes back with the error seen below, and I can't find any helpful information on the internet relating to this specific error:

Core Data: error: -executeRequest: encountered exception = The database appears corrupt. (invalid primary key) with userInfo = { NSFilePath = "/Users/user/Library/Application Support/iPhone Simulator/7.0.3/Documents/db.sqlite";

The question here, is what causes this error, as I can't find any information relating to it.

For a little background, this is my setup, please assume I've got good reasons for the design that has been made and don't provide an answer of "Change your design" unless you can see something fundamentally broken with the pattern itself.

I have 3 Managed object contexts, all of them are NSPrivateQueueConcurrencyType, the first (A) is attached to the Persistent Store Coordinator, the second (B) has A set as its parent context, and the third (C) has B set as it's parent context -- A chain. The reason for this is that C is a writable context, fetching data from a network source and synchronizing it and saving it, B is a context shared by the UI elements, and I want it to be responsive, finally A is a background context designed to offload any delays for saving to disk off Context B & C

PSC <-- A <-- B <-- C

If I take out the last step (Saving A to PSC) then the application runs great, keeping everything in memory and querying against the in memory contexts. The crash only occurs after I add the save step back in, and only on the first query run against the DB after that save. Both my Save's and my fetch execution are wrapped in performBlock:

Here is the last save:

- (void)deepSave
{
    // Save to the Save Context which happens in memory, so the actual write to disk operation occurs on background thread
    // Expects to be called with performBlock

    NSError *error = nil;
    [super save:&error];
    NSAssert(!error, error.localizedDescription);

    // Trigger the save context to save to disk, operation will be queued and free up read only context
    NSManagedObjectContext *saveContext = self.parentContext;
    [saveContext performBlock:^{
        NSError *error = nil;
        [saveContext save:&error];
        NSAssert(!error, error.localizedDescription);
    }];
}

And here is the execute stack (on NSManagedObjectContext Queue thread)

#0  0x0079588a in objc_exception_throw ()
#1  0x079d98e7 in -[NSSQLiteConnection handleCorruptedDB:] ()
#2  0x078d9b8d in -[NSSQLiteConnection fetchResultSet:usingFetchPlan:] ()
#3  0x078e24a5 in newFetchedRowsForFetchPlan_MT ()
#4  0x078cd48e in -[NSSQLCore newRowsForFetchPlan:] ()
#5  0x078cca8d in -[NSSQLCore objectsForFetchRequest:inContext:] ()
#6  0x078cc53f in -[NSSQLCore executeRequest:withContext:error:] ()
#7  0x078cbf62 in -[NSPersistentStoreCoordinator executeRequest:withContext:error:] ()
#8  0x078c96c6 in -[NSManagedObjectContext executeFetchRequest:error:] ()
#9  0x0791e526 in -[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:] ()
#10 0x0799c1f4 in __82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke ()
#11 0x0791e321 in internalBlockToNSManagedObjectContextPerform ()
#12 0x013c34b0 in _dispatch_client_callout ()
#13 0x013b0778 in _dispatch_barrier_sync_f_invoke ()
#14 0x013b0422 in dispatch_barrier_sync_f ()
#15 0x0791e2a2 in _perform ()
#16 0x0791e14e in -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:] ()
#17 0x078c96c6 in -[NSManagedObjectContext executeFetchRequest:error:] ()
#18 0x0791e526 in -[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:] ()
#19 0x0799c1f4 in __82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke ()
#20 0x0791e321 in internalBlockToNSManagedObjectContextPerform ()
#21 0x013c34b0 in _dispatch_client_callout ()
#22 0x013b0778 in _dispatch_barrier_sync_f_invoke ()
#23 0x013b0422 in dispatch_barrier_sync_f ()
#24 0x0791e2a2 in _perform ()
#25 0x0791e14e in -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:] ()
#26 0x078c96c6 in -[NSManagedObjectContext executeFetchRequest:error:] ()
like image 725
BadPirate Avatar asked Feb 27 '14 01:02

BadPirate


2 Answers

Okay, I've tracked it down. It appears that there is something broken in propertiesToFetch against NSManagedObject resultType (not supposed to be used, our mistake) in this setup -- against a context that has a parent context instead of a persistent coordinator. This unit test shows that all you have to do is set a property to fetch to get this error (while doing the query without the properties to fetch works correctly). The fix here for us was to stop incorrectly using properties to fetch :)

- (void)testManagedObjectContextDefect
{        
    NSManagedObjectContext *contextA = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    contextA.persistentStoreCoordinator = sqllitePersistentStoreCoordinator;
    NSManagedObjectContext *contextB = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    contextB.parentContext = contextA;

    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"GCSCObject" inManagedObjectContext:contextB];

    [contextB performBlockAndWait:^{
        GCSCObject *object = [[GCSCObject alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:contextB];
        object.serverID = @"1";
        NSError *error = nil;
        XCTAssert([contextB save:&error] && !error, @"Failed to save - %@",error); // B -> A save
    }];

    [contextA performBlock:^{
        NSError *error = nil;
        XCTAssert([contextA save:&error] && !error, @"Failed to save - %@",error); // A -> PSC, background save
    }];

    [contextB performBlockAndWait:^{
        NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:@"GCSCObject"];
        NSError *error = nil;
        NSArray *results = [contextB executeFetchRequest:request error:&error];
        XCTAssert(results.count == 1 && !error, @"Fetch failed to retrieve - %@ / %@",results,error);
        GCSCObject *object = results[0];
        XCTAssert([object.serverID isEqualToString:@"1"], @"Value retrieval failed");

        // Everything passes up to here, so far so good!

        request = [[NSFetchRequest alloc] initWithEntityName:@"GCSCObject"];
        request.propertiesToFetch = @[@"serverID"]; // This is the culprit of the index crash
        results = [contextB executeFetchRequest:request error:&error];
        XCTAssert(!error, @"%@", error.localizedDescription); // !!! HERE we have a failure, assert: "Core Data: error: -executeRequest: encountered exception = The database appears corrupt.  (invalid primary key) with userInfo = { NSFilePath = "/path/db.sqlite }";
    }];
}

In this case GCSCObject is a regular entity, and serverID is one of it's parameters (it doesn't matter which parameter is used, or what type it is, I tried with multiple. Here is the description of the serverID parameter I used for this test:

serverID description

The crash occurs wether or not we provide the andWait to contextA save (though doing so would kind of void the point of having a background queue for saving)

I'd love feedback regarding why this might be the case, but for now, not using properties to fetch allows our application to work smoothly. I'm contemplating filing an Apple Bug here.

like image 195
BadPirate Avatar answered Oct 14 '22 13:10

BadPirate


If you experience this error when performing a fetch request that is retrieving some aggregate result (sum, max, min, ...) make sure you set

fetchRequest.resultType = NSDictionaryResultType;
like image 9
ancajic Avatar answered Oct 14 '22 13:10

ancajic