From 7680760537f0e9f4355d28058909f6cec5c435a5 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 9 Sep 2017 12:57:24 -0700 Subject: [PATCH] Fix some Database.framework build errors. Add Author cache. --- Frameworks/Database/ArticlesTable.swift | 6 +- Frameworks/Database/AuthorsTable.swift | 22 +---- .../Extensions/Article+Database.swift | 39 +++----- .../Database/Extensions/Author+Database.swift | 91 +++++++++++++++--- Frameworks/Database/StatusesTable.swift | 2 +- ToDo.ooutline | Bin 3097 -> 3258 bytes 6 files changed, 92 insertions(+), 68 deletions(-) diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index 2d48e5835..0531631b4 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -293,7 +293,7 @@ private extension ArticlesTable { // MARK: Update Existing Articles - func articlesWithRelatedObjectChanges(_ comparisonKeyPath: Keypath>, _ updatedArticles: Set
, _ fetchedArticles: [String: Article]) -> Set
{ + func articlesWithRelatedObjectChanges(_ comparisonKeyPath: KeyPath>, _ updatedArticles: Set
, _ fetchedArticles: [String: Article]) -> Set
{ return updatedArticles.filter{ (updatedArticle) -> Bool in if let fetchedArticle = fetchedArticles[updatedArticle.articleID] { @@ -304,7 +304,7 @@ private extension ArticlesTable { } } - func updateRelatedObjects(_ comparisonKeyPath: Keypath>, _ updatedArticles: Set
, _ fetchedArticles: [String: Article], _ lookupTable: DatabaseLookupTable, _ database: FMDatabase) { + func updateRelatedObjects(_ comparisonKeyPath: KeyPath>, _ updatedArticles: Set
, _ fetchedArticles: [String: Article], _ lookupTable: DatabaseLookupTable, _ database: FMDatabase) { let articlesWithChanges = articlesWithRelatedObjectChanges(comparisonKeyPath, updatedArticles, fetchedArticles) if !articlesWithChanges.isEmpty { @@ -312,7 +312,7 @@ private extension ArticlesTable { } } - func saveUpdatedRelatedObjects(_ updatedArticles: Set
, _fetchedArticles: [String: Article], _ database: FMDatabase) { + func saveUpdatedRelatedObjects(_ updatedArticles: Set
, _ fetchedArticles: [String: Article], _ database: FMDatabase) { updateRelatedObjects(\Article.tags, updatedArticles, fetchedArticles, tagsLookupTable, database) updateRelatedObjects(\Article.authors, updatedArticles, fetchedArticles, authorsLookupTable, database) diff --git a/Frameworks/Database/AuthorsTable.swift b/Frameworks/Database/AuthorsTable.swift index 69fe7e135..e867b644e 100644 --- a/Frameworks/Database/AuthorsTable.swift +++ b/Frameworks/Database/AuthorsTable.swift @@ -31,7 +31,7 @@ final class AuthorsTable: DatabaseRelatedObjectsTable { func objectWithRow(_ row: FMResultSet) -> DatabaseObject? { - if let author = authorWithRow(row) { + if let author = Author.authorWithRow(row) { return author as DatabaseObject } return nil @@ -42,23 +42,3 @@ final class AuthorsTable: DatabaseRelatedObjectsTable { } } -private extension AuthorsTable { - - func authorWithRow(_ row: FMResultSet) -> Author? { - - guard let authorID = row.string(forColumn: DatabaseKey.authorID) else { - return nil - } - - if let cachedAuthor = Author.cachedAuthor[authorID] { - return cachedAuthor - } - - guard let author = Author(authorID: authorID, row: row) else { - return nil - } - - cache[authorID] = author - return author - } -} diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index 20440a4e9..f25ce50c7 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -35,7 +35,7 @@ extension Article { let dateModified = row.date(forColumn: DatabaseKey.dateModified) let accountInfo: [String: Any]? = nil // TODO - self.init(account: account, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo) + self.init(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo) } init(parsedItem: ParsedItem, accountID: String, feedID: String) { @@ -44,7 +44,7 @@ extension Article { let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments) let tags = tagSetWithParsedTags(parsedItem.tags) - self.init(account: account, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: nil) + self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: nil) } func databaseDictionary() -> NSDictionary { @@ -72,7 +72,7 @@ extension Article { return d.copy() as! NSDictionary } - private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) { + private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) { if self[keyPath: comparisonKeyPath] != otherArticle[keyPath: comparisonKeyPath] { dictionary.addOptionalStringDefaultingEmpty(self[keyPath: comparisonKeyPath], key) @@ -86,8 +86,13 @@ extension Article { } let d = NSMutableDictionary() + if uniqueID != otherArticle.uniqueID { + // This should be super-rare, if ever. + if !otherArticle.uniqueID.isEmpty { + d[DatabaseKey.uniqueID] = otherArticle.uniqueID + } + } - addPossibleStringChangeWithKeyPath(\Article.uniqueID, otherArticle, DatabaseKey.uniqueID, d) addPossibleStringChangeWithKeyPath(\Article.title, otherArticle, DatabaseKey.title, d) addPossibleStringChangeWithKeyPath(\Article.contentHTML, otherArticle, DatabaseKey.contentHTML, d) addPossibleStringChangeWithKeyPath(\Article.contentText, otherArticle, DatabaseKey.contentText, d) @@ -100,12 +105,12 @@ extension Article { // If updated versions of dates are nil, and we have existing dates, keep the existing dates. // This is data that’s good to have, and it’s likely that a feed removing dates is doing so in error. - if article.datePublished != otherArticle.datePublished { + if datePublished != otherArticle.datePublished { if let updatedDatePublished = otherArticle.datePublished { d[DatabaseKey.datePublished] = updatedDatePublished } } - if article.dateModified != otherArticle.dateModified { + if dateModified != otherArticle.dateModified { if let updatedDateModified = otherArticle.dateModified { d[DatabaseKey.dateModified] = updatedDateModified } @@ -113,7 +118,7 @@ extension Article { // TODO: accountInfo - if d.isEmpty { + if d.count < 1 { return nil } @@ -143,26 +148,6 @@ extension Set where Element == Article { return Set(map { $0.databaseID }) } - func eachHasAStatus() -> Bool { - - for article in self { - if article.status == nil { - return false - } - } - return true - } - - func missingStatuses() -> Set
{ - - return Set
(self.filter { $0.status == nil }) - } - - func statuses() -> Set { - - return Set(self.flatMap { $0.status }) - } - func dictionary() -> [String: Article] { var d = [String: Article]() diff --git a/Frameworks/Database/Extensions/Author+Database.swift b/Frameworks/Database/Extensions/Author+Database.swift index 4a4a1ddc7..452d000ad 100644 --- a/Frameworks/Database/Extensions/Author+Database.swift +++ b/Frameworks/Database/Extensions/Author+Database.swift @@ -13,32 +13,39 @@ import RSParser extension Author { - init?(authorID: String, row: FMResultSet) { - - let name = row.string(forColumn: DatabaseKey.name) - let url = row.string(forColumn: DatabaseKey.url) - let avatarURL = row.string(forColumn: DatabaseKey.avatarURL) - let emailAddress = row.string(forColumn: DatabaseKey.emailAddress) - - self.init(authorID: authorID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress) - } - - init?(parsedAuthor: ParsedAuthor) { - - self.init(authorID: nil, name: parsedAuthor.name, url: parsedAuthor.url, avatarURL: parsedAuthor.avatarURL, emailAddress: parsedAuthor.emailAddress) - } - static func authorsWithParsedAuthors(_ parsedAuthors: [ParsedAuthor]?) -> Set? { + assert(!Thread.isMainThread) + guard let parsedAuthors = parsedAuthors else { return nil } - let authors = Set(parsedAuthors.flatMap { Author(parsedAuthor: $0) }) + let authors = Set(parsedAuthors.flatMap { authorWithParsedAuthor($0) }) return authors.isEmpty ? nil : authors } + + static func authorWithRow(_ row: FMResultSet) -> Author? { + + guard let authorID = row.string(forColumn: DatabaseKey.authorID) else { + return nil + } + + if let cachedAuthor = cachedAuthor(authorID) { + return cachedAuthor + } + + guard let author = Author(authorID: authorID, row: row) else { + return nil + } + + cacheAuthor(author) + return author + } } +// MARK: - DatabaseObject + extension Author: DatabaseObject { public var databaseID: String { @@ -47,3 +54,55 @@ extension Author: DatabaseObject { } } } + +// MARK: - Private + +private extension Author { + + init?(authorID: String, row: FMResultSet) { + + let name = row.string(forColumn: DatabaseKey.name) + let url = row.string(forColumn: DatabaseKey.url) + let avatarURL = row.string(forColumn: DatabaseKey.avatarURL) + let emailAddress = row.string(forColumn: DatabaseKey.emailAddress) + + self.init(authorID: authorID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress) + } + + init?(parsedAuthor: ParsedAuthor) { + + self.init(authorID: nil, name: parsedAuthor.name, url: parsedAuthor.url, avatarURL: parsedAuthor.avatarURL, emailAddress: parsedAuthor.emailAddress) + } + + static func authorWithParsedAuthor(_ parsedAuthor: ParsedAuthor) -> Author? { + + if let author = Author(parsedAuthor: parsedAuthor) { + if let authorFromCache = cachedAuthor(author.authorID) { + return authorFromCache + } + cacheAuthor(author) + return author + } + + return nil + } + + // The authorCache isn’t because we need uniquing — it’s just to cut down + // on the number of Author instances, since they would be frequently duplicated. + // (That is, a given feed might have 10 or 20 or whatever of the same Author.) + + private static var authorCache = [String: Author]() //queue-only + + static func cachedAuthor(_ authorID: String) -> Author? { + + assert(!Thread.isMainThread) + return authorCache[authorID] + } + + static func cacheAuthor(_ author: Author) { + + assert(!Thread.isMainThread) + authorCache[author.authorID] = author + } +} + diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index 2efbcd925..64905361a 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -169,7 +169,7 @@ private final class StatusCache { } } - subscript(_ articleID: String) -> ArticleStatus { + subscript(_ articleID: String) -> ArticleStatus? { get { return self[articleID] } diff --git a/ToDo.ooutline b/ToDo.ooutline index 1718e4fa036cd472e3696b24c40acf3740b5c85f..13a6de0eff4d01e9038e67eaa71f5ffce4281ceb 100644 GIT binary patch literal 3258 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 A-T(jq