Update Articles.framework to use revised DatabaseQueue — use Result types and throwing functions.

This commit is contained in:
Brent Simmons 2019-12-15 22:09:27 -08:00
parent 9cb0bcbc94
commit df45484205
4 changed files with 349 additions and 258 deletions

View File

@ -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<Article>?, Set<Article>?) -> Void //newArticles, updatedArticles
public typealias UnreadCountDictionaryCompletionResult = Result<UnreadCountDictionary, ArticlesDatabaseError>
public typealias UnreadCountDictionaryCompletionBlock = (UnreadCountDictionaryCompletionResult) -> Void
public typealias SingleUnreadCountResult = Result<Int, ArticlesDatabaseError>
public typealias SingleUnreadCountCompletionBlock = (SingleUnreadCountResult) -> Void
public struct NewAndUpdatedArticles {
let newArticles: Set<Article>?
let updatedArticles: Set<Article>?
}
public typealias UpdateArticlesResult = Result<NewAndUpdatedArticles, ArticlesDatabaseError>
public typealias UpdateArticlesCompletionBlock = (UpdateArticlesResult) -> Void
public typealias ArticleSetResult = Result<Set<Article>, ArticlesDatabaseError>
public typealias ArticleSetResultBlock = (ArticleSetResult) -> Void
public typealias DatabaseCompletionBlock = (ArticlesDatabaseError?) -> Void
public typealias ArticleIDsResult = Result<Set<String>, ArticlesDatabaseError>
public typealias ArticleIDsCompletionBlock = (ArticleIDsResult) -> Void
public typealias ArticleStatusesResult = Result<Set<ArticleStatus>, 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<Article> {
return articlesTable.fetchArticles(webFeedID)
public func fetchArticles(_ webFeedID: String) throws -> Set<Article> {
return try articlesTable.fetchArticles(webFeedID)
}
public func fetchArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticles(webFeedIDs)
public func fetchArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticles(webFeedIDs)
}
public func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticles(articleIDs: articleIDs)
public func fetchArticles(articleIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticles(articleIDs: articleIDs)
}
public func fetchUnreadArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchUnreadArticles(webFeedIDs)
public func fetchUnreadArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchUnreadArticles(webFeedIDs)
}
public func fetchTodayArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate())
public func fetchTodayArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate())
}
public func fetchStarredArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchStarredArticles(webFeedIDs)
public func fetchStarredArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchStarredArticles(webFeedIDs)
}
public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticlesMatching(searchString, webFeedIDs)
public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticlesMatching(searchString, webFeedIDs)
}
public func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs)
public func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) throws -> Set<Article> {
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<String>, _ completion: @escaping ArticleSetBlock) {
public func fetchArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesAsync(webFeedIDs, completion)
}
public func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
public func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion)
}
public func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
public func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchUnreadArticlesAsync(webFeedIDs, completion)
}
public func fetchTodayArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
public func fetchTodayArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), completion)
}
public func fetchedStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
public func fetchedStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchStarredArticlesAsync(webFeedIDs, completion)
}
public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesMatchingAsync(searchString, webFeedIDs, completion)
}
public func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
public func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, completion)
}
// MARK: - Unread Counts
public func fetchUnreadCounts(for webFeedIDs: Set<String>, _ completion: @escaping UnreadCountCompletionBlock) {
public func fetchUnreadCounts(for webFeedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) {
articlesTable.fetchUnreadCounts(webFeedIDs, completion)
}
public func fetchUnreadCountForToday(for webFeedIDs: Set<String>, completion: @escaping (Int) -> Void) {
public func fetchAllNonZeroUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) {
articlesTable.fetchAllUnreadCounts(completion)
}
public func fetchUnreadCountForToday(for webFeedIDs: Set<String>, completion: @escaping SingleUnreadCountCompletionBlock) {
fetchUnreadCount(for: webFeedIDs, since: todayCutoffDate(), completion: completion)
}
public func fetchUnreadCount(for webFeedIDs: Set<String>, since: Date, completion: @escaping (Int) -> Void) {
public func fetchUnreadCount(for webFeedIDs: Set<String>, since: Date, completion: @escaping SingleUnreadCountCompletionBlock) {
articlesTable.fetchUnreadCount(webFeedIDs, since, completion)
}
public func fetchStarredAndUnreadCount(for webFeedIDs: Set<String>, completion: @escaping (Int) -> Void) {
public func fetchStarredAndUnreadCount(for webFeedIDs: Set<String>, 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<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: VoidCompletionBlock? = nil) {
public func ensureStatuses(_ articleIDs: Set<String>, _ 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<String>, completion: @escaping (Set<String>) -> Void) {
public func fetchUnreadArticleIDsAsync(webFeedIDs: Set<String>, completion: @escaping ArticleIDsCompletionBlock) {
articlesTable.fetchUnreadArticleIDsAsync(webFeedIDs, completion)
}
/// Fetch the articleIDs of starred articles in feeds specified by webFeedIDs.
public func fetchStarredArticleIDsAsync(webFeedIDs: Set<String>, completion: @escaping (Set<String>) -> Void) {
public func fetchStarredArticleIDsAsync(webFeedIDs: Set<String>, completion: @escaping ArticleIDsCompletionBlock) {
articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion)
}
public func fetchArticleIDsForStatusesWithoutArticles() -> Set<String> {
return articlesTable.fetchArticleIDsForStatusesWithoutArticles()
public func fetchArticleIDsForStatusesWithoutArticles() throws -> Set<String> {
return try articlesTable.fetchArticleIDsForStatusesWithoutArticles()
}
public func mark(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<ArticleStatus>? {
return articlesTable.mark(articles, statusKey, flag)
public func mark(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) throws -> Set<ArticleStatus>? {
return try articlesTable.mark(articles, statusKey, flag)
}
public func fetchStatuses(articleIDs: Set<String>, createIfNeeded: Bool, completion: @escaping (Set<ArticleStatus>?) -> Void) {
public func fetchStatuses(articleIDs: Set<String>, 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 {

View File

@ -43,167 +43,101 @@ final class ArticlesTable: DatabaseTable {
// MARK: - Fetching Articles for Feed
func fetchArticles(_ webFeedID: String) -> Set<Article> {
return fetchArticles{ self.fetchArticlesForFeedID(webFeedID, withLimits: true, $0) }
func fetchArticles(_ webFeedID: String) throws -> Set<Article> {
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<Article> {
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits)
func fetchArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try fetchArticles{ self.fetchArticles(webFeedIDs, $0) }
}
func fetchArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
return fetchArticles{ self.fetchArticles(webFeedIDs, $0) }
}
func fetchArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
func fetchArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchArticles(webFeedIDs, $0) }, completion)
}
private func fetchArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
if webFeedIDs.isEmpty {
return Set<Article>()
}
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<String>) -> Set<Article> {
return fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) }
func fetchArticles(articleIDs: Set<String>) throws -> Set<Article> {
return try fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) }
}
func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
func fetchArticlesAsync(articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
return fetchArticlesAsync({ self.fetchArticles(articleIDs: articleIDs, $0) }, completion)
}
private func fetchArticles(articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
if articleIDs.isEmpty {
return Set<Article>()
}
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<String>) -> Set<Article> {
return fetchArticles{ self.fetchUnreadArticles(webFeedIDs, $0) }
func fetchUnreadArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, $0) }
}
func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, $0) }, completion)
}
private func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
if webFeedIDs.isEmpty {
return Set<Article>()
}
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<String>, _ cutoffDate: Date) -> Set<Article> {
return fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date) throws -> Set<Article> {
return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }
}
func fetchArticlesSinceAsync(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ completion: @escaping ArticleSetBlock) {
func fetchArticlesSinceAsync(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }, completion)
}
private func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ database: FMDatabase) -> Set<Article> {
// 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<Article>()
}
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<String>) -> Set<Article> {
return fetchArticles{ self.fetchStarredArticles(webFeedIDs, $0) }
func fetchStarredArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, $0) }
}
func fetchStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
func fetchStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, $0) }, completion)
}
private func fetchStarredArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
// 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<Article>()
}
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<Article> {
func fetchArticlesMatching(_ searchString: String) throws -> Set<Article> {
var articles: Set<Article> = Set<Article>()
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<String>) -> Set<Article> {
var articles = fetchArticlesMatching(searchString)
func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) throws -> Set<Article> {
var articles = try fetchArticlesMatching(searchString)
articles = articles.filter{ webFeedIDs.contains($0.webFeedID) }
return articles
}
func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) -> Set<Article> {
var articles = fetchArticlesMatching(searchString)
func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) throws -> Set<Article> {
var articles = try fetchArticlesMatching(searchString)
articles = articles.filter{ articleIDs.contains($0.articleID) }
return articles
}
func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchArticlesMatching(searchString, webFeedIDs, $0) }, completion)
}
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetBlock) {
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs, $0) }, completion)
}
private func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
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<String>, _ database: FMDatabase) -> Set<Article> {
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<String>, in database: FMDatabase) -> Set<ArticleSearchInfo>? {
@ -235,7 +169,7 @@ final class ArticlesTable: DatabaseTable {
func update(_ webFeedIDsAndItems: [String: Set<ParsedItem>], _ 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<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: VoidCompletionBlock? = nil) {
guard !queue.isSuspended else {
if let handler = completion {
callVoidCompletionBlock(handler)
func ensureStatuses(_ articleIDs: Set<String>, _ 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<String>, _ createIfNeeded: Bool, _ completion: @escaping (Set<ArticleStatus>?) -> Void) {
guard !queue.isSuspended else {
completion(nil)
return
}
func fetchStatuses(_ articleIDs: Set<String>, _ 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<String>, _ completion: @escaping UnreadCountCompletionBlock) {
func fetchUnreadCounts(_ webFeedIDs: Set<String>, _ 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<String>, _ since: Date, _ completion: @escaping (Int) -> Void) {
func fetchUnreadCount(_ webFeedIDs: Set<String>, _ 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<String>, _ completion: @escaping (Int) -> Void) {
if webFeedIDs.isEmpty || queue.isSuspended {
completion(0)
func fetchStarredAndUnreadCount(_ webFeedIDs: Set<String>, _ 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<String>, _ completion: @escaping (Set<String>) -> Void) {
func fetchUnreadArticleIDsAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleIDsCompletionBlock) {
fetchArticleIDsAsync(.read, false, webFeedIDs, completion)
}
func fetchStarredArticleIDsAsync(_ webFeedIDs: Set<String>, _ completion: @escaping (Set<String>) -> Void) {
func fetchStarredArticleIDsAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleIDsCompletionBlock) {
fetchArticleIDsAsync(.starred, true, webFeedIDs, completion)
}
func fetchStarredArticleIDs() -> Set<String> {
return statusesTable.fetchStarredArticleIDs()
func fetchStarredArticleIDs() throws -> Set<String> {
return try statusesTable.fetchStarredArticleIDs()
}
func fetchArticleIDsForStatusesWithoutArticles() -> Set<String> {
return statusesTable.fetchArticleIDsForStatusesWithoutArticles()
func fetchArticleIDsForStatusesWithoutArticles() throws -> Set<String> {
return try statusesTable.fetchArticleIDsForStatusesWithoutArticles()
}
func mark(_ articles: Set<Article>, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set<ArticleStatus>? {
func mark(_ articles: Set<Article>, _ statusKey: ArticleStatus.Key, _ flag: Bool) throws -> Set<ArticleStatus>? {
var statuses: Set<ArticleStatus>?
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<String>) {
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<Article> {
private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) throws -> Set<Article> {
var articles = Set<Article>()
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<Article>())
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<String>, _ completion: @escaping (Set<String>) -> Void) {
guard !queue.isSuspended && !webFeedIDs.isEmpty else {
completion(Set<String>())
func fetchArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ value: Bool, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleIDsCompletionBlock) {
guard !webFeedIDs.isEmpty else {
completion(.success(Set<String>()))
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<String>())
completion(.success(Set<String>()))
}
return
}
let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) }
DispatchQueue.main.async {
completion(articleIDs)
completion(.success(articleIDs))
}
}
}
func fetchArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
if webFeedIDs.isEmpty {
return Set<Article>()
}
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<String>, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
if webFeedIDs.isEmpty {
return Set<Article>()
}
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<Article> {
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits)
}
func fetchArticles(articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
if articleIDs.isEmpty {
return Set<Article>()
}
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<String>, _ cutoffDate: Date, _ database: FMDatabase) -> Set<Article> {
// 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<Article>()
}
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<String>, _ database: FMDatabase) -> Set<Article> {
// 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<Article>()
}
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<String>, _ database: FMDatabase) -> Set<Article> {
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<String>, _ database: FMDatabase) -> Set<Article> {
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<Article>?, _ updatedArticles: Set<Article>?, _ completion: @escaping UpdateArticlesCompletionBlock) {
let newAndUpdatedArticles = NewAndUpdatedArticles(newArticles: newArticles, updatedArticles: updatedArticles)
DispatchQueue.main.async {
completion(newArticles, updatedArticles)
completion(.success(newAndUpdatedArticles))
}
}

View File

@ -74,11 +74,13 @@ final class SearchTable: DatabaseTable {
}
func ensureIndexedArticles(for articleIDs: Set<String>) {
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)
}
}
}

View File

@ -87,28 +87,34 @@ final class StatusesTable: DatabaseTable {
// MARK: - Fetching
func fetchUnreadArticleIDs() -> Set<String> {
return fetchArticleIDs("select articleID from statuses where read=0 and userDeleted=0;")
func fetchUnreadArticleIDs() throws -> Set<String> {
return try fetchArticleIDs("select articleID from statuses where read=0 and userDeleted=0;")
}
func fetchStarredArticleIDs() -> Set<String> {
return fetchArticleIDs("select articleID from statuses where starred=1 and userDeleted=0;")
func fetchStarredArticleIDs() throws -> Set<String> {
return try fetchArticleIDs("select articleID from statuses where starred=1 and userDeleted=0;")
}
func fetchArticleIDsForStatusesWithoutArticles() -> Set<String> {
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<String> {
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<String> {
func fetchArticleIDs(_ sql: String) throws -> Set<String> {
var error: ArticlesDatabaseError?
var articleIDs = Set<String>()
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
}