mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-09 18:33:16 -06:00
## Summary Table migration logic for adding new columns and removing obsolete columns has been abstracted into `DatabaseTableProtocol` with a default implementation. All 13 table implementations now conform to the extended protocol and support automatic column migration. **Protocol Changes:** - Added `tableName: String` property requirement - Added `definedColumns: [String]` property requirement - Added default `migrateColumns(database:)` implementation that handles adding/removing columns **Updated Tables:** All tables now implement the protocol and support column migration: - `HAppEntityTable`, `WatchConfigTable`, `CarPlayConfigTable`, `AssistPipelinesTable` - `AppEntityRegistryListForDisplayTable`, `AppEntityRegistryTable`, `AppDeviceRegistryTable` - `AppPanelTable`, `CustomWidgetTable`, `AppAreaTable` - `HomeViewConfigurationTable`, `CameraListConfigurationTable`, `AssistConfigurationTable` **Column Enums:** All column enums in `DatabaseTables` now conform to `CaseIterable` for consistent migration support. Relies on `ALTER TABLE ... DROP COLUMN` support (SQLite 3.35+, iOS 15+). ## Screenshots N/A - Database internals only. ## Link to pull request in Documentation repository N/A - No user-facing changes. ## Any other notes The migration logic is now centralized in the protocol extension, eliminating code duplication across table implementations. Each table provides its `tableName` and `definedColumns`, and the shared `migrateColumns(database:)` method handles the actual column synchronization. <!-- START COPILOT CODING AGENT SUFFIX --> <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > In GRDB+Initialization.swift each tablet has a logic to create table columns that may not exist when the user updates the app, update those table creationg to also delete columns that are not defined in the create method anymore </details> <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/home-assistant/iOS/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bgoncal <5808343+bgoncal@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
118 lines
4.4 KiB
Swift
118 lines
4.4 KiB
Swift
import Foundation
|
|
import GRDB
|
|
|
|
public extension DatabaseQueue {
|
|
// Following GRDB cocnurrency rules, we have just one database instance
|
|
// https://swiftpackageindex.com/groue/grdb.swift/v6.29.3/documentation/grdb/concurrency#Concurrency-Rules
|
|
static var appDatabase: DatabaseQueue = {
|
|
do {
|
|
let database = try DatabaseQueue(path: databasePath())
|
|
|
|
// Create tables if needed
|
|
for table in DatabaseQueue.tables() {
|
|
do {
|
|
try table.createIfNeeded(database: database)
|
|
} catch {
|
|
let className = String(describing: type(of: table))
|
|
let errorMessage = "Failed create GRDB table \(className), error: \(error.localizedDescription)"
|
|
Current.clientEventStore.addEvent(ClientEvent(text: errorMessage, type: .database))
|
|
}
|
|
}
|
|
DatabaseQueue.deleteOldTables(database: database)
|
|
|
|
#if targetEnvironment(simulator)
|
|
print("GRDB App database is stored at \(AppConstants.appGRDBFile.description)")
|
|
#endif
|
|
return database
|
|
} catch {
|
|
let errorMessage = "Failed to initialize GRDB, error: \(error.localizedDescription)"
|
|
Current.Log.error(errorMessage)
|
|
fatalError(errorMessage)
|
|
}
|
|
}()
|
|
|
|
static func databasePath() -> String {
|
|
// Path for tests
|
|
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
|
|
let tempDirectory = NSTemporaryDirectory()
|
|
return (tempDirectory as NSString).appendingPathComponent("test_database.sqlite")
|
|
} else {
|
|
// Path for App use
|
|
return AppConstants.appGRDBFile.path
|
|
}
|
|
}
|
|
|
|
internal static func tables() -> [DatabaseTableProtocol] {
|
|
[
|
|
HAppEntityTable(),
|
|
WatchConfigTable(),
|
|
CarPlayConfigTable(),
|
|
AssistPipelinesTable(),
|
|
AppEntityRegistryListForDisplayTable(),
|
|
AppEntityRegistryTable(),
|
|
AppDeviceRegistryTable(),
|
|
AppPanelTable(),
|
|
CustomWidgetTable(),
|
|
AppAreaTable(),
|
|
HomeViewConfigurationTable(),
|
|
CameraListConfigurationTable(),
|
|
AssistConfigurationTable(),
|
|
]
|
|
}
|
|
|
|
internal static func deleteOldTables(database: DatabaseQueue) {
|
|
do {
|
|
/*
|
|
ClientEvent used to be saved in GRDB, but because of a problem of one process holding
|
|
lock on the database and causing crash 0xdead10cc now it is saved as a json file
|
|
More information: https://github.com/groue/GRDB.swift/issues/1626#issuecomment-2623927815
|
|
*/
|
|
try database.write { db in
|
|
try db.drop(table: GRDBDatabaseTable.clientEvent.rawValue)
|
|
}
|
|
} catch {
|
|
let errorMessage =
|
|
"Failed or not needed delete client event GRDB info, error: \(error.localizedDescription)"
|
|
Current.Log.verbose(errorMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
protocol DatabaseTableProtocol {
|
|
/// The name of the database table
|
|
var tableName: String { get }
|
|
|
|
/// The list of column names defined for this table
|
|
var definedColumns: [String] { get }
|
|
|
|
/// Creates the table if it doesn't exist, or migrates it if it does
|
|
func createIfNeeded(database: DatabaseQueue) throws
|
|
}
|
|
|
|
extension DatabaseTableProtocol {
|
|
/// Migrates the table by adding new columns and removing obsolete columns
|
|
func migrateColumns(database: DatabaseQueue) throws {
|
|
try database.write { db in
|
|
let existingColumns = try db.columns(in: tableName)
|
|
let definedColumnSet = Set(definedColumns)
|
|
|
|
// Add new columns that don't exist yet
|
|
for columnName in definedColumns {
|
|
let shouldCreateColumn = !existingColumns.contains { $0.name == columnName }
|
|
if shouldCreateColumn {
|
|
try db.alter(table: tableName) { tableAlteration in
|
|
tableAlteration.add(column: columnName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove columns that are no longer defined
|
|
for existingColumn in existingColumns where !definedColumnSet.contains(existingColumn.name) {
|
|
try db.alter(table: tableName) { tableAlteration in
|
|
tableAlteration.drop(column: existingColumn.name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|