diff --git a/Frameworks/Data/DatabaseID.swift b/Frameworks/Data/DatabaseID.swift index 2d3ae1304..2729a04b2 100644 --- a/Frameworks/Data/DatabaseID.swift +++ b/Frameworks/Data/DatabaseID.swift @@ -14,9 +14,15 @@ import RSCore // * Collisions aren’t going to happen with feed data private var databaseIDCache = [String: String]() +private var databaseIDCacheLock = os_unfair_lock_s() public func databaseIDWithString(_ s: String) -> String { + os_unfair_lock_lock(&databaseIDCacheLock) + defer { + os_unfair_lock_unlock(&databaseIDCacheLock) + } + if let identifier = databaseIDCache[s] { return identifier } diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index c8521b273..99eda3874 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -85,52 +85,27 @@ final class ArticlesTable: DatabaseTable { return } - // 1. Ensure statuses for all the parsedItems. - // 2. Ignore parsedItems that are userDeleted || (!starred and really old) - // 3. Fetch all articles for the feed. - // 4. Create Articles with parsedItems. + // 1. Create incoming articles with parsedItems. + // 2. Ensure statuses for all the incoming articles. + // 3. Ignore incoming articles that are userDeleted || (!starred and really old) + // 4. Fetch all articles for the feed. // 5. Create array of Articles not in database and save them. // 6. Create array of updated Articles and save what’s changed. // 7. Call back with new and updated Articles. let feedID = feed.feedID - let parsedItemArticleIDs = Set(parsedFeed.items.map { $0.databaseIdentifierWithFeed(feed) }) - - statusesTable.ensureStatusesForArticleIDs(parsedItemArticleIDs) { (statusesDictionary) in // 1 - let filteredParsedItems = self.filterParsedItems(Set(parsedFeed.items), statusesDictionary) // 2 - if filteredParsedItems.isEmpty { - completion(nil, nil) + self.queue.run { (database) in + + // This doesn’t hit the database, but it should be done on the database queue. + let allIncomingArticles = Article.articlesWithParsedItems(parsedFeed.items, self.accountID, feedID) //1 + if allIncomingArticles.isEmpty { + self.callUpdateArticlesCompletionBlock(nil, nil, completion) return } - - self.queue.update{ (database) in - - let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database: database) //3 - let fetchedArticlesDictionary = fetchedArticles.dictionary() - - let incomingArticles = Article.articlesWithParsedItems(filteredParsedItems, self.accountID, feedID) //4 - - let newArticles = Set(incomingArticles.filter { fetchedArticlesDictionary[$0.articleID] == nil }) //5 - if !newArticles.isEmpty { - self.saveNewArticles(newArticles, database) - } - - let updatedArticles = incomingArticles.filter{ (incomingArticle) -> Bool in //6 - if let existingArticle = fetchedArticlesDictionary[incomingArticle.articleID] { - if existingArticle != incomingArticle { - return true - } - } - return false - } - if !updatedArticles.isEmpty { - self.saveUpdatedArticles(Set(updatedArticles), fetchedArticlesDictionary, database) - } - - DispatchQueue.main.async { - completion(newArticles, updatedArticles) //7 - } + + DispatchQueue.main.async { + self.ensureStatusesAndSaveArticles(allIncomingArticles, feedID, completion) //2-7 } } } @@ -266,8 +241,59 @@ private extension ArticlesTable { return articlesWithResultSet(resultSet, database) } + // MARK: Saving Parsed Items + + private func ensureStatusesAndSaveArticles(_ allIncomingArticles: Set
, _ feedID: String, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { + + statusesTable.ensureStatusesForArticleIDs(allIncomingArticles.articleIDs()) { (statusesDictionary) in // 2 + + self.queue.update{ (database) in + self.saveArticlesWithDatabase(allIncomingArticles, statusesDictionary, feedID, database, completion) + } + } + } + + private func saveArticlesWithDatabase(_ allIncomingArticles: Set
, _ statusesDictionary: [String: ArticleStatus], _ feedID: String, _ database: FMDatabase, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { // 3-7 + + let incomingArticles = filterIncomingArticles(allIncomingArticles, statusesDictionary) //3 + if incomingArticles.isEmpty { + callUpdateArticlesCompletionBlock(nil, nil, completion) + return + } + + let fetchedArticles = fetchArticlesForFeedID(feedID, withLimits: false, database: database) //4 + let fetchedArticlesDictionary = fetchedArticles.dictionary() + + let newArticles = findAndSaveNewArticles(incomingArticles, fetchedArticlesDictionary, database) //5 + let updatedArticles = findAndSaveUpdatedArticles(incomingArticles, fetchedArticlesDictionary, database) //6 + + callUpdateArticlesCompletionBlock(newArticles, updatedArticles, completion) + } + + func callUpdateArticlesCompletionBlock(_ newArticles: Set
?, _ updatedArticles: Set
?, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { + + DispatchQueue.main.async { + completion(newArticles, updatedArticles) + } + } + // MARK: Save New Articles + func findNewArticles(_ incomingArticles: Set
, _ fetchedArticlesDictionary: [String: Article]) -> Set
? { + + let newArticles = Set(incomingArticles.filter { fetchedArticlesDictionary[$0.articleID] == nil }) + return newArticles.isEmpty ? nil : newArticles + } + + func findAndSaveNewArticles(_ incomingArticles: Set
, _ fetchedArticlesDictionary: [String: Article], _ database: FMDatabase) -> Set
? { //5 + + guard let newArticles = findNewArticles(incomingArticles, fetchedArticlesDictionary) else { + return nil + } + self.saveNewArticles(newArticles, database) + return newArticles + } + func saveNewArticles(_ articles: Set
, _ database: FMDatabase) { saveRelatedObjectsForNewArticles(articles, database) @@ -313,6 +339,30 @@ private extension ArticlesTable { updateRelatedObjects(\Article.attachments, updatedArticles, fetchedArticles, attachmentsLookupTable, database) } + func findUpdatedArticles(_ incomingArticles: Set
, _ fetchedArticlesDictionary: [String: Article]) -> Set
? { + + let updatedArticles = incomingArticles.filter{ (incomingArticle) -> Bool in //6 + if let existingArticle = fetchedArticlesDictionary[incomingArticle.articleID] { + if existingArticle != incomingArticle { + return true + } + } + return false + } + + return updatedArticles.isEmpty ? nil : updatedArticles + } + + func findAndSaveUpdatedArticles(_ incomingArticles: Set
, _ fetchedArticlesDictionary: [String: Article], _ database: FMDatabase) -> Set
? { //6 + + guard let updatedArticles = findUpdatedArticles(incomingArticles, fetchedArticlesDictionary) else { + return nil + } + saveUpdatedArticles(Set(updatedArticles), fetchedArticlesDictionary, database) + return updatedArticles + } + + func saveUpdatedArticles(_ updatedArticles: Set
, _ fetchedArticles: [String: Article], _ database: FMDatabase) { saveUpdatedRelatedObjects(updatedArticles, fetchedArticles, database) @@ -354,16 +404,16 @@ private extension ArticlesTable { return status.dateArrived < maximumArticleCutoffDate } - func filterParsedItems(_ parsedItems: Set, _ statuses: [String: ArticleStatus]) -> Set { - - // Drop parsedItems that we can ignore. - - return Set(parsedItems.filter{ (parsedItem) -> Bool in - let articleID = parsedItem.articleID + func filterIncomingArticles(_ articles: Set
, _ statuses: [String: ArticleStatus]) -> Set
{ + + // Drop Articles that we can ignore. + + return Set(articles.filter{ (article) -> Bool in + let articleID = article.articleID if let status = statuses[articleID] { return !statusIndicatesArticleIsIgnorable(status) } - assertionFailure("Expected a status for each parsedItem.") + assertionFailure("Expected a status for each Article.") return true }) } diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index 57f35c695..4143635bc 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 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 */; }; - 844ECFC91F5B4F0E005E405A /* ParsedItem+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844ECFC81F5B4F0E005E405A /* ParsedItem+Database.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 */; }; @@ -118,7 +117,6 @@ 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 = ""; }; - 844ECFC81F5B4F0E005E405A /* ParsedItem+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "ParsedItem+Database.swift"; path = "Extensions/ParsedItem+Database.swift"; 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 = ""; }; @@ -221,7 +219,6 @@ 84F20F901F1810DD00D8E682 /* Author+Database.swift */, 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */, 84D0DEA01F4A429800073503 /* String+Database.swift */, - 844ECFC81F5B4F0E005E405A /* ParsedItem+Database.swift */, ); name = Extensions; sourceTree = ""; @@ -484,7 +481,6 @@ 84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */, 84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */, 84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */, - 844ECFC91F5B4F0E005E405A /* ParsedItem+Database.swift in Sources */, 84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/Database/Extensions/ParsedItem+Database.swift b/Frameworks/Database/Extensions/ParsedItem+Database.swift deleted file mode 100644 index cf53c8455..000000000 --- a/Frameworks/Database/Extensions/ParsedItem+Database.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ParsedItem+Database.swift -// Database -// -// Created by Brent Simmons on 9/2/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import Foundation -import RSParser -import Data - -extension ParsedItem { - - func databaseIdentifierWithFeed(_ feed: Feed) -> String { - - if let identifier = syncServiceID { - return identifier - } - - // Must be, and is, the same calculation as in Article.init. - return databaseIDWithString("\(feed.feedID) \(uniqueID)") - } -} - - - - diff --git a/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj b/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj index f8e972fda..b8968615b 100755 --- a/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj +++ b/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj @@ -33,8 +33,10 @@ 84419AE71B5ABD7F00C26BB2 /* RSDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 84419ADD1B5ABD7F00C26BB2 /* RSDatabaseQueue.m */; }; 84419B051B5ABFF700C26BB2 /* FMResultSet+RSExtras.h in Headers */ = {isa = PBXBuildFile; fileRef = 84419B031B5ABFF700C26BB2 /* FMResultSet+RSExtras.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84419B061B5ABFF700C26BB2 /* FMResultSet+RSExtras.m in Sources */ = {isa = PBXBuildFile; fileRef = 84419B041B5ABFF700C26BB2 /* FMResultSet+RSExtras.m */; }; - 844D97411F2D32F300CEDDEA /* DatabaseObjectCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844D97401F2D32F300CEDDEA /* DatabaseObjectCache.swift */; }; 844ECFB91F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */; }; + 848E22541F6652990031D7C5 /* DatabaseRelatedObjectsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */; }; + 848E22561F6652C70031D7C5 /* RelatedObjectsLookupTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848E22551F6652C70031D7C5 /* RelatedObjectsLookupTable.swift */; }; + 848E22581F6653960031D7C5 /* RelatedObjectIDsLookupTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848E22571F6653960031D7C5 /* RelatedObjectIDsLookupTable.swift */; }; 849BF8C61C94FB8E0071D1DA /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 849BF8C51C94FB8E0071D1DA /* libsqlite3.tbd */; }; 84ABC1D11F364B07000DCC55 /* DatabaseLookupTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ABC1D01F364B07000DCC55 /* DatabaseLookupTable.swift */; }; 84ABC1D21F364B07000DCC55 /* DatabaseLookupTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ABC1D01F364B07000DCC55 /* DatabaseLookupTable.swift */; }; @@ -73,10 +75,11 @@ 84419ADD1B5ABD7F00C26BB2 /* RSDatabaseQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RSDatabaseQueue.m; path = RSDatabase/RSDatabaseQueue.m; sourceTree = ""; }; 84419B031B5ABFF700C26BB2 /* FMResultSet+RSExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FMResultSet+RSExtras.h"; path = "RSDatabase/FMResultSet+RSExtras.h"; sourceTree = ""; }; 84419B041B5ABFF700C26BB2 /* FMResultSet+RSExtras.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FMResultSet+RSExtras.m"; path = "RSDatabase/FMResultSet+RSExtras.m"; sourceTree = ""; }; - 844D97401F2D32F300CEDDEA /* DatabaseObjectCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatabaseObjectCache.swift; path = RSDatabase/DatabaseObjectCache.swift; sourceTree = ""; }; - 844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatabaseRelatedObjectsTable.swift; path = RSDatabase/DatabaseRelatedObjectsTable.swift; sourceTree = ""; }; + 844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRelatedObjectsTable.swift; sourceTree = ""; }; + 848E22551F6652C70031D7C5 /* RelatedObjectsLookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedObjectsLookupTable.swift; sourceTree = ""; }; + 848E22571F6653960031D7C5 /* RelatedObjectIDsLookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedObjectIDsLookupTable.swift; sourceTree = ""; }; 849BF8C51C94FB8E0071D1DA /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; - 84ABC1D01F364B07000DCC55 /* DatabaseLookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatabaseLookupTable.swift; path = RSDatabase/DatabaseLookupTable.swift; sourceTree = ""; }; + 84ABC1D01F364B07000DCC55 /* DatabaseLookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseLookupTable.swift; sourceTree = ""; }; 84C6DD001F395C13009AFB47 /* DatabaseObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatabaseObject.swift; path = RSDatabase/DatabaseObject.swift; sourceTree = ""; }; 84DDF18B1C94FC45005E6CF5 /* FMDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabase.h; sourceTree = ""; }; 84DDF18C1C94FC45005E6CF5 /* FMDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabase.m; sourceTree = ""; }; @@ -136,6 +139,17 @@ name = Frameworks; sourceTree = ""; }; + 848E22531F66528C0031D7C5 /* Related Objects */ = { + isa = PBXGroup; + children = ( + 84ABC1D01F364B07000DCC55 /* DatabaseLookupTable.swift */, + 848E22551F6652C70031D7C5 /* RelatedObjectsLookupTable.swift */, + 848E22571F6653960031D7C5 /* RelatedObjectIDsLookupTable.swift */, + 844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */, + ); + path = "Related Objects"; + sourceTree = ""; + }; 84DDF18A1C94FC45005E6CF5 /* FMDB */ = { isa = PBXGroup; children = ( @@ -164,9 +178,7 @@ 84419AD91B5ABD7400C26BB2 /* NSString+RSDatabase.m */, 84C6DD001F395C13009AFB47 /* DatabaseObject.swift */, 840405DA1F1C158C00DF0296 /* DatabaseTable.swift */, - 844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */, - 84ABC1D01F364B07000DCC55 /* DatabaseLookupTable.swift */, - 844D97401F2D32F300CEDDEA /* DatabaseObjectCache.swift */, + 848E22531F66528C0031D7C5 /* Related Objects */, 84DDF18A1C94FC45005E6CF5 /* FMDB */, 84F22C5A1B52E0D9000060CE /* Info.plist */, 849BF8C51C94FB8E0071D1DA /* libsqlite3.tbd */, @@ -358,6 +370,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 848E22541F6652990031D7C5 /* DatabaseRelatedObjectsTable.swift in Sources */, 8400AC001E0CFC0700AA7C57 /* RSDatabaseQueue.m in Sources */, 8400AC061E0CFC0700AA7C57 /* NSString+RSDatabase.m in Sources */, 84ABC1D21F364B07000DCC55 /* DatabaseLookupTable.swift in Sources */, @@ -385,7 +398,8 @@ 844ECFB91F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift in Sources */, 84419B061B5ABFF700C26BB2 /* FMResultSet+RSExtras.m in Sources */, 84DDF1991C94FC45005E6CF5 /* FMDatabaseAdditions.m in Sources */, - 844D97411F2D32F300CEDDEA /* DatabaseObjectCache.swift in Sources */, + 848E22581F6653960031D7C5 /* RelatedObjectIDsLookupTable.swift in Sources */, + 848E22561F6652C70031D7C5 /* RelatedObjectsLookupTable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseObjectCache.swift b/Frameworks/RSDatabase/RSDatabase/DatabaseObjectCache.swift deleted file mode 100644 index 596ba1a68..000000000 --- a/Frameworks/RSDatabase/RSDatabase/DatabaseObjectCache.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// DatabaseObjectCache.swift -// RSDatabase -// -// Created by Brent Simmons on 7/29/17. -// Copyright © 2017 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -// Not thread-safe. - -public final class DatabaseObjectCache { - - private var dictionary = [String: DatabaseObject]() - - public init() { - // Compiler seems to want a public init method. - } - - public func addObjects(_ objects: [DatabaseObject]) { - - objects.forEach { add($0) } - } - - public func addObjectsNotCached(_ objects: [DatabaseObject]) { - - objects.forEach { addIfNotCached($0) } - } - - public func add(_ object: DatabaseObject) { - - self[object.databaseID] = object - } - - public func addIfNotCached(_ object: DatabaseObject) { - - let identifier = object.databaseID - if let _ = self[identifier] { - return - } - self[identifier] = object - } - - public func removeObjects(_ objects: [DatabaseObject]) { - - objects.forEach { removeObject($0) } - } - - public func removeObject(_ object: DatabaseObject) { - - self[object.databaseID] = nil - } - - public func uniquedObjects(_ objects: [DatabaseObject]) -> [DatabaseObject] { - - // Return cached version of each object. - // When an object is not already cached, cache it, - // then consider that version the unique version. - - return objects.map { (object) -> DatabaseObject in - - if let cachedObject = self[object.databaseID] { - return cachedObject - } - add(object) - return object - } - } - - public func objectWithIDIsCached(_ identifier: String) -> Bool { - - return self[identifier] != nil - } - - public subscript(_ identifier: String) -> DatabaseObject? { - get { - return dictionary[identifier] - } - set { - dictionary[identifier] = newValue - } - } -} - diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift b/Frameworks/RSDatabase/Related Objects/DatabaseLookupTable.swift similarity index 85% rename from Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift rename to Frameworks/RSDatabase/Related Objects/DatabaseLookupTable.swift index b0f01e579..2ff2883fc 100644 --- a/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift +++ b/Frameworks/RSDatabase/Related Objects/DatabaseLookupTable.swift @@ -20,7 +20,8 @@ public final class DatabaseLookupTable { private let relationshipName: String private let relatedTable: DatabaseRelatedObjectsTable private let cache: DatabaseLookupTableCache - + private var objectIDsWithNoRelatedObjects = Set() + public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseRelatedObjectsTable, relationshipName: String) { self.name = name @@ -31,6 +32,31 @@ public final class DatabaseLookupTable { self.cache = DatabaseLookupTableCache(relationshipName) } + public func fetchRelatedObjects(for objectIDs: Set, in database: FMDatabase) -> RelatedObjectsLookupTable? { + + let objectIDsThatMayHaveRelatedObjects = objectIDs.subtracting(objectIDsWithNoRelatedObjects) + if objectIDsThatMayHaveRelatedObjects.isEmpty { + return nil + } + + guard let lookupTable = fetchLookupTable(objectIDsThatMayHaveRelatedObjects, database) else { + objectIDsWithNoRelatedObjects.formUnion(objectIDsThatMayHaveRelatedObjects) + return nil + } + + if let relatedObjects = fetchRelatedObjectsReferencedByLookupTable(LookupTable, database) { + + let relatedObjectsDictionary = relatedObjectsDictionary(lookupTable, relatedObjects) + + let objectIDsWithNoFetchedRelatedObjects = objectIDsThatMayHaveRelatedObjects.subtracting(Set(relatedObjectsDictionary.keys)) + objectIDsWithNoRelatedObjects.formUnion(objectIDsWithNoFetchedRelatedObjects) + + return relatedObjectsDictionary + } + + return nil + } + public func attachRelatedObjects(to objects: [DatabaseObject], in database: FMDatabase) { let objectsThatMayHaveRelatedObjects = cache.objectsThatMayHaveRelatedObjects(objects) @@ -198,6 +224,16 @@ private extension DatabaseLookupTable { attachRelatedObjectsUsingLookupTable(objects, lookupTable, database) } + func fetchRelatedObjectsReferencedByLookupTable(_ lookupTable: LookupTable, _ database: FMDatabase) -> [DatabaseObject]? { + + let relatedObjectIDs = lookupTable.relatedObjectIDs() + if (relatedObjectIDs.isEmpty) { + return nil + } + + return fetchRelatedObjectsWithIDs(relatedObjectIDs) + } + func attachRelatedObjectsUsingLookupTable(_ objects: [DatabaseObject], _ lookupTable: LookupTable, _ database: FMDatabase) { let relatedObjectIDs = lookupTable.relatedObjectIDs() @@ -269,6 +305,17 @@ private extension DatabaseLookupTable { } return LookupValue(objectID: objectID, relatedObjectID: relatedObjectID) } + + func relatedObjectsDictionary(_ lookupTable: LookupTable, relatedObjects: [DatabaseObject]) -> RelatedObjectsDictionary? { + + var relatedObjectsDictionary = RelatedObjectsDictionary() + let d = relatedObjects.dictionary() + + + + + return relatedObjectsDictionary.isEmpty ? nil : relatedObjectsDictionary + } } // MARK: - @@ -300,6 +347,11 @@ private struct LookupTable { self.init(dictionary: d) } + func objectIDs() -> Set { + + return Set(dictionary.keys) + } + func relatedObjectIDs() -> Set { var ids = Set() @@ -376,12 +428,18 @@ private final class DatabaseLookupTableCache { } } - func objectsThatMayHaveRelatedObjects(_ objects: [DatabaseObject]) -> [DatabaseObject] { + func objectIDsThatMayHaveRelatedObjects(_ objectIDs: Set) -> Set { // Filter out objects that are known to have no related objects - return objects.filter{ !objectIDsWithNoRelationship.contains($0.databaseID) } + return Set(objectIDs.filter{ !objectIDsWithNoRelationship.contains($0) }) } +// func objectsThatMayHaveRelatedObjects(_ objects: [DatabaseObject]) -> [DatabaseObject] { +// +// // Filter out objects that are known to have no related objects +// return objects.filter{ !objectIDsWithNoRelationship.contains($0.databaseID) } +// } + func lookupTableForObjectIDs(_ objectIDs: Set) -> LookupTable { var d = [String: Set]() diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseRelatedObjectsTable.swift b/Frameworks/RSDatabase/Related Objects/DatabaseRelatedObjectsTable.swift similarity index 100% rename from Frameworks/RSDatabase/RSDatabase/DatabaseRelatedObjectsTable.swift rename to Frameworks/RSDatabase/Related Objects/DatabaseRelatedObjectsTable.swift diff --git a/Frameworks/RSDatabase/Related Objects/RelatedObjectIDsLookupTable.swift b/Frameworks/RSDatabase/Related Objects/RelatedObjectIDsLookupTable.swift new file mode 100644 index 000000000..e8cf4a76f --- /dev/null +++ b/Frameworks/RSDatabase/Related Objects/RelatedObjectIDsLookupTable.swift @@ -0,0 +1,59 @@ +// +// RelatedObjectIDsLookupTable.swift +// RSDatabase +// +// Created by Brent Simmons on 9/10/17. +// Copyright © 2017 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +// Maps objectIDs to Set where the Strings are relatedObjectIDs. + +struct RelatedObjectIDsLookupTable { + + private let dictionary: [String: Set] // objectID: Set + + init(dictionary: [String: Set]) { + + self.dictionary = dictionary + } + + init(lookupValues: Set) { + + var d = [String: Set]() + + for lookupValue in lookupValues { + let objectID = lookupValue.objectID + let relatedObjectID: String = lookupValue.relatedObjectID + if d[objectID] == nil { + d[objectID] = Set([relatedObjectID]) + } + else { + d[objectID]!.insert(relatedObjectID) + } + } + + self.init(dictionary: d) + } + + func objectIDs() -> Set { + + return Set(dictionary.keys) + } + + func relatedObjectIDs() -> Set { + + var ids = Set() + for (_, relatedObjectIDs) in dictionary { + ids.formUnion(relatedObjectIDs) + } + return ids + } + + subscript(_ objectID: String) -> Set? { + get { + return dictionary[objectID] + } + } +} diff --git a/Frameworks/RSDatabase/Related Objects/RelatedObjectsLookupTable.swift b/Frameworks/RSDatabase/Related Objects/RelatedObjectsLookupTable.swift new file mode 100644 index 000000000..6f9137772 --- /dev/null +++ b/Frameworks/RSDatabase/Related Objects/RelatedObjectsLookupTable.swift @@ -0,0 +1,45 @@ +// +// RelatedObjectsLookupTable.swift +// RSDatabase +// +// Created by Brent Simmons on 9/10/17. +// Copyright © 2017 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public struct RelatedObjectsLookupTable { + + private let dictionary: [String: [DatabaseObject]] // objectID: relatedObjects + + init(relatedObjects: relatedObjects, lookupTable: LookupTable) { + + var d = [String: [DatabaseObject]]() + + let relatedObjectsDictionary = relatedObjects.dictionary() + let objectIDs = lookupTable.objectIDs() + + for objectID in lookupTable.objectIDs() { + + if let relatedObjectIDs = lookupTable[objectID] { + let relatedObjects = relatedObjectIDs.flatMap{ relatedObjectsDictionary[$0] } + if !relatedObjects.isEmpty { + d[objectID] = relatedObjects + } + } + } + + self.dictionary = d + } + + public func objectIDs() -> Set { + + return Set(dictionary.keys) + } + + public subscript(_ objectID: String) -> [DatabaseObject]? { + get { + return dictionary[objectID] + } + } +} diff --git a/ToDo.ooutline b/ToDo.ooutline index ab37eba21..b389d9cfd 100644 Binary files a/ToDo.ooutline and b/ToDo.ooutline differ