Skillshub axiom-sqlitedata-migration
Use when migrating from SwiftData to SQLiteData — decision guide, pattern equivalents, code examples, CloudKit sharing (SwiftData can't), performance benchmarks, gradual migration strategy
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/CharlesWiltgen/Axiom/axiom-sqlitedata-migration" ~/.claude/skills/comeonoliver-skillshub-axiom-sqlitedata-migration && rm -rf "$T"
manifest:
skills/CharlesWiltgen/Axiom/axiom-sqlitedata-migration/SKILL.mdsource content
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 |
|---|---|
| |
| or SQL UNIQUE |
| + join query |
| |
| |
| |
| |
| |
| |
| Automatic in block |
| |
Code Example
SwiftData (Before)
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)
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
withstructnonisolated
→@Model@Table
→@Query@FetchAll
→@Environment(\.modelContext)@Dependency(\.defaultDatabase)- Implicit save → Explicit
blockdatabase.write { } - Direct init →
type for inserts.Draft
→ Explicit foreign key + join@Relationship
CloudKit Sharing (SwiftData Can't Do This)
SwiftData supports CloudKit sync but NOT sharing. SQLiteData is the only Apple-native option for record sharing.
// 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.
// 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:
- Add SQLiteData for new features — Keep SwiftData for existing simple CRUD
- Migrate one model at a time — Start with the performance bottleneck
- Use separate databases initially — SQLiteData for heavy data/sharing, SwiftData for preferences
- Consolidate if needed — Or keep hybrid if it works
Common Gotchas
Relationships → Foreign Keys
// 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
// SwiftData: @Relationship(deleteRule: .cascade) // SQLiteData: Define in SQL schema // "REFERENCES parent(id) ON DELETE CASCADE"
No Automatic Inverse
// 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:
— Full SQLiteData API referenceaxiom-sqlitedata
— SwiftData patterns if staying with Apple's frameworkaxiom-swiftdata
— Raw GRDB for complex queriesaxiom-grdb
History: See git log for changes