Skip to content

Commit 7647dde

Browse files
CopilotbgoncalCopilot
authored
Abstract column migration logic into DatabaseTableProtocol for all tables (#4243)
## 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>
1 parent b91d61f commit 7647dde

15 files changed

+171
-86
lines changed

Sources/Shared/Database/AppDeviceRegistryTable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import Foundation
22
import GRDB
33

44
final class AppDeviceRegistryTable: DatabaseTableProtocol {
5+
var tableName: String { GRDBDatabaseTable.deviceRegistry.rawValue }
6+
7+
var definedColumns: [String] { DatabaseTables.DeviceRegistry.allCases.filter { $0 != .id }.map(\.rawValue) }
8+
59
func createIfNeeded(database: DatabaseQueue) throws {
610
let shouldCreateTable = try database.read { db in
7-
try !db.tableExists(GRDBDatabaseTable.deviceRegistry.rawValue)
11+
try !db.tableExists(tableName)
812
}
913
if shouldCreateTable {
1014
try database.write { db in
11-
try db.create(table: GRDBDatabaseTable.deviceRegistry.rawValue) { t in
15+
try db.create(table: tableName) { t in
1216
// Core identifiers
1317
t.column(DatabaseTables.DeviceRegistry.serverId.rawValue, .text).notNull().indexed()
1418
t.column(DatabaseTables.DeviceRegistry.deviceId.rawValue, .text).notNull().indexed()
@@ -55,6 +59,8 @@ final class AppDeviceRegistryTable: DatabaseTableProtocol {
5559
])
5660
}
5761
}
62+
} else {
63+
try migrateColumns(database: database)
5864
}
5965
}
6066
}

Sources/Shared/Database/AppEntityRegistryTable.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@ import Foundation
22
import GRDB
33

44
final class AppEntityRegistryTable: DatabaseTableProtocol {
5+
var tableName: String { GRDBDatabaseTable.entityRegistry.rawValue }
6+
7+
var definedColumns: [String] {
8+
DatabaseTables.EntityRegistry.allCases
9+
.filter { $0 != .id }
10+
.map(\.rawValue)
11+
}
12+
513
func createIfNeeded(database: DatabaseQueue) throws {
614
let shouldCreateTable = try database.read { db in
7-
try !db.tableExists(GRDBDatabaseTable.entityRegistry.rawValue)
15+
try !db.tableExists(tableName)
816
}
917
if shouldCreateTable {
1018
try database.write { db in
11-
try db.create(table: GRDBDatabaseTable.entityRegistry.rawValue) { t in
19+
try db.create(table: tableName) { t in
1220
// Core identifiers
1321
t.column(DatabaseTables.EntityRegistry.serverId.rawValue, .text).notNull().indexed()
1422
t.column(DatabaseTables.EntityRegistry.uniqueId.rawValue, .text).notNull().indexed()
@@ -51,6 +59,8 @@ final class AppEntityRegistryTable: DatabaseTableProtocol {
5159
])
5260
}
5361
}
62+
} else {
63+
try migrateColumns(database: database)
5464
}
5565
}
5666
}

Sources/Shared/Database/DatabaseTables.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,37 +33,37 @@ public enum DatabaseTables {
3333
case disabledBy
3434
}
3535

36-
public enum WatchConfig: String {
36+
public enum WatchConfig: String, CaseIterable {
3737
case id
3838
case assist
3939
case items
4040
}
4141

4242
// Assist pipelines
43-
public enum AssistPipelines: String {
43+
public enum AssistPipelines: String, CaseIterable {
4444
case serverId
4545
case preferredPipeline
4646
case pipelines
4747
}
4848

4949
// CarPlay configuration
50-
public enum CarPlayConfig: String {
50+
public enum CarPlayConfig: String, CaseIterable {
5151
case id
5252
case tabs
5353
case quickAccessItems
5454
}
5555

5656
// Table where it is store frontend related values such as
5757
// precision for sensors
58-
public enum AppEntityRegistryListForDisplay: String {
58+
public enum AppEntityRegistryListForDisplay: String, CaseIterable {
5959
case id
6060
case serverId
6161
case entityId
6262
case registry
6363
}
6464

6565
// Sidebar dashboard panels
66-
public enum AppPanel: String {
66+
public enum AppPanel: String, CaseIterable {
6767
case id
6868
case serverId
6969
case icon
@@ -73,15 +73,15 @@ public enum DatabaseTables {
7373
case showInSidebar
7474
}
7575

76-
public enum CustomWidget: String {
76+
public enum CustomWidget: String, CaseIterable {
7777
case id
7878
case name
7979
case items
8080
case itemsStates
8181
}
8282

8383
// Areas from Home Assistant
84-
public enum AppArea: String {
84+
public enum AppArea: String, CaseIterable {
8585
case id
8686
case serverId
8787
case areaId
@@ -106,14 +106,14 @@ public enum DatabaseTables {
106106
}
107107

108108
// Camera List Configuration (per server)
109-
public enum CameraListConfiguration: String {
109+
public enum CameraListConfiguration: String, CaseIterable {
110110
case serverId
111111
case areaOrders
112112
case sectionOrder
113113
}
114114

115115
// Entity Registry (full entity registry data)
116-
public enum EntityRegistry: String {
116+
public enum EntityRegistry: String, CaseIterable {
117117
case id // Auto generated by GRDB (serverId-uniqueId)
118118
case serverId
119119
case uniqueId
@@ -142,7 +142,7 @@ public enum DatabaseTables {
142142
}
143143

144144
// Device Registry (full device registry data)
145-
public enum DeviceRegistry: String {
145+
public enum DeviceRegistry: String, CaseIterable {
146146
case id // Auto generated by GRDB (serverId-deviceId)
147147
case serverId
148148
case deviceId
@@ -169,7 +169,7 @@ public enum DatabaseTables {
169169
case viaDeviceID
170170
}
171171

172-
public enum AssistConfiguration: String {
172+
public enum AssistConfiguration: String, CaseIterable {
173173
case id
174174
case enableOnDeviceSTT
175175
case enableModernUI

Sources/Shared/Database/GRDB+Initialization.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,39 @@ public extension DatabaseQueue {
7979
}
8080

8181
protocol DatabaseTableProtocol {
82+
/// The name of the database table
83+
var tableName: String { get }
84+
85+
/// The list of column names defined for this table
86+
var definedColumns: [String] { get }
87+
88+
/// Creates the table if it doesn't exist, or migrates it if it does
8289
func createIfNeeded(database: DatabaseQueue) throws
8390
}
91+
92+
extension DatabaseTableProtocol {
93+
/// Migrates the table by adding new columns and removing obsolete columns
94+
func migrateColumns(database: DatabaseQueue) throws {
95+
try database.write { db in
96+
let existingColumns = try db.columns(in: tableName)
97+
let definedColumnSet = Set(definedColumns)
98+
99+
// Add new columns that don't exist yet
100+
for columnName in definedColumns {
101+
let shouldCreateColumn = !existingColumns.contains { $0.name == columnName }
102+
if shouldCreateColumn {
103+
try db.alter(table: tableName) { tableAlteration in
104+
tableAlteration.add(column: columnName)
105+
}
106+
}
107+
}
108+
109+
// Remove columns that are no longer defined
110+
for existingColumn in existingColumns where !definedColumnSet.contains(existingColumn.name) {
111+
try db.alter(table: tableName) { tableAlteration in
112+
tableAlteration.drop(column: existingColumn.name)
113+
}
114+
}
115+
}
116+
}
117+
}

Sources/Shared/Database/Tables/AppEntityRegistryListForDisplayTable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@ import Foundation
22
import GRDB
33

44
final class AppEntityRegistryListForDisplayTable: DatabaseTableProtocol {
5+
var tableName: String { GRDBDatabaseTable.appEntityRegistryListForDisplay.rawValue }
6+
7+
var definedColumns: [String] { DatabaseTables.AppEntityRegistryListForDisplay.allCases.map(\.rawValue) }
8+
59
func createIfNeeded(database: DatabaseQueue) throws {
610
let shouldCreateTable = try database.read { db in
7-
try !db.tableExists(GRDBDatabaseTable.appEntityRegistryListForDisplay.rawValue)
11+
try !db.tableExists(tableName)
812
}
913
if shouldCreateTable {
1014
try database.write { db in
11-
try db.create(table: GRDBDatabaseTable.appEntityRegistryListForDisplay.rawValue) { t in
15+
try db.create(table: tableName) { t in
1216
t.primaryKey(DatabaseTables.AppEntityRegistryListForDisplay.id.rawValue, .text).notNull()
1317
t.column(DatabaseTables.AppEntityRegistryListForDisplay.serverId.rawValue, .text).notNull()
1418
t.column(DatabaseTables.AppEntityRegistryListForDisplay.entityId.rawValue, .text).notNull()
1519
t.column(DatabaseTables.AppEntityRegistryListForDisplay.registry.rawValue, .jsonText).notNull()
1620
}
1721
}
22+
} else {
23+
try migrateColumns(database: database)
1824
}
1925
}
2026
}

Sources/Shared/Database/Tables/AppPanelTable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import Foundation
22
import GRDB
33

44
final class AppPanelTable: DatabaseTableProtocol {
5+
var tableName: String { GRDBDatabaseTable.appPanel.rawValue }
6+
7+
var definedColumns: [String] { DatabaseTables.AppPanel.allCases.map(\.rawValue) }
8+
59
func createIfNeeded(database: DatabaseQueue) throws {
610
let shouldCreateTable = try database.read { db in
7-
try !db.tableExists(GRDBDatabaseTable.appPanel.rawValue)
11+
try !db.tableExists(tableName)
812
}
913
if shouldCreateTable {
1014
try database.write { db in
11-
try db.create(table: GRDBDatabaseTable.appPanel.rawValue) { t in
15+
try db.create(table: tableName) { t in
1216
t.primaryKey(DatabaseTables.AppPanel.id.rawValue, .text).notNull()
1317
t.column(DatabaseTables.AppPanel.serverId.rawValue, .text).notNull()
1418
t.column(DatabaseTables.AppPanel.icon.rawValue, .text)
@@ -18,6 +22,8 @@ final class AppPanelTable: DatabaseTableProtocol {
1822
t.column(DatabaseTables.AppPanel.showInSidebar.rawValue, .boolean).notNull()
1923
}
2024
}
25+
} else {
26+
try migrateColumns(database: database)
2127
}
2228
}
2329
}

Sources/Shared/Database/Tables/AssistConfigurationTable.swift

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,27 @@ import Foundation
22
import GRDB
33

44
struct AssistConfigurationTable: DatabaseTableProtocol {
5+
var tableName: String { GRDBDatabaseTable.assistConfiguration.rawValue }
6+
7+
var definedColumns: [String] { DatabaseTables.AssistConfiguration.allCases.map(\.rawValue) }
8+
59
func createIfNeeded(database: DatabaseQueue) throws {
6-
try database.write { db in
7-
try db.create(table: GRDBDatabaseTable.assistConfiguration.rawValue, ifNotExists: true) { table in
8-
table.primaryKey(DatabaseTables.AssistConfiguration.id.rawValue, .text)
9-
table.column(DatabaseTables.AssistConfiguration.enableOnDeviceSTT.rawValue, .boolean)
10-
table.column(DatabaseTables.AssistConfiguration.enableModernUI.rawValue, .boolean)
11-
table.column(DatabaseTables.AssistConfiguration.theme.rawValue, .text)
12-
table.column(DatabaseTables.AssistConfiguration.muteTTS.rawValue, .boolean)
10+
let shouldCreateTable = try database.read { db in
11+
try !db.tableExists(tableName)
12+
}
13+
14+
if shouldCreateTable {
15+
try database.write { db in
16+
try db.create(table: tableName) { table in
17+
table.primaryKey(DatabaseTables.AssistConfiguration.id.rawValue, .text)
18+
table.column(DatabaseTables.AssistConfiguration.enableOnDeviceSTT.rawValue, .boolean)
19+
table.column(DatabaseTables.AssistConfiguration.enableModernUI.rawValue, .boolean)
20+
table.column(DatabaseTables.AssistConfiguration.theme.rawValue, .text)
21+
table.column(DatabaseTables.AssistConfiguration.muteTTS.rawValue, .boolean)
22+
}
1323
}
24+
} else {
25+
try migrateColumns(database: database)
1426
}
1527
}
1628
}

Sources/Shared/Database/Tables/AssistPipelinesTable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ import Foundation
22
import GRDB
33

44
final class AssistPipelinesTable: DatabaseTableProtocol {
5+
var tableName: String { GRDBDatabaseTable.assistPipelines.rawValue }
6+
7+
var definedColumns: [String] { DatabaseTables.AssistPipelines.allCases.map(\.rawValue) }
8+
59
func createIfNeeded(database: DatabaseQueue) throws {
610
let shouldCreateTable = try database.read { db in
7-
try !db.tableExists(GRDBDatabaseTable.assistPipelines.rawValue)
11+
try !db.tableExists(tableName)
812
}
913
if shouldCreateTable {
1014
try database.write { db in
11-
try db.create(table: GRDBDatabaseTable.assistPipelines.rawValue) { t in
15+
try db.create(table: tableName) { t in
1216
t.primaryKey(DatabaseTables.AssistPipelines.serverId.rawValue, .text).notNull()
1317
t.column(DatabaseTables.AssistPipelines.preferredPipeline.rawValue, .text).notNull()
1418
t.column(DatabaseTables.AssistPipelines.pipelines.rawValue, .jsonText).notNull()
1519
}
1620
}
21+
} else {
22+
try migrateColumns(database: database)
1723
}
1824
}
1925
}

Sources/Shared/Database/Tables/CameraListConfigurationTable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@ import Foundation
22
import GRDB
33

44
final class CameraListConfigurationTable: DatabaseTableProtocol {
5+
var tableName: String { GRDBDatabaseTable.cameraListConfiguration.rawValue }
6+
7+
var definedColumns: [String] { DatabaseTables.CameraListConfiguration.allCases.map(\.rawValue) }
8+
59
func createIfNeeded(database: DatabaseQueue) throws {
610
let shouldCreateTable = try database.read { db in
7-
try !db.tableExists(GRDBDatabaseTable.cameraListConfiguration.rawValue)
11+
try !db.tableExists(tableName)
812
}
913

1014
if shouldCreateTable {
1115
try database.write { db in
12-
try db.create(table: GRDBDatabaseTable.cameraListConfiguration.rawValue) { t in
16+
try db.create(table: tableName) { t in
1317
t.primaryKey(DatabaseTables.CameraListConfiguration.serverId.rawValue, .text).notNull()
1418
t.column(DatabaseTables.CameraListConfiguration.areaOrders.rawValue, .jsonText).notNull()
1519
t.column(DatabaseTables.CameraListConfiguration.sectionOrder.rawValue, .jsonText)
1620
}
1721
}
22+
} else {
23+
try migrateColumns(database: database)
1824
}
1925
}
2026
}

Sources/Shared/Database/Tables/CarPlayConfigTable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ import Foundation
22
import GRDB
33

44
final class CarPlayConfigTable: DatabaseTableProtocol {
5+
var tableName: String { GRDBDatabaseTable.carPlayConfig.rawValue }
6+
7+
var definedColumns: [String] { DatabaseTables.CarPlayConfig.allCases.map(\.rawValue) }
8+
59
func createIfNeeded(database: DatabaseQueue) throws {
610
let shouldCreateTable = try database.read { db in
7-
try !db.tableExists(GRDBDatabaseTable.carPlayConfig.rawValue)
11+
try !db.tableExists(tableName)
812
}
913
if shouldCreateTable {
1014
try database.write { db in
11-
try db.create(table: GRDBDatabaseTable.carPlayConfig.rawValue) { t in
15+
try db.create(table: tableName) { t in
1216
t.primaryKey(DatabaseTables.CarPlayConfig.id.rawValue, .text).notNull()
1317
t.column(DatabaseTables.CarPlayConfig.tabs.rawValue, .text).notNull()
1418
t.column(DatabaseTables.CarPlayConfig.quickAccessItems.rawValue, .jsonText).notNull()
1519
}
1620
}
21+
} else {
22+
try migrateColumns(database: database)
1723
}
1824
}
1925
}

0 commit comments

Comments
 (0)