Realm schema migration block not getting triggered

Hi,

I’m using Realm database with iOS and SwiftUI. I added one new field to one of my Realm object schema, let’s call it Project. Now when I do realm.objects(Project.self) it complaints that I need to do a migration. So as per the iOS migration doc, I added a schemaVersion and migrationBlock to my realm init code as follows.

config.schemaVersion = 1 // Older version was 0
config.migrationBlock = { migration, oldSchemaVersion in
    logger.debug("Performing migration since old schema \(oldSchemaVersion) is behind current schema \(currentSchemaVersion) ...")
    if oldSchemaVersion < 1 {
        migration.enumerateObjects(ofType: Project.className()) { (old, new) in
            new!["isDeleted"] = false
        }
    })
}

I printed out config just before opening the Realm, and I can see that the config has a migration block field set.

...
schemaVersion = 1;
migrationBlock = <__NSMallocBlock__: 0x600001537ed0>;
...

I printed the Realm config again after I tried opening Realm as Realm.asyncOpen(configuration: config) and Realm(configuration: config), but the migration block is missing.

...
schemaVersion = 1;
migrationBlock = (null);
...

My migration block is not getting triggered and I suspect that I’m missing something! Thank you in advance for any help in the right direction :smiley:

Edit

As per some Stack Overflow posts, I also tried using a bigger schema version (2, 5, 10 to be specific). Also, I initialize my main realm (which Project.self is a part of) when the SwiftUI.App is initialized.

It think a more complete code sample and additional info is needed.

  1. Did you put that code in didFinishLaunchingWithOptions?
  2. Are you using MongoDB Realm SYNC or just a local Realm?
  3. Did you include Realm.Configuration.defaultConfiguration = config which will cause that code to fire?

Oh and this is a cross post to SO in case an answer pops up at either site

1 Like

Hi @Jay,

I’ll provide some more context and code. First let me answer your 3 questions.

  1. I think didFinishLaunchingWithOptions is a UIKit concept from AppDelegate. But I’m using SwiftUI so I’m not 100% sure what is the equivalent of it there. Correct me if I’m mistaken.
  2. I’m using Realm Sync for premium users and local realms for free users. This can be seen in the code below.
  3. I tried assigning Realm.Configuration.defaultConfiguration = config in my withRealmConfig() method below, but that had no effect on migration block whatsoever.

Full Code - I initialize a class called AppState as a singleton. This class handles user login and creation of user realm with partition as user=\(user.id). Since, it is a static singleton object, it will be instantiated at application start.

// AppState.swift

import Foundation
import RealmSwift
import Combine

class AppState : ObservableObject {

    static let shared = AppState()

    let app = RealmSwift.App(id: "tasktracker-abcd")

    @Published var isPremiumUser = false
    @Published var user: RealmSwift.User? = nil
    @Published var userRealm: Realm? = nil

    // Other fields and vars

    private init() {
        // Open realm when a user logs in
        $user.compactMap { $0 }
            .eraseToAnyPublisher()
            .receive(on: DispatchQueue.main)
            .flatMap { openRealm(for: "user=\($0.id)") }
            .sink(receiveCompletion: { [weak self] completion in
                if case let .failure(error) = completion {
                    print("Unable to open realm due to error: \(error.localizedDescription)")
                }
            }, receiveValue: { [weak self] realm in
                print("User realm opened successfully! Realm config: \(realm.configuration)")
                self?.userRealm = realm
            })
            .store(in: &subscriptions)
    }
}

And here’s my Realm initialization code. As described earlier, I use Sync’ed realm as well local realm based on whether the user is a premium user or not. Also, my custom config mainly changes the realm path to .../Documents/<user_id>/<user_partition>.realm instead of using the default one. Apart from that, it also adds the migration block and schema version.

// RealmService.swift

import Foundation
import RealmSwift
import Combine

func openRealm(for partition: String) -> AnyPublisher<Realm, Error> {
    return Just(partition)
        .filter { !$0.isEmpty }
        .receive(on: DispatchQueue.main)
        .compactMap(withRealmConfig(partitionValue:))
        .flatMap(openCorrectRealmFlavor(with:))
        .eraseToAnyPublisher()
}

private func withRealmConfig(partitionValue: String) -> Realm.Configuration? {
    
    var config: Realm.Configuration

    // Init realm config with or without sync    
    if AppState.shared.isPremiumUser {
        config = user.configuration(partitionValue: partitionValue)
    } else {
        config = Realm.Configuration.defaultConfiguration
    }
    deletePathComponentsTillDocsDir(config.fileURL) // Reduces path to app containers "Documents" dir
    config.fileURL?.appendPathComponent(user.id) // Creates a user dir with the user.id
    
    // Create dir if not exists using FileManager
    do {
        try createRealmDirIfNotPresent(dir: config.fileURL!)
    } catch let error {
        logger.error("Error creating directory for realm: \(error.localizedDescription)")
        return nil
    }
    
    // Set realm filename and extension
    config.fileURL?.appendPathComponent(partitionValue.encoded())
    config.fileURL?.appendPathExtension("realm")

    // Perform migrations if required
    config.schemaVersion = currentSchemaVersion
    config.migrationBlock = { migration, oldSchemaVersion in
        logger.debug("Performing migration since old schema \(oldSchemaVersion) is behind current schema \(currentSchemaVersion) ...")
        if oldSchemaVersion < 1 {
            migration.enumerateObjects(ofType: Project.className()) { (old, new) in
                new!["isDeleted"] = false
            }
        }
    }
    
    print("Realm config: \(config)")
    return config
}

private func openCorrectRealmFlavor(with config: Realm.Configuration) -> AnyPublisher<Realm, Error> {
    if AppState.shared.isPremiumUser {
        print("Opening online sync'ed realm...")
        return openSyncedRealm(with: config)
    } else {
        print("Opening local realm...")
        return openLocalRealm(with: config)
    }
}

private func openSyncedRealm(with config: Realm.Configuration) -> AnyPublisher<Realm, Error> {
    return Realm.asyncOpen(configuration: config).eraseToAnyPublisher()
}

private func openLocalRealm(with config: Realm.Configuration) -> AnyPublisher<Realm, Error> {
    return Result { try Realm(configuration: config) }.publisher.eraseToAnyPublisher()
}

Thank you in advance for all the assistance!

The migration needs to start at app launch. This is untested but you can add a class to give your app the traditional appDelegate functionality like this

import SwiftUI
import RealmSwift

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print("Migration Code Goes here")
        return true
    }
}

@main
struct SwiftUI_TestApp: App {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

and then add your migration code in the AppDelegate. Again, that’s off the top of my head and 100% untested so if it doesn’t work, I will update. There are probably better options as well, but this was the first that came to mind considering the ObjC underpinnings of Realm.

I don’t think you can migrate sync’d realms - the file structure is different. It’s not clear if you are attempting to migrate a local realm as well as a sync’d realm though. I could be wrong on that point so please correct me if so

This should now work if the AppDelegate code works.

1 Like

Hi @Jay,

I’ll try point 1 and 3, out and let you know. Regarding point 2 - yes my migration covers sync’ed realms as well as local realms. The case that I tried was for a sync’ed realm. If the migration doesn’t work in sync’ed realms, what is the alternate option - populate the new fields manually in Atlas, delete the realm and reinitialize it?

Regards,
Sid

Hi @Jay,

Thank you for pointing out that migration blocks don’t work for sync’d realms. I tried my code for local realm and it triggers the migration block when schema version is changed.

It would be a good idea to have this behavior highlighted on the docs page as a gotcha! I hope someone from Realm team updates it.

Regards,
Sid

Adding @Chris_Bush here since this might be of interest for the docs team too. :+1:

1 Like