From 5ee58458a2f5d49edd2cf85306652ab0ee5c8986 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 11 Sep 2017 06:46:32 -0700 Subject: [PATCH] Make progress on related objects. --- Frameworks/Data/DatabaseID.swift | 6 + Frameworks/Database/ArticlesTable.swift | 140 ++++++++++++------ .../Database.xcodeproj/project.pbxproj | 4 - .../Extensions/ParsedItem+Database.swift | 28 ---- .../RSDatabase.xcodeproj/project.pbxproj | 30 +++- .../RSDatabase/DatabaseObjectCache.swift | 85 ----------- .../DatabaseLookupTable.swift | 64 +++++++- .../DatabaseRelatedObjectsTable.swift | 0 .../RelatedObjectIDsLookupTable.swift | 59 ++++++++ .../RelatedObjectsLookupTable.swift | 45 ++++++ ToDo.ooutline | Bin 3413 -> 3416 bytes 11 files changed, 288 insertions(+), 173 deletions(-) delete mode 100644 Frameworks/Database/Extensions/ParsedItem+Database.swift delete mode 100644 Frameworks/RSDatabase/RSDatabase/DatabaseObjectCache.swift rename Frameworks/RSDatabase/{RSDatabase => Related Objects}/DatabaseLookupTable.swift (85%) rename Frameworks/RSDatabase/{RSDatabase => Related Objects}/DatabaseRelatedObjectsTable.swift (100%) create mode 100644 Frameworks/RSDatabase/Related Objects/RelatedObjectIDsLookupTable.swift create mode 100644 Frameworks/RSDatabase/Related Objects/RelatedObjectsLookupTable.swift 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 ab37eba21c21587ad01780ae35eb9392d7ce5ff5..b389d9cfdbe614d4e12691cfc7d5a21dbafbd9fb 100644 GIT binary patch literal 3416 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 N_x%$`{}E1re*xS*Y>@x} literal 3413 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