Data Migrations & Schema Updates
Relevant source files
The following files were used as context for generating this wiki page:
- Trackurr/Dependencies/Persistence/Migration/AppMigrationPlan.swift
- Trackurr/Dependencies/Persistence/Migration/SchemaV1.swift
- Trackurr/Dependencies/Persistence/Migration/SchemaV2.swift
- Trackurr/Dependencies/Persistence/Migration/SchemaV3.swift
- Trackurr/Dependencies/Persistence/Migration/SchemaV5.swift
- Trackurr/Dependencies/Persistence/Migration/SchemaV8.swift
- Trackurr/Dependencies/Persistence/Store/DataExportServiceImpl.swift
- TrackurrTests/Persistence/Seeding/DataSeederTests.swift
The Trackurr project utilizes SwiftData for persistent storage, implementing a robust versioning system to manage evolutionary changes in the data model. This system ensures that user data—such as subscriptions, categories, and tags—remains consistent across app updates while accommodating new features and architectural shifts, such as CloudKit synchronization requirements.
The architecture relies on VersionedSchema definitions and an AppMigrationPlan to orchestrate transitions between states. These transitions range from lightweight migrations (adding attributes) to complex custom migrations (restructuring entity relationships and handling system-level data).
Sources: AppMigrationPlan.swift:7-14, SchemaV1.swift:5-8, README.md:92-94
Migration Architecture
The migration system is centralized in the AppMigrationPlan, which implements the SchemaMigrationPlan protocol. It defines the historical sequence of schemas and the specific stages required to move between them.
The diagram above illustrates the linear progression of schema versions from the initial release to the current implementation.
Sources: AppMigrationPlan.swift:18-30
Migration Stages
| Stage | From | To | Type | Description |
|---|---|---|---|---|
migrateV1toV2 | V1 | V2 | Lightweight | Introduced the Preferences entity. |
migrateV2toV3 | V2 | V3 | Lightweight | Removed @Attribute(.unique) constraints for CloudKit compatibility. |
migrateV3toV4 | V3 | V4 | Lightweight | Added default values and made relationships optional for CloudKit. |
migrateV4toV5 | V4 | V5 | Lightweight | Added appearanceMode to Preferences. |
migrateV5toV6 | V5 | V6 | Lightweight | Added SubscriptionProvider entity. |
migrateV6toV7 | V6 | V7 | Lightweight | Added brandColorHex to Subscription. |
migrateV7toV8 | V7 | V8 | Custom | Decoupled system entities from CloudKit sync; restructured relationships. |
Sources: AppMigrationPlan.swift:33-68, SchemaV2.swift:141-143, SchemaV8.swift:10-17
Evolution of Schema Entities
The schema evolved significantly to handle advanced features and synchronization requirements. Key changes are documented below:
CloudKit Compatibility (V3 & V4)
Starting with V3, unique constraints were removed because CloudKit does not support them natively. Instead, the system relies on the inherent uniqueness of UUIDs. V4 further improved sync stability by providing default values for all attributes and making relationships optional to prevent sync deadlocks.
Sources: SchemaV3.swift:6-7, SchemaV5.swift:35-42
System vs. User Entities (V8)
Schema V8 introduced a major architectural shift by separating "System" data (pre-seeded categories and payment methods) from user-created data.
- Custom Entities:
CategoryandPaymentMethodare now exclusively user-created and synced via CloudKit. - System Entities:
SystemCategoryandSystemPaymentMethodare local-only and seeded from JSON. - Subscription References: The
Subscriptionentity was updated to includesystemCategoryIdandsystemPaymentMethodIdstrings to reference these local-only entities without a traditional relationship.
Sources: SchemaV8.swift:11-30, SchemaV8.swift:53-56
This diagram shows how Subscription entities in V8 reference either a user-defined Category (via relationship) or a local SystemCategory (via a stable ID string).
Sources: SchemaV8.swift:49-65
Custom Migration Logic: V7 to V8
The migration from V7 to V8 is the most complex transition in the project, requiring manual context manipulation to preserve data integrity while moving from relationship-based system categories to ID-based references.
Migration Sequence:
- Map Identifiers: Hardcoded dictionaries map existing category names (e.g., "Finance") to stable system IDs (e.g.,
Category.SystemID.finance). - Update Subscriptions: Iterates through all subscriptions. If a subscription is linked to a category that matches a system name, the
systemCategoryIdis set and the relationship is nullified. - Cleanup: Deletes legacy system records from the
CategoryandPaymentMethodtables that previously heldisSystem = true. - Provider Reset: All
SubscriptionProviderrecords are deleted to be re-seeded in the new local-only store.
Sources: AppMigrationPlan.swift:76-110, AppMigrationPlan.swift:113-125
Data Seeding and Versioning
To ensure that system data is correctly populated and updated after migrations or new installs, the project uses a DataSeeder and a SeedVersionStore.
Seeding Logic
Seeding is version-tracked. The DataSeeder only inserts or updates data if the version in the JSON seed files is higher than the storedVersion in UserDefaults.
| Seed Type | Version | Items | Description |
|---|---|---|---|
| Categories | 2 | 12 | Finance, Media, AI, Utilities, etc. |
| Payment Methods | 2 | 6 | Apple Pay, Visa, MasterCard, etc. |
Sources: DataSeederTests.swift:60-70, DataSeederTests.swift:150-160
External Data Portability
Data migrations are complemented by the DataExportServiceImpl, which manages JSON-based backups. The export/import logic respects schema versioning.
- Version Validation: During import, the service checks
backup.version. If the file version is higher than theAppBackup.currentVersion, the import is rejected. - Deduplication: The import process deduplicates records by ID first, then by name, ensuring that restored data does not create redundant entries in the current schema.
- Relationship Resolution: When importing subscriptions, the service resolves relationships by looking up existing Category or PaymentMethod entities in the current context.
Sources: DataExportServiceImpl.swift:42-50, DataExportServiceImpl.swift:72-80, DataExportServiceTests.swift:145-155
Summary
The Data Migration & Schema Update system in Trackurr provides a structured lifecycle for persistent data. By utilizing SwiftData's versioned schemas and custom migration stages, the project handles complex transitions such as the decoupling of system entities for CloudKit compatibility. This ensures that as the application grows, user data is safely transformed to meet new architectural requirements without loss or duplication.