fix(axiom-data): stop to-one cascades from deleting parents and siblings

Track.album carried deleteRule: .cascade while Album.tracks cascades too, so
deleting one track deleted its album and, through it, every sibling track.
The NoteTag junction example in swiftdata-migration.md cascaded both of its
to-one sides the same way, and never compiled: Note and Tag had no
initializer.

Drop the cascade from the to-one sides (the default .nullify applies),
declare the junction's inverses, and add the missing initializers.

Verified: deleting one of two tracks now leaves 1 album and 1 track (was 0
and 0); removing one tag from a note keeps every note and tag (was all
deleted). The edited blocks compile against the iOS 27.1 SDK.
This commit is contained in:
Charles Wiltgen
2026-09-18 17:54:24 -07:00
parent 70bf8cb631
commit 1f9d0a94f8
2 changed files with 18 additions and 5 deletions
@@ -420,10 +420,12 @@ final class NoteTag {
@Attribute(.unique) var id: String
var addedAt: Date // Metadata on relationship
@Relationship(deleteRule: .cascade)
// No cascade on the junction's own sides: removing one tag from one note
// would delete that note and that tag, and their cascades would take the rest
@Relationship(inverse: \Note.noteTags)
var note: Note?
@Relationship(deleteRule: .cascade)
@Relationship(inverse: \Tag.noteTags)
var tag: Tag?
init(id: String, note: Note, tag: Tag, addedAt: Date) {
@@ -445,6 +447,11 @@ final class Note {
var tags: [Tag] {
noteTags.compactMap { $0.tag }
}
init(id: String, title: String) {
self.id = id
self.title = title
}
}
@Model
@@ -458,6 +465,11 @@ final class Tag {
var notes: [Note] {
noteTags.compactMap { $0.note }
}
init(id: String, name: String) {
self.id = id
self.name = name
}
}
```
@@ -121,7 +121,9 @@ final class Track {
@Attribute(.unique) var id: String
var title: String
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
// No cascade on this side: deleting one track would delete its album,
// and Album.tracks' cascade would then delete every sibling track
@Relationship(inverse: \Album.tracks)
var album: Album?
init(id: String, title: String, album: Album? = nil) {
@@ -805,7 +807,7 @@ Run the sweep on a background context (not the main one), keyed by the natural I
```swift
@Model
final class Track {
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
@Relationship(inverse: \Album.tracks)
var album: Album? // Must be optional for CloudKit
init(album: Album? = nil) {
@@ -887,7 +889,6 @@ let container = try ModelContainer(for: schema, configurations: testConfig)
@Model
final class Track {
@Relationship(
deleteRule: .cascade,
minimumModelCount: 0,
maximumModelCount: 1, // Track belongs to at most one album
inverse: \Album.tracks