iosswiftrealmdatabase-migrationrealm-migration

New Realm migration gets called on new app installs


I have an app which uses Realm, it currently has two different migrations and I'm working on the third one, but for some reason this new migration is having a weird behavior. The way I understand migrations is that the new ones will only run if previous schema version already exists otherwise they will be ignored and the schema will be created base on the model's structure.

Here is what I'm seeing.

Scenario 1: WORKS FINE.

If the app was previously installed, everything works fine, the new migration 3 runs and it does everything it supposed to do, data gets converted successfully and everything works fine as expected.

Scenario 2: ERROR.

If the app is being installed for the first time and the user starts adding data as soon as the app launches, the app will crash the next time the app is launched (the second time AppDelegate is called). I put a print statement and it looks like the migration 3 runs the second time the app is launched but not in the first launched.

Scenario 3: WORKS FINE after hack.

If the app is being installed for the first time but this time the user doesn't immediately enter any data, instead, he/she kills the app and then restarts the app again, everything works fine, no errors and data can be entered without a problem.

In other words, it looks like migration 3 tries to run the second time the app is launched and it's when the error occurs because the database already has the right schema structure from the first run.

Error Scenario 2:

libc++abi.dylib: terminating with uncaught exception of type NSException

Any idea why does migration 3 runs on the second launch on new installs? This doesn’t seem like normal behavior, why would it run the second time but not the first time it is launched.

Any thoughts?

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

            /// Migration 1:
            Realm.Configuration.defaultConfiguration = Realm.Configuration(
                schemaVersion: 1,
                migrationBlock: { migration, oldSchemaVersion in
                    if (oldSchemaVersion < 1) {
                        // configuration for schema 1
                    }
            })        
            /// Migration 2:
            Realm.Configuration.defaultConfiguration = Realm.Configuration(
                schemaVersion: 2,
                migrationBlock: { migration, oldSchemaVersion in
                    if (oldSchemaVersion < 2) {
                        // configuration for schema 2
                    }
            })
            /// Migration 3:
            Realm.Configuration.defaultConfiguration = Realm.Configuration(
                schemaVersion: 3,
                migrationBlock: { migration, oldSchemaVersion in
                    if (oldSchemaVersion < 3) {
                        // configuration for schema 3
                    }
            })
        }
    }

FYI - I have a print in each migration and only migration 3 runs on the second app launch after the app is being installed for the first time.


Solution

  • I will preface this by stating this is being posted as an answer as it attempts to validate that the code in the question is functioning correctly outside of the OP's environment.

    I've taken the code and added print statements throughout so the code flow could be easily followed in console.

    We start with all files deleted so it would emulate a new install of the app. Your comment says you entered data but that would not be fresh install as the user would not have any data yet.

    Here's duplicate code with some print statements added. For me, this is a macOS app, but an iOS app will behave the same.

    The migration function is located in the AppDelegate and called like this

    @NSApplicationMain
    class AppDelegate: NSObject, NSApplicationDelegate {
    
        func applicationDidFinishLaunching(_ aNotification: Notification) {
            self.doMigrate()
    

    and then the function

    func doMigrate() {
    
        print("starting schema version: \(Realm.Configuration.defaultConfiguration.schemaVersion)")
    
        /// Migration 1:
        Realm.Configuration.defaultConfiguration = Realm.Configuration(
            schemaVersion: 1,
            migrationBlock: { migration, oldSchemaVersion in
                print("migration 1")
                if (oldSchemaVersion < 1) {
                    print(" old schema < 1, performaing migration")
                    // configuration for schema 1
                }
        })
    
        print("schema is now: \(Realm.Configuration.defaultConfiguration.schemaVersion)")
    
        /// Migration 2:
        Realm.Configuration.defaultConfiguration = Realm.Configuration(
            schemaVersion: 2,
            migrationBlock: { migration, oldSchemaVersion in
                print("migration 2")
                if (oldSchemaVersion < 2) {
                    print(" old schema < 2, performaing migration")
                    // configuration for schema 2
                }
        })
    
        print("schema is now: \(Realm.Configuration.defaultConfiguration.schemaVersion)")
    
        /// Migration 3:
        Realm.Configuration.defaultConfiguration = Realm.Configuration(
            schemaVersion: 3,
            migrationBlock: { migration, oldSchemaVersion in
                print("migration 3")
                if (oldSchemaVersion < 3) {
                    print(" old schema < 3, performaing migration")
                    // configuration for schema 3
                }
        })
    
        print("schema is now: \(Realm.Configuration.defaultConfiguration.schemaVersion)")
    }
    

    On first run, here's the output, noting no migration happens

    starting schema version: 0
    schema is now: 1
    schema is now: 2
    schema is now: 3
    

    When the code is run a second time, the output is identical

    starting schema version: 0
    schema is now: 1
    schema is now: 2
    schema is now: 3
    

    Even if I add or change the models, add or change data, the output remains the same - with the exception of it does crash because it needs a migration if an object property is removed.

    So the answer is: If you're not seeing the same behavior, there's something else affecting the code/realm structure in your environment. This works the same way whether it's run once or 3 times.