// // GRDBDatabaseManager.swift // FluxRead // // Created on 16/12/2025. // import Foundation import GRDB class GRDBDatabaseManager { private var dbQueue: DatabaseQueue? init() { do { try setupDatabase() } catch { print("❌ Failed to setup GRDB database: \(error)") } } // MARK: - Database Setup private func setupDatabase() throws { let fileManager = FileManager.default let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] let dbPath = documentsPath.appendingPathComponent("fluxread_grdb.sqlite").path // Create database queue dbQueue = try DatabaseQueue(path: dbPath) // Create tables try dbQueue?.write { db in try createTables(db) } } private func createTables(_ db: Database) throws { /* try db.create(table: GRDBComicBookFile.databaseTableName, ifNotExists: true) { t in t.column("fileHash", .text).primaryKey() t.column("name", .text).notNull() t.column("fileSize", .integer).notNull() t.column("dateAdded", .datetime).notNull() t.column("filePath", .text).notNull() t.column("numberOfPages", .integer).notNull() t.column("pageCount", .integer) t.column("lastReadDate", .datetime).indexed() t.column("issueDate", .datetime) t.column("isRead", .boolean).notNull().defaults(to: false).indexed() t.column("readCompletion", .integer).notNull().defaults(to: 0).indexed() t.column("syncDate", .datetime).indexed() t.column("estimatedProcessingDate", .datetime) t.column("providerId", .text) t.column("providerPath", .text) t.column("isAvailableLocally", .boolean).indexed() t.column("parentFileHash", .text).indexed() } try db.create(table: GRDBComicBookPage.databaseTableName, ifNotExists: true) { t in t.column("comicBookId", .text).notNull() .indexed() t.column("fileHash", .text).notNull() t.column("pageNumber", .integer).notNull().indexed() t.column("syncDate", .datetime).indexed() t.primaryKey(["comicBookId", "fileHash"]) } try db.create(table: GRDBComicBookPagePanel.databaseTableName, ifNotExists: true) { t in t.column("comicBookId", .text).notNull() .indexed() t.column("comicBookPageId", .text).notNull() .indexed() t.column("panelNumberInPage", .integer).notNull() t.column("pageNumber", .integer).notNull() t.column("left", .integer).notNull() t.column("top", .integer).notNull() t.column("right", .integer).notNull() t.column("bottom", .integer).notNull() t.column("syncDate", .datetime).indexed() t.primaryKey(["comicBookId", "comicBookPageId", "panelNumberInPage", "pageNumber"]) } try db.create(table: GRDBComicBookTitle.databaseTableName, ifNotExists: true) { t in t.column("id", .text).primaryKey() t.column("name", .text).notNull() t.column("age", .integer).notNull() } try db.create(table: GRDBComicBookPageReadingStat.databaseTableName, ifNotExists: true) { t in t.column("comicBookId", .text).notNull().indexed() t.column("comicBookPageId", .text).notNull().indexed() t.column("comicBookPageNumber", .integer).notNull().indexed() t.column("startDate", .datetime).notNull() t.column("endDate", .datetime).notNull() t.column("numberOfSeconds", .integer).notNull() t.column("syncDate", .datetime).indexed() t.primaryKey(["comicBookId", "comicBookPageId", "comicBookPageNumber", "startDate"]) } try db.create(table: LibraryImportProvider.databaseTableName, ifNotExists: true) { t in t.column("providerId", .text).primaryKey() t.column("connectionName", .text).notNull() t.column("providerType", .integer).notNull() t.column("hostName", .text).notNull() t.column("port", .integer).notNull() t.column("username", .text).notNull() t.column("path", .text).notNull() t.column("authenticationType", .integer).notNull() } try db.create(table: ComicBookMetaDataAttribute.databaseTableName, ifNotExists: true) { t in t.column("attributeId", .integer).notNull() t.column("attributeValue", .text).notNull() t.column("attributeType", .integer).notNull() } try db.create(table: ComicBookMetaDataAttributeRelation.databaseTableName, ifNotExists: true) { t in t.column("attributeId", .integer).notNull() t.column("comicBookId", .text).notNull() t.primaryKey(["attributeId", "comicBookId"]) } try db.create(table: ComicBookMetadata.databaseTableName, ifNotExists: true) { t in t.column("comicBookId", .text).primaryKey() t.column("creationDate", .datetime).notNull() t.column("metadata", .text).notNull() } try db.create(table: GRDBComicBookPagePanelText.databaseTableName, ifNotExists: true) { t in t.column("comicBookId", .text).notNull() t.column("comicBookPageId", .integer).notNull() t.column("textBlockNumber", .integer).notNull() t.column("textBlock", .text).notNull() t.column("left", .double).notNull() t.column("top", .double).notNull() t.column("right", .double).notNull() t.column("bottom", .double).notNull() t.primaryKey(["comicBookId", "comicBookPageId", "textBlockNumber"]) } try db.create(table: UserActivity.databaseTableName, ifNotExists: true) { t in t.column("activityId", .integer).primaryKey() t.column("activityType", .integer).notNull() t.column("creationDate", .datetime).notNull() t.column("activityDescription", .text) t.column("comicId", .integer) t.column("activityDetail", .text) } try db.create(table: SavedSearch.databaseTableName, ifNotExists: true) { t in t.column("id", .text).primaryKey() t.column("query", .text).notNull() t.column("attributeId", .integer).notNull().indexed() } try db.create(table: SavedSearchComic.databaseTableName, ifNotExists: true) { t in t.column("id", .text).primaryKey() t.column("savedSearchId", .text).notNull().indexed() t.column("comicId", .text).notNull() } */ } // MARK: - Public Access func getDatabase() -> DatabaseQueue? { return dbQueue } func write(_ updates: (Database) throws -> T) throws -> T { guard let dbQueue = dbQueue else { throw DatabaseError.notInitialized } return try dbQueue.write(updates) } func read(_ value: (Database) throws -> T) throws -> T { guard let dbQueue = dbQueue else { throw DatabaseError.notInitialized } return try dbQueue.read(value) } // MARK: - Error Types enum DatabaseError: Error { case notInitialized case recordNotFound case insertFailed case updateFailed case deleteFailed } }