// // SQLiteDatabase.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 CSQLite import Foundation import LuminateCore /// A synchronous SQLite connection owned by the storage actor. package final class SQLiteDatabase { package let handle: OpaquePointer /// Opens or creates a private SQLite database at `url` and configures local durability. /// /// - Parameter url: The database file location. /// - Throws: ``PreferenceStoreError/cannotOpen(path:message:)`` when SQLite cannot open it. package init(url: URL) throws { let directory = url.deletingLastPathComponent() try FileManager.default.createDirectory( at: directory, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700] ) var opened: OpaquePointer? let result = url.path.withCString { path in sqlite3_open_v2( path, &opened, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, nil ) } guard result == SQLITE_OK, let opened else { let message = opened.map { String(cString: sqlite3_errmsg($0)) } ?? "SQLite returned no handle" if let opened { sqlite3_close_v2(opened) } throw PreferenceStoreError.cannotOpen(path: url.path, message: message) } handle = opened try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path) try exec("PRAGMA journal_mode = WAL;") try exec("PRAGMA synchronous = NORMAL;") try exec("PRAGMA foreign_keys = ON;") // SQLite has no busy handler by default, so concurrent writers would fail immediately. try exec("PRAGMA busy_timeout = 5000;") } deinit { sqlite3_close_v2(handle) } /// The latest SQLite diagnostic for this connection. package var errorMessage: String { String(cString: sqlite3_errmsg(handle)) } /// Executes SQL that does not return rows. /// /// - Parameter sql: The SQL statements to execute. /// - Throws: ``PreferenceStoreError/sqlite(message:)`` when execution fails. package func exec(_ sql: String) throws { var errorPointer: UnsafeMutablePointer? let result = sqlite3_exec(handle, sql, nil, nil, &errorPointer) defer { if let errorPointer { sqlite3_free(errorPointer) } } guard result == SQLITE_OK else { let message = errorPointer.map { String(cString: $0) } ?? errorMessage throw PreferenceStoreError.sqlite(message: message) } } /// Runs a closure inside one SQLite transaction and rolls back failures. /// /// - Parameter body: Statements to execute while the transaction is open. /// - Throws: The first error raised by the body or transaction control statements. package func transaction(_ body: () throws -> Void) throws { try exec("BEGIN;") do { try body() try exec("COMMIT;") } catch { try? exec("ROLLBACK;") throw error } } /// Reads SQLite's schema version pragma. package var userVersion: Int32 { get throws { let statement = try SQLiteStatement(database: self, sql: "PRAGMA user_version;") guard try statement.step() else { throw PreferenceStoreError.sqlite(message: errorMessage) } return statement.integer(at: 0) } } /// Sets SQLite's schema version pragma. /// /// - Parameter version: The non-negative schema version. /// - Throws: ``PreferenceStoreError/sqlite(message:)`` when SQLite rejects the pragma. package func setUserVersion(_ version: Int32) throws { try exec("PRAGMA user_version = \(version);") } /// Reads SQLite's application identifier pragma. package var applicationID: Int32 { get throws { let statement = try SQLiteStatement(database: self, sql: "PRAGMA application_id;") guard try statement.step() else { throw PreferenceStoreError.sqlite(message: errorMessage) } return statement.integer(at: 0) } } /// Sets SQLite's application identifier pragma. /// /// - Parameter applicationID: The application identifier to persist in the file header. /// - Throws: ``PreferenceStoreError/sqlite(message:)`` when SQLite rejects the pragma. package func setApplicationID(_ applicationID: Int32) throws { try exec("PRAGMA application_id = \(applicationID);") } }