diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index a2add367d..623c65775 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -17,17 +17,40 @@ import Articles // Main thread only. +/// Sync methods may throw this error. Async methods use a result type which will include +/// this error when the database is suspended and therefore not available. +public enum ArticlesDatabaseError: Error { + case databaseIsSuspended +} + public typealias UnreadCountDictionary = [String: Int] // webFeedID: unreadCount -public typealias UnreadCountCompletionBlock = (UnreadCountDictionary) -> Void -public typealias UpdateArticlesCompletionBlock = (Set
?, Set
?) -> Void //newArticles, updatedArticles +public typealias UnreadCountDictionaryCompletionResult = Result +public typealias UnreadCountDictionaryCompletionBlock = (UnreadCountDictionaryCompletionResult) -> Void + +public typealias SingleUnreadCountResult = Result +public typealias SingleUnreadCountCompletionBlock = (SingleUnreadCountResult) -> Void + +public struct NewAndUpdatedArticles { + let newArticles: Set
? + let updatedArticles: Set
? +} + +public typealias UpdateArticlesResult = Result +public typealias UpdateArticlesCompletionBlock = (UpdateArticlesResult) -> Void + +public typealias ArticleSetResult = Result, ArticlesDatabaseError> +public typealias ArticleSetResultBlock = (ArticleSetResult) -> Void + +public typealias DatabaseCompletionBlock = (ArticlesDatabaseError?) -> Void + +public typealias ArticleIDsResult = Result, ArticlesDatabaseError> +public typealias ArticleIDsCompletionBlock = (ArticleIDsResult) -> Void + +public typealias ArticleStatusesResult = Result, ArticlesDatabaseError> +public typealias ArticleStatusesResultBlock = (ArticleStatusesResult) -> Void public final class ArticlesDatabase { - /// When ArticlesDatabase is suspended, database calls will crash the app. - public var isSuspended: Bool { - return queue.isSuspended - } - private let articlesTable: ArticlesTable private let queue: DatabaseQueue @@ -54,94 +77,94 @@ public final class ArticlesDatabase { // MARK: - Fetching Articles - public func fetchArticles(_ webFeedID: String) -> Set
{ - return articlesTable.fetchArticles(webFeedID) + public func fetchArticles(_ webFeedID: String) throws -> Set
{ + return try articlesTable.fetchArticles(webFeedID) } - public func fetchArticles(_ webFeedIDs: Set) -> Set
{ - return articlesTable.fetchArticles(webFeedIDs) + public func fetchArticles(_ webFeedIDs: Set) throws -> Set
{ + return try articlesTable.fetchArticles(webFeedIDs) } - public func fetchArticles(articleIDs: Set) -> Set
{ - return articlesTable.fetchArticles(articleIDs: articleIDs) + public func fetchArticles(articleIDs: Set) throws -> Set
{ + return try articlesTable.fetchArticles(articleIDs: articleIDs) } - public func fetchUnreadArticles(_ webFeedIDs: Set) -> Set
{ - return articlesTable.fetchUnreadArticles(webFeedIDs) + public func fetchUnreadArticles(_ webFeedIDs: Set) throws -> Set
{ + return try articlesTable.fetchUnreadArticles(webFeedIDs) } - public func fetchTodayArticles(_ webFeedIDs: Set) -> Set
{ - return articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate()) + public func fetchTodayArticles(_ webFeedIDs: Set) throws -> Set
{ + return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate()) } - public func fetchStarredArticles(_ webFeedIDs: Set) -> Set
{ - return articlesTable.fetchStarredArticles(webFeedIDs) + public func fetchStarredArticles(_ webFeedIDs: Set) throws -> Set
{ + return try articlesTable.fetchStarredArticles(webFeedIDs) } - public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set) -> Set
{ - return articlesTable.fetchArticlesMatching(searchString, webFeedIDs) + public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set) throws -> Set
{ + return try articlesTable.fetchArticlesMatching(searchString, webFeedIDs) } - public func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set) -> Set
{ - return articlesTable.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs) + public func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set) throws -> Set
{ + return try articlesTable.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs) } // MARK: - Fetching Articles Async - public func fetchArticlesAsync(_ webFeedID: String, _ completion: @escaping ArticleSetBlock) { + public func fetchArticlesAsync(_ webFeedID: String, _ completion: @escaping ArticleSetResultBlock) { articlesTable.fetchArticlesAsync(webFeedID, completion) } - public func fetchArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + public func fetchArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { articlesTable.fetchArticlesAsync(webFeedIDs, completion) } - public func fetchArticlesAsync(articleIDs: Set, _ completion: @escaping ArticleSetBlock) { + public func fetchArticlesAsync(articleIDs: Set, _ completion: @escaping ArticleSetResultBlock) { articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion) } - public func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + public func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { articlesTable.fetchUnreadArticlesAsync(webFeedIDs, completion) } - public func fetchTodayArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + public func fetchTodayArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), completion) } - public func fetchedStarredArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + public func fetchedStarredArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { articlesTable.fetchStarredArticlesAsync(webFeedIDs, completion) } - public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { articlesTable.fetchArticlesMatchingAsync(searchString, webFeedIDs, completion) } - public func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set, _ completion: @escaping ArticleSetBlock) { + public func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set, _ completion: @escaping ArticleSetResultBlock) { articlesTable.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, completion) } // MARK: - Unread Counts - public func fetchUnreadCounts(for webFeedIDs: Set, _ completion: @escaping UnreadCountCompletionBlock) { + public func fetchUnreadCounts(for webFeedIDs: Set, _ completion: @escaping UnreadCountDictionaryCompletionBlock) { articlesTable.fetchUnreadCounts(webFeedIDs, completion) } - public func fetchUnreadCountForToday(for webFeedIDs: Set, completion: @escaping (Int) -> Void) { + public func fetchAllNonZeroUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) { + articlesTable.fetchAllUnreadCounts(completion) + } + + public func fetchUnreadCountForToday(for webFeedIDs: Set, completion: @escaping SingleUnreadCountCompletionBlock) { fetchUnreadCount(for: webFeedIDs, since: todayCutoffDate(), completion: completion) } - public func fetchUnreadCount(for webFeedIDs: Set, since: Date, completion: @escaping (Int) -> Void) { + public func fetchUnreadCount(for webFeedIDs: Set, since: Date, completion: @escaping SingleUnreadCountCompletionBlock) { articlesTable.fetchUnreadCount(webFeedIDs, since, completion) } - public func fetchStarredAndUnreadCount(for webFeedIDs: Set, completion: @escaping (Int) -> Void) { + public func fetchStarredAndUnreadCount(for webFeedIDs: Set, completion: @escaping SingleUnreadCountCompletionBlock) { articlesTable.fetchStarredAndUnreadCount(webFeedIDs, completion) } - public func fetchAllNonZeroUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) { - articlesTable.fetchAllUnreadCounts(completion) - } - // MARK: - Saving and Updating Articles /// Update articles and save new ones. The key for ewbFeedIDsAndItems is webFeedID. @@ -149,31 +172,31 @@ public final class ArticlesDatabase { articlesTable.update(webFeedIDsAndItems, defaultRead, completion) } - public func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: VoidCompletionBlock? = nil) { + public func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: DatabaseCompletionBlock? = nil) { articlesTable.ensureStatuses(articleIDs, defaultRead, statusKey, flag, completion: completion) } // MARK: - Status /// Fetch the articleIDs of unread articles in feeds specified by webFeedIDs. - public func fetchUnreadArticleIDsAsync(webFeedIDs: Set, completion: @escaping (Set) -> Void) { + public func fetchUnreadArticleIDsAsync(webFeedIDs: Set, completion: @escaping ArticleIDsCompletionBlock) { articlesTable.fetchUnreadArticleIDsAsync(webFeedIDs, completion) } /// Fetch the articleIDs of starred articles in feeds specified by webFeedIDs. - public func fetchStarredArticleIDsAsync(webFeedIDs: Set, completion: @escaping (Set) -> Void) { + public func fetchStarredArticleIDsAsync(webFeedIDs: Set, completion: @escaping ArticleIDsCompletionBlock) { articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion) } - public func fetchArticleIDsForStatusesWithoutArticles() -> Set { - return articlesTable.fetchArticleIDsForStatusesWithoutArticles() + public func fetchArticleIDsForStatusesWithoutArticles() throws -> Set { + return try articlesTable.fetchArticleIDsForStatusesWithoutArticles() } - public func mark(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set? { - return articlesTable.mark(articles, statusKey, flag) + public func mark(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) throws -> Set? { + return try articlesTable.mark(articles, statusKey, flag) } - public func fetchStatuses(articleIDs: Set, createIfNeeded: Bool, completion: @escaping (Set?) -> Void) { + public func fetchStatuses(articleIDs: Set, createIfNeeded: Bool, completion: @escaping ArticleStatusesResultBlock) { articlesTable.fetchStatuses(articleIDs, createIfNeeded, completion) } @@ -208,6 +231,13 @@ public final class ArticlesDatabase { } } +func databaseError(with databaseQueueError: DatabaseQueueError) -> ArticlesDatabaseError { + switch databaseQueueError { + case .isSuspended: + return .databaseIsSuspended + } +} + // MARK: - Private private extension ArticlesDatabase { diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 5e661f240..d552d750d 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -43,167 +43,101 @@ final class ArticlesTable: DatabaseTable { // MARK: - Fetching Articles for Feed - func fetchArticles(_ webFeedID: String) -> Set
{ - return fetchArticles{ self.fetchArticlesForFeedID(webFeedID, withLimits: true, $0) } + func fetchArticles(_ webFeedID: String) throws -> Set
{ + return try fetchArticles{ self.fetchArticlesForFeedID(webFeedID, withLimits: true, $0) } } - func fetchArticlesAsync(_ webFeedID: String, _ completion: @escaping ArticleSetBlock) { + func fetchArticlesAsync(_ webFeedID: String, _ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync({ self.fetchArticlesForFeedID(webFeedID, withLimits: true, $0) }, completion) } - private func fetchArticlesForFeedID(_ webFeedID: String, withLimits: Bool, _ database: FMDatabase) -> Set
{ - return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits) + func fetchArticles(_ webFeedIDs: Set) throws -> Set
{ + return try fetchArticles{ self.fetchArticles(webFeedIDs, $0) } } - func fetchArticles(_ webFeedIDs: Set) -> Set
{ - return fetchArticles{ self.fetchArticles(webFeedIDs, $0) } - } - - func fetchArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + func fetchArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync({ self.fetchArticles(webFeedIDs, $0) }, completion) } - private func fetchArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ - // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 - if webFeedIDs.isEmpty { - return Set
() - } - let parameters = webFeedIDs.map { $0 as AnyObject } - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders)" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true) - } - // MARK: - Fetching Articles by articleID - func fetchArticles(articleIDs: Set) -> Set
{ - return fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) } + func fetchArticles(articleIDs: Set) throws -> Set
{ + return try fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) } } - func fetchArticlesAsync(articleIDs: Set, _ completion: @escaping ArticleSetBlock) { + func fetchArticlesAsync(articleIDs: Set, _ completion: @escaping ArticleSetResultBlock) { return fetchArticlesAsync({ self.fetchArticles(articleIDs: articleIDs, $0) }, completion) } - private func fetchArticles(articleIDs: Set, _ database: FMDatabase) -> Set
{ - if articleIDs.isEmpty { - return Set
() - } - let parameters = articleIDs.map { $0 as AnyObject } - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! - let whereClause = "articleID in \(placeholders)" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) - } - // MARK: - Fetching Unread Articles - func fetchUnreadArticles(_ webFeedIDs: Set) -> Set
{ - return fetchArticles{ self.fetchUnreadArticles(webFeedIDs, $0) } + func fetchUnreadArticles(_ webFeedIDs: Set) throws -> Set
{ + return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, $0) } } - func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, $0) }, completion) } - private func fetchUnreadArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ - // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 - if webFeedIDs.isEmpty { - return Set
() - } - let parameters = webFeedIDs.map { $0 as AnyObject } - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and read=0" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true) - } - // MARK: - Fetching Today Articles - func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date) -> Set
{ - return fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) } + func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date) throws -> Set
{ + return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) } } - func fetchArticlesSinceAsync(_ webFeedIDs: Set, _ cutoffDate: Date, _ completion: @escaping ArticleSetBlock) { + func fetchArticlesSinceAsync(_ webFeedIDs: Set, _ cutoffDate: Date, _ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }, completion) } - private func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ database: FMDatabase) -> Set
{ - // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?) - // - // datePublished may be nil, so we fall back to dateArrived. - if webFeedIDs.isEmpty { - return Set
() - } - let parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject] - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and userDeleted = 0" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) - } - // MARK: - Fetching Starred Articles - func fetchStarredArticles(_ webFeedIDs: Set) -> Set
{ - return fetchArticles{ self.fetchStarredArticles(webFeedIDs, $0) } + func fetchStarredArticles(_ webFeedIDs: Set) throws -> Set
{ + return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, $0) } } - func fetchStarredArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + func fetchStarredArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, $0) }, completion) } - private func fetchStarredArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ - // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred = 1 and userDeleted = 0; - if webFeedIDs.isEmpty { - return Set
() - } - let parameters = webFeedIDs.map { $0 as AnyObject } - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and starred = 1 and userDeleted = 0" - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) - } - // MARK: - Fetching Search Articles - func fetchArticlesMatching(_ searchString: String) -> Set
{ + func fetchArticlesMatching(_ searchString: String) throws -> Set
{ var articles: Set
= Set
() - guard !queue.isSuspended else { - return articles + var error: ArticlesDatabaseError? = nil + queue.runInDatabaseSync { (databaseResult) in + switch databaseResult { + case .success(let database): + articles = self.fetchArticlesMatching(searchString, database) + case .failure(let databaseQueueError): + error = databaseError(with: databaseQueueError) + } } - queue.runInDatabaseSync { (database) in - articles = self.fetchArticlesMatching(searchString, database) + if let error = error { + throw(error) } return articles } - func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set) -> Set
{ - var articles = fetchArticlesMatching(searchString) + func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set) throws -> Set
{ + var articles = try fetchArticlesMatching(searchString) articles = articles.filter{ webFeedIDs.contains($0.webFeedID) } return articles } - func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set) -> Set
{ - var articles = fetchArticlesMatching(searchString) + func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set) throws -> Set
{ + var articles = try fetchArticlesMatching(searchString) articles = articles.filter{ articleIDs.contains($0.articleID) } return articles } - func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set, _ completion: @escaping ArticleSetBlock) { + func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync({ self.fetchArticlesMatching(searchString, webFeedIDs, $0) }, completion) } - func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set, _ completion: @escaping ArticleSetBlock) { + func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set, _ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync({ self.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs, $0) }, completion) } - private func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ - let articles = fetchArticlesMatching(searchString, database) - // TODO: include the feedIDs in the SQL rather than filtering here. - return articles.filter{ webFeedIDs.contains($0.webFeedID) } - } - - private func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set, _ database: FMDatabase) -> Set
{ - let articles = fetchArticlesMatching(searchString, database) - // TODO: include the articleIDs in the SQL rather than filtering here. - return articles.filter{ articleIDs.contains($0.articleID) } - } - // MARK: - Fetching Articles for Indexer func fetchArticleSearchInfos(_ articleIDs: Set, in database: FMDatabase) -> Set? { @@ -235,7 +169,7 @@ final class ArticlesTable: DatabaseTable { func update(_ webFeedIDsAndItems: [String: Set], _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) { if webFeedIDsAndItems.isEmpty { - completion(nil, nil) + callUpdateArticlesCompletionBlock(nil, nil, completion) return } @@ -253,12 +187,14 @@ final class ArticlesTable: DatabaseTable { articleIDs.formUnion(parsedItems.articleIDs()) } - guard !self.queue.isSuspended else { - self.callUpdateArticlesCompletionBlock(nil, nil, completion) - return - } - - self.queue.runInTransaction { (database) in + self.queue.runInTransaction { (databaseResult) in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion(.failure(databaseError(with: databaseResult.error!))) + } + return + } + let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1 assert(statusesDictionary.count == articleIDs.count) @@ -293,31 +229,35 @@ final class ArticlesTable: DatabaseTable { } } - func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: VoidCompletionBlock? = nil) { - guard !queue.isSuspended else { - if let handler = completion { - callVoidCompletionBlock(handler) + func ensureStatuses(_ articleIDs: Set, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: DatabaseCompletionBlock? = nil) { + queue.runInTransaction { databaseResult in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion?(databaseError(with: databaseResult.error!)) + } + return } - return - } - - queue.runInTransaction { (database) in + let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, defaultRead, database) let statuses = Set(statusesDictionary.values) self.statusesTable.mark(statuses, statusKey, flag, database) - if let handler = completion { - callVoidCompletionBlock(handler) + if let completion = completion { + DispatchQueue.main.async { + completion(nil) + } } } } - func fetchStatuses(_ articleIDs: Set, _ createIfNeeded: Bool, _ completion: @escaping (Set?) -> Void) { - guard !queue.isSuspended else { - completion(nil) - return - } + func fetchStatuses(_ articleIDs: Set, _ createIfNeeded: Bool, _ completion: @escaping ArticleStatusesResultBlock) { + queue.runInTransaction { databaseResult in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion(.failure(databaseError(with: databaseResult.error!))) + } + return + } - queue.runInTransaction { (database) in var statusesDictionary = [String: ArticleStatus]() if createIfNeeded { statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) @@ -327,45 +267,53 @@ final class ArticlesTable: DatabaseTable { } let statuses = Set(statusesDictionary.values) DispatchQueue.main.async { - completion(statuses) + completion(.success(statuses)) } } } // MARK: - Unread Counts - func fetchUnreadCounts(_ webFeedIDs: Set, _ completion: @escaping UnreadCountCompletionBlock) { + func fetchUnreadCounts(_ webFeedIDs: Set, _ completion: @escaping UnreadCountDictionaryCompletionBlock) { if webFeedIDs.isEmpty { - completion(UnreadCountDictionary()) + completion(.success(UnreadCountDictionary())) return } - var unreadCountDictionary = UnreadCountDictionary() - guard !queue.isSuspended else { - completion(unreadCountDictionary) - return - } - - queue.runInDatabase { (database) in + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion(.failure(databaseError(with: databaseResult.error!))) + } + return + } + + var unreadCountDictionary = UnreadCountDictionary() for webFeedID in webFeedIDs { unreadCountDictionary[webFeedID] = self.fetchUnreadCount(webFeedID, database) } DispatchQueue.main.async { - completion(unreadCountDictionary) + completion(.success(unreadCountDictionary)) } } } - func fetchUnreadCount(_ webFeedIDs: Set, _ since: Date, _ completion: @escaping (Int) -> Void) { + func fetchUnreadCount(_ webFeedIDs: Set, _ since: Date, _ completion: @escaping SingleUnreadCountCompletionBlock) { // Get unread count for today, for instance. - - if webFeedIDs.isEmpty || queue.isSuspended { - completion(0) + if webFeedIDs.isEmpty { + completion(.success(0)) return } - queue.runInDatabase { (database) in + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion(.failure(databaseError(with: databaseResult.error!))) + } + return + } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and read=0 and userDeleted=0;" @@ -377,27 +325,28 @@ final class ArticlesTable: DatabaseTable { let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database) DispatchQueue.main.async { - completion(unreadCount) + completion(.success(unreadCount)) } } } - func fetchAllUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) { + func fetchAllUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) { // Returns only where unreadCount > 0. let cutoffDate = articleCutoffDate + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion(.failure(databaseError(with: databaseResult.error!))) + } + return + } - guard !queue.isSuspended else { - completion(UnreadCountDictionary()) - return - } - - queue.runInDatabase { (database) in let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and userDeleted=0 and (starred=1 or (datePublished > ? or (datePublished is null and dateArrived > ?))) group by feedID;" guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate, cutoffDate]) else { DispatchQueue.main.async { - completion(UnreadCountDictionary()) + completion(.success(UnreadCountDictionary())) } return } @@ -411,18 +360,25 @@ final class ArticlesTable: DatabaseTable { } DispatchQueue.main.async { - completion(d) + completion(.success(d)) } } } - func fetchStarredAndUnreadCount(_ webFeedIDs: Set, _ completion: @escaping (Int) -> Void) { - if webFeedIDs.isEmpty || queue.isSuspended { - completion(0) + func fetchStarredAndUnreadCount(_ webFeedIDs: Set, _ completion: @escaping SingleUnreadCountCompletionBlock) { + if webFeedIDs.isEmpty { + completion(.success(0)) return } - queue.runInDatabase { (database) in + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion(.failure(databaseError(with: databaseResult.error!))) + } + return + } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1 and userDeleted=0;" let parameters = Array(webFeedIDs) as [Any] @@ -430,50 +386,54 @@ final class ArticlesTable: DatabaseTable { let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database) DispatchQueue.main.async { - completion(unreadCount) + completion(.success(unreadCount)) } } } // MARK: - Statuses - func fetchUnreadArticleIDsAsync(_ webFeedIDs: Set, _ completion: @escaping (Set) -> Void) { + func fetchUnreadArticleIDsAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { fetchArticleIDsAsync(.read, false, webFeedIDs, completion) } - func fetchStarredArticleIDsAsync(_ webFeedIDs: Set, _ completion: @escaping (Set) -> Void) { + func fetchStarredArticleIDsAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { fetchArticleIDsAsync(.starred, true, webFeedIDs, completion) } - func fetchStarredArticleIDs() -> Set { - return statusesTable.fetchStarredArticleIDs() + func fetchStarredArticleIDs() throws -> Set { + return try statusesTable.fetchStarredArticleIDs() } - func fetchArticleIDsForStatusesWithoutArticles() -> Set { - return statusesTable.fetchArticleIDsForStatusesWithoutArticles() + func fetchArticleIDsForStatusesWithoutArticles() throws -> Set { + return try statusesTable.fetchArticleIDsForStatusesWithoutArticles() } - func mark(_ articles: Set
, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set? { + func mark(_ articles: Set
, _ statusKey: ArticleStatus.Key, _ flag: Bool) throws -> Set? { var statuses: Set? - - guard !self.queue.isSuspended else { - return statuses + var error: ArticlesDatabaseError? + self.queue.runInTransactionSync { databaseResult in + switch databaseResult { + case .success(let database): + statuses = self.statusesTable.mark(articles.statuses(), statusKey, flag, database) + case .failure(let databaseQueueError): + error = databaseError(with: databaseQueueError) + } } - - self.queue.runInTransactionSync { (database) in - statuses = self.statusesTable.mark(articles.statuses(), statusKey, flag, database) + + if let error = error { + throw error } - return statuses } // MARK: - Indexing func indexUnindexedArticles() { - guard !queue.isSuspended else { - return - } - queue.runInDatabase { (database) in + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + return + } let sql = "select articleID from articles where searchRowID is null limit 500;" guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { return @@ -504,10 +464,14 @@ final class ArticlesTable: DatabaseTable { /// This deletes from the articles and articleStatuses tables, /// and, via a trigger, it also deletes from the search index. func deleteArticlesNotInSubscribedToFeedIDs(_ webFeedIDs: Set) { - if webFeedIDs.isEmpty || queue.isSuspended { + if webFeedIDs.isEmpty { return } - queue.runInDatabase { (database) in + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + return + } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let sql = "select articleID from articles where feedID not in \(placeholders);" let parameters = Array(webFeedIDs) as [Any] @@ -530,26 +494,35 @@ private extension ArticlesTable { // MARK: - Fetching - private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) -> Set
{ + private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) throws -> Set
{ var articles = Set
() - guard !queue.isSuspended else { - return articles + var error: ArticlesDatabaseError? = nil + queue.runInDatabaseSync { databaseResult in + switch databaseResult { + case .success(let database): + articles = fetchMethod(database) + case .failure(let databaseQueueError): + error = databaseError(with: databaseQueueError) + } } - queue.runInDatabaseSync { (database) in - articles = fetchMethod(database) + if let error = error { + throw(error) } return articles } - private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ completion: @escaping ArticleSetBlock) { - guard !queue.isSuspended else { - completion(Set
()) - return - } - queue.runInDatabase { (database) in + private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ completion: @escaping ArticleSetResultBlock) { + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion(.failure(databaseError(with: databaseResult.error!))) + } + return + } + let articles = fetchMethod(database) DispatchQueue.main.async { - completion(articles) + completion(.success(articles)) } } } @@ -702,12 +675,20 @@ private extension ArticlesTable { return articlesWithResultSet(resultSet, database) } - func fetchArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ value: Bool, _ webFeedIDs: Set, _ completion: @escaping (Set) -> Void) { - guard !queue.isSuspended && !webFeedIDs.isEmpty else { - completion(Set()) + func fetchArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ value: Bool, _ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { + guard !webFeedIDs.isEmpty else { + completion(.success(Set())) return } - queue.runInDatabase { database in + + queue.runInDatabase { databaseResult in + guard let database = databaseResult.database else { + DispatchQueue.main.async { + completion(.failure(databaseError(with: databaseResult.error!))) + } + return + } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! var sql = "select articleID from articles natural join statuses where feedID in \(placeholders) and \(statusKey.rawValue)=" sql += value ? "1" : "0" @@ -720,24 +701,96 @@ private extension ArticlesTable { guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { DispatchQueue.main.async { - completion(Set()) + completion(.success(Set())) } return } let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) } DispatchQueue.main.async { - completion(articleIDs) + completion(.success(articleIDs)) } } } + func fetchArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 + if webFeedIDs.isEmpty { + return Set
() + } + let parameters = webFeedIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! + let whereClause = "feedID in \(placeholders)" + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true) + } + + func fetchUnreadArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 + if webFeedIDs.isEmpty { + return Set
() + } + let parameters = webFeedIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! + let whereClause = "feedID in \(placeholders) and read=0" + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true) + } + + func fetchArticlesForFeedID(_ webFeedID: String, withLimits: Bool, _ database: FMDatabase) -> Set
{ + return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits) + } + + func fetchArticles(articleIDs: Set, _ database: FMDatabase) -> Set
{ + if articleIDs.isEmpty { + return Set
() + } + let parameters = articleIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! + let whereClause = "articleID in \(placeholders)" + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) + } + + func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ database: FMDatabase) -> Set
{ + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?) + // + // datePublished may be nil, so we fall back to dateArrived. + if webFeedIDs.isEmpty { + return Set
() + } + let parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject] + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! + let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and userDeleted = 0" + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) + } + + func fetchStarredArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred = 1 and userDeleted = 0; + if webFeedIDs.isEmpty { + return Set
() + } + let parameters = webFeedIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! + let whereClause = "feedID in \(placeholders) and starred = 1 and userDeleted = 0" + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) + } + + func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + let articles = fetchArticlesMatching(searchString, database) + // TODO: include the feedIDs in the SQL rather than filtering here. + return articles.filter{ webFeedIDs.contains($0.webFeedID) } + } + + func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set, _ database: FMDatabase) -> Set
{ + let articles = fetchArticlesMatching(searchString, database) + // TODO: include the articleIDs in the SQL rather than filtering here. + return articles.filter{ articleIDs.contains($0.articleID) } + } // MARK: - Saving Parsed Items func callUpdateArticlesCompletionBlock(_ newArticles: Set
?, _ updatedArticles: Set
?, _ completion: @escaping UpdateArticlesCompletionBlock) { + let newAndUpdatedArticles = NewAndUpdatedArticles(newArticles: newArticles, updatedArticles: updatedArticles) DispatchQueue.main.async { - completion(newArticles, updatedArticles) + completion(.success(newAndUpdatedArticles)) } } diff --git a/Frameworks/ArticlesDatabase/SearchTable.swift b/Frameworks/ArticlesDatabase/SearchTable.swift index 77a83fc5e..6bcbd36bd 100644 --- a/Frameworks/ArticlesDatabase/SearchTable.swift +++ b/Frameworks/ArticlesDatabase/SearchTable.swift @@ -74,11 +74,13 @@ final class SearchTable: DatabaseTable { } func ensureIndexedArticles(for articleIDs: Set) { - guard !queue.isSuspended && !articleIDs.isEmpty else { + guard !articleIDs.isEmpty else { return } - queue.runInTransaction { (database) in - self.ensureIndexedArticles(articleIDs, database) + queue.runInTransaction { databaseResult in + if let database = databaseResult.database { + self.ensureIndexedArticles(articleIDs, database) + } } } diff --git a/Frameworks/ArticlesDatabase/StatusesTable.swift b/Frameworks/ArticlesDatabase/StatusesTable.swift index 63388841c..4cf414c8f 100644 --- a/Frameworks/ArticlesDatabase/StatusesTable.swift +++ b/Frameworks/ArticlesDatabase/StatusesTable.swift @@ -87,28 +87,34 @@ final class StatusesTable: DatabaseTable { // MARK: - Fetching - func fetchUnreadArticleIDs() -> Set { - return fetchArticleIDs("select articleID from statuses where read=0 and userDeleted=0;") + func fetchUnreadArticleIDs() throws -> Set { + return try fetchArticleIDs("select articleID from statuses where read=0 and userDeleted=0;") } - func fetchStarredArticleIDs() -> Set { - return fetchArticleIDs("select articleID from statuses where starred=1 and userDeleted=0;") + func fetchStarredArticleIDs() throws -> Set { + return try fetchArticleIDs("select articleID from statuses where starred=1 and userDeleted=0;") } - func fetchArticleIDsForStatusesWithoutArticles() -> Set { - return fetchArticleIDs("select articleID from statuses s where (read=0 or starred=1) and userDeleted=0 and not exists (select 1 from articles a where a.articleID = s.articleID);") + func fetchArticleIDsForStatusesWithoutArticles() throws -> Set { + return try fetchArticleIDs("select articleID from statuses s where (read=0 or starred=1) and userDeleted=0 and not exists (select 1 from articles a where a.articleID = s.articleID);") } - func fetchArticleIDs(_ sql: String) -> Set { + func fetchArticleIDs(_ sql: String) throws -> Set { + var error: ArticlesDatabaseError? var articleIDs = Set() - guard !queue.isSuspended else { - return articleIDs - } - queue.runInDatabaseSync { (database) in - guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { - return + queue.runInDatabaseSync { databaseResult in + switch databaseResult { + case .success(let database): + if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) { + articleIDs = resultSet.mapToSet(self.articleIDWithRow) + } + case .failure(let databaseQueueError): + error = databaseError(with: databaseQueueError) } - articleIDs = resultSet.mapToSet(self.articleIDWithRow) + } + + if let error = error { + throw(error) } return articleIDs }