import SwiftData import Foundation import CoreData @MainActor class SwiftDataCordinator { static let sharedModelContainer: ModelContainer = { let mainDataSchema = Schema([...]) let modelConfiguration = ModelConfiguration("MainData", cloudKitDatabase: .automatic) /** Set DBRepairCompleted to false to trigger the repair, if the database exists. Remove this piece of code to make the repair a one-time deal. */ #if DEBUG UserDefaults.standard.set(false, forKey: "DBRepairCompleted") #endif /** Repair the database, if needed. */ let repairCompleted = UserDefaults.standard.bool(forKey: "DBRepairCompleted") let dbExists = FileManager.default.fileExists(atPath: modelConfiguration.url.path) if dbExists && !repairCompleted { do { try autoreleasepool { try repairDB(mainDataSchema: mainDataSchema, config: modelConfiguration) UserDefaults.standard.set(true, forKey: "DBRepairCompleted") } } catch { fatalError() } } /** Now the database is ready to use. Return the model container as usual. */ do { return try ModelContainer(for: mainDataSchema, configurations: modelConfiguration) } catch let error { fatalError("Failed to create ModelContainer: \(error)") } }() static private func repairDB(mainDataSchema: Schema, config: ModelConfiguration) throws { guard let currentAppModel: NSManagedObjectModel = NSManagedObjectModel.makeManagedObjectModel(for: mainDataSchema) else { print("Failed to create NSManagedObjectModel from Schema") return } // Repair the DataStore // Get the current DataStore's VersionHashes let currentStoreMetadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: config.url) guard let currentStoreVersionHashes = currentStoreMetadata["NSStoreModelVersionHashes"] as? [String: Data] else { print("Version hashes needed for repair not found: \(currentStoreMetadata)") return } // Determine which entities/properties need to be repaired var repairsNeeded = [(String, [String])]() for (entityName, storeEntityHash) in currentStoreVersionHashes { // Find the entities with a skew guard let currentEntityHash = currentAppModel.entityVersionHashesByName[entityName] else { print("Model version hashes not found for repair - no hash for \(entityName)") return } if storeEntityHash == currentEntityHash { continue } guard let modelEntity = currentAppModel.entitiesByName[entityName] else { print("Entity not found but has version hash: \(entityName) in \(currentAppModel)") return } var propertiesToRepair = [String]() // Gather any attributes of concern for attribute in modelEntity.attributesByName.values { if attribute.name == "samedaySummaryNotificationTimesInSecondsByDayOfWeek", attribute.attributeType == .transformableAttributeType { propertiesToRepair.append(attribute.name) } } if !(propertiesToRepair.isEmpty) { repairsNeeded.append((entityName, propertiesToRepair)) } } /** Return if no offending entities found. */ guard !repairsNeeded.isEmpty else { print("No offending entities found for repair") return } guard let originalMOM = currentAppModel.copy() as? NSManagedObjectModel else { print("Failed to copy NSManagedObjectModel for repair") return } for (entityName, attributesToRepair) in repairsNeeded { guard let originalEntity = originalMOM.entitiesByName[entityName] else { print("Failed to find \(entityName) in model - \(originalMOM)") return } for attribute in attributesToRepair { guard let originalAttribute = originalEntity.attributesByName[attribute] else { print("No property found") return } //convert originalAttribute from Transformable to Binary Data blob originalAttribute.attributeType = .binaryDataAttributeType originalAttribute.valueTransformerName = nil print("Repairing \(entityName) with property: \(attribute)") } } // Now move the existing datastore to a two column approach // Add a column that is _property but transformable guard let addColumnMOM = originalMOM.copy() as? NSManagedObjectModel else { print("Failed to copy NSManagedObjectmodel to add column for repair") return } for (entityName, attributesToRepair) in repairsNeeded { guard let addColumnEntity = addColumnMOM.entitiesByName[entityName] else { print("Failed to find \(entityName) in model - \(addColumnMOM)") return } for attribute in attributesToRepair { guard let addColumnOrginalAttribute = addColumnEntity.attributesByName[attribute] else { print("Property not found") return } guard let addColumnTempAttribute = addColumnOrginalAttribute.copy() as? NSAttributeDescription else { print("Failed to copy NSAttributeDescription to add column for repair") return } addColumnTempAttribute.name = "_REPAIRCODABLEPROP\(attribute)" addColumnTempAttribute.isOptional = true addColumnTempAttribute.attributeType = .transformableAttributeType addColumnTempAttribute.valueTransformerName = "NSSecureUnarchiveFromData" addColumnEntity.properties.append(addColumnTempAttribute) } } // Migration from oldSchema to interSchema - customStage let originalToAddColumn = NSCustomMigrationStage(migratingFrom: .init(model: originalMOM, versionChecksum: originalMOM.versionChecksum), to: .init(model: addColumnMOM, versionChecksum: addColumnMOM.versionChecksum)) originalToAddColumn.didMigrateHandler = { (migrationManager, migrationStage) throws -> Void in if let container = migrationManager.container { let ctx = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) ctx.persistentStoreCoordinator = container.persistentStoreCoordinator try ctx.performAndWait { for (entityName, attributesToRepair) in repairsNeeded { let fr = NSFetchRequest(entityName: entityName) fr.propertiesToFetch = attributesToRepair let objects = try ctx.fetch(fr) for object in objects { for attribute in attributesToRepair { if let existingValue = object.value(forKey: attribute) { if let dataValue = existingValue as? Data { let value = try! JSONDecoder().decode([Int?].self, from: dataValue) object.setValue(value, forKey:"_REPAIRCODABLEPROP\(attribute)") } }//no existing value - do nothing }// attributes to repair }//objects loop } // repairs needed try ctx.save() }//performAndWait } else { //no container do nothing } } // Now drop the old codable column guard let dropColumnMOM = addColumnMOM.copy() as? NSManagedObjectModel else { print("Failed to copy NSManagedObjectmodel to drop column for repair") return } for (entityName, attributesToRepair) in repairsNeeded { guard let dropColumnEntity = dropColumnMOM.entitiesByName[entityName] else { print("Failed to find \(entityName) in model - \(dropColumnMOM)") return } for attribute in attributesToRepair { dropColumnEntity.properties.removeAll(where: { $0.name == attribute }) } } let addColumnToDropColumn = NSCustomMigrationStage(migratingFrom: .init(model: addColumnMOM, versionChecksum: addColumnMOM.versionChecksum), to: .init(model: dropColumnMOM, versionChecksum: dropColumnMOM.versionChecksum)) // Add new column with old name and move data into it guard let addCurrentColumnMOM = dropColumnMOM.copy() as? NSManagedObjectModel else { //might have to skew version hash print("Failed to copy NSManagedObjectmodel to add current column for repair") return } for (entityName, attributesToRepair) in repairsNeeded { guard let addNewColumnEntity = addCurrentColumnMOM.entitiesByName[entityName] else { print("Failed to find \(entityName) in model - \(addCurrentColumnMOM)") return } for attribute in attributesToRepair { guard let addNewColumnTempProperty = addNewColumnEntity.attributesByName["_REPAIRCODABLEPROP\(attribute)"] else { print("No column!") return } guard let addNewColumnAltCurrentProperty = addNewColumnTempProperty.copy() as? NSAttributeDescription else { print("Failed to copy an attribute description!") return } addNewColumnAltCurrentProperty.name = attribute addNewColumnAltCurrentProperty.isOptional = true addNewColumnAltCurrentProperty.valueTransformerName = "NSSecureUnarchiveFromData" addNewColumnEntity.properties.append(addNewColumnAltCurrentProperty) } } let dropColumnToAddAltCurrentColumn = NSCustomMigrationStage(migratingFrom: .init(model: dropColumnMOM, versionChecksum: dropColumnMOM.versionChecksum), to: .init(model: addCurrentColumnMOM, versionChecksum: addCurrentColumnMOM.versionChecksum)) dropColumnToAddAltCurrentColumn.didMigrateHandler = { (migrationManager, migrationStage) throws -> Void in if let container = migrationManager.container { let ctx = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) ctx.persistentStoreCoordinator = container.persistentStoreCoordinator try ctx.performAndWait { for (entityName, attributesToRepair) in repairsNeeded { let fr = NSFetchRequest(entityName: "\(entityName)") fr.propertiesToFetch = attributesToRepair.map({"_REPAIRCODABLEPROP\($0)"}) let objects = try ctx.fetch(fr) for object in objects { for attribute in attributesToRepair { if let existingValue = object.value(forKey: "_REPAIRCODABLEPROP\(attribute)") { object.setValue(existingValue,forKey: attribute) }//no existing value - do nothing } // attributes }//objects loop try ctx.save() }// entity to repair }//performAndWait } else { //no container do nothing } } // Drop _REPAIRCODABLEPROP column, and nonOptional if it was before - ie back to the original schema let backToFinalSchema = NSLightweightMigrationStage([currentAppModel.versionChecksum]) // Spin it up let newMigrationManager = NSStagedMigrationManager([originalToAddColumn, addColumnToDropColumn, dropColumnToAddAltCurrentColumn, backToFinalSchema]) let newContainer = NSPersistentContainer(name: "SwiftDataRepairCodableToTransformable", managedObjectModel: currentAppModel) guard let newDesc = newContainer.persistentStoreDescriptions.first else { print("Container did not have a store description") return } newDesc.setOption(newMigrationManager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey) newDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) newDesc.url = config.url newContainer.persistentStoreDescriptions = [newDesc] newContainer.loadPersistentStores(completionHandler: {_,_ in }) } }