---
name: axiom-sqlitedata-migration
description: Use when migrating from SwiftData to SQLiteData — decision guide, pattern equivalents, code examples, CloudKit sharing (SwiftData can't), performance benchmarks, gradual migration strategy
license: MIT
metadata:
  version: "1.0.0"
---

# Migrating from SwiftData to SQLiteData

## When to Switch

```
┌─────────────────────────────────────────────────────────┐
│ Should I switch from SwiftData to SQLiteData?           │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Performance problems with 10k+ records?                │
│    YES → SQLiteData (10-50x faster for large datasets)  │
│                                                         │
│  Need CloudKit record SHARING (not just sync)?          │
│    YES → SQLiteData (SwiftData cannot share records)    │
│                                                         │
│  Complex queries across multiple tables?                │
│    YES → SQLiteData + raw GRDB when needed              │
│                                                         │
│  Need Sendable models for Swift 6 concurrency?          │
│    YES → SQLiteData (value types, not classes)          │
│                                                         │
│  Testing @Model classes is painful?                     │
│    YES → SQLiteData (pure structs, easy to mock)        │
│                                                         │
│  Happy with SwiftData for simple CRUD?                  │
│    YES → Stay with SwiftData (simpler for basic apps)   │
│                                                         │
└─────────────────────────────────────────────────────────┘
```

---

## Pattern Equivalents

| SwiftData | SQLiteData |
|-----------|------------|
| `@Model class Item` | `@Table nonisolated struct Item` |
| `@Attribute(.unique)` | `@Column(primaryKey: true)` or SQL UNIQUE |
| `@Relationship var tags: [Tag]` | `var tagIDs: [Tag.ID]` + join query |
| `@Query var items: [Item]` | `@FetchAll var items: [Item]` |
| `@Query(sort: \.title)` | `@FetchAll(Item.order(by: \.title))` |
| `@Query(filter: #Predicate { $0.isActive })` | `@FetchAll(Item.where(\.isActive))` |
| `@Environment(\.modelContext)` | `@Dependency(\.defaultDatabase)` |
| `context.insert(item)` | `Item.insert { Item.Draft(...) }.execute(db)` |
| `context.delete(item)` | `Item.find(id).delete().execute(db)` |
| `try context.save()` | Automatic in `database.write { }` block |
| `ModelContainer(for:)` | `prepareDependencies { $0.defaultDatabase = }` |

---

## Code Example

**SwiftData (Before)**

```swift
import SwiftData

@Model
class Task {
    var id: UUID
    var title: String
    var isCompleted: Bool
    var project: Project?

    init(title: String) {
        self.id = UUID()
        self.title = title
        self.isCompleted = false
    }
}

struct TaskListView: View {
    @Environment(\.modelContext) private var context
    @Query(sort: \.title) private var tasks: [Task]

    var body: some View {
        List(tasks) { task in
            Text(task.title)
        }
    }

    func addTask(_ title: String) {
        let task = Task(title: title)
        context.insert(task)
    }

    func deleteTask(_ task: Task) {
        context.delete(task)
    }
}
```

**SQLiteData (After)**

```swift
import SQLiteData

@Table
nonisolated struct Task: Identifiable {
    let id: UUID
    var title = ""
    var isCompleted = false
    var projectID: Project.ID?
}

struct TaskListView: View {
    @Dependency(\.defaultDatabase) var database
    @FetchAll(Task.order(by: \.title)) var tasks

    var body: some View {
        List(tasks) { task in
            Text(task.title)
        }
    }

    func addTask(_ title: String) {
        try database.write { db in
            try Task.insert {
                Task.Draft(title: title)
            }
            .execute(db)
        }
    }

    func deleteTask(_ task: Task) {
        try database.write { db in
            try Task.find(task.id).delete().execute(db)
        }
    }
}
```

**Key differences:**
- `class` → `struct` with `nonisolated`
- `@Model` → `@Table`
- `@Query` → `@FetchAll`
- `@Environment(\.modelContext)` → `@Dependency(\.defaultDatabase)`
- Implicit save → Explicit `database.write { }` block
- Direct init → `.Draft` type for inserts
- `@Relationship` → Explicit foreign key + join

---

## CloudKit Sharing (SwiftData Can't Do This)

SwiftData supports CloudKit **sync** but NOT **sharing**. SQLiteData is the only Apple-native option for record sharing.

```swift
// 1. Setup SyncEngine with sharing
prepareDependencies {
    $0.defaultDatabase = try! appDatabase()
    $0.defaultSyncEngine = try SyncEngine(
        for: $0.defaultDatabase,
        tables: Task.self, Project.self
    )
}

// 2. Share a record
@Dependency(\.defaultSyncEngine) var syncEngine
@State var sharedRecord: SharedRecord?

func shareProject(_ project: Project) async throws {
    sharedRecord = try await syncEngine.share(record: project) { share in
        share[CKShare.SystemFieldKey.title] = "Join my project!"
    }
}

// 3. Present native sharing UI
.sheet(item: $sharedRecord) { record in
    CloudSharingView(sharedRecord: record)
}
```

**Sharing enables:** Collaborative lists, shared workspaces, family sharing, team features.

---

## Performance Comparison

| Operation | SwiftData | SQLiteData | Improvement |
|-----------|-----------|------------|-------------|
| Insert 50k records | ~4 minutes | ~45 seconds | **5x** |
| Query 10k with predicate | ~2 seconds | ~50ms | **40x** |
| Memory (10k objects) | ~80MB | ~20MB | **4x smaller** |
| Cold launch (large DB) | ~3 seconds | ~200ms | **15x** |

*Benchmarks approximate, vary by device and data shape.*

---

## Migrating Existing User Data

**Critical**: Schema migration alone loses all user data. You must export from SwiftData and import into SQLiteData.

```swift
// 1. Read all records from SwiftData's backing store
func migrateExistingData(from modelContext: ModelContext, to database: any DatabaseWriter) throws {
    // Fetch all SwiftData records
    let descriptor = FetchDescriptor<SwiftDataTask>()
    let existingTasks = try modelContext.fetch(descriptor)

    // 2. Bulk insert into SQLiteData
    try database.write { db in
        for task in existingTasks {
            try SQLiteTask.insert {
                SQLiteTask.Draft(
                    id: task.id,
                    title: task.title,
                    isCompleted: task.isCompleted,
                    projectID: task.project?.id
                )
            }
            .execute(db)
        }
    }

    // 3. Verify migration
    let count = try database.read { db in
        try SQLiteTask.fetchCount(db)
    }
    assert(count == existingTasks.count, "Migration count mismatch!")
}
```

**Migration checklist:**
- [ ] Export all models before deleting SwiftData container
- [ ] Migrate relationships (fetch parent IDs for foreign keys)
- [ ] Verify record counts match after migration
- [ ] Keep SwiftData container as backup until confirmed working
- [ ] Run migration on first launch with a version flag in UserDefaults

## Gradual Migration Strategy

You don't have to migrate everything at once:

1. **Add SQLiteData for new features** — Keep SwiftData for existing simple CRUD
2. **Migrate one model at a time** — Start with the performance bottleneck
3. **Use separate databases initially** — SQLiteData for heavy data/sharing, SwiftData for preferences
4. **Consolidate if needed** — Or keep hybrid if it works

---

## Common Gotchas

### Relationships → Foreign Keys

```swift
// SwiftData: implicit relationship
@Relationship var tasks: [Task]

// SQLiteData: explicit column + query
// In child: var projectID: Project.ID
// To fetch: Task.where { $0.projectID.eq(#bind(project.id)) }
```

### Cascade Deletes

```swift
// SwiftData: @Relationship(deleteRule: .cascade)

// SQLiteData: Define in SQL schema
// "REFERENCES parent(id) ON DELETE CASCADE"
```

### No Automatic Inverse

```swift
// SwiftData: @Relationship(inverse: \Task.project)

// SQLiteData: Query both directions manually
let tasks = Task.where { $0.projectID.eq(#bind(project.id)) }
let project = Project.find(task.projectID)
```

---

**Related Skills:**
- `axiom-sqlitedata` — Full SQLiteData API reference
- `axiom-swiftdata` — SwiftData patterns if staying with Apple's framework
- `axiom-grdb` — Raw GRDB for complex queries

---

**History:** See git log for changes
