Additive Change in Schema: Why "Migration is reguired"? (Or, Is adding a List<> additiv?)

I have an app running on query-based Realm Cloud.

This is the old Schema:

class Collection: Object {

    @objc dynamic var id: String = UUID().uuidString
    @objc dynamic var title: String = ""
    
    let permissions = List<Permission>()
    
    let items = List<Item>()
    
    override static func primaryKey() -> String? {
        return "id"
    }
}

class Item: Object {
    
    @objc dynamic var id: String = UUID().uuidString

    @objc dynamic var title: String?
    
    let permissions = List<Permission>()
    
    var collections = LinkingObjects(fromType: Collection.self, property: "items")
    
    override static func primaryKey() -> String? {
        return "id"
    }
    
}

The new Schema adds Mention and some references and is successfully written to realm cloud:

class Collection: Object {

    @objc dynamic var id: String = UUID().uuidString
    @objc dynamic var title: String = ""
    
    let permissions = List<Permission>()
    
    let items = List<Item>()
    
    let mentions = List<Mention>()
    
    override static func primaryKey() -> String? {
        return "id"
    }
}

class Item: Object {
    
    @objc dynamic var id: String = UUID().uuidString

    @objc dynamic var title: String?
    
    let permissions = List<Permission>()
    
    var collections = LinkingObjects(fromType: Collection.self, property: "items")

    let mentions = List<Mention>()
    
    override static func primaryKey() -> String? {
        return "id"
    }
    
}

class Mention: Object {

    @objc dynamic var id: String = UUID().uuidString
    @objc dynamic var title: String = ""
    
    var collections = LinkingObjects(fromType: Collection.self, property: "mentions")
    var items = LinkingObjects(fromType: Collection.self, property: "mentions")

    override static func primaryKey() -> String? {
        return "id"
    }

}

If I update the app running on the old schema with the new version, I get this error:

Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=10 "Migration is required due to the following errors:
- Property 'Collection.mentions' has been added.
- Property 'Item.mentions' has been added." 

Of course, when I first delete the old app and install the new one freshly, there is no problem.

I consider my changes additive. So why is a migration required?

https://docs.realm.io/sync/using-synced-realms/syncing-data#additive-changes says that I must not use migration blocks. Same here.

When I add a Schema Version, I run into this issue:
Schema mismatch detected: another process has modified the Realm file's schema in an incompatible way

How can I fix this?

Edit: This appeared to be a local migration but based on additional comments, this is a sync’d realm so the below code doesn’t apply - it’s only useful for local.

Not sure there’s anything to fix. That’s adding an entirely new object and also creating an inverse relationship.

One option is to just init the new List object in your migration. Here it is for the Mention class

let vers = UInt64(2)
let config = Realm.Configuration( schemaVersion: vers, migrationBlock: { migration, oldSchemaVersion in
     print("oldSchemaVersion: \(oldSchemaVersion)")
     if (oldSchemaVersion < vers) {
        print("  performing migration")

        migration.enumerateObjects(ofType: PersonClass.className()) { oldItem, newItem in
            newItem!["mentions"] = List<Mention>()
         }
     }
 })
1 Like

How do you open the Realm?

In AppDelegate.swift like this:

        do {
            _ = try Realm()
        } catch {
            print("Error initialising new realm, \(error)")
        }

In StorageController.swift then like this:
self.realm = try! Realm(configuration: SyncUser.current!.configuration())

In AppDelegate.swift you appear to be opening a local Realm, not a synchronized one. That one needs migrations.

1 Like

I see. Does the local Realm need migration, or is there a way to apply the Schema changes from the cloud automatically to the local realm?

Both “local” and “synchronized” Realms are actually backed by a local database, but they are placed in different physical locations and thus are different databases for all intents and purposes. The schema change rules are also different - “local” Realms require explicit migrations, whereas synchronized Realms do not.

Thank you for clearing things up! In my case, everything should be “synchronized”. I didn’t intentionally create a “local” Realm, but I’m aware that they are now around out there. Is it possible to delete* them, so that only a “synchronized” Realm exists in the app, eliminating the need for migration?

*for example: check at app start, if there is a “local” realm and delete it.

If you are using query based sync, be aware from the documentation that

Query-based Sync is not recommended. For applications using Realm Sync, we recommend Full Sync. Learn more about our plans for the future of Realm Sync here.

I know, thanks, but I started development before this change came. Would this specific case make any difference with a fully synced Realm?

My question is now, do I have to deal with a “local” Realm, when I already have a synced Realm, or may I get rid of the “local” Realm?

Hm, when I remove the “local” Realm at app start with:

 let realmURL = Realm.Configuration.defaultConfiguration.fileURL!
        let realmURLs = [
            realmURL,
            realmURL.appendingPathExtension("lock"),
            realmURL.appendingPathExtension("note"),
            realmURL.appendingPathExtension("management")
        ]
        for URL in realmURLs {
            do {
                try FileManager.default.removeItem(at: URL)
            } catch {
                // handle error
            }
        }

I get this error after the “synchronized” Realm successfully connected to the cloud and loaded data:

uncaught exception in notifier thread: N5realm5_impl23UnsupportedSchemaChangeE: Schema mismatch detected: another process has modified the Realm file's schema in an incompatible way

Why does this happen in the Realm notification listener, after the “synchronized” Realm seams to work fine?

I know, thanks, but I started development before this change came. Would this specific case make any difference with a fully synced Realm?

Big difference. It’s not been officially stated but all signs point to Query-based Sync being depreciated (soon). I may be incorrect but in looking over the Mongo/Realm roadmap, due to it’s implementation, Full-Sync will be ‘how it works’ going forward (at some point). Just IMO… and any Realm’ers feel free to jump-in.

Also note this from the docs

Query Based Sync, which replicates only a subset of the objects specified with a set of queries. While this allows you to use sophisticated permissions rules on specific objects within the realm, it supports significantly fewer concurrent users. Therefore, query-based sync is generally not recommended for production workloads.

Two messages here. Don’t use query-based sync but more importantly, you always have a ‘local’ Realm. Realm is not a cloud database, it’s a cloud SYNC database so no, you cannot get rid of that local realm. Well, you can but it will be replicated based on the queries set up in your app.

Keep in mind there is a local Realm which is used for on device only situations, not sync’d, and then a Realm database that’s stored locally which is sync’d and they are stored in two (or more) different locations on your drive/device.

Also, via the 4.2 release we have two new, and long overdue, commands for working with the local (non-synch’d) realm files.

  • Add -[RLMRealm fileExistsForConfiguration:] / Realm.fileExists(for:) ,
    which checks if a local Realm file exists for the given configuration.
  • Add -[RLMRealm deleteFilesForConfiguration:] / Realm.deleteFiles(for:)
    to delete the Realm file and all auxiliary files for the given configuration.

Jay, I’m aware of the situation of query based sync. I’v raised my concerns here, and would like to keep that discussion there. Yes, don’t use query based sync, anymore! :wink:

My migration is working now, and I would like to wrap it up:

  • My app depends solely on a query based Cloud Realm
  • Accidentally, I additionally instantiated a local realm, but never used it accross the app.
  • Adding a new object (Mention), and adding relationships to it, required me to do a migration like this:
let vers = UInt64(2)
        let config = Realm.Configuration( schemaVersion: vers, migrationBlock: { migration, oldSchemaVersion in
             print("oldSchemaVersion: \(oldSchemaVersion)")
             if (oldSchemaVersion < vers) {
                print("performing migration")

                migration.enumerateObjects(ofType: Collection.className()) { oldItem, newItem in
                    newItem!["mentions"] = List<Mention>()
                }
                migration.enumerateObjects(ofType: Item.className()) { oldItem, newItem in
                    newItem!["mentions"] = List<Mention>()
                }
             }
         })
        
        Realm.Configuration.defaultConfiguration = config
        
        do {
            _ = try Realm()
        } catch {
            print("Error initialising new realm, \(error)")
        }

Normally, this is it. (Migration is required, though no local realm is used.)

In my specific case, I run into an exception in the notifier thread right after the migration went successfully through AND (most) data were successfully loaded from Realm Cloud.

So why did that happen? I use two separate Realm Cloud instances to simulate the migration: One has the old schema, the other the new. I deleted and freshly installed the app to get its data from the Realm with the old schema. Then, I installed the app over it pointing to the other Realm with the new schema, which triggered the migration. This led to the exception. I’m not allowed to migrate a realm that pointed to another realm instance before. Doing the migration within the same instance works fine.