mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2024-12-19 13:02:55 +01:00
Make progress on related objects.
This commit is contained in:
parent
07a44f7af0
commit
5ee58458a2
@ -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
|
||||
}
|
||||
|
@ -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<Article>, _ 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<Article>, _ 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<Article>?, _ updatedArticles: Set<Article>?, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(newArticles, updatedArticles)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Save New Articles
|
||||
|
||||
func findNewArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article]) -> Set<Article>? {
|
||||
|
||||
let newArticles = Set(incomingArticles.filter { fetchedArticlesDictionary[$0.articleID] == nil })
|
||||
return newArticles.isEmpty ? nil : newArticles
|
||||
}
|
||||
|
||||
func findAndSaveNewArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article], _ database: FMDatabase) -> Set<Article>? { //5
|
||||
|
||||
guard let newArticles = findNewArticles(incomingArticles, fetchedArticlesDictionary) else {
|
||||
return nil
|
||||
}
|
||||
self.saveNewArticles(newArticles, database)
|
||||
return newArticles
|
||||
}
|
||||
|
||||
func saveNewArticles(_ articles: Set<Article>, _ database: FMDatabase) {
|
||||
|
||||
saveRelatedObjectsForNewArticles(articles, database)
|
||||
@ -313,6 +339,30 @@ private extension ArticlesTable {
|
||||
updateRelatedObjects(\Article.attachments, updatedArticles, fetchedArticles, attachmentsLookupTable, database)
|
||||
}
|
||||
|
||||
func findUpdatedArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article]) -> Set<Article>? {
|
||||
|
||||
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<Article>, _ fetchedArticlesDictionary: [String: Article], _ database: FMDatabase) -> Set<Article>? { //6
|
||||
|
||||
guard let updatedArticles = findUpdatedArticles(incomingArticles, fetchedArticlesDictionary) else {
|
||||
return nil
|
||||
}
|
||||
saveUpdatedArticles(Set(updatedArticles), fetchedArticlesDictionary, database)
|
||||
return updatedArticles
|
||||
}
|
||||
|
||||
|
||||
func saveUpdatedArticles(_ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article], _ database: FMDatabase) {
|
||||
|
||||
saveUpdatedRelatedObjects(updatedArticles, fetchedArticles, database)
|
||||
@ -354,16 +404,16 @@ private extension ArticlesTable {
|
||||
return status.dateArrived < maximumArticleCutoffDate
|
||||
}
|
||||
|
||||
func filterParsedItems(_ parsedItems: Set<ParsedItem>, _ statuses: [String: ArticleStatus]) -> Set<ParsedItem> {
|
||||
|
||||
// Drop parsedItems that we can ignore.
|
||||
|
||||
return Set(parsedItems.filter{ (parsedItem) -> Bool in
|
||||
let articleID = parsedItem.articleID
|
||||
func filterIncomingArticles(_ articles: Set<Article>, _ statuses: [String: ArticleStatus]) -> Set<Article> {
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
@ -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 = "<group>"; };
|
||||
844BEE471F0AB3AB004AB7CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
844ECFC81F5B4F0E005E405A /* ParsedItem+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "ParsedItem+Database.swift"; path = "Extensions/ParsedItem+Database.swift"; sourceTree = "<group>"; };
|
||||
845580661F0AEBCD003CCFA1 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
845580751F0AF670003CCFA1 /* Article+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Article+Database.swift"; path = "Extensions/Article+Database.swift"; sourceTree = "<group>"; };
|
||||
845580771F0AF678003CCFA1 /* Folder+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Folder+Database.swift"; path = "Extensions/Folder+Database.swift"; sourceTree = "<group>"; };
|
||||
@ -221,7 +219,6 @@
|
||||
84F20F901F1810DD00D8E682 /* Author+Database.swift */,
|
||||
8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */,
|
||||
84D0DEA01F4A429800073503 /* String+Database.swift */,
|
||||
844ECFC81F5B4F0E005E405A /* ParsedItem+Database.swift */,
|
||||
);
|
||||
name = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -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;
|
||||
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -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 = "<group>"; };
|
||||
84419B031B5ABFF700C26BB2 /* FMResultSet+RSExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FMResultSet+RSExtras.h"; path = "RSDatabase/FMResultSet+RSExtras.h"; sourceTree = "<group>"; };
|
||||
84419B041B5ABFF700C26BB2 /* FMResultSet+RSExtras.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FMResultSet+RSExtras.m"; path = "RSDatabase/FMResultSet+RSExtras.m"; sourceTree = "<group>"; };
|
||||
844D97401F2D32F300CEDDEA /* DatabaseObjectCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatabaseObjectCache.swift; path = RSDatabase/DatabaseObjectCache.swift; sourceTree = "<group>"; };
|
||||
844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatabaseRelatedObjectsTable.swift; path = RSDatabase/DatabaseRelatedObjectsTable.swift; sourceTree = "<group>"; };
|
||||
844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRelatedObjectsTable.swift; sourceTree = "<group>"; };
|
||||
848E22551F6652C70031D7C5 /* RelatedObjectsLookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedObjectsLookupTable.swift; sourceTree = "<group>"; };
|
||||
848E22571F6653960031D7C5 /* RelatedObjectIDsLookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedObjectIDsLookupTable.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
84ABC1D01F364B07000DCC55 /* DatabaseLookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseLookupTable.swift; sourceTree = "<group>"; };
|
||||
84C6DD001F395C13009AFB47 /* DatabaseObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatabaseObject.swift; path = RSDatabase/DatabaseObject.swift; sourceTree = "<group>"; };
|
||||
84DDF18B1C94FC45005E6CF5 /* FMDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabase.h; sourceTree = "<group>"; };
|
||||
84DDF18C1C94FC45005E6CF5 /* FMDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabase.m; sourceTree = "<group>"; };
|
||||
@ -136,6 +139,17 @@
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
848E22531F66528C0031D7C5 /* Related Objects */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
84ABC1D01F364B07000DCC55 /* DatabaseLookupTable.swift */,
|
||||
848E22551F6652C70031D7C5 /* RelatedObjectsLookupTable.swift */,
|
||||
848E22571F6653960031D7C5 /* RelatedObjectIDsLookupTable.swift */,
|
||||
844ECFB81F5B17F9005E405A /* DatabaseRelatedObjectsTable.swift */,
|
||||
);
|
||||
path = "Related Objects";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ public final class DatabaseLookupTable {
|
||||
private let relationshipName: String
|
||||
private let relatedTable: DatabaseRelatedObjectsTable
|
||||
private let cache: DatabaseLookupTableCache
|
||||
|
||||
private var objectIDsWithNoRelatedObjects = Set<String>()
|
||||
|
||||
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<String>, 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<String> {
|
||||
|
||||
return Set(dictionary.keys)
|
||||
}
|
||||
|
||||
func relatedObjectIDs() -> Set<String> {
|
||||
|
||||
var ids = Set<String>()
|
||||
@ -376,12 +428,18 @@ private final class DatabaseLookupTableCache {
|
||||
}
|
||||
}
|
||||
|
||||
func objectsThatMayHaveRelatedObjects(_ objects: [DatabaseObject]) -> [DatabaseObject] {
|
||||
func objectIDsThatMayHaveRelatedObjects(_ objectIDs: Set<String>) -> Set<String> {
|
||||
|
||||
// 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<String>) -> LookupTable {
|
||||
|
||||
var d = [String: Set<String>]()
|
@ -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<String> where the Strings are relatedObjectIDs.
|
||||
|
||||
struct RelatedObjectIDsLookupTable {
|
||||
|
||||
private let dictionary: [String: Set<String>] // objectID: Set<relatedObjectID>
|
||||
|
||||
init(dictionary: [String: Set<String>]) {
|
||||
|
||||
self.dictionary = dictionary
|
||||
}
|
||||
|
||||
init(lookupValues: Set<LookupValue>) {
|
||||
|
||||
var d = [String: Set<String>]()
|
||||
|
||||
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<String> {
|
||||
|
||||
return Set(dictionary.keys)
|
||||
}
|
||||
|
||||
func relatedObjectIDs() -> Set<String> {
|
||||
|
||||
var ids = Set<String>()
|
||||
for (_, relatedObjectIDs) in dictionary {
|
||||
ids.formUnion(relatedObjectIDs)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
subscript(_ objectID: String) -> Set<String>? {
|
||||
get {
|
||||
return dictionary[objectID]
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String> {
|
||||
|
||||
return Set(dictionary.keys)
|
||||
}
|
||||
|
||||
public subscript(_ objectID: String) -> [DatabaseObject]? {
|
||||
get {
|
||||
return dictionary[objectID]
|
||||
}
|
||||
}
|
||||
}
|
BIN
ToDo.ooutline
BIN
ToDo.ooutline
Binary file not shown.
Loading…
Reference in New Issue
Block a user