Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
292
.claude/skills/axiom-sqlitedata-migration/SKILL.md
Normal file
292
.claude/skills/axiom-sqlitedata-migration/SKILL.md
Normal file
@@ -0,0 +1,292 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user