medmap-ios/medmap/Managers/GRDBDatabaseManager.swift
2026-08-27 23:26:14 +01:00

220 lines
7.5 KiB
Swift

//
// 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<T>(_ updates: (Database) throws -> T) throws -> T {
guard let dbQueue = dbQueue else {
throw DatabaseError.notInitialized
}
return try dbQueue.write(updates)
}
func read<T>(_ 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
}
}