Migrate remaining Realm models to GRDB#4954
Conversation
Rebuilds the remaining Realm models on GRDB: - RLMZone becomes the AppZone record (same region-monitoring helpers) - NotificationCategory/NotificationAction become value types; actions are embedded in the category row as JSON - WatchComplication keeps its ObjectMapper wire format (used for phone-to-watch sync) but persists via GRDB - LocationHistoryEntry/LocationError become records with explicit saves LegacyModelManager keeps its public surface (observe/fetch/subscribe/ cleanup/store) but operates on GRDB: observation via ValueObservation, sync diffing via the reworked UpdatableModel protocol, and cleanup via declarative age/orphan definitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
Replaces Realm+Initialization with RealmToGRDBMigration: on first launch after updating, the legacy Realm store is opened (running the historic schema migrations for very old stores) and its zones, notification categories/actions and watch complications are imported into GRDB. The completion flag is only set on success, so a failed import retries on the next launch. Location history/errors are not imported (short-lived debug data with a 256-hour retention). This file is now the only RealmSwift import in the codebase; once the migration has shipped for a few releases, the pod and this file can be deleted together (noted in the Podfile). Wired up from AppDelegate and the watch ExtensionDelegate in the target migration commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
- HAAPI location submission reads zones and writes location history via GRDB - Watch context sync and complication template rendering fetch complications from GRDB - Geocoder sensor and one-shot location error logging use the new records - Environment drops Current.realm/realmFatalPresentation, log export no longer attaches a Realm backup, and the unused ObjectMapper Realm mapping helpers are removed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
- ZoneManager observes tracked zones via GRDB ValueObservation (immediate initial delivery preserves the start-of-life region sync) and its collector/processor/fuzzers query and persist AppZone records - Notification category list/editor views hold value-type state and persist through the category records; UNNotificationCategory registration observes the GRDB table - RealmResultsObserver becomes the generic DatabaseResultsObserver - Location history views, watch complication view models and location settings observe/persist via GRDB - AppDelegate runs the Realm-to-GRDB import at startup, the Copy Realm developer tool and the Realm fatal-error presentation are removed, and resetStores wipes the migrated tables Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
The watch extension runs the Realm-to-GRDB import at startup and reads/ replaces complications via GRDB; the widget timeline providers and CarPlay quick access drop their unused RealmSwift imports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
Tests build their fixtures in in-memory GRDB databases (overriding Current.database) instead of in-memory Realms. ModelManager tests get GRDB-based store/cleanup fixtures, the complication webhook tests exercise the real WatchComplication template rendering instead of a Realm subclass, and the ZoneManager tests wait for the asynchronous zone observation where Realm used to deliver synchronously on refresh. Table count/schema tests cover the five new tables. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
Renames the project references for the moved files (AppZone, UpdatableModel, RealmToGRDBMigration, DatabaseResultsObserver, AppZone.test) and drops the dangling ObjectMapper+RealmList reference, which was not compiled into any target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
Wrap inline loop bodies in the ModelManager tests, drop a redundant explicit self in LocationError.save, and drop a redundant Void return type in the complication webhook handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
There was a problem hiding this comment.
Pull request overview
This PR completes the migration of the remaining legacy Realm-backed models (zones, actionable notification categories/actions, watch complications, and location history/errors) to GRDB, including a one-time on-device import path so existing user data survives updates while Realm is phased out.
Changes:
- Introduces GRDB-backed replacements for
RLMZone,NotificationCategory/NotificationAction,WatchComplication, andLocationHistoryEntry/LocationError, plus new table registrations/schema tests. - Adds
RealmToGRDBMigrationto import legacy Realm stores into GRDB on first launch (app + watch), and removes almost allRealmSwiftusage from the codebase. - Updates app/watch/UI consumers and test fixtures to read/write/observe via GRDB (
ValueObservation) instead of Realm notifications.
Reviewed changes
Copilot reviewed 68 out of 68 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| Tests/Shared/Webhook/WebhookUpdateLocation.test.swift | Updates zone test fixtures from RLMZone to AppZone. |
| Tests/Shared/Webhook/WebhookResponseUpdateComplications.test.swift | Migrates complication response tests from Realm to GRDB storage. |
| Tests/Shared/Sensors/GeocoderSensor.test.swift | Migrates geocoder sensor tests to GRDB-backed zones. |
| Tests/Shared/Database/TableSchemaTests.test.swift | Adds schema validation tests for the new GRDB tables and updates table count. |
| Tests/Shared/Database/GRDB+Initialization.test.swift | Updates GRDB initialization tests for the expanded table set. |
| Tests/Shared/ClientEventTests.swift | Removes Realm import from client event tests. |
| Tests/Shared/AppZone.test.swift | Renames/migrates Realm zone tests to AppZone + GRDB. |
| Tests/App/ZoneManager/ZoneManagerRegionFilter.test.swift | Migrates region filter tests from RLMZone to AppZone. |
| Tests/App/ZoneManager/ZoneManagerProcessor.test.swift | Migrates processor tests to GRDB database queue + AppZone. |
| Tests/App/ZoneManager/ZoneManagerCollector.test.swift | Migrates collector tests to GRDB and updates naming. |
| Tests/App/ZoneManager/ZoneManager.test.swift | Migrates ZoneManager tests to GRDB observations and async expectations. |
| Sources/Shared/Location/CLLocationManager+OneShotLocation.swift | Writes LocationError via GRDB instead of Realm. |
| Sources/Shared/Environment/Environment.swift | Removes Current.realm and related Realm environment hooks. |
| Sources/Shared/Database/GRDB+Initialization.swift | Registers the newly added GRDB tables. |
| Sources/Shared/Database/DatabaseTables.swift | Adds table/column enums for migrated models. |
| Sources/Shared/Common/Extensions/XCGLogger+Export.swift | Removes Realm backup export from log archives. |
| Sources/Shared/Common/Extensions/RealmToGRDBMigration.swift | Adds the one-time Realm→GRDB importer and legacy schema migration block. |
| Sources/Shared/Common/Extensions/Realm+Initialization.swift | Removes the legacy Realm initialization/migration/backup helper. |
| Sources/Shared/Common/Extensions/ObjectMapper+RealmList.swift | Removes Realm-specific ObjectMapper List mapping helpers. |
| Sources/Shared/API/Webhook/Sensors/GeocoderSensor.swift | Switches zone lookup for geocoder attributes to AppZone/GRDB. |
| Sources/Shared/API/Webhook/Networking/WebhookResponseUpdateComplications.swift | Updates complication rendering persistence from Realm writes to GRDB fetch+save. |
| Sources/Shared/API/Webhook/Networking/WebhookResponseLocation.swift | Updates metadata structs to use AppZone instead of RLMZone. |
| Sources/Shared/API/WatchHelpers.swift | Switches watch context complication serialization to GRDB-backed queries. |
| Sources/Shared/API/Models/WebhookUpdateLocation.swift | Updates model initializers and zone field access to AppZone. |
| Sources/Shared/API/Models/WatchComplication.swift | Reworks WatchComplication from Realm Object to GRDB PersistableRecord with query helpers. |
| Sources/Shared/API/Models/UpdatableModel.swift | Introduces GRDB-based UpdatableModel protocol used by sync/store paths. |
| Sources/Shared/API/Models/RealmZone.swift | Deletes Realm-backed zone model. |
| Sources/Shared/API/Models/RealmPersistable.swift | Deletes Realm-specific UpdatableModel protocol definitions. |
| Sources/Shared/API/Models/NotificationCategory.swift | Reworks notification categories as GRDB records with embedded JSON actions and query helpers. |
| Sources/Shared/API/Models/NotificationAction.swift | Reworks actions into Codable value types embedded in category JSON. |
| Sources/Shared/API/Models/LocationHistory.swift | Reworks location history/errors into GRDB records with query helpers and tables. |
| Sources/Shared/API/Models/LegacyModelManager.swift | Replaces Realm observation/cleanup/store with GRDB ValueObservation + SQL-based cleanup. |
| Sources/Shared/API/Models/AppZone.swift | Adds GRDB-backed AppZone model with query helpers and update logic. |
| Sources/Shared/API/HAAPI.swift | Updates location submission path and history persistence to GRDB-backed models. |
| Sources/Extensions/Watch/ExtensionDelegate.swift | Runs Realm→GRDB migration at watch startup and switches complication reads/writes to GRDB. |
| Sources/Extensions/Watch/Complication/ComplicationController.swift | Migrates complication data source reads to GRDB and updates property names. |
| Sources/Extensions/AppIntents/Widget/Sensor/WidgetSensorsAppIntentTimelineProvider.swift | Removes Realm import after migration. |
| Sources/Extensions/AppIntents/Widget/Script/WidgetScriptsAppIntentTimelineProvider.swift | Removes Realm import after migration. |
| Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift | Removes Realm import after migration. |
| Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift | Removes Realm import after migration. |
| Sources/CarPlay/Templates/QuickAccess/CarPlayQuickAccessViewModel.swift | Removes Realm import after migration. |
| Sources/CarPlay/Templates/QuickAccess/CarPlayQuickAccessTemplate.swift | Removes Realm import after migration. |
| Sources/App/ZoneManager/ZoneManagerRegionFilter.swift | Migrates region filtering from RLMZone to AppZone. |
| Sources/App/ZoneManager/ZoneManagerProcessor.swift | Updates processor logic to mutate/persist AppZone via GRDB. |
| Sources/App/ZoneManager/ZoneManagerEvent.swift | Updates event payloads from Realm zone references to AppZone. |
| Sources/App/ZoneManager/ZoneManagerCollector.swift | Migrates region→zone lookup from Realm query to GRDB-backed zones. |
| Sources/App/ZoneManager/ZoneManagerAccuracyFuzzer.swift | Updates fuzzer logic for new AppZone field names and identity semantics. |
| Sources/App/ZoneManager/ZoneManager.swift | Replaces Realm collection observation with GRDB ValueObservation and tracks zones as values. |
| Sources/App/Utilities/Utils.swift | Updates debug “reset stores” to clear GRDB tables instead of resetting Realm. |
| Sources/App/Utilities/MenuManager.swift | Removes Realm import after migration. |
| Sources/App/Settings/Observation/NotificationCategory+Observation.swift | Switches notification category observation from Realm to GRDB-backed LegacyModelManager.observe. |
| Sources/App/Settings/Notifications/NotificationCategoryListView.swift | Replaces Realm observers with GRDB DatabaseResultsObserver and value semantics persistence. |
| Sources/App/Settings/Notifications/NotificationCategoryEditorView.swift | Reworks editor to operate on value-type categories/actions and hand back saved values. |
| Sources/App/Settings/Notifications/NotificationActionEditorView.swift | Reworks action editor to return edited values (no direct persistence). |
| Sources/App/Settings/Location/LocationSettingsViewModel.swift | Migrates zone observation to GRDB ValueObservation and AppZone. |
| Sources/App/Settings/DebugView.swift | Removes Realm copy tool UI and associated implementation. |
| Sources/App/Settings/Components/RealmResultsObserver.swift | Removes Realm SwiftUI observation helper. |
| Sources/App/Settings/Components/DatabaseResultsObserver.swift | Adds GRDB SwiftUI observation helper (ValueObservation → @Published). |
| Sources/App/Settings/AppleWatch/Complications/ComplicationListViewModel.swift | Switches complication observation from Realm token to GRDB observation. |
| Sources/App/Settings/AppleWatch/Complications/ComplicationEditViewModel.swift | Migrates save/delete flows to GRDB-backed WatchComplication persistence. |
| Sources/App/Scenes/SceneManager.swift | Removes Realm fatal-presentation wiring. |
| Sources/App/ClientEvents/LocationHistoryListView.swift | Migrates location history list observation/clearing to GRDB. |
| Sources/App/ClientEvents/LocationHistoryEntryListItemView.swift | Updates location history row rendering to new property names. |
| Sources/App/ClientEvents/LocationHistoryDetailViewController.swift | Migrates history detail observation/region overlay generation to GRDB + AppZone. |
| Sources/App/AppDelegate.swift | Runs Realm→GRDB migration at startup and updates category existence check. |
| Podfile | Documents retaining RealmSwift temporarily for the importer path. |
Comments suppressed due to low confidence (1)
Sources/App/ZoneManager/ZoneManagerCollector.swift:60
didDetermineStatecurrently loads all zones from the database (AppZone.all()) and then searches in memory. This is on a CoreLocation delegate path and can be called frequently; it should fetch by identifier instead to avoid unnecessary DB reads and allocations.
let zone = AppZone.all()
.first(where: {
$0.identifier == region.identifier ||
$0.identifier == region.identifier.components(separatedBy: "@").first
})
Addresses review feedback: the CoreLocation delegate path fetched every zone and scanned in memory; fetch the exact region identifier (and the pre-@ prefix used by multi-region small zones) by primary key instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
- WatchComplication: inline the two watchOS `with(...)` call sites. The model now conforms to GRDB TableRecord, whose static `with<RowDecoder>` shadowed the global helper inside the #if os(watchOS) block, failing the Shared-watchOS compile (the only target that builds that block). - resetStores(): delete the legacy Realm store on reset. Clearing the app-group defaults also clears the migration flag, so leaving the store on disk let the importer resurrect the wiped data on next launch. - RealmToGRDBMigration: bound the import retry so a corrupt/unopenable store no longer retries and logs an error on every launch forever. - DatabaseResultsObserver: deliver the initial value synchronously via scheduling: .immediate, matching the old RealmResultsObserver and removing a one-frame empty-list flicker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LocationHistoryListViewModel and LocationSettingsViewModel populated their lists synchronously at init under Realm; the GRDB observations delivered the initial value asynchronously, leaving the first rendered frame empty (which the location history snapshot test catches). Use scheduling: .immediate like DatabaseResultsObserver. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
DatabaseTableProtocol.test still asserted 17 registered tables; the Realm to GRDB migration added five (appZone, notificationCategory, watchComplication, locationHistory, locationError), so the registry now holds 22 — matching the counts already updated in the other database test suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
|
Found 2 unused localization strings in the codebase. Click to see detailsTo remove them, run the |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4954 +/- ##
=======================================
Coverage ? 53.37%
=======================================
Files ? 299
Lines ? 19882
Branches ? 0
=======================================
Hits ? 10613
Misses ? 9269
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Covers the value-type logic that Codecov flagged as untested in the rewritten models: UNNotificationAction/Category building and option flags, construction from server push-category config, GRDB round-trips (including the JSON-embedded actions), the watch sync wire format and its stable ObjectMapper keys, family/template fallbacks, template rendering bookkeeping, identifier-based equality, and percentile parsing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
The comment added to the Podfile changed its checksum; CocoaPods embeds that checksum in Podfile.lock, so the [CP] Check Pods Manifest.lock build phase failed once CI cached a Pods sandbox regenerated against the new Podfile. Verified: the previous checksum is the SHA-1 of main's Podfile, and the new value is the SHA-1 of the updated Podfile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
|
@copilot resolve the merge conflicts on this branch. |
Resolved. There were two conflicts, both in Podfile/Podfile.lock:
All other changes from |
…migration-e9u9qd Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Moves the last remaining Realm models — zones (
RLMZone→AppZone), notification categories/actions, watch complications and location history/errors — to GRDB, and migrates users' existing data before Realm is retired. After this PR,import RealmSwiftexists in exactly one file:RealmToGRDBMigration.swift, a one-time importer that reads the legacy Realm store (running the historic schema migrations for very old stores) and writes the rows into GRDB on first launch of the app and the watch app. Once the migration has shipped for a few releases, deleting that file plus thepod 'RealmSwift'entry removes the dependency entirely — the pod is kept for now solely so user-authored data (locally created notification categories/actions, watch complications, per-zone settings) is not lost on update.Highlights, in commit/review order:
AppZone,NotificationCategory/NotificationAction(actions embedded as JSON in the category row),WatchComplication(keeps its ObjectMapper wire format for phone↔watch context sync),LocationHistoryEntry/LocationError, plus five new tables registered inGRDB+Initialization.LegacyModelManagerkeeps its public surface but now diffs/stores/cleans up via GRDB, and observes viaValueObservation.RealmToGRDBMigrationwith private legacyObjectmirrors (@objcnames match the historic class names); retries on next launch if the import fails, and logs aClientEventwith imported counts.Current.realm/realmFatalPresentationremoved from the environment.ValueObservation(.immediatescheduling preserves the monitor-at-startup behavior), notification category editors reworked around value semantics,RealmResultsObserver→DatabaseResultsObserver, location history views, watch complication view models, developer tools.DatabaseQueues; ZoneManager tests wait for the async observation where Realm delivered synchronously onrefresh().ObjectMapper+RealmList.swiftreference (it was not compiled into any target).Behavior notes:
Screenshots
No visual changes intended — settings screens (notification categories, watch complications, location, history) are re-backed by GRDB but keep the same UI.
Link to pull request in Documentation repository
Documentation: home-assistant/companion.home-assistant#
Any other notes
Follow-up (separate PR, after this has been in production for a few releases): delete
RealmToGRDBMigration.swift, removepod 'RealmSwift'from the Podfile, and clean up the legacydataStore/store.realmfiles.🤖 Generated with Claude Code
https://claude.ai/code/session_01WjzY3ZsTdqA44cEq9PdrjQ
Generated by Claude Code