Make progress on Database.framework.
This commit is contained in:
parent
2ace9ec0d2
commit
9d37d88c2f
|
@ -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) {
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */,
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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 shouldn’t 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue