diff --git a/Frameworks/Database/AttachmentsTable.swift b/Frameworks/Database/AttachmentsTable.swift index 65d9aa89d..2cf1bba8a 100644 --- a/Frameworks/Database/AttachmentsTable.swift +++ b/Frameworks/Database/AttachmentsTable.swift @@ -33,6 +33,8 @@ final class AttachmentsTable: DatabaseTable { let name: String let queue: RSDatabaseQueue + private let cacheByArticleID = ObjectCache(keyPathForID: \Attachment.articleID) + private let cacheByDatabaseID = ObjectCache(keyPathForID: \Attachment.databaseID) init(name: String, queue: RSDatabaseQueue) { @@ -115,7 +117,7 @@ final class AttachmentsTable: DatabaseTable { } } -private extension AttachmentsManager { +private extension AttachmentsTable { func deleteAttachmentsForArticles(_ articles: Set
, _ database: FMDatabase) { diff --git a/Frameworks/Database/AuthorsTable.swift b/Frameworks/Database/AuthorsTable.swift index 1547824c2..6c6db6fac 100644 --- a/Frameworks/Database/AuthorsTable.swift +++ b/Frameworks/Database/AuthorsTable.swift @@ -7,12 +7,14 @@ // import Foundation +import RSDatabase import Data final class AuthorsTable: DatabaseTable { let name: String let queue: RSDatabaseQueue + private let cache = ObjectCache(keyPathForID: \Author.databaseID) init(name: String, queue: RSDatabaseQueue) { @@ -20,30 +22,27 @@ final class AuthorsTable: DatabaseTable { self.queue = queue } - var cachedAuthors = [String: Author]() - - func cachedAuthor(_ databaseID: String) -> Author? { - - return cachedAuthors[databaseID] - } - - func cacheAuthor(_ author: Author) { - - cachedAuthors[author.databaseID] = author - } - func authorWithRow(_ row: FMResultSet) -> Author? { - - let databaseID = row.string(forColumn: DatabaseKey.databaseID) - if let author = cachedAuthor(databaseID) { - return author + + // Since: + // 1. anything to do with an FMResultSet runs inside the database serial queue, and + // 2. the cache is referenced only within this method, + // this is safe. + + guard let databaseID = row.string(forColumn: DatabaseKey.databaseID) else { + return nil + } + + if let cachedAuthor = cache[databaseID] { + return cachedAuthor } guard let author = Author(row: row) else { return nil } - - cacheAuthor(author) + + cache[databaseID] = author return author } } + diff --git a/Frameworks/Database/Constants.swift b/Frameworks/Database/Constants.swift index 84b1c9277..129bbfe21 100644 --- a/Frameworks/Database/Constants.swift +++ b/Frameworks/Database/Constants.swift @@ -11,6 +11,7 @@ import Foundation public struct DatabaseTableName { static let articles = "articles" + static let authors = "authors" static let statuses = "statuses" static let tags = "tags" static let attachments = "attachments" diff --git a/Frameworks/Database/CreateStatements.sql b/Frameworks/Database/CreateStatements.sql index 5881078c4..0e0d0ece3 100644 --- a/Frameworks/Database/CreateStatements.sql +++ b/Frameworks/Database/CreateStatements.sql @@ -2,7 +2,7 @@ CREATE TABLE if not EXISTS articles (articleID TEXT NOT NULL PRIMARY KEY, feedID CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0, accountInfo BLOB); -CREATE TABLE if not EXISTS authors (authorID TEXT NOT NULL PRIMARY KEY, name TEXT, url TEXT, avatarURL TEXT, emailAddress TEXT); +CREATE TABLE if not EXISTS authors (databaseID TEXT NOT NULL PRIMARY KEY, name TEXT, url TEXT, avatarURL TEXT, emailAddress TEXT); CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID)); CREATE TABLE if not EXISTS tags(tagName TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(tagName, articleID)); diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 929f2480e..3717eac19 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -26,9 +26,11 @@ final class Database { fileprivate let queue: RSDatabaseQueue private let databaseFile: String + private let articlesTable: ArticlesTable + private let authorsTable: AuthorsTable private let attachmentsTable: AttachmentsTable - fileprivate let statusesManager: StatusesManager - fileprivate let articleCache = ArticlesManager() + private let statusesTable: StatusesTable + private let tagsTable: TagsTable fileprivate var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! fileprivate let minimumNumberOfArticles = 10 fileprivate weak var delegate: AccountDelegate? @@ -38,8 +40,12 @@ final class Database { self.delegate = delegate self.databaseFile = databaseFile self.queue = RSDatabaseQueue(filepath: databaseFile, excludeFromBackup: false) + + self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, queue: queue) + self.authorsTable = AuthorsTable(name: DatabaseTableName.authors, queue: queue) self.attachmentsTable = AttachmentsTable(name: DatabaseTableName.attachments, queue: queue) - self.statusesManager = StatusesManager(queue: self.queue) + self.statusesTable = StatusesTable(name: DatabaseTableName.statuses, queue: queue) + self.tagsTable = TagsTable(name: DatabaseTableName.tags, 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) @@ -327,7 +333,7 @@ private extension Database { func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set
{ - let sql = "select * from articles natural join statuses where \(whereClause);" + let sql = "select * from articles where \(whereClause);" logSQL(sql) if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) { @@ -344,10 +350,15 @@ private extension Database { while (resultSet.next()) { if let oneArticle = Article(account: self.account, row: resultSet) { - oneArticle.status = ArticleStatus(row: resultSet) fetchedArticles.insert(oneArticle) } } + resultSet.close() + + statusesTable.attachStatuses(fetchedArticles, database) + authorsTable.attachAuthors(fetchedArticles, database) + tagsTable.attachTags(fetchedArticles, database) + attachmentsTable.attachAttachments(fetchedArticles, database) return fetchedArticles } diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index 6c3ddd904..2e92d784a 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */; }; + 843CB9961F34174100EE6581 /* Author+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F901F1810DD00D8E682 /* Author+Database.swift */; }; 844BEE411F0AB3AB004AB7CD /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* Database.framework */; }; 844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; }; 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; }; @@ -163,9 +164,9 @@ 84E156E91F0AB80500F8CC05 /* Database.swift */, 845580661F0AEBCD003CCFA1 /* Constants.swift */, 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */, - 84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */, 84F20F8E1F180D8700D8E682 /* AuthorsTable.swift */, 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */, + 84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */, 84BB4BA81F11A32800858766 /* TagsTable.swift */, 8461462A1F0AC44100870CB3 /* Extensions */, 84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */, @@ -467,6 +468,7 @@ 84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */, 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */, 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */, + 843CB9961F34174100EE6581 /* Author+Database.swift in Sources */, 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */, 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */, 845580721F0AEE49003CCFA1 /* AccountInfo.swift in Sources */, diff --git a/Frameworks/Database/Extensions/Attachment+Database.swift b/Frameworks/Database/Extensions/Attachment+Database.swift index d4b578dab..5676a72cc 100644 --- a/Frameworks/Database/Extensions/Attachment+Database.swift +++ b/Frameworks/Database/Extensions/Attachment+Database.swift @@ -21,7 +21,7 @@ extension Attachment { let sizeInBytes = optionalIntForColumn(row, DatabaseKey.sizeInBytes) let durationInSeconds = optionalIntForColumn(row, DatabaseKey.durationInSeconds) - init(databaseID: databaseID, articleID: articleID, url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds) + self.init(databaseID: databaseID, articleID: articleID, url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds) } private func optionalIntForColumn(_ row: FMResultSet, _ columnName: String) -> Int? { diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index 2cf9782c8..8f9b1ebf1 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -29,6 +29,25 @@ final class StatusesTable: DatabaseTable { assertNoMissingStatuses(articles) let statuses = Set(articles.flatMap { $0.status }) markArticleStatuses(statuses, statusKey: statusKey, flag: flag) + } + + func attachStatuses(_ articles: Set
, _ database: FMDatabase) { + + attachCachedStatuses(articles) + let articlesNeedingStatuses = articlesMissingStatuses(articles) + if articlesNeedingStatuses.isEmpty { + return + } + + fetchAndCacheStatusesForArticles(Set(articlesNeedingStatuses)) + attachCachedStatuses(articlesNeedingStatuses) + + // It shouldn’t happen that an Article in the database has no corresponding ArticleStatus, + // but the case should be handled anyway. + + + + } func attachCachedStatuses(_ articles: Set
) { @@ -90,7 +109,7 @@ private extension StatusesTable { // MARK: Fetching - func fetchStatusesForArticleIDs(_ articleIDs: Set, database: FMDatabase) -> Set { + func fetchStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) -> Set { if !articleIDs.isEmpty, let resultSet = selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) { return articleStatusesWithResultSet(resultSet) @@ -161,7 +180,18 @@ private extension StatusesTable { func articleIDsMissingStatuses(_ articleIDs: Set) -> Set { - return Set(articleIDs.filter { !objectWithIDIsCached[$0] }) + return Set(articleIDs.filter { !cache.objectWithIDIsCached[$0] }) + } + + func articlesMissingStatuses(_ articles: Set
) -> Set
{ + + let missing = articles.flatMap { (article) -> Article? in + if article.status == nil { + return article + } + return nil + } + return Set(missing) } } diff --git a/Frameworks/Database/TagsTable.swift b/Frameworks/Database/TagsTable.swift index 1cbd5f057..c5b375d37 100644 --- a/Frameworks/Database/TagsTable.swift +++ b/Frameworks/Database/TagsTable.swift @@ -58,7 +58,7 @@ final class TagsTable: DatabaseTable { } } -private extension TagsManager { +private extension TagsTable { func cacheTagsForArticle(_ article: Article, tags: TagNameSet) {