Make progress fetching articles.
This commit is contained in:
parent
2cefb87f20
commit
2d1c63403d
@ -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, 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<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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user