From 451f2e0d19ee53da7af5203511027570c02f2d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 13:03:23 +0700 Subject: [PATCH] fix: add PRAGMA user_version schema migration infrastructure to SQLite databases --- CHANGELOG.md | 1 + .../Core/Storage/QueryHistoryStorage.swift | 34 +++++++++++++++++++ .../Core/Storage/SQLFavoriteStorage.swift | 34 +++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14165fc1a..38601ee00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix crash when closing window during SSH tunnel connection (use-after-free in libssh2) - Fix potential deadlock in SSH host key verification prompts (semaphore → async/await) - Fix data race in ConnectionStorage, GroupStorage, and TagStorage (added @MainActor isolation) +- Add schema versioning to SQLite databases (query history, favorites) for future migrations ### Added diff --git a/TablePro/Core/Storage/QueryHistoryStorage.swift b/TablePro/Core/Storage/QueryHistoryStorage.swift index 139b4260a..314811e38 100644 --- a/TablePro/Core/Storage/QueryHistoryStorage.swift +++ b/TablePro/Core/Storage/QueryHistoryStorage.swift @@ -150,8 +150,42 @@ final class QueryHistoryStorage { execute("PRAGMA synchronous=NORMAL;") createTables() + migrateIfNeeded() } + // MARK: - Schema Migration + + private func migrateIfNeeded() { + let currentVersion = getUserVersion() + + // Future migrations go here: + // if currentVersion < 2 { + // execute("ALTER TABLE history ADD COLUMN new_col TEXT;") + // } + + let targetVersion: Int32 = 1 + if currentVersion < targetVersion { + setUserVersion(targetVersion) + } + } + + private func getUserVersion() -> Int32 { + var statement: OpaquePointer? + defer { sqlite3_finalize(statement) } + guard sqlite3_prepare_v2(db, "PRAGMA user_version", -1, &statement, nil) == SQLITE_OK, + sqlite3_step(statement) == SQLITE_ROW + else { + return 0 + } + return sqlite3_column_int(statement, 0) + } + + private func setUserVersion(_ version: Int32) { + execute("PRAGMA user_version = \(version);") + } + + // MARK: - Table Creation + private func createTables() { // History table let historyTable = """ diff --git a/TablePro/Core/Storage/SQLFavoriteStorage.swift b/TablePro/Core/Storage/SQLFavoriteStorage.swift index 174f61231..6becb3ced 100644 --- a/TablePro/Core/Storage/SQLFavoriteStorage.swift +++ b/TablePro/Core/Storage/SQLFavoriteStorage.swift @@ -109,8 +109,42 @@ internal final class SQLFavoriteStorage { execute("PRAGMA synchronous=NORMAL;") createTables() + migrateIfNeeded() } + // MARK: - Schema Migration + + private func migrateIfNeeded() { + let currentVersion = getUserVersion() + + // Future migrations go here: + // if currentVersion < 2 { + // execute("ALTER TABLE favorites ADD COLUMN new_col TEXT;") + // } + + let targetVersion: Int32 = 1 + if currentVersion < targetVersion { + setUserVersion(targetVersion) + } + } + + private func getUserVersion() -> Int32 { + var statement: OpaquePointer? + defer { sqlite3_finalize(statement) } + guard sqlite3_prepare_v2(db, "PRAGMA user_version", -1, &statement, nil) == SQLITE_OK, + sqlite3_step(statement) == SQLITE_ROW + else { + return 0 + } + return sqlite3_column_int(statement, 0) + } + + private func setUserVersion(_ version: Int32) { + execute("PRAGMA user_version = \(version);") + } + + // MARK: - Table Creation + private func createTables() { let favoritesTable = """ CREATE TABLE IF NOT EXISTS favorites (