From 3c47b23b8bd6509e7199e2abc44d1838ac66bc26 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 9 Sep 2017 12:09:48 -0700 Subject: [PATCH] Add article.changesFrom() to get changes in an Article. --- Frameworks/Database/ArticlesTable.swift | 36 +++-- .../Extensions/Article+Database.swift | 145 +++++++----------- Frameworks/RSDatabase/DatabaseTable.swift | 5 + 3 files changed, 89 insertions(+), 97 deletions(-) diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index fecb0c949..2c186b4fd 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -442,20 +442,34 @@ private extension ArticlesTable { saveUpdatedRelatedObjects(updatedArticles, fetchedArticles, database) + for updatedArticle in updatedArticles { + guard let fetchedArticle = fetchedArticle[updatedArticle.articleID] else { + assertionFailure("Expected to find matching fetched article."); + + } + if let changesDictionary = updatedArticle.changesFrom(fetchedArticles[upd]) + } } - func articlesWithParsedItems(_ parsedItems: Set, _ feed: Feed) -> Set
{ - - assert(!Thread.isMainThread) - let feedID = feed.feedID - return Set(parsedItems.flatMap{ articleWithParsedItem($0, feedID) }) + func saveUpdatedArticle(_ updatedArticle: Article, _ fetchedArticles: [String: Article], _ database: FMDatabase) { + + // Only update exactly what has changed in the Article (if anything). + // Untested theory: this gets us better performance and less database fragmentation. + + guard let fetchedArticle = fetchedArticle[updatedArticle.articleID] else { + assertionFailure("Expected to find matching fetched article."); + saveNewArticles(Set([updatedArticle]), database) + return + } + + guard let changesDictionary = updatedArticle.changesFrom(fetchedArticle), !changesDictionary.isEmpty else { + // Not unexpected. There may be no changes. + return + } + + updateRowsWithDictionary(changesDictionary, whereKey: DatabaseKey.articleID, matches: updatedArticle.articleID, database: database) } - - func articleWithParsedItem(_ parsedItem: ParsedItem, _ feedID: String) -> Article? { - - return Article(parsedItem: parsedItem, feedID: feedID, accountID: accountID) - } - + func statusIndicatesArticleIsIgnorable(_ status: ArticleStatus) -> Bool { // Ignorable articles: either userDeleted==1 or (not starred and arrival date > 4 months). diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index bf487b4fa..20440a4e9 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -57,6 +57,7 @@ extension Article { d.addOptionalString(title, DatabaseKey.title) d.addOptionalString(contentHTML, DatabaseKey.contentHTML) + d.addOptionalString(contentText, DatabaseKey.contentText) d.addOptionalString(url, DatabaseKey.url) d.addOptionalString(externalURL, DatabaseKey.externalURL) d.addOptionalString(summary, DatabaseKey.summary) @@ -70,98 +71,60 @@ extension Article { return d.copy() as! NSDictionary } + + private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) { + + if self[keyPath: comparisonKeyPath] != otherArticle[keyPath: comparisonKeyPath] { + dictionary.addOptionalStringDefaultingEmpty(self[keyPath: comparisonKeyPath], key) + } + } + + func changesFrom(_ otherArticle: Article) -> NSDictionary? { + + if self == otherArticle { + return nil + } + + let d = NSMutableDictionary() + + 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) + addPossibleStringChangeWithKeyPath(\Article.url, otherArticle, DatabaseKey.url, d) + addPossibleStringChangeWithKeyPath(\Article.externalURL, otherArticle, DatabaseKey.externalURL, d) + addPossibleStringChangeWithKeyPath(\Article.summary, otherArticle, DatabaseKey.summary, d) + addPossibleStringChangeWithKeyPath(\Article.imageURL, otherArticle, DatabaseKey.imageURL, d) + addPossibleStringChangeWithKeyPath(\Article.bannerImageURL, otherArticle, DatabaseKey.bannerImageURL, d) + + // 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 let updatedDatePublished = otherArticle.datePublished { + d[DatabaseKey.datePublished] = updatedDatePublished + } + } + if article.dateModified != otherArticle.dateModified { + if let updatedDateModified = otherArticle.dateModified { + d[DatabaseKey.dateModified] = updatedDateModified + } + } + + // TODO: accountInfo + + if d.isEmpty { + return nil + } + + return d + } static func articlesWithParsedItems(_ parsedItems: [ParsedItem], _ accountID: String, _ feedID: String) -> Set
{ return Set(parsedItems.map{ Article(parsedItem: $0, accountID: accountID, feedID: feedID) }) } - // MARK: Updating with ParsedItem - -// func updateTagsWithParsedTags(_ parsedTags: [String]?) -> Bool { -// -// // Return true if there's a change. -// -// let currentTags = tags -// -// if parsedTags == nil && currentTags == nil { -// return false -// } -// if parsedTags != nil && currentTags == nil { -// tags = Set(parsedItemTags!) -// return true -// } -// if parsedTags == nil && currentTags != nil { -// tags = nil -// return true -// } -// let parsedTagSet = Set(parsedTags!) -// if parsedTagSet == tags! { -// return false -// } -// tags = parsedTagSet -// return true -// } -// -// func updateAttachmentsWithParsedAttachments(_ parsedAttachments: [ParsedAttachment]?) -> Bool { -// -// // Return true if there's a change. -// -// let currentAttachments = attachments -// let updatedAttachments = Attachment.attachmentsWithParsedAttachments(parsedAttachments) -// -// if updatedAttachments == nil && currentAttachments == nil { -// return false -// } -// if updatedAttachments != nil && currentAttachments == nil { -// attachments = updatedAttachments -// return true -// } -// if updatedAttachments == nil && currentAttachments != nil { -// attachments = nil -// return true -// } -// -// guard let currentAttachments = currentAttachments, let updatedAttachments = updatedAttachments else { -// assertionFailure("currentAttachments and updatedAttachments must both be non-nil.") -// return false -// } -// if currentAttachments != updatedAttachments { -// attachments = updatedAttachments -// return true -// } -// return false -// } -// -// func updateAuthorsWithParsedAuthors(_ parsedAuthors: [ParsedAuthor]?) -> Bool { -// -// // Return true if there's a change. -// -// let currentAuthors = authors -// let updatedAuthors = Author.authorsWithParsedAuthors(parsedAuthors) -// -// if updatedAuthors == nil && currentAuthors == nil { -// return false -// } -// if updatedAuthors != nil && currentAuthors == nil { -// authors = updatedAuthors -// return true -// } -// if updatedAuthors == nil && currentAuthors != nil { -// authors = nil -// return true -// } -// -// guard let currentAuthors = currentAuthors, let updatedAuthors = updatedAuthors else { -// assertionFailure("currentAuthors and updatedAuthors must both be non-nil.") -// return false -// } -// if currentAuthors != updatedAuthors { -// authors = updatedAuthors -// return true -// } -// return false -// } } extension Article: DatabaseObject { @@ -224,6 +187,16 @@ private extension NSMutableDictionary { } } + func addOptionalStringDefaultingEmpty(_ value: String?, _ key: String) { + + if let value = value { + self[key] = value + } + else { + self[key] = "" + } + } + func addOptionalDate(_ date: Date?, _ key: String) { if let date = date { diff --git a/Frameworks/RSDatabase/DatabaseTable.swift b/Frameworks/RSDatabase/DatabaseTable.swift index 4f0f80b43..d7ea0df15 100644 --- a/Frameworks/RSDatabase/DatabaseTable.swift +++ b/Frameworks/RSDatabase/DatabaseTable.swift @@ -47,6 +47,11 @@ public extension DatabaseTable { let _ = database.rs_updateRows(withValue: value, valueKey: valueKey, whereKey: whereKey, inValues: matches, tableName: self.name) } + public func updateRowsWithDictionary(_ dictionary: NSDictionary, whereKey: String, matches: Any, database: FMDatabase) { + + let _ = database.rs_updateRows(with: dictionary as! [AnyHashable : Any], whereKey: whereKey, equalsValue: matches, tableName: self.name) + } + // MARK: Saving public func insertRows(_ dictionaries: [NSDictionary], insertType: RSDatabaseInsertType, in database: FMDatabase) {