Skip to content

Commit d03515f

Browse files
authored
Simplify database updates by replacing upserts with full deletes (#4267)
Refactors area, entity, and device registry persistence methods to delete all existing records for a server before inserting fresh data, instead of performing upserts and differential deletes. This simplifies the logic and ensures data consistency by always replacing old data in a single transaction. <!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. -->
1 parent 2211129 commit d03515f

File tree

1 file changed

+27
-74
lines changed

1 file changed

+27
-74
lines changed

Sources/Shared/Environment/AppDatabaseUpdater.swift

Lines changed: 27 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,7 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
492492
}
493493

494494
/// Persists areas and their entity relationships for a server.
495-
/// Uses a single asyncWrite transaction for batching, replaces existing rows, and deletes stale ones.
496-
/// For simplicity and speed, we upsert via `save(onConflict: .replace)`; deeper diffing can be added if needed.
495+
/// Deletes all existing areas for the server and inserts fresh data in a single transaction.
497496
private func saveAreasToDatabase(
498497
areas: [HAAreasRegistryResponse],
499498
areasAndEntities: [String: Set<String>],
@@ -519,33 +518,18 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
519518
return result
520519
}.value
521520

522-
// Nothing to persist; keep going (delete pass below might still remove stale rows).
523-
if appAreas.isEmpty {
524-
Current.Log.verbose("No areas to save for server \(serverId)")
525-
}
526-
527521
do {
528522
let dbTimer = ProfilingTimer("Step 5.2.2: Database write transaction")
529523
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
530-
// Database writes are already async and happen on GRDB's background queue
531524
Current.database().asyncWrite { db in
532-
let existingAreaIds = try AppArea
525+
// Delete all existing areas for this server
526+
try AppArea
533527
.filter(Column(DatabaseTables.AppArea.serverId.rawValue) == serverId)
534-
.fetchAll(db).map(\.id)
528+
.deleteAll(db)
535529

536-
// Insert or update new areas
530+
// Insert fresh areas
537531
for area in appAreas {
538-
try area.save(db, onConflict: .replace)
539-
}
540-
541-
// Delete areas that no longer exist
542-
let newAreaIds = areas.map { "\(serverId)-\($0.areaId)" }
543-
let areaIdsToDelete = existingAreaIds.filter { !newAreaIds.contains($0) }
544-
545-
if !areaIdsToDelete.isEmpty {
546-
try AppArea
547-
.filter(areaIdsToDelete.contains(Column(DatabaseTables.AppArea.id.rawValue)))
548-
.deleteAll(db)
532+
try area.insert(db)
549533
}
550534
} completion: { _, result in
551535
switch result {
@@ -573,8 +557,8 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
573557
}
574558
}
575559

576-
/// Persists the entity registry list-for-display for a server with batched writes and stale deletions.
577-
/// Builds the payload with a streaming loop to reduce intermediate allocations vs filter+map.
560+
/// Persists the entity registry list-for-display for a server.
561+
/// Deletes all existing records for the server and inserts fresh data in a single transaction.
578562
private func saveEntityRegistryListForDisplay(_ response: EntityRegistryListForDisplay, serverId: String) async {
579563
// Check for cancellation before starting database work
580564
guard !isUpdateCancelled() else {
@@ -606,28 +590,15 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
606590
continuation.resume(throwing: CancellationError())
607591
return
608592
}
609-
// Note: we batch entities into memory before this write. This is a trade-off for simpler, atomic
610-
// updates;
611-
// if memory usage becomes an issue for very large datasets, consider a streaming or chunked approach.
612593
Current.database().asyncWrite { [entitiesListForDisplay] db in
613-
// Get existing IDs for this server
614-
let existingIds = try AppEntityRegistryListForDisplay
594+
// Delete all existing records for this server
595+
try AppEntityRegistryListForDisplay
615596
.filter(Column(DatabaseTables.AppEntityRegistryListForDisplay.serverId.rawValue) == serverId)
616-
.fetchAll(db)
617-
.map(\.id)
597+
.deleteAll(db)
618598

619-
// Insert or update new records
599+
// Insert fresh records
620600
for record in entitiesListForDisplay {
621-
try record.save(db, onConflict: .replace)
622-
}
623-
624-
// Delete records that no longer exist
625-
let newIds = entitiesListForDisplay.map(\.id)
626-
let idsToDelete = existingIds.filter { !newIds.contains($0) }
627-
628-
if !idsToDelete.isEmpty {
629-
try AppEntityRegistryListForDisplay
630-
.deleteAll(db, keys: idsToDelete)
601+
try record.insert(db)
631602
}
632603
} completion: { _, result in
633604
switch result {
@@ -654,7 +625,8 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
654625
}
655626
}
656627

657-
/// Persists the entity registry for a server using a single transaction and differential deletes.
628+
/// Persists the entity registry for a server.
629+
/// Deletes all existing records for the server and inserts fresh data in a single transaction.
658630
private func saveEntityRegistry(_ registryEntries: [EntityRegistryEntry], serverId: String) async {
659631
// If cancelled before touching the DB, bail out early to avoid unnecessary work.
660632
guard !isUpdateCancelled() else {
@@ -677,24 +649,14 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
677649
return
678650
}
679651
Current.database().asyncWrite { db in
680-
// Get existing unique IDs for this server
681-
let existingIds = try AppEntityRegistry
652+
// Delete all existing registry entries for this server
653+
try AppEntityRegistry
682654
.filter(Column(DatabaseTables.EntityRegistry.serverId.rawValue) == serverId)
683-
.fetchAll(db)
684-
.map(\.id)
655+
.deleteAll(db)
685656

686-
// Insert or update new registry entries
657+
// Insert fresh registry entries
687658
for registry in appEntityRegistries {
688-
try registry.save(db, onConflict: .replace)
689-
}
690-
691-
// Delete registry entries that no longer exist
692-
let newIds = appEntityRegistries.map(\.id)
693-
let idsToDelete = existingIds.filter { !newIds.contains($0) }
694-
695-
if !idsToDelete.isEmpty {
696-
try AppEntityRegistry
697-
.deleteAll(db, keys: idsToDelete)
659+
try registry.insert(db)
698660
}
699661
} completion: { _, result in
700662
switch result {
@@ -724,7 +686,8 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
724686
}
725687
}
726688

727-
/// Persists the device registry for a server using a single transaction and differential deletes.
689+
/// Persists the device registry for a server.
690+
/// Deletes all existing records for the server and inserts fresh data in a single transaction.
728691
private func saveDeviceRegistry(_ registryEntries: [DeviceRegistryEntry], serverId: String) async {
729692
// If cancelled before touching the DB, bail out early to avoid unnecessary work.
730693
guard !isUpdateCancelled() else {
@@ -747,24 +710,14 @@ final class AppDatabaseUpdater: AppDatabaseUpdaterProtocol {
747710
return
748711
}
749712
Current.database().asyncWrite { db in
750-
// Get existing device IDs for this server
751-
let existingIds = try AppDeviceRegistry
713+
// Delete all existing device registry entries for this server
714+
try AppDeviceRegistry
752715
.filter(Column(DatabaseTables.DeviceRegistry.serverId.rawValue) == serverId)
753-
.fetchAll(db)
754-
.map(\.id)
716+
.deleteAll(db)
755717

756-
// Insert or update new registry entries
718+
// Insert fresh registry entries
757719
for registry in appDeviceRegistries {
758-
try registry.save(db, onConflict: .replace)
759-
}
760-
761-
// Delete registry entries that no longer exist
762-
let newIds = appDeviceRegistries.map(\.id)
763-
let idsToDelete = existingIds.filter { !newIds.contains($0) }
764-
765-
if !idsToDelete.isEmpty {
766-
try AppDeviceRegistry
767-
.deleteAll(db, keys: idsToDelete)
720+
try registry.insert(db)
768721
}
769722
} completion: { _, result in
770723
switch result {

0 commit comments

Comments
 (0)