// // SchemaMigrator.swift // // Copyright 2026 Brendan Szymanski // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-or-later // import LuminateCore /// Applies the append-only schema migration history to a Luminate database. package enum SchemaMigrator { /// `PRAGMA application_id` marking a file as Luminate's: ASCII "LUMN" as a big-endian Int32. package static let applicationID: Int32 = 0x4C55_4D4E /// Every migration ever shipped, in ascending version order. package static let migrations: [StoreMigration] = [ StoreMigration( version: 1, name: "create preference table", minimumReaderVersion: 1, statements: [ """ CREATE TABLE schema_metadata ( id INTEGER PRIMARY KEY CHECK (id = 1), minimum_reader_version INTEGER NOT NULL ) STRICT; """, """ CREATE TABLE preference ( key TEXT PRIMARY KEY NOT NULL, value ANY ) STRICT; """, ] ) ] /// The newest schema version shipped by this application. package static var latestVersion: Int32 { migrations.last?.version ?? 0 } /// Migrates `database` and validates its application and downgrade metadata. /// /// - Parameter database: An open SQLite connection. /// - Throws: A ``PreferenceStoreError`` when the file belongs to another application or requires /// a newer reader. package static func migrate( _ database: SQLiteDatabase, migrations: [StoreMigration] = SchemaMigrator.migrations ) throws { var previous: Int32 = 0 for migration in migrations { guard migration.version > previous else { throw PreferenceStoreError.malformedMigrationList(version: migration.version) } previous = migration.version } let latest = migrations.last?.version ?? 0 let applicationID = try database.applicationID guard applicationID == 0 || applicationID == Self.applicationID else { throw PreferenceStoreError.notALuminateDatabase(applicationID: applicationID) } let current = try database.userVersion if current > 0 { let minimumReaderVersion = try readMinimumReaderVersion(from: database) guard minimumReaderVersion <= latest else { throw PreferenceStoreError.requiresNewerApplication( minimumReaderVersion: minimumReaderVersion, supportedVersion: latest ) } } for migration in migrations where migration.version > current { try database.transaction { for statement in migration.statements { try database.exec(statement) } try database.exec( "INSERT INTO schema_metadata (id, minimum_reader_version) VALUES (1, " + "\(migration.minimumReaderVersion)) ON CONFLICT(id) DO UPDATE SET " + "minimum_reader_version = excluded.minimum_reader_version;" ) try database.setUserVersion(migration.version) } } if applicationID == 0 { try database.setApplicationID(Self.applicationID) } } private static func readMinimumReaderVersion(from database: SQLiteDatabase) throws -> Int32 { let statement = try SQLiteStatement( database: database, sql: "SELECT minimum_reader_version FROM schema_metadata WHERE id = 1;" ) guard try statement.step() else { throw PreferenceStoreError.sqlite(message: database.errorMessage) } return statement.integer(at: 0) } }