Use makeDatabaseCalls/switch pattern in ArticlesTable.

This commit is contained in:
Brent Simmons 2019-12-16 12:49:46 -08:00
parent 3981312d6f
commit aa5859ff27
2 changed files with 222 additions and 174 deletions

View File

@ -164,7 +164,7 @@ public final class ArticlesDatabase {
articlesTable.update(webFeedIDsAndItems, defaultRead, completion) articlesTable.update(webFeedIDsAndItems, defaultRead, completion)
} }
public func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: DatabaseCompletionBlock? = nil) { public func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: @escaping DatabaseCompletionBlock) {
articlesTable.ensureStatuses(articleIDs, defaultRead, statusKey, flag, completion: completion) articlesTable.ensureStatuses(articleIDs, defaultRead, statusKey, flag, completion: completion)
} }

View File

@ -104,6 +104,7 @@ final class ArticlesTable: DatabaseTable {
func fetchArticlesMatching(_ searchString: String) throws -> Set<Article> { func fetchArticlesMatching(_ searchString: String) throws -> Set<Article> {
var articles: Set<Article> = Set<Article>() var articles: Set<Article> = Set<Article>()
var error: DatabaseError? = nil var error: DatabaseError? = nil
queue.runInDatabaseSync { (databaseResult) in queue.runInDatabaseSync { (databaseResult) in
switch databaseResult { switch databaseResult {
case .success(let database): case .success(let database):
@ -112,6 +113,7 @@ final class ArticlesTable: DatabaseTable {
error = databaseError error = databaseError
} }
} }
if let error = error { if let error = error {
throw(error) throw(error)
} }
@ -188,86 +190,99 @@ final class ArticlesTable: DatabaseTable {
} }
self.queue.runInTransaction { (databaseResult) in self.queue.runInTransaction { (databaseResult) in
guard let database = databaseResult.database else {
DispatchQueue.main.async { func makeDatabaseCalls(_ database: FMDatabase) {
completion(.failure(databaseResult.error!)) let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
assert(statusesDictionary.count == articleIDs.count)
let allIncomingArticles = Article.articlesWithWebFeedIDsAndItems(webFeedIDsAndItems, self.accountID, statusesDictionary) //2
if allIncomingArticles.isEmpty {
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
return
}
let incomingArticles = self.filterIncomingArticles(allIncomingArticles) //3
if incomingArticles.isEmpty {
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
return
}
let incomingArticleIDs = incomingArticles.articleIDs()
let fetchedArticles = self.fetchArticles(articleIDs: incomingArticleIDs, database) //4
let fetchedArticlesDictionary = fetchedArticles.dictionary()
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, completion) //7
// 8. Update search index.
if let newArticles = newArticles {
self.searchTable.indexNewArticles(newArticles, database)
}
if let updatedArticles = updatedArticles {
self.searchTable.indexUpdatedArticles(updatedArticles, database)
} }
return
} }
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1 switch databaseResult {
assert(statusesDictionary.count == articleIDs.count) case .success(let database):
makeDatabaseCalls(database)
let allIncomingArticles = Article.articlesWithWebFeedIDsAndItems(webFeedIDsAndItems, self.accountID, statusesDictionary) //2 case .failure(let databaseError):
if allIncomingArticles.isEmpty { DispatchQueue.main.async {
self.callUpdateArticlesCompletionBlock(nil, nil, completion) completion(.failure(databaseError))
return }
}
let incomingArticles = self.filterIncomingArticles(allIncomingArticles) //3
if incomingArticles.isEmpty {
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
return
}
let incomingArticleIDs = incomingArticles.articleIDs()
let fetchedArticles = self.fetchArticles(articleIDs: incomingArticleIDs, database) //4
let fetchedArticlesDictionary = fetchedArticles.dictionary()
let newArticles = self.findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5
let updatedArticles = self.findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6
self.callUpdateArticlesCompletionBlock(newArticles, updatedArticles, completion) //7
// 8. Update search index.
if let newArticles = newArticles {
self.searchTable.indexNewArticles(newArticles, database)
}
if let updatedArticles = updatedArticles {
self.searchTable.indexUpdatedArticles(updatedArticles, database)
} }
} }
} }
func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: DatabaseCompletionBlock? = nil) { func ensureStatuses(_ articleIDs: Set<String>, _ defaultRead: Bool, _ statusKey: ArticleStatus.Key, _ flag: Bool, completion: @escaping DatabaseCompletionBlock) {
queue.runInTransaction { databaseResult in queue.runInTransaction { databaseResult in
guard let database = databaseResult.database else {
DispatchQueue.main.async {
completion?(databaseResult.error!)
}
return
}
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, defaultRead, database) func makeDatabaseCalls(_ database: FMDatabase) {
let statuses = Set(statusesDictionary.values) let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, defaultRead, database)
self.statusesTable.mark(statuses, statusKey, flag, database) let statuses = Set(statusesDictionary.values)
if let completion = completion { self.statusesTable.mark(statuses, statusKey, flag, database)
DispatchQueue.main.async { DispatchQueue.main.async {
completion(nil) completion(nil)
} }
} }
switch databaseResult {
case .success(let database):
makeDatabaseCalls(database)
case .failure(let databaseError):
DispatchQueue.main.async {
completion(databaseError)
}
}
} }
} }
func fetchStatuses(_ articleIDs: Set<String>, _ createIfNeeded: Bool, _ completion: @escaping ArticleStatusesResultBlock) { func fetchStatuses(_ articleIDs: Set<String>, _ createIfNeeded: Bool, _ completion: @escaping ArticleStatusesResultBlock) {
queue.runInTransaction { databaseResult in queue.runInTransaction { databaseResult in
guard let database = databaseResult.database else {
DispatchQueue.main.async { func makeDatabaseCalls(_ database: FMDatabase) {
completion(.failure(databaseResult.error!)) var statusesDictionary = [String: ArticleStatus]()
if createIfNeeded {
statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database)
}
else {
statusesDictionary = self.statusesTable.existingStatusesForArticleIDs(articleIDs, database)
}
let statuses = Set(statusesDictionary.values)
DispatchQueue.main.async {
completion(.success(statuses))
} }
return
} }
var statusesDictionary = [String: ArticleStatus]() switch databaseResult {
if createIfNeeded { case .success(let database):
statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, false, database) makeDatabaseCalls(database)
} case .failure(let databaseError):
else { DispatchQueue.main.async {
statusesDictionary = self.statusesTable.existingStatusesForArticleIDs(articleIDs, database) completion(.failure(databaseError))
} }
let statuses = Set(statusesDictionary.values)
DispatchQueue.main.async {
completion(.success(statuses))
} }
} }
} }
@ -281,20 +296,25 @@ final class ArticlesTable: DatabaseTable {
} }
queue.runInDatabase { databaseResult in queue.runInDatabase { databaseResult in
guard let database = databaseResult.database else {
DispatchQueue.main.async { func makeDatabaseCalls(_ database: FMDatabase) {
completion(.failure(databaseResult.error!)) var unreadCountDictionary = UnreadCountDictionary()
for webFeedID in webFeedIDs {
unreadCountDictionary[webFeedID] = self.fetchUnreadCount(webFeedID, database)
}
DispatchQueue.main.async {
completion(.success(unreadCountDictionary))
} }
return
} }
var unreadCountDictionary = UnreadCountDictionary() switch databaseResult {
for webFeedID in webFeedIDs { case .success(let database):
unreadCountDictionary[webFeedID] = self.fetchUnreadCount(webFeedID, database) makeDatabaseCalls(database)
} case .failure(let databaseError):
DispatchQueue.main.async {
DispatchQueue.main.async { completion(.failure(databaseError))
completion(.success(unreadCountDictionary)) }
} }
} }
} }
@ -307,25 +327,30 @@ final class ArticlesTable: DatabaseTable {
} }
queue.runInDatabase { databaseResult in queue.runInDatabase { databaseResult in
guard let database = databaseResult.database else {
func makeDatabaseCalls(_ database: FMDatabase) {
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;"
var parameters = [Any]()
parameters += Array(webFeedIDs) as [Any]
parameters += [since] as [Any]
parameters += [since] as [Any]
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
DispatchQueue.main.async { DispatchQueue.main.async {
completion(.failure(databaseResult.error!)) completion(.success(unreadCount))
} }
return
} }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! switch databaseResult {
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;" case .success(let database):
makeDatabaseCalls(database)
var parameters = [Any]() case .failure(let databaseError):
parameters += Array(webFeedIDs) as [Any] DispatchQueue.main.async {
parameters += [since] as [Any] completion(.failure(databaseError))
parameters += [since] as [Any] }
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
DispatchQueue.main.async {
completion(.success(unreadCount))
} }
} }
} }
@ -335,32 +360,37 @@ final class ArticlesTable: DatabaseTable {
let cutoffDate = articleCutoffDate let cutoffDate = articleCutoffDate
queue.runInDatabase { databaseResult in queue.runInDatabase { databaseResult in
guard let database = databaseResult.database else {
func makeDatabaseCalls(_ database: FMDatabase) {
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(.success(UnreadCountDictionary()))
}
return
}
var d = UnreadCountDictionary()
while resultSet.next() {
let unreadCount = resultSet.long(forColumnIndex: 1)
if let webFeedID = resultSet.string(forColumnIndex: 0) {
d[webFeedID] = unreadCount
}
}
DispatchQueue.main.async { DispatchQueue.main.async {
completion(.failure(databaseResult.error!)) completion(.success(d))
} }
return
} }
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;" switch databaseResult {
case .success(let database):
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate, cutoffDate]) else { makeDatabaseCalls(database)
case .failure(let databaseError):
DispatchQueue.main.async { DispatchQueue.main.async {
completion(.success(UnreadCountDictionary())) completion(.failure(databaseError))
} }
return
}
var d = UnreadCountDictionary()
while resultSet.next() {
let unreadCount = resultSet.long(forColumnIndex: 1)
if let webFeedID = resultSet.string(forColumnIndex: 0) {
d[webFeedID] = unreadCount
}
}
DispatchQueue.main.async {
completion(.success(d))
} }
} }
} }
@ -372,21 +402,26 @@ final class ArticlesTable: DatabaseTable {
} }
queue.runInDatabase { databaseResult in queue.runInDatabase { databaseResult in
guard let database = databaseResult.database else {
func makeDatabaseCalls(_ database: FMDatabase) {
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]
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
DispatchQueue.main.async { DispatchQueue.main.async {
completion(.failure(databaseResult.error!)) completion(.success(unreadCount))
} }
return
} }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! switch databaseResult {
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1 and userDeleted=0;" case .success(let database):
let parameters = Array(webFeedIDs) as [Any] makeDatabaseCalls(database)
case .failure(let databaseError):
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database) DispatchQueue.main.async {
completion(.failure(databaseError))
DispatchQueue.main.async { }
completion(.success(unreadCount))
} }
} }
} }
@ -431,21 +466,25 @@ final class ArticlesTable: DatabaseTable {
func indexUnindexedArticles() { func indexUnindexedArticles() {
queue.runInDatabase { databaseResult 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
}
let articleIDs = resultSet.mapToSet{ $0.string(forColumn: DatabaseKey.articleID) }
if articleIDs.isEmpty {
return
}
self.searchTable.ensureIndexedArticles(articleIDs, database)
DispatchQueue.main.async { func makeDatabaseCalls(_ database: FMDatabase) {
self.indexUnindexedArticles() let sql = "select articleID from articles where searchRowID is null limit 500;"
guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else {
return
}
let articleIDs = resultSet.mapToSet{ $0.string(forColumn: DatabaseKey.articleID) }
if articleIDs.isEmpty {
return
}
self.searchTable.ensureIndexedArticles(articleIDs, database)
DispatchQueue.main.async {
self.indexUnindexedArticles()
}
}
if let database = databaseResult.database {
makeDatabaseCalls(database)
} }
} }
} }
@ -468,22 +507,25 @@ final class ArticlesTable: DatabaseTable {
return return
} }
queue.runInDatabase { databaseResult in queue.runInDatabase { databaseResult in
guard let database = databaseResult.database else {
return func makeDatabaseCalls(_ database: FMDatabase) {
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]
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
return
}
let articleIDs = resultSet.mapToSet{ $0.string(forColumn: DatabaseKey.articleID) }
if articleIDs.isEmpty {
return
}
self.removeArticles(articleIDs, database)
self.statusesTable.removeStatuses(articleIDs, database)
} }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! if let database = databaseResult.database {
let sql = "select articleID from articles where feedID not in \(placeholders);" makeDatabaseCalls(database)
let parameters = Array(webFeedIDs) as [Any]
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
return
} }
let articleIDs = resultSet.mapToSet{ $0.string(forColumn: DatabaseKey.articleID) }
if articleIDs.isEmpty {
return
}
self.removeArticles(articleIDs, database)
self.statusesTable.removeStatuses(articleIDs, database)
} }
} }
} }
@ -513,16 +555,17 @@ private extension ArticlesTable {
private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ completion: @escaping ArticleSetResultBlock) { private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ completion: @escaping ArticleSetResultBlock) {
queue.runInDatabase { databaseResult in queue.runInDatabase { databaseResult in
guard let database = databaseResult.database else {
DispatchQueue.main.async {
completion(.failure(databaseResult.error!))
}
return
}
let articles = fetchMethod(database) switch databaseResult {
DispatchQueue.main.async { case .success(let database):
completion(.success(articles)) let articles = fetchMethod(database)
DispatchQueue.main.async {
completion(.success(articles))
}
case .failure(let databaseError):
DispatchQueue.main.async {
completion(.failure(databaseError))
}
} }
} }
} }
@ -682,33 +725,38 @@ private extension ArticlesTable {
} }
queue.runInDatabase { databaseResult in queue.runInDatabase { databaseResult in
guard let database = databaseResult.database else {
DispatchQueue.main.async { func makeDatabaseCalls(_ database: FMDatabase) {
completion(.failure(databaseResult.error!)) 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"
if statusKey != .userDeleted {
sql += " and userDeleted=0"
} }
return sql += ";"
}
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! let parameters = Array(webFeedIDs) as [Any]
var sql = "select articleID from articles natural join statuses where feedID in \(placeholders) and \(statusKey.rawValue)="
sql += value ? "1" : "0"
if statusKey != .userDeleted {
sql += " and userDeleted=0"
}
sql += ";"
let parameters = Array(webFeedIDs) as [Any] guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
DispatchQueue.main.async {
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { completion(.success(Set<String>()))
DispatchQueue.main.async { }
completion(.success(Set<String>())) return
}
let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) }
DispatchQueue.main.async {
completion(.success(articleIDs))
} }
return
} }
let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) } switch databaseResult {
DispatchQueue.main.async { case .success(let database):
completion(.success(articleIDs)) makeDatabaseCalls(database)
case .failure(let databaseError):
DispatchQueue.main.async {
completion(.failure(databaseError))
}
} }
} }
} }