Make progress on Database.framework.

This commit is contained in:
Brent Simmons 2017-08-03 21:10:01 -07:00
parent 2ace9ec0d2
commit 9d37d88c2f
9 changed files with 75 additions and 30 deletions

View File

@ -33,6 +33,8 @@ final class AttachmentsTable: DatabaseTable {
let name: String let name: String
let queue: RSDatabaseQueue let queue: RSDatabaseQueue
private let cacheByArticleID = ObjectCache<Attachment>(keyPathForID: \Attachment.articleID)
private let cacheByDatabaseID = ObjectCache<Attachment>(keyPathForID: \Attachment.databaseID)
init(name: String, queue: RSDatabaseQueue) { init(name: String, queue: RSDatabaseQueue) {
@ -115,7 +117,7 @@ final class AttachmentsTable: DatabaseTable {
} }
} }
private extension AttachmentsManager { private extension AttachmentsTable {
func deleteAttachmentsForArticles(_ articles: Set<Article>, _ database: FMDatabase) { func deleteAttachmentsForArticles(_ articles: Set<Article>, _ database: FMDatabase) {

View File

@ -7,12 +7,14 @@
// //
import Foundation import Foundation
import RSDatabase
import Data import Data
final class AuthorsTable: DatabaseTable { final class AuthorsTable: DatabaseTable {
let name: String let name: String
let queue: RSDatabaseQueue let queue: RSDatabaseQueue
private let cache = ObjectCache<Author>(keyPathForID: \Author.databaseID)
init(name: String, queue: RSDatabaseQueue) { init(name: String, queue: RSDatabaseQueue) {
@ -20,30 +22,27 @@ final class AuthorsTable: DatabaseTable {
self.queue = queue 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? { func authorWithRow(_ row: FMResultSet) -> Author? {
let databaseID = row.string(forColumn: DatabaseKey.databaseID) // Since:
if let author = cachedAuthor(databaseID) { // 1. anything to do with an FMResultSet runs inside the database serial queue, and
return author // 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 { guard let author = Author(row: row) else {
return nil return nil
} }
cacheAuthor(author) cache[databaseID] = author
return author return author
} }
} }

View File

@ -11,6 +11,7 @@ import Foundation
public struct DatabaseTableName { public struct DatabaseTableName {
static let articles = "articles" static let articles = "articles"
static let authors = "authors"
static let statuses = "statuses" static let statuses = "statuses"
static let tags = "tags" static let tags = "tags"
static let attachments = "attachments" static let attachments = "attachments"

View File

@ -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 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 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)); CREATE TABLE if not EXISTS tags(tagName TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(tagName, articleID));

View File

@ -26,9 +26,11 @@ final class Database {
fileprivate let queue: RSDatabaseQueue fileprivate let queue: RSDatabaseQueue
private let databaseFile: String private let databaseFile: String
private let articlesTable: ArticlesTable
private let authorsTable: AuthorsTable
private let attachmentsTable: AttachmentsTable private let attachmentsTable: AttachmentsTable
fileprivate let statusesManager: StatusesManager private let statusesTable: StatusesTable
fileprivate let articleCache = ArticlesManager() private let tagsTable: TagsTable
fileprivate var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! fileprivate var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)!
fileprivate let minimumNumberOfArticles = 10 fileprivate let minimumNumberOfArticles = 10
fileprivate weak var delegate: AccountDelegate? fileprivate weak var delegate: AccountDelegate?
@ -38,8 +40,12 @@ final class Database {
self.delegate = delegate self.delegate = delegate
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, queue: queue)
self.authorsTable = AuthorsTable(name: DatabaseTableName.authors, queue: queue)
self.attachmentsTable = AttachmentsTable(name: DatabaseTableName.attachments, 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 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)
@ -327,7 +333,7 @@ private extension Database {
func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set<Article> { func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set<Article> {
let sql = "select * from articles natural join statuses where \(whereClause);" let sql = "select * from articles where \(whereClause);"
logSQL(sql) logSQL(sql)
if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) { if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) {
@ -344,10 +350,15 @@ private extension Database {
while (resultSet.next()) { while (resultSet.next()) {
if let oneArticle = Article(account: self.account, row: resultSet) { if let oneArticle = Article(account: self.account, row: resultSet) {
oneArticle.status = ArticleStatus(row: resultSet)
fetchedArticles.insert(oneArticle) fetchedArticles.insert(oneArticle)
} }
} }
resultSet.close()
statusesTable.attachStatuses(fetchedArticles, database)
authorsTable.attachAuthors(fetchedArticles, database)
tagsTable.attachTags(fetchedArticles, database)
attachmentsTable.attachAttachments(fetchedArticles, database)
return fetchedArticles return fetchedArticles
} }

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */; }; 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 */; }; 844BEE411F0AB3AB004AB7CD /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* Database.framework */; };
844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; }; 844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; };
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; }; 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; };
@ -163,9 +164,9 @@
84E156E91F0AB80500F8CC05 /* Database.swift */, 84E156E91F0AB80500F8CC05 /* Database.swift */,
845580661F0AEBCD003CCFA1 /* Constants.swift */, 845580661F0AEBCD003CCFA1 /* Constants.swift */,
84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */, 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */,
84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */,
84F20F8E1F180D8700D8E682 /* AuthorsTable.swift */, 84F20F8E1F180D8700D8E682 /* AuthorsTable.swift */,
840405CE1F1A963700DF0296 /* AttachmentsTable.swift */, 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */,
84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */,
84BB4BA81F11A32800858766 /* TagsTable.swift */, 84BB4BA81F11A32800858766 /* TagsTable.swift */,
8461462A1F0AC44100870CB3 /* Extensions */, 8461462A1F0AC44100870CB3 /* Extensions */,
84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */, 84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */,
@ -467,6 +468,7 @@
84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */, 84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */,
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */, 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */,
840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */, 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */,
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */,
845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */, 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */,
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */, 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */,
845580721F0AEE49003CCFA1 /* AccountInfo.swift in Sources */, 845580721F0AEE49003CCFA1 /* AccountInfo.swift in Sources */,

View File

@ -21,7 +21,7 @@ extension Attachment {
let sizeInBytes = optionalIntForColumn(row, DatabaseKey.sizeInBytes) let sizeInBytes = optionalIntForColumn(row, DatabaseKey.sizeInBytes)
let durationInSeconds = optionalIntForColumn(row, DatabaseKey.durationInSeconds) 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? { private func optionalIntForColumn(_ row: FMResultSet, _ columnName: String) -> Int? {

View File

@ -29,6 +29,25 @@ final class StatusesTable: DatabaseTable {
assertNoMissingStatuses(articles) assertNoMissingStatuses(articles)
let statuses = Set(articles.flatMap { $0.status }) let statuses = Set(articles.flatMap { $0.status })
markArticleStatuses(statuses, statusKey: statusKey, flag: flag) markArticleStatuses(statuses, statusKey: statusKey, flag: flag)
}
func attachStatuses(_ articles: Set<Article>, _ database: FMDatabase) {
attachCachedStatuses(articles)
let articlesNeedingStatuses = articlesMissingStatuses(articles)
if articlesNeedingStatuses.isEmpty {
return
}
fetchAndCacheStatusesForArticles(Set(articlesNeedingStatuses))
attachCachedStatuses(articlesNeedingStatuses)
// It shouldnt happen that an Article in the database has no corresponding ArticleStatus,
// but the case should be handled anyway.
} }
func attachCachedStatuses(_ articles: Set<Article>) { func attachCachedStatuses(_ articles: Set<Article>) {
@ -90,7 +109,7 @@ private extension StatusesTable {
// MARK: Fetching // MARK: Fetching
func fetchStatusesForArticleIDs(_ articleIDs: Set<String>, database: FMDatabase) -> Set<ArticleStatus> { func fetchStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) -> Set<ArticleStatus> {
if !articleIDs.isEmpty, let resultSet = selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) { if !articleIDs.isEmpty, let resultSet = selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) {
return articleStatusesWithResultSet(resultSet) return articleStatusesWithResultSet(resultSet)
@ -161,7 +180,18 @@ private extension StatusesTable {
func articleIDsMissingStatuses(_ articleIDs: Set<String>) -> Set<String> { func articleIDsMissingStatuses(_ articleIDs: Set<String>) -> Set<String> {
return Set(articleIDs.filter { !objectWithIDIsCached[$0] }) return Set(articleIDs.filter { !cache.objectWithIDIsCached[$0] })
}
func articlesMissingStatuses(_ articles: Set<Article>) -> Set<Article> {
let missing = articles.flatMap { (article) -> Article? in
if article.status == nil {
return article
}
return nil
}
return Set(missing)
} }
} }

View File

@ -58,7 +58,7 @@ final class TagsTable: DatabaseTable {
} }
} }
private extension TagsManager { private extension TagsTable {
func cacheTagsForArticle(_ article: Article, tags: TagNameSet) { func cacheTagsForArticle(_ article: Article, tags: TagNameSet) {