diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index 21bc02135..5552e8ed4 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -175,14 +175,56 @@ private extension ArticlesTable { // Note: the row is a result of a JOIN query with the statuses table, // so we can get the status at the same time and avoid additional database lookups. - article.status = statusesTable.statusWithRow(row) +// article.status = statusesTable.statusWithRow(row) return article } func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set
{ - let articles = resultSet.mapToSet(articleWithRow) - attachRelatedObjects(articles, database) + // Create set of stub Articles without related objects. + // Then fetch the related objects, given the set of articleIDs. + // Then create set of Articles *with* related objects and return it. + + let stubArticles = resultSet.mapToSet(articleWithRow) + if stubArticles.isEmpty { + return stubArticles + } + + // Fetch related objects. + + let articleIDs = stubArticles.articleIDs() + let authorsMap = authorsLookupTable.fetchRelatedObjects(for: articleIDs, in: database) + let attachmentsMap = attachmentsLookupTable.fetchRelatedObjects(for: articleIDs, in: database) + let tagsMap = tagsLookupTable.fetchRelatedObjects(for: articleIDs, in: database) + + if authorsMap == nil && attachmentsMap == nil && tagsMap == nil { + return stubArticles + } + + // Create articles with related objects. + + var articles = Set
() + for stubArticle in articles { + + var authors: Set? = nil + var attachments: Set? = nil + var tags: Set? = nil + let articleID = stubArticle.articleID + + if let authorsMap = authorsMap { + authors = authorsMap.authors(for: articleID) + } + if let attachmentsMap = attachmentsMap { + attachments = attachmentsMap.attachments(for: articleID) + } + if let tagsMap = tagsMap { + tags = tagsMap.tags(for: articleID) + } + + let realArticle = stubArticle.articleByAttaching(authors, attachments, tags) + articles.insert(realArticle) + } + return articles } @@ -378,13 +420,13 @@ private extension ArticlesTable { // 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 { + guard let fetchedArticle = fetchedArticles[updatedArticle.articleID] else { assertionFailure("Expected to find matching fetched article."); saveNewArticles(Set([updatedArticle]), database) return } - guard let changesDictionary = updatedArticle.changesFrom(fetchedArticle), !changesDictionary.isEmpty else { + guard let changesDictionary = updatedArticle.changesFrom(fetchedArticle), changesDictionary.count > 0 else { // Not unexpected. There may be no changes. return } diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index 4143635bc..d9e4bf76d 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -8,12 +8,13 @@ /* Begin PBXBuildFile section */ 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */; }; + 84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */; }; + 84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.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 */; }; 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580751F0AF670003CCFA1 /* Article+Database.swift */; }; - 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580771F0AF678003CCFA1 /* Folder+Database.swift */; }; 8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; }; 8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */; }; 846146271F0ABC7B00870CB3 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846146241F0ABC7400870CB3 /* RSParser.framework */; }; @@ -113,13 +114,14 @@ /* Begin PBXFileReference section */ 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsTable.swift; sourceTree = ""; }; + 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseObject+Database.swift"; sourceTree = ""; }; + 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelatedObjectsMap+Database.swift"; sourceTree = ""; }; 844BEE371F0AB3AA004AB7CD /* Database.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Database.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 844BEE401F0AB3AB004AB7CD /* DatabaseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatabaseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = ""; }; 844BEE471F0AB3AB004AB7CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 845580661F0AEBCD003CCFA1 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 845580751F0AF670003CCFA1 /* Article+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Article+Database.swift"; path = "Extensions/Article+Database.swift"; sourceTree = ""; }; - 845580771F0AF678003CCFA1 /* Folder+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Folder+Database.swift"; path = "Extensions/Folder+Database.swift"; sourceTree = ""; }; 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ArticleStatus+Database.swift"; path = "Extensions/ArticleStatus+Database.swift"; sourceTree = ""; }; 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Attachment+Database.swift"; path = "Extensions/Attachment+Database.swift"; sourceTree = ""; }; 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; @@ -212,13 +214,14 @@ 8461462A1F0AC44100870CB3 /* Extensions */ = { isa = PBXGroup; children = ( - 845580771F0AF678003CCFA1 /* Folder+Database.swift */, 846FB36A1F4A937B00EAB81D /* Feed+Database.swift */, 845580751F0AF670003CCFA1 /* Article+Database.swift */, 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */, 84F20F901F1810DD00D8E682 /* Author+Database.swift */, 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */, 84D0DEA01F4A429800073503 /* String+Database.swift */, + 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */, + 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */, ); name = Extensions; sourceTree = ""; @@ -472,13 +475,14 @@ 84D0DEA11F4A429800073503 /* String+Database.swift in Sources */, 843CB9961F34174100EE6581 /* Author+Database.swift in Sources */, 848AD2961F58A91E004FB0EC /* UnreadCountDictionary.swift in Sources */, - 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */, 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */, 8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */, 8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */, + 84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */, 84BB4BA91F11A32800858766 /* TagsTable.swift in Sources */, 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */, 84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */, + 84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */, 84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */, 84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */, 84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */, diff --git a/Frameworks/Database/DatabaseObject+Database.swift b/Frameworks/Database/DatabaseObject+Database.swift new file mode 100644 index 000000000..c5a48db67 --- /dev/null +++ b/Frameworks/Database/DatabaseObject+Database.swift @@ -0,0 +1,32 @@ +// +// DatabaseObject+Database.swift +// Database +// +// Created by Brent Simmons on 9/13/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSDatabase +import Data + +extension Array where Element == DatabaseObject { + + func asTags() -> Set? { + + let tags = Set(self.map { $0 as! String }) + return tags.isEmpty ? nil : tags + } + + func asAuthors() -> Set? { + + let authors = Set(self.map { $0 as! Author }) + return authors.isEmpty ? nil : authors + } + + func asAttachments() -> Set? { + + let attachments = Set(self.map { $0 as! Attachment }) + return attachments.isEmpty ? nil : attachments + } +} diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index f001671ee..e26db7cba 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -13,7 +13,7 @@ import RSParser extension Article { - init?(row: FMResultSet, authors: Set, attachments: Set, tags: Set, accountID: String) { + init?(row: FMResultSet, accountID: String, authors: Set? = nil, attachments: Set? = nil, tags: Set? = nil) { guard let feedID = row.string(forColumn: DatabaseKey.feedID) else { return nil @@ -40,12 +40,21 @@ extension Article { init(parsedItem: ParsedItem, accountID: String, feedID: String) { - let authors = parsedItem.authors?.authors() + let authors = Author.authorsWithParsedAuthors(parsedItem.authors) let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments) 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: parsedItem.tags, attachments: attachments, accountInfo: nil) } + func articleByAttaching(_ authors: Set?, _ attachments: Set?, _ tags: Set?) -> Article { + + if authors == nil && attachments == nil && tags == nil { + return self + } + + return Article(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) + } + private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) { if self[keyPath: comparisonKeyPath] != otherArticle[keyPath: comparisonKeyPath] { diff --git a/Frameworks/Database/Extensions/Author+Database.swift b/Frameworks/Database/Extensions/Author+Database.swift index 48dcedb91..fd3bb89e6 100644 --- a/Frameworks/Database/Extensions/Author+Database.swift +++ b/Frameworks/Database/Extensions/Author+Database.swift @@ -30,6 +30,16 @@ extension Author { self.init(authorID: nil, name: parsedAuthor.name, url: parsedAuthor.url, avatarURL: parsedAuthor.avatarURL, emailAddress: parsedAuthor.emailAddress) } + + static func authorsWithParsedAuthors(_ parsedAuthors: Set?) -> Set? { + + guard let parsedAuthors = parsedAuthors else { + return nil + } + + let authors = Set(parsedAuthors.flatMap { Author(parsedAuthor: $0) }) + return authors.isEmpty ? nil: authors + } } extension Author: DatabaseObject { @@ -42,25 +52,24 @@ extension Author: DatabaseObject { public func databaseDictionary() -> NSDictionary? { - var d = NSMutableDictionary() + let d = NSMutableDictionary() - // TODO - - if d.count < 1 { - return nil + d[DatabaseKey.authorID] = authorID + + if let name = name { + d[DatabaseKey.name] = name } + if let url = url { + d[DatabaseKey.url] = url + } + if let avatarURL = avatarURL { + d[DatabaseKey.avatarURL] = avatarURL + } + if let emailAddress = emailAddress { + d[DatabaseKey.emailAddress] = emailAddress + } + return (d.copy() as! NSDictionary) } } -// MARK: Author creation from Set - -extension Set where Element == ParsedAuthor { - - func authors() -> Set? { - - let createdAuthors = Set(self.flatMap { Author(parsedAuthor: $0) }) - return createdAuthors.isEmpty ? nil: createdAuthors - } -} - diff --git a/Frameworks/Database/Extensions/Folder+Database.swift b/Frameworks/Database/Extensions/Folder+Database.swift deleted file mode 100644 index 7579e5d37..000000000 --- a/Frameworks/Database/Extensions/Folder+Database.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Folder+Database.swift -// Database -// -// Created by Brent Simmons on 7/3/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import Foundation -import Data - -extension Folder { - - func flattenedFeedIDs() -> [String] { - - return flattenedFeeds().map { $0.feedID } - } -} diff --git a/Frameworks/Database/Extensions/String+Database.swift b/Frameworks/Database/Extensions/String+Database.swift index 8a9cbbe66..b6e3d7f70 100644 --- a/Frameworks/Database/Extensions/String+Database.swift +++ b/Frameworks/Database/Extensions/String+Database.swift @@ -17,10 +17,8 @@ extension String: DatabaseObject { public func databaseDictionary() -> NSDictionary? { preconditionFailure("databaseDictionary() called for a tag: this should never happen.") - return nil // unused } - public var databaseID: String { get { return self diff --git a/Frameworks/Database/RelatedObjectsMap+Database.swift b/Frameworks/Database/RelatedObjectsMap+Database.swift new file mode 100644 index 000000000..867755a6a --- /dev/null +++ b/Frameworks/Database/RelatedObjectsMap+Database.swift @@ -0,0 +1,38 @@ +// +// RelatedObjectsMap+Database.swift +// Database +// +// Created by Brent Simmons on 9/13/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSDatabase +import Data + +extension RelatedObjectsMap { + + func attachments(for articleID: String) -> Set? { + + if let objects = self[articleID] { + return objects.asAttachments() + } + return nil + } + + func authors(for articleID: String) -> Set? { + + if let objects = self[articleID] { + return objects.asAuthors() + } + return nil + } + + func tags(for articleID: String) -> Set? { + + if let objects = self[articleID] { + return objects.asTags() + } + return nil + } +} diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index 6a8c15157..9f14fd8a1 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -54,13 +54,12 @@ final class StatusesTable: DatabaseTable { fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus) { let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs) - if articleIDsNeedingStatus.isEmpty { - completion(self.statusesDictionary(articleIDs)) - return + if !articleIDsNeedingStatus.isEmpty { + // Create new statuses. + self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus) } - // Create new statuses. - self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, completion) + completion(self.statusesDictionary(articleIDs)) } } @@ -121,12 +120,12 @@ private extension StatusesTable { func saveStatuses(_ statuses: Set) { queue.update { (database) in - let statusArray = statuses.map { $0.databaseDictionary() } + let statusArray = statuses.map { $0.databaseDictionary()! } self.insertRows(statusArray, insertType: .orIgnore, in: database) } } - func createAndSaveStatusesForArticleIDs(_ articleIDs: Set, _ completion: @escaping RSVoidCompletionBlock) { + func createAndSaveStatusesForArticleIDs(_ articleIDs: Set) { assert(Thread.isMainThread) @@ -134,10 +133,6 @@ private extension StatusesTable { let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) }) cache.addIfNotCached(statuses) - // No need to wait for database to return before calling completion, - // since the new statuses have been cached at this point. - - completion() saveStatuses(statuses) } diff --git a/ToDo.opml b/ToDo.opml index c3d4d5b15..f2d6ad87b 100644 --- a/ToDo.opml +++ b/ToDo.opml @@ -6,12 +6,12 @@ --> ToDo Tue, 12 Sep 2017 20:15:17 GMT - 11,16,17,20,24,29,31,34,37,39,41,43,47,52,55,57,59,61,63,72,77,79,85,90 - 0 - 209 + 11,16,17,20,24,29,31,34,37,39,40,44,49,52,54,56,58,67,72,78,83 + 59 + 248 30 762 - 968 + 1007 @@ -63,20 +63,16 @@ - - - - - + @@ -91,9 +87,6 @@ - - - @@ -111,9 +104,6 @@ - - -