From 2d1c63403d2b01963649050da716b4edbf058394 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 26 Aug 2017 15:37:15 -0700 Subject: [PATCH] Make progress fetching articles. --- Frameworks/Database/ArticlesTable.swift | 109 ++++++++++++++++-- Frameworks/Database/Database.swift | 8 +- .../Extensions/Article+Database.swift | 15 +++ .../Extensions/ArticleStatus+Database.swift | 9 +- Frameworks/Database/StatusesTable.swift | 106 +++++++++-------- 5 files changed, 180 insertions(+), 67 deletions(-) diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index dfe3542ed..b3594db01 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -16,16 +16,20 @@ final class ArticlesTable: DatabaseTable { let name: String let databaseIDKey = DatabaseKey.articleID + private weak var account: Account? + private let queue: RSDatabaseQueue private let statusesTable: StatusesTable private let authorsLookupTable: DatabaseLookupTable private let attachmentsLookupTable: DatabaseLookupTable private let tagsLookupTable: DatabaseLookupTable private let articleCache = ArticleCache() - init(name: String) { + init(name: String, account: Account, queue: RSDatabaseQueue) { self.name = name - + self.account = account + self.queue = queue + self.statusesTable = StatusesTable(name: DatabaseTableName.statuses) let authorsTable = AuthorsTable(name: DatabaseTableName.authors) self.authorsLookupTable = DatabaseLookupTable(name: DatabaseTableName.authorsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.authorID, relatedTable: authorsTable, relationshipName: RelationshipName.authors) @@ -40,11 +44,11 @@ final class ArticlesTable: DatabaseTable { // MARK: DatabaseTable Methods func objectWithRow(_ row: FMResultSet) -> DatabaseObject? { - -// if let article = articleWithRow(row) { -// -// } - return nil // TODO + + if let article = articleWithRow(row) { + return article as DatabaseObject + } + return nil } func save(_ objects: [DatabaseObject], in database: FMDatabase) { @@ -56,12 +60,31 @@ final class ArticlesTable: DatabaseTable { func fetchArticles(_ feed: Feed) -> Set
{ - return Set
() // TODO + let feedID = feed.feedID + + var fetchedArticles = Set
() + + queue.fetchSync { (database: FMDatabase!) -> Void in + + fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database) + } + + return articleCache.uniquedArticles(fetchedArticles) } func fetchArticlesAsync(_ feed: Feed, _ resultBlock: @escaping ArticleResultBlock) { - // TODO + let feedID = feed.feedID + + queue.fetch { (database: FMDatabase!) -> Void in + + let fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database) + + DispatchQueue.main.async { + let articles = self.articleCache.uniquedArticles(fetchedArticles) + resultBlock(articles) + } + } } func fetchUnreadArticles(_ feeds: Set) -> Set
{ @@ -138,7 +161,66 @@ final class ArticlesTable: DatabaseTable { // } } -//private extension ArticlesTable { +// MARK: - + +private extension ArticlesTable { + + // MARK: Fetching + + func attachRelatedObjects(_ articles: Set
, _ database: FMDatabase) { + + let articleArray = articles.map { $0 as DatabaseObject } + + authorsLookupTable.attachRelatedObjects(to: articleArray, in: database) + attachmentsLookupTable.attachRelatedObjects(to: articleArray, in: database) + tagsLookupTable.attachRelatedObjects(to: articleArray, in: database) + + // In theory, it’s impossible to have a fetched article without a status. + // Let’s handle that impossibility anyway. + // Remember that, if nothing else, the user can edit the SQLite database, + // and thus could delete all their statuses. + + statusesTable.ensureStatusesForArticles(articles, database) + } + + func articleWithRow(_ row: FMResultSet) -> Article? { + + guard let account = account else { + return nil + } + guard let article = Article(row: row, account: account) else { + return nil + } + + // Note: the row is a result of a JOIN query with the statuses table, + // so we can get the status at the same time and avoid additional database lookups. + + article.status = statusesTable.statusWithRow(row) + return article + } + + func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set
{ + + let articles = resultSet.mapToSet(articleWithRow) + attachRelatedObjects(articles, database) + return articles + } + + func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set
{ + + let sql = "select * from articles natural join statuses where \(whereClause);" + + if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) { + return articlesWithResultSet(resultSet, database) + } + + return Set
() + } + + func fetchArticlesForFeedID(_ feedID: String, database: FMDatabase) -> Set
{ + + return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject]) + } // func cachedArticle(_ articleID: String) -> Article? { // @@ -154,11 +236,11 @@ final class ArticlesTable: DatabaseTable { // // articles.forEach { cacheArticle($0) } // } -//} +} private struct ArticleCache { - // Main thread only. + // Main thread only — unlike the other object caches. // The cache contains a given article only until all outside references are gone. // Cache key is articleID. @@ -179,6 +261,9 @@ private struct ArticleCache { } } + // At this point, every Article must have an attached Status. + assert(articlesToReturn.eachHasAStatus()) + return articlesToReturn } diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index d61a8ba9c..699ac4c25 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -24,14 +24,16 @@ public final class Database { private var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! private let minimumNumberOfArticles = 10 private weak var delegate: AccountDelegate? - - public init(databaseFile: String, delegate: AccountDelegate) { + private weak var account: Account? + + public init(databaseFile: String, delegate: AccountDelegate, account: Account) { self.delegate = delegate + self.account = account self.databaseFile = databaseFile self.queue = RSDatabaseQueue(filepath: databaseFile, excludeFromBackup: false) - self.articlesTable = ArticlesTable(name: DatabaseTableName.articles) + self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, account: account, queue: queue) let createStatementsPath = Bundle(for: type(of: self)).path(forResource: "CreateStatements", ofType: "sql")! let createStatements = try! NSString(contentsOfFile: createStatementsPath, encoding: String.Encoding.utf8.rawValue) diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index 4a00b8ae2..db7ebd8ec 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -71,4 +71,19 @@ extension Set where Element == Article { return Set(map { $0.databaseID }) } + + func eachHasAStatus() -> Bool { + + for article in self { + if article.status == nil { + return false + } + } + return true + } + + func missingStatuses() -> Set
{ + + return withNilProperty(\Article.status) + } } diff --git a/Frameworks/Database/Extensions/ArticleStatus+Database.swift b/Frameworks/Database/Extensions/ArticleStatus+Database.swift index 24ad25b77..2774c2dff 100644 --- a/Frameworks/Database/Extensions/ArticleStatus+Database.swift +++ b/Frameworks/Database/Extensions/ArticleStatus+Database.swift @@ -12,20 +12,15 @@ import Data extension ArticleStatus { - convenience init?(articleID: String, row: FMResultSet) { + convenience init(articleID: String, dateArrived: Date, row: FMResultSet) { let read = row.bool(forColumn: DatabaseKey.read) let starred = row.bool(forColumn: DatabaseKey.starred) let userDeleted = row.bool(forColumn: DatabaseKey.userDeleted) - var dateArrived = row.date(forColumn: DatabaseKey.dateArrived) - if (dateArrived == nil) { - dateArrived = NSDate.distantPast - } - // let accountInfoPlist = accountInfoWithRow(row) - self.init(articleID: articleID, read: read, starred: starred, userDeleted: userDeleted, dateArrived: dateArrived!, accountInfo: nil) + self.init(articleID: articleID, read: read, starred: starred, userDeleted: userDeleted, dateArrived: dateArrived, accountInfo: nil) } func databaseDictionary() -> NSDictionary { diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index 7dfbcfc82..bb4a4c98c 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -40,9 +40,41 @@ final class StatusesTable: DatabaseTable { // TODO } - - + // MARK: Fetching + + func statusWithRow(_ row: FMResultSet) -> ArticleStatus? { + + guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { + return nil + } + if let cachedStatus = cache[articleID] as? ArticleStatus { + return cachedStatus + } + + guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else { + return nil + } + + let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row) + cache[articleID] = articleStatus + return articleStatus + } + + // MARK: Creating + + func ensureStatusesForArticles(_ articles: Set
, _ database: FMDatabase) { + + let articlesNeedingStatuses = articles.missingStatuses() + if articlesNeedingStatuses.isEmpty { + return + } + + createAndSaveStatusesForArticles(articlesNeedingStatuses, database) + + attachCachedStatuses(articlesNeedingStatuses) + assert(articles.eachHasAStatus()) + } // func markArticles(_ articles: Set
, statusKey: String, flag: Bool) { // @@ -113,33 +145,16 @@ final class StatusesTable: DatabaseTable { } private extension StatusesTable { - - // MARK: Fetching - - func statusWithRow(_ row: FMResultSet) -> ArticleStatus? { - guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { - return nil + func attachCachedStatuses(_ articles: Set
) { + + for article in articles { + if let cachedStatus = cache[article.articleID] as? ArticleStatus { + article.status = cachedStatus + } } - if let cachedStatus = cache[articleID] as? ArticleStatus { - return cachedStatus - } - - let status = ArticleStatus(articleID: articleID, row: row) - cache[articleID] = status - return status } -// func attachCachedStatuses(_ articles: Set
) { -// -// articles.forEach { (oneArticle) in -// -// if let cachedStatus = cache[oneArticle.databaseID] { -// oneArticle.status = cachedStatus -// } -// } -// } - // func assertNoMissingStatuses(_ articles: Set
) { // // for oneArticle in articles { @@ -220,26 +235,27 @@ private extension StatusesTable { // MARK: Creating -// func saveStatuses(_ statuses: Set, _ database: FMDatabase) { -// -// let statusArray = statuses.map { $0.databaseDictionary() } -// insertRows(statusArray, insertType: .orIgnore, in: database) -// } -// -// func createAndSaveStatusesForArticles(_ articles: Set
, _ database: FMDatabase) { -// -// let articleIDs = Set(articles.map { $0.databaseID }) -// createAndSaveStatusesForArticleIDs(articleIDs, database) -// } -// -// func createAndSaveStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) { -// -// let now = Date() -// let statuses = articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) } -// cache.addObjectsNotCached(statuses) -// -// saveStatuses(Set(statuses), database) -// } + func saveStatuses(_ statuses: Set, _ database: FMDatabase) { + + let statusArray = statuses.map { $0.databaseDictionary() } + insertRows(statusArray, insertType: .orIgnore, in: database) + } + + func createAndSaveStatusesForArticles(_ articles: Set
, _ database: FMDatabase) { + + let articleIDs = Set(articles.map { $0.articleID }) + createAndSaveStatusesForArticleIDs(articleIDs, database) + } + + func createAndSaveStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) { + + let now = Date() + let statuses = articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) } + let databaseObjects = statuses.map { $0 as DatabaseObject } + cache.addObjectsNotCached(databaseObjects) + + saveStatuses(Set(statuses), database) + } // MARK: Utilities