diff --git a/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift b/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift index 1291985d6..9b4adcf1e 100644 --- a/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift +++ b/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift @@ -9,62 +9,105 @@ import Foundation import RSCore import Database +import FMDB -public struct SyncDatabase: Sendable { +public actor SyncDatabase { - private let syncStatusTable: SyncStatusTable + private var database: FMDatabase? + private var databasePath: String + private let syncStatusTable = SyncStatusTable() - public init(databaseFilePath: String) { + public init(databasePath: String) { - self.syncStatusTable = SyncStatusTable(databasePath: databaseFilePath) + let database = FMDatabase.openAndSetUpDatabase(path: databasePath) + database.runCreateStatements(Self.creationStatements) + database.vacuum() + + self.database = database + self.databasePath = databasePath } // MARK: - API - public func insertStatuses(_ statuses: [SyncStatus]) async throws { - try await syncStatusTable.insertStatuses(statuses) + public func insertStatuses(_ statuses: [SyncStatus]) throws { + + guard let database else { + throw DatabaseError.suspended + } + syncStatusTable.insertStatuses(statuses, database: database) } - public func selectForProcessing(limit: Int? = nil) async throws -> Set? { - try await syncStatusTable.selectForProcessing(limit: limit) + public func selectForProcessing(limit: Int? = nil) throws -> Set? { + + guard let database else { + throw DatabaseError.suspended + } + return syncStatusTable.selectForProcessing(limit: limit, database: database) } - public func selectPendingCount() async throws -> Int? { - try await syncStatusTable.selectPendingCount() + public func selectPendingCount() throws -> Int? { + + guard let database else { + throw DatabaseError.suspended + } + return syncStatusTable.selectPendingCount(database: database) } - public func selectPendingReadStatusArticleIDs() async throws -> Set? { - try await syncStatusTable.selectPendingReadStatusArticleIDs() + public func selectPendingReadStatusArticleIDs() throws -> Set? { + + guard let database else { + throw DatabaseError.suspended + } + return syncStatusTable.selectPendingReadStatusArticleIDs(database: database) } - public func selectPendingStarredStatusArticleIDs() async throws -> Set? { - try await syncStatusTable.selectPendingStarredStatusArticleIDs() + public func selectPendingStarredStatusArticleIDs() throws -> Set? { + + guard let database else { + throw DatabaseError.suspended + } + return syncStatusTable.selectPendingStarredStatusArticleIDs(database: database) } - public func resetAllSelectedForProcessing() async throws { - try await syncStatusTable.resetAllSelectedForProcessing() + public func resetAllSelectedForProcessing() throws { + + guard let database else { + throw DatabaseError.suspended + } + syncStatusTable.resetAllSelectedForProcessing(database: database) } - public func resetSelectedForProcessing(_ articleIDs: [String]) async throws { - try await syncStatusTable.resetSelectedForProcessing(articleIDs) + public func resetSelectedForProcessing(_ articleIDs: [String]) throws { + + guard let database else { + throw DatabaseError.suspended + } + syncStatusTable.resetSelectedForProcessing(articleIDs, database: database) } - public func deleteSelectedForProcessing(_ articleIDs: [String]) async throws { - try await syncStatusTable.deleteSelectedForProcessing(articleIDs) + public func deleteSelectedForProcessing(_ articleIDs: [String]) throws { + + guard let database else { + throw DatabaseError.suspended + } + syncStatusTable.deleteSelectedForProcessing(articleIDs, database: database) } // MARK: - Suspend and Resume (for iOS) - /// Close the database and stop running database calls. - /// - /// On Macs, suspend() and resume() do nothing. They’re not needed. - public func suspend() async { - await syncStatusTable.suspend() + public func suspend() { +#if os(iOS) + database?.close() + database = nil +#endif } - /// Open the database and allow for running database calls again. - public func resume() async { - await syncStatusTable.resume() + func resume() { +#if os(iOS) + if database == nil { + self.database = FMDatabase.openAndSetUpDatabase(path: databasePath) + } +#endif } } @@ -78,9 +121,9 @@ public typealias SyncStatusesCompletionBlock = @Sendable (SyncStatusesResult) -> public typealias SyncStatusArticleIDsResult = Result, DatabaseError> public typealias SyncStatusArticleIDsCompletionBlock = @Sendable (SyncStatusArticleIDsResult) -> Void -extension SyncDatabase { +public extension SyncDatabase { - public func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) { + nonisolated func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) { Task { do { @@ -92,7 +135,7 @@ extension SyncDatabase { } } - public func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) { + nonisolated func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) { Task { do { @@ -107,7 +150,7 @@ extension SyncDatabase { } } - public func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) { + nonisolated func selectPendingCount(completion: @escaping DatabaseIntCompletionBlock) { Task { do { @@ -123,7 +166,7 @@ extension SyncDatabase { } } - public func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { + nonisolated func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { Task { do { @@ -138,7 +181,7 @@ extension SyncDatabase { } } - public func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { + nonisolated func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { Task { do { @@ -153,7 +196,7 @@ extension SyncDatabase { } } - public func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) { + nonisolated func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) { Task { do { @@ -165,7 +208,7 @@ extension SyncDatabase { } } - public func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { + nonisolated func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { Task { do { @@ -177,7 +220,7 @@ extension SyncDatabase { } } - public func deleteSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { + nonisolated func deleteSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { Task { do { @@ -188,23 +231,12 @@ extension SyncDatabase { } } } - - // MARK: - Suspend and Resume (for iOS) - - /// Close the database and stop running database calls. - /// Any pending calls will complete first. - public func suspend() { - - Task { - await self.suspend() - } - } - - /// Open the database and allow for running database calls again. - public func resume() { - - Task { - await self.resume() - } - } +} + +private extension SyncDatabase { + + static let creationStatements = """ + CREATE TABLE if not EXISTS syncStatus (articleID TEXT NOT NULL, key TEXT NOT NULL, flag BOOL NOT NULL DEFAULT 0, selected BOOL NOT NULL DEFAULT 0, PRIMARY KEY (articleID, key)); + """ + } diff --git a/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift b/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift index 10bb06d83..d96e5c99c 100644 --- a/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift +++ b/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift @@ -12,48 +12,11 @@ import Articles import Database import FMDB -actor SyncStatusTable { +struct SyncStatusTable { - static private let tableName = "syncStatus" + static private let name = "syncStatus" - private var database: FMDatabase? - private let databasePath: String - - init(databasePath: String) { - - let database = FMDatabase.openAndSetUpDatabase(path: databasePath) - database.runCreateStatements(SyncStatusTable.creationStatements) - database.vacuum() - - self.database = database - self.databasePath = databasePath - } - - func suspend() { -#if os(iOS) - database?.close() - database = nil -#endif - } - - func resume() { -#if os(iOS) - if database == nil { - self.database = FMDatabase.openAndSetUpDatabase(path: databasePath) - } -#endif - } - - func close() { - - database?.close() - } - - func selectForProcessing(limit: Int?) throws -> Set? { - - guard let database else { - throw DatabaseError.suspended - } + func selectForProcessing(limit: Int?, database: FMDatabase) -> Set? { let updateSQL = "update syncStatus set selected = true" database.executeUpdateInTransaction(updateSQL, withArgumentsIn: nil) @@ -73,11 +36,7 @@ actor SyncStatusTable { return statuses } - func selectPendingCount() throws -> Int? { - - guard let database else { - throw DatabaseError.suspended - } + func selectPendingCount(database: FMDatabase) -> Int? { let sql = "select count(*) from syncStatus" guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { @@ -88,32 +47,27 @@ actor SyncStatusTable { return count } - func selectPendingReadStatusArticleIDs() throws -> Set? { - try selectPendingArticleIDs(.read) + func selectPendingReadStatusArticleIDs(database: FMDatabase) -> Set? { + + selectPendingArticleIDs(.read, database: database) } - func selectPendingStarredStatusArticleIDs() throws -> Set? { - try selectPendingArticleIDs(.starred) + func selectPendingStarredStatusArticleIDs(database: FMDatabase) -> Set? { + + selectPendingArticleIDs(.starred, database: database) } - func resetAllSelectedForProcessing() throws { - - guard let database else { - throw DatabaseError.suspended - } + func resetAllSelectedForProcessing(database: FMDatabase) { let updateSQL = "update syncStatus set selected = false" database.executeUpdateInTransaction(updateSQL, withArgumentsIn: nil) } - func resetSelectedForProcessing(_ articleIDs: [String]) throws { + func resetSelectedForProcessing(_ articleIDs: [String], database: FMDatabase) { guard !articleIDs.isEmpty else { return } - guard let database else { - throw DatabaseError.suspended - } let parameters = articleIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! @@ -122,14 +76,11 @@ actor SyncStatusTable { database.executeUpdateInTransaction(updateSQL, withArgumentsIn: parameters) } - func deleteSelectedForProcessing(_ articleIDs: [String]) throws { + func deleteSelectedForProcessing(_ articleIDs: [String], database: FMDatabase) { guard !articleIDs.isEmpty else { return } - guard let database else { - throw DatabaseError.suspended - } let parameters = articleIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! @@ -138,16 +89,12 @@ actor SyncStatusTable { database.executeUpdateInTransaction(deleteSQL, withArgumentsIn: parameters) } - func insertStatuses(_ statuses: [SyncStatus]) throws { - - guard let database else { - throw DatabaseError.suspended - } + func insertStatuses(_ statuses: [SyncStatus], database: FMDatabase) { database.beginTransaction() let statusArray = statuses.map { $0.databaseDictionary() } - database.insertRows(statusArray, insertType: .orReplace, tableName: Self.tableName) + database.insertRows(statusArray, insertType: .orReplace, tableName: Self.name) database.commit() } @@ -155,10 +102,6 @@ actor SyncStatusTable { private extension SyncStatusTable { - static let creationStatements = """ - CREATE TABLE if not EXISTS syncStatus (articleID TEXT NOT NULL, key TEXT NOT NULL, flag BOOL NOT NULL DEFAULT 0, selected BOOL NOT NULL DEFAULT 0, PRIMARY KEY (articleID, key)); - """ - func statusWithRow(_ row: FMResultSet) -> SyncStatus? { guard let articleID = row.string(forColumn: DatabaseKey.articleID), @@ -173,11 +116,7 @@ private extension SyncStatusTable { return SyncStatus(articleID: articleID, key: key, flag: flag, selected: selected) } - func selectPendingArticleIDs(_ statusKey: ArticleStatus.Key) throws -> Set? { - - guard let database else { - throw DatabaseError.suspended - } + func selectPendingArticleIDs(_ statusKey: ArticleStatus.Key, database: FMDatabase) -> Set? { let sql = "select articleID from syncStatus where selected == false and key = \"\(statusKey.rawValue)\";" guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else {