Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Debugging/Fixing deleted relationship objects with SwiftData

Using SwiftData and this is the simplest example I could boil down:

@Model
final class Item {
    var timestamp: Date
    var tag: Tag?
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

@Model
final class Tag {
    var timestamp: Date
        
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

Notice Tag has no reference to Item.

So if I create a bunch of items and set their Tag. Later on I add the ability to delete a Tag. Since I haven't added inverse relationship Item now references a tag that no longer exists so so I get these types of errors:

SwiftData/BackingData.swift:875: Fatal error: This model instance was invalidated because its backing data could no longer be found the store. PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(url: x-coredata://EEC1D410-F87E-4F1F-B82D-8F2153A0B23C/Tag/p1), implementation: SwiftData.PersistentIdentifierImplementation)

I think I understand now that I just need to add the item reference to Tag and SwiftData will nullify all Item references to that tag when a Tag is deleted.

But, the damage is already done. How can I iterate through all Items that referenced a deleted tag and set them to nil or to a placeholder Tag? Or how can I catch that error and fix it when it comes up?

The crash doesn't occur when loading an Item, only when accessing item.tag?.timestamp, in fact, item.tag?.id is still ok and doesn't crash since it doesn't have to load the backing data.

I've tried things like just looping through all items and setting tag to nil, but saving the model context fails because somewhere in there it still tries to validate the old value.

Thanks!

If you don't need to reserve the data, the easiest way is probably just deleting the existing SwiftData store, and start with a new one. You can find the store URL by using yourModelContainer.configurations.first?.url.

If you do need to reserve the data, maybe try the following:

  1. Set the delete rule of the relationship to .noAction, as shown in the following code snippet.
  2. Go through all items and set item.tag to nil.
  3. Save your model context.
@Relationship(deleteRule: .noAction)
var tag: Tag?

I think SwiftData should be able to handle this change, but haven't tried by my own. Let me know if that doesn't work.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thanks.

That worked in this simple project, but not in my full project. I'm not yet sure what the difference is, but it throws the instance was invalidated error when setting object.relationship = nil

I've resorted to using sqlite directly (with the SQLite.swift package) and then setting to nil with a separate query:

class IntegrityCheck {
    var model: String
    var relationship: String
    var relationshipType: String
    var count: Int64 = 0
    var checked: Bool = false
    var orphanedObjectPKs: [Int64] = []
    
    init(model: String, relationship: String, relationshipType: String) {
        self.model = model
        self.relationship = relationship
        self.relationshipType = relationshipType
    }
    
    init(model: String, relationship: String) {
        self.model = model
        self.relationship = relationship
        self.relationshipType = relationship
    }
    
    func run(modelContext: ModelContext) {
        guard let db = modelContext.sqliteDatabase else {
            print("could not get sqlite database")
            return
        }
        
        do {
            let query = try db.prepare(sql)
            count = try query.scalar() as! Int64
            checked = true
            if count > 0 {
                let orphanedQuery = try db.prepare(orpahnedObjectSQL)
                for row in orphanedQuery {
                    orphanedObjectPKs.append(row[0] as! Int64)
                }
            }
        } catch {
            print(error)
        }
    }
    
    private var modelTable: String {
        "Z\(model.uppercased())"
    }
    
    private var relationshipColumn: String {
        "Z\(relationship.uppercased())"
    }
    
    private var relationshipTable: String {
        "Z\(relationshipType.uppercased())"
    }
    
    var sql: String {
        """
        select
            count(*)
        from 
            \(modelTable)
            left join \(relationshipTable) on \(relationshipTable).Z_PK = \(modelTable).\(relationshipColumn)
        where
            \(modelTable).\(relationshipColumn) is not null
            and \(relationshipTable).Z_PK is null
            
        """
    }
    
    var orpahnedObjectSQL: String {
        """
        select
            distinct(\(modelTable).\(relationshipColumn))
        from 
            \(modelTable)
            left join \(relationshipTable) on \(relationshipTable).Z_PK = \(modelTable).\(relationshipColumn)
        where
            \(modelTable).\(relationshipColumn) is not null
            and \(relationshipTable).Z_PK is null
            
        """
    }
}

extension ModelContext {
    var sqliteUrl: URL? {
        return container.configurations.first?.url
    }
    
    var sqliteDatabase: Connection? {
        guard let url = sqliteUrl else {
            return nil
        }
        print(url.path)
        do {
            return try SQLite.Connection(url.path)
        } catch {
            print("Error opening database: \(error)")
            return nil
        }
    }
}

Debugging/Fixing deleted relationship objects with SwiftData
 
 
Q