Make progress fetching articles.

This commit is contained in:
Brent Simmons 2017-08-26 15:37:15 -07:00
parent 2cefb87f20
commit 2d1c63403d
5 changed files with 180 additions and 67 deletions

View File

@ -16,16 +16,20 @@ final class ArticlesTable: DatabaseTable {
let name: String let name: String
let databaseIDKey = DatabaseKey.articleID let databaseIDKey = DatabaseKey.articleID
private weak var account: Account?
private let queue: RSDatabaseQueue
private let statusesTable: StatusesTable private let statusesTable: StatusesTable
private let authorsLookupTable: DatabaseLookupTable private let authorsLookupTable: DatabaseLookupTable
private let attachmentsLookupTable: DatabaseLookupTable private let attachmentsLookupTable: DatabaseLookupTable
private let tagsLookupTable: DatabaseLookupTable private let tagsLookupTable: DatabaseLookupTable
private let articleCache = ArticleCache() private let articleCache = ArticleCache()
init(name: String) { init(name: String, account: Account, queue: RSDatabaseQueue) {
self.name = name self.name = name
self.account = account
self.queue = queue
self.statusesTable = StatusesTable(name: DatabaseTableName.statuses) self.statusesTable = StatusesTable(name: DatabaseTableName.statuses)
let authorsTable = AuthorsTable(name: DatabaseTableName.authors) let authorsTable = AuthorsTable(name: DatabaseTableName.authors)
self.authorsLookupTable = DatabaseLookupTable(name: DatabaseTableName.authorsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.authorID, relatedTable: authorsTable, relationshipName: RelationshipName.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 // MARK: DatabaseTable Methods
func objectWithRow(_ row: FMResultSet) -> DatabaseObject? { func objectWithRow(_ row: FMResultSet) -> DatabaseObject? {
// if let article = articleWithRow(row) { if let article = articleWithRow(row) {
// return article as DatabaseObject
// } }
return nil // TODO return nil
} }
func save(_ objects: [DatabaseObject], in database: FMDatabase) { func save(_ objects: [DatabaseObject], in database: FMDatabase) {
@ -56,12 +60,31 @@ final class ArticlesTable: DatabaseTable {
func fetchArticles(_ feed: Feed) -> Set<Article> { func fetchArticles(_ feed: Feed) -> Set<Article> {
return Set<Article>() // TODO let feedID = feed.feedID
var fetchedArticles = Set<Article>()
queue.fetchSync { (database: FMDatabase!) -> Void in
fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database)
}
return articleCache.uniquedArticles(fetchedArticles)
} }
func fetchArticlesAsync(_ feed: Feed, _ resultBlock: @escaping ArticleResultBlock) { 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<Feed>) -> Set<Article> { func fetchUnreadArticles(_ feeds: Set<Feed>) -> Set<Article> {
@ -138,7 +161,66 @@ final class ArticlesTable: DatabaseTable {
// } // }
} }
//private extension ArticlesTable { // MARK: -
private extension ArticlesTable {
// MARK: Fetching
func attachRelatedObjects(_ articles: Set<Article>, _ 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, its impossible to have a fetched article without a status.
// Lets 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<Article> {
let articles = resultSet.mapToSet(articleWithRow)
attachRelatedObjects(articles, database)
return articles
}
func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set<Article> {
let sql = "select * from articles natural join statuses where \(whereClause);"
if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) {
return articlesWithResultSet(resultSet, database)
}
return Set<Article>()
}
func fetchArticlesForFeedID(_ feedID: String, database: FMDatabase) -> Set<Article> {
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject])
}
// func cachedArticle(_ articleID: String) -> Article? { // func cachedArticle(_ articleID: String) -> Article? {
// //
@ -154,11 +236,11 @@ final class ArticlesTable: DatabaseTable {
// //
// articles.forEach { cacheArticle($0) } // articles.forEach { cacheArticle($0) }
// } // }
//} }
private struct ArticleCache { 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. // The cache contains a given article only until all outside references are gone.
// Cache key is articleID. // 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 return articlesToReturn
} }

View File

@ -24,14 +24,16 @@ public final class Database {
private var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! private var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)!
private let minimumNumberOfArticles = 10 private let minimumNumberOfArticles = 10
private weak var delegate: AccountDelegate? private weak var delegate: AccountDelegate?
private weak var account: Account?
public init(databaseFile: String, delegate: AccountDelegate) {
public init(databaseFile: String, delegate: AccountDelegate, account: Account) {
self.delegate = delegate self.delegate = delegate
self.account = account
self.databaseFile = databaseFile self.databaseFile = databaseFile
self.queue = RSDatabaseQueue(filepath: databaseFile, excludeFromBackup: false) 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 createStatementsPath = Bundle(for: type(of: self)).path(forResource: "CreateStatements", ofType: "sql")!
let createStatements = try! NSString(contentsOfFile: createStatementsPath, encoding: String.Encoding.utf8.rawValue) let createStatements = try! NSString(contentsOfFile: createStatementsPath, encoding: String.Encoding.utf8.rawValue)

View File

@ -71,4 +71,19 @@ extension Set where Element == Article {
return Set<String>(map { $0.databaseID }) return Set<String>(map { $0.databaseID })
} }
func eachHasAStatus() -> Bool {
for article in self {
if article.status == nil {
return false
}
}
return true
}
func missingStatuses() -> Set<Article> {
return withNilProperty(\Article.status)
}
} }

View File

@ -12,20 +12,15 @@ import Data
extension ArticleStatus { extension ArticleStatus {
convenience init?(articleID: String, row: FMResultSet) { convenience init(articleID: String, dateArrived: Date, row: FMResultSet) {
let read = row.bool(forColumn: DatabaseKey.read) let read = row.bool(forColumn: DatabaseKey.read)
let starred = row.bool(forColumn: DatabaseKey.starred) let starred = row.bool(forColumn: DatabaseKey.starred)
let userDeleted = row.bool(forColumn: DatabaseKey.userDeleted) let userDeleted = row.bool(forColumn: DatabaseKey.userDeleted)
var dateArrived = row.date(forColumn: DatabaseKey.dateArrived)
if (dateArrived == nil) {
dateArrived = NSDate.distantPast
}
// let accountInfoPlist = accountInfoWithRow(row) // 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 { func databaseDictionary() -> NSDictionary {

View File

@ -40,9 +40,41 @@ final class StatusesTable: DatabaseTable {
// TODO // 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<Article>, _ database: FMDatabase) {
let articlesNeedingStatuses = articles.missingStatuses()
if articlesNeedingStatuses.isEmpty {
return
}
createAndSaveStatusesForArticles(articlesNeedingStatuses, database)
attachCachedStatuses(articlesNeedingStatuses)
assert(articles.eachHasAStatus())
}
// func markArticles(_ articles: Set<Article>, statusKey: String, flag: Bool) { // func markArticles(_ articles: Set<Article>, statusKey: String, flag: Bool) {
// //
@ -113,33 +145,16 @@ final class StatusesTable: DatabaseTable {
} }
private extension StatusesTable { private extension StatusesTable {
// MARK: Fetching
func statusWithRow(_ row: FMResultSet) -> ArticleStatus? {
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { func attachCachedStatuses(_ articles: Set<Article>) {
return nil
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<Article>) {
//
// articles.forEach { (oneArticle) in
//
// if let cachedStatus = cache[oneArticle.databaseID] {
// oneArticle.status = cachedStatus
// }
// }
// }
// func assertNoMissingStatuses(_ articles: Set<Article>) { // func assertNoMissingStatuses(_ articles: Set<Article>) {
// //
// for oneArticle in articles { // for oneArticle in articles {
@ -220,26 +235,27 @@ private extension StatusesTable {
// MARK: Creating // MARK: Creating
// func saveStatuses(_ statuses: Set<ArticleStatus>, _ database: FMDatabase) { func saveStatuses(_ statuses: Set<ArticleStatus>, _ database: FMDatabase) {
//
// let statusArray = statuses.map { $0.databaseDictionary() } let statusArray = statuses.map { $0.databaseDictionary() }
// insertRows(statusArray, insertType: .orIgnore, in: database) insertRows(statusArray, insertType: .orIgnore, in: database)
// } }
//
// func createAndSaveStatusesForArticles(_ articles: Set<Article>, _ database: FMDatabase) { func createAndSaveStatusesForArticles(_ articles: Set<Article>, _ database: FMDatabase) {
//
// let articleIDs = Set(articles.map { $0.databaseID }) let articleIDs = Set(articles.map { $0.articleID })
// createAndSaveStatusesForArticleIDs(articleIDs, database) createAndSaveStatusesForArticleIDs(articleIDs, database)
// } }
//
// func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) { func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
//
// let now = Date() let now = Date()
// let statuses = articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) } let statuses = articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) }
// cache.addObjectsNotCached(statuses) let databaseObjects = statuses.map { $0 as DatabaseObject }
// cache.addObjectsNotCached(databaseObjects)
// saveStatuses(Set(statuses), database)
// } saveStatuses(Set(statuses), database)
}
// MARK: Utilities // MARK: Utilities