// // SchemaMigratorTests.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 Foundation import Testing @testable import LuminateCore @testable import LuminateStore @Suite struct SchemaMigratorTests { @Test("Migrating a current database is a no-op") func currentDatabase() throws { let (url, directory) = makeDatabaseURL() defer { try? FileManager.default.removeItem(at: directory) } let database = try SQLiteDatabase(url: url) try SchemaMigrator.migrate(database) let snapshot = try SQLitePreferenceStore(url: url).initialSnapshot try SchemaMigrator.migrate(database) #expect(try database.userVersion == SchemaMigrator.latestVersion) #expect(try SQLitePreferenceStore(url: url).initialSnapshot == snapshot) } @Test("A newer minimum reader version rejects the database") func newerReaderVersion() throws { let (url, directory) = makeDatabaseURL() defer { try? FileManager.default.removeItem(at: directory) } let database = try SQLiteDatabase(url: url) try SchemaMigrator.migrate(database) try database.exec( "UPDATE schema_metadata SET minimum_reader_version = \(SchemaMigrator.latestVersion + 1);" ) do { _ = try SQLitePreferenceStore(url: url) Issue.record("Expected a newer-reader migration error") } catch let error as PreferenceStoreError { #expect( error == .requiresNewerApplication( minimumReaderVersion: SchemaMigrator.latestVersion + 1, supportedVersion: SchemaMigrator.latestVersion ) ) } } @Test("A non-Luminate application identifier is rejected") func foreignApplication() throws { let (url, directory) = makeDatabaseURL() defer { try? FileManager.default.removeItem(at: directory) } let database = try SQLiteDatabase(url: url) try database.setApplicationID(1234) do { try SchemaMigrator.migrate(database) Issue.record("Expected a foreign-database error") } catch let error as PreferenceStoreError { #expect(error == .notALuminateDatabase(applicationID: 1234)) } } @Test("A higher compatible schema version remains readable") func compatibleHigherVersion() throws { let (url, directory) = makeDatabaseURL() defer { try? FileManager.default.removeItem(at: directory) } let database = try SQLiteDatabase(url: url) try SchemaMigrator.migrate(database) try database.setUserVersion(SchemaMigrator.latestVersion + 1) let statement = try SQLiteStatement( database: database, sql: "INSERT INTO preference (key, value) VALUES (?1, ?2);" ) try statement.bind(1, text: "server.url") try statement.bind(2, .text("http://compatible")) _ = try statement.step() let store = try SQLitePreferenceStore(url: url) #expect(store.initialSnapshot["server.url"] == .text("http://compatible")) } @Test("A failed migration rolls back its statements and version") func failedMigrationRollsBack() throws { let (url, directory) = makeDatabaseURL() defer { try? FileManager.default.removeItem(at: directory) } let database = try SQLiteDatabase(url: url) let failingMigration = StoreMigration( version: 2, name: "failing migration", minimumReaderVersion: 1, statements: [ "CREATE TABLE version_two_marker (id INTEGER);", "CREATE TABLE preference (x);", ] ) #expect(throws: PreferenceStoreError.self) { try SchemaMigrator.migrate(database, migrations: SchemaMigrator.migrations + [failingMigration]) } #expect(try database.userVersion == 1) #expect(try tableExists("version_two_marker", in: database) == false) } @Test("Migration versions must be strictly increasing") func rejectsMalformedMigrationOrder() throws { let (url, directory) = makeDatabaseURL() defer { try? FileManager.default.removeItem(at: directory) } let database = try SQLiteDatabase(url: url) let migrations = [ StoreMigration(version: 2, name: "second", minimumReaderVersion: 1, statements: []), StoreMigration(version: 1, name: "first", minimumReaderVersion: 1, statements: []), ] #expect(throws: PreferenceStoreError.malformedMigrationList(version: 1)) { try SchemaMigrator.migrate(database, migrations: migrations) } } /// Checks whether SQLite exposes a table with the requested name. private func tableExists(_ name: String, in database: SQLiteDatabase) throws -> Bool { let statement = try SQLiteStatement( database: database, sql: "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?1;" ) try statement.bind(1, text: name) return try statement.step() } private func makeDatabaseURL() -> (URL, URL) { let directory = FileManager.default.temporaryDirectory .appending(path: "luminate-schema-\(UUID().uuidString)", directoryHint: .isDirectory) return (directory.appending(path: "preferences.sqlite"), directory) } }