From 4503f771da43e08aaefa2c8851f86c6b288e5a27 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 6 Aug 2017 12:37:47 -0700 Subject: [PATCH] Attach authors. --- Frameworks/Database/AttachmentsTable.swift | 10 +- Frameworks/Database/AuthorsTable.swift | 119 ++++++++++++++++-- Frameworks/Database/Constants.swift | 2 + Frameworks/Database/Database.swift | 28 ++--- .../Database.xcodeproj/project.pbxproj | 4 +- .../Extensions/ArticleStatus+Database.swift | 8 +- .../Database/Extensions/Author+Database.swift | 3 +- Frameworks/Database/StatusesTable.swift | 41 +++--- Frameworks/Database/TagsTable.swift | 6 + Frameworks/RSDatabase/DatabaseTable.swift | 37 +++++- .../RSDatabase.xcodeproj/project.pbxproj | 6 + .../RSDatabase/RSDatabase/LookupTable.swift | 73 +++++++++++ 12 files changed, 276 insertions(+), 61 deletions(-) create mode 100644 Frameworks/RSDatabase/RSDatabase/LookupTable.swift diff --git a/Frameworks/Database/AttachmentsTable.swift b/Frameworks/Database/AttachmentsTable.swift index 2cf1bba8a..c58d6b9d6 100644 --- a/Frameworks/Database/AttachmentsTable.swift +++ b/Frameworks/Database/AttachmentsTable.swift @@ -238,15 +238,7 @@ private extension AttachmentsTable { func attachmentsWithResultSet(_ resultSet: FMResultSet) -> Set { - var attachments = Set() - - while (resultSet.next()) { - if let oneAttachment = attachmentWithRow(resultSet) { - attachments.insert(oneAttachment) - } - } - - return attachments + return resultSet.mapToSet(attachmentWithRow) } func attachmentWithRow(_ row: FMResultSet) -> Attachment? { diff --git a/Frameworks/Database/AuthorsTable.swift b/Frameworks/Database/AuthorsTable.swift index 6c6db6fac..3a03cc4f4 100644 --- a/Frameworks/Database/AuthorsTable.swift +++ b/Frameworks/Database/AuthorsTable.swift @@ -10,11 +10,21 @@ import Foundation import RSDatabase import Data +// article->authors is a many-to-many relationship. +// There’s a lookup table relating authorID and articleID. +// +// CREATE TABLE if not EXISTS authors (databaseID TEXT NOT NULL PRIMARY KEY, name TEXT, url TEXT, avatarURL TEXT, emailAddress TEXT); +// CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID)); + + final class AuthorsTable: DatabaseTable { let name: String let queue: RSDatabaseQueue private let cache = ObjectCache(keyPathForID: \Author.databaseID) + private var articleIDToAuthorsCache = [String: Set]() + private var articleIDsWithNoAuthors = Set() + private let authorsLookupTable = LookupTable(name: DatabaseTableName.authorsLookup, primaryKey: DatabaseKey.authorID, foreignKey: DatabaseKey.articleID) init(name: String, queue: RSDatabaseQueue) { @@ -22,12 +32,108 @@ final class AuthorsTable: DatabaseTable { self.queue = queue } - func authorWithRow(_ row: FMResultSet) -> Author? { + func attachAuthors(_ articles: Set
, _ database: FMDatabase) { - // Since: - // 1. anything to do with an FMResultSet runs inside the database serial queue, and - // 2. the cache is referenced only within this method, - // this is safe. + attachCachedAuthors(articles) + + let articlesNeedingAuthors = articlesMissingAuthors(articles) + if articlesNeedingAuthors.isEmpty { + return + } + + let articleIDs = Set(articlesNeedingAuthors.map { $0.databaseID }) + let authorTable = fetchAuthorsForArticleIDs(articleIDs, database) + + for article in articlesNeedingAuthors { + + let articleID = article.databaseID + + if let authors = authorTable?[articleID] { + articleIDsWithNoAuthors.remove(articleID) + article.authors = Array(authors) + } + else { + articleIDsWithNoAuthors.insert(articleID) + } + } + } +} + +private extension AuthorsTable { + + func attachCachedAuthors(_ articles: Set
) { + + for article in articles { + if let authors = articleIDToAuthorsCache[article.databaseID] { + article.authors = Array(authors) + } + } + } + + func articlesMissingAuthors(_ articles: Set
) -> Set
{ + + return articles.filter{ (article) -> Bool in + + if let _ = article.authors { + return false + } + if articleIDsWithNoAuthors.contains(article.databaseID) { + return false + } + + return true + } + } + + func fetchAuthorsForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) -> [String: Set]? { + + let lookupValues = authorsLookupTable.fetchLookupValues(articleIDs, database: database) + let authorIDs = Set(lookupValues.map { $0.primaryID }) + if authorIDs.isEmpty { + return nil + } + + guard let resultSet = selectRowsWhere(key: DatabaseKey.databaseID, inValues: Array(authorIDs), in: database) else { + return nil + } + + let authors = authorsWithResultSet(resultSet) + if authors.isEmpty { + return nil + } + + return authorTableWithLookupValues(lookupValues) + } + + func authorTableWithLookupValues(_ lookupValues: Set) -> [String: Set] { + + var authorTable = [String: Set]() + + for lookupValue in lookupValues { + + let authorID = lookupValue.primaryID + guard let author = cache[authorID] else { + continue + } + + let articleID = lookupValue.foreignID + if authorTable[articleID] == nil { + authorTable[articleID] = Set([author]) + } + else { + authorTable[articleID]!.insert(author) + } + } + + return authorTable + } + + func authorsWithResultSet(_ resultSet: FMResultSet) -> Set { + + return resultSet.mapToSet(authorWithRow) + } + + func authorWithRow(_ row: FMResultSet) -> Author? { guard let databaseID = row.string(forColumn: DatabaseKey.databaseID) else { return nil @@ -37,7 +143,7 @@ final class AuthorsTable: DatabaseTable { return cachedAuthor } - guard let author = Author(row: row) else { + guard let author = Author(databaseID: databaseID, row: row) else { return nil } @@ -45,4 +151,3 @@ final class AuthorsTable: DatabaseTable { return author } } - diff --git a/Frameworks/Database/Constants.swift b/Frameworks/Database/Constants.swift index 129bbfe21..dc5baeb58 100644 --- a/Frameworks/Database/Constants.swift +++ b/Frameworks/Database/Constants.swift @@ -12,6 +12,7 @@ public struct DatabaseTableName { static let articles = "articles" static let authors = "authors" + static let authorsLookup = "authorLookup" static let statuses = "statuses" static let tags = "tags" static let attachments = "attachments" @@ -56,6 +57,7 @@ public struct DatabaseKey { static let tagName = "tagName" // Author + static let authorID = "authorID" static let name = "name" static let avatarURL = "avatarURL" static let emailAddress = "emailAddress" diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 3717eac19..2cf56b796 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -65,7 +65,7 @@ final class Database { fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database) } - let articles = articleCache.uniquedArticles(fetchedArticles, statusesManager: statusesManager) + let articles = articleCache.uniquedArticles(fetchedArticles, statusesTable: statusesTable) return filteredArticles(articles, feedCounts: [feed.feedID: fetchedArticles.count]) } @@ -79,7 +79,7 @@ final class Database { DispatchQueue.main.async() { () -> Void in - let articles = self.articleCache.uniquedArticles(fetchedArticles, statusesManager: self.statusesManager) + let articles = self.articleCache.uniquedArticles(fetchedArticles, statusesTable: self.statusesTable) let filteredArticles = self.filteredArticles(articles, feedCounts: [feed.feedID: fetchedArticles.count]) resultBlock(filteredArticles) } @@ -155,7 +155,7 @@ final class Database { } } - let articles = articleCache.uniquedArticles(fetchedArticles, statusesManager: statusesManager) + let articles = articleCache.uniquedArticles(fetchedArticles, statusesTable: statusesTable) return filteredArticles(articles, feedCounts: counts) } @@ -199,7 +199,7 @@ final class Database { func markArticles(_ articles: NSSet, statusKey: ArticleStatusKey, flag: Bool) { - statusesManager.markArticles(articles as! Set
, statusKey: statusKey, flag: flag) + statusesTable.markArticles(articles as! Set
, statusKey: statusKey, flag: flag) } } @@ -215,7 +215,7 @@ private extension Database { return } - statusesManager.assertNoMissingStatuses(newArticles) + statusesTable.assertNoMissingStatuses(newArticles) articleCache.cacheArticles(newArticles) let newArticleDictionaries = newArticles.map { (oneArticle) in @@ -249,7 +249,7 @@ private extension Database { func updateArticles(_ articles: [String: Article], parsedArticles: [String: ParsedItem], feed: Feed, completionHandler: @escaping RSVoidCompletionBlock) { - statusesManager.ensureStatusesForParsedArticles(Set(parsedArticles.values)) { + statusesTable.ensureStatusesForParsedArticles(Set(parsedArticles.values)) { let articleChanges = self.updateExistingArticles(articles, parsedArticles) let newArticles = self.createNewArticles(articles, parsedArticles: parsedArticles, feedID: feed.feedID) @@ -309,7 +309,7 @@ private extension Database { let newParsedArticles = parsedArticlesMinusExistingArticles(parsedArticles, existingArticles: existingArticles) let newArticles = createNewArticlesWithParsedArticles(newParsedArticles, feedID: feedID) - statusesManager.attachCachedUniqueStatuses(newArticles) + statusesTable.attachCachedUniqueStatuses(newArticles) return newArticles } @@ -337,24 +337,16 @@ private extension Database { logSQL(sql) if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) { - return articlesWithResultSet(resultSet) + return articlesWithResultSet(resultSet, database) } return Set
() } - func articlesWithResultSet(_ resultSet: FMResultSet) -> Set
{ + func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set
{ - var fetchedArticles = Set
() + let fetchedArticles = resultSet.mapToSet { Article(account: self.account, row: $0) } - while (resultSet.next()) { - - if let oneArticle = Article(account: self.account, row: resultSet) { - fetchedArticles.insert(oneArticle) - } - } - resultSet.close() - statusesTable.attachStatuses(fetchedArticles, database) authorsTable.attachAuthors(fetchedArticles, database) tagsTable.attachTags(fetchedArticles, database) diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index 2e92d784a..2f2177e2f 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -164,10 +164,10 @@ 84E156E91F0AB80500F8CC05 /* Database.swift */, 845580661F0AEBCD003CCFA1 /* Constants.swift */, 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */, - 84F20F8E1F180D8700D8E682 /* AuthorsTable.swift */, - 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */, 84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */, + 84F20F8E1F180D8700D8E682 /* AuthorsTable.swift */, 84BB4BA81F11A32800858766 /* TagsTable.swift */, + 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */, 8461462A1F0AC44100870CB3 /* Extensions */, 84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */, 84E156E81F0AB75600F8CC05 /* Info.plist */, diff --git a/Frameworks/Database/Extensions/ArticleStatus+Database.swift b/Frameworks/Database/Extensions/ArticleStatus+Database.swift index 0cc99260f..44235644e 100644 --- a/Frameworks/Database/Extensions/ArticleStatus+Database.swift +++ b/Frameworks/Database/Extensions/ArticleStatus+Database.swift @@ -12,12 +12,8 @@ import Data extension ArticleStatus { - convenience init?(row: FMResultSet) { + convenience init?(articleID: String, row: FMResultSet) { - let articleID = row.string(forColumn: DatabaseKey.articleID) - if (articleID == nil) { - return nil - } let read = row.bool(forColumn: DatabaseKey.read) let starred = row.bool(forColumn: DatabaseKey.starred) let userDeleted = row.bool(forColumn: DatabaseKey.userDeleted) @@ -29,7 +25,7 @@ extension ArticleStatus { let accountInfoPlist = accountInfoWithRow(row) - self.init(articleID: articleID!, read: read, starred: starred, userDeleted: userDeleted, dateArrived: dateArrived!, accountInfo: accountInfoPlist) + self.init(articleID: articleID, read: read, starred: starred, userDeleted: userDeleted, dateArrived: dateArrived!, accountInfo: accountInfoPlist) } func databaseDictionary() -> NSDictionary { diff --git a/Frameworks/Database/Extensions/Author+Database.swift b/Frameworks/Database/Extensions/Author+Database.swift index 0f70effce..5ba5805d9 100644 --- a/Frameworks/Database/Extensions/Author+Database.swift +++ b/Frameworks/Database/Extensions/Author+Database.swift @@ -12,9 +12,8 @@ import RSDatabase extension Author { - init?(row: FMResultSet) { + init?(databaseID: String, row: FMResultSet) { - let databaseID = row.string(forColumn: DatabaseKey.databaseID) let name = row.string(forColumn: DatabaseKey.name) let url = row.string(forColumn: DatabaseKey.url) let avatarURL = row.string(forColumn: DatabaseKey.avatarURL) diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index 6dab3c4db..69b49d9f9 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -11,6 +11,10 @@ import RSCore import RSDatabase import Data +// Article->ArticleStatus is a to-one relationship. +// +// CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0, accountInfo BLOB); + final class StatusesTable: DatabaseTable { let name: String @@ -122,30 +126,35 @@ private extension StatusesTable { func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) { - let statuses = fetchStatusesForArticleIDs(articleIDs, database) - cache.addObjectsNotCached(Array(statuses)) + if let statuses = fetchStatusesForArticleIDs(articleIDs, database) { + cache.addObjectsNotCached(Array(statuses)) + } } - func fetchStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) -> Set { + func fetchStatusesForArticleIDs(_ articleIDs: Set, _ database: FMDatabase) -> Set? { - if !articleIDs.isEmpty, let resultSet = selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) { - return articleStatusesWithResultSet(resultSet) + guard let resultSet = selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else { + return nil } - - return Set() + return articleStatusesWithResultSet(resultSet) } func articleStatusesWithResultSet(_ resultSet: FMResultSet) -> Set { - - var statuses = Set() - - while(resultSet.next()) { - if let oneArticleStatus = ArticleStatus(row: resultSet) { - statuses.insert(oneArticleStatus) - } + + return resultSet.mapToSet(articleStatusWithRow) + } + + func articleStatusWithRow(_ row: FMResultSet) -> ArticleStatus? { + + guard let articleID = row.string(forColumn: DatabaseKey.articleID) else { + return nil } - - return statuses + if let cachedStatus = cache[articleID] { + return cachedStatus + } + let status = ArticleStatus(articleID: articleID, row: row) + cache[articleID] = status + return status } // MARK: Updating diff --git a/Frameworks/Database/TagsTable.swift b/Frameworks/Database/TagsTable.swift index c5b375d37..fad223d3e 100644 --- a/Frameworks/Database/TagsTable.swift +++ b/Frameworks/Database/TagsTable.swift @@ -10,9 +10,15 @@ import Foundation import RSDatabase import Data +// Article->tags is a many-to-many relationship. +// Since a tag is just a simple string, the tags table and the lookup table are the same table. +// // Tags — and the non-existence of tags — are cached, once fetched, for the lifetime of the run. // This uses some extra memory but cuts way down on the amount of database time spent // maintaining the tags table. +// +// CREATE TABLE if not EXISTS tags(tagName TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(tagName, articleID)); +// CREATE INDEX if not EXISTS tags_tagName_index on tags (tagName COLLATE NOCASE); typealias TagNameSet = Set diff --git a/Frameworks/RSDatabase/DatabaseTable.swift b/Frameworks/RSDatabase/DatabaseTable.swift index c4f50fe1d..bdcd9af0c 100644 --- a/Frameworks/RSDatabase/DatabaseTable.swift +++ b/Frameworks/RSDatabase/DatabaseTable.swift @@ -27,6 +27,9 @@ public extension DatabaseTable { public func selectRowsWhere(key: String, inValues values: [Any], in database: FMDatabase) -> FMResultSet? { + if values.isEmpty { + return nil + } return database.rs_selectRowsWhereKey(key, inValues: values, tableName: name) } @@ -37,7 +40,6 @@ public extension DatabaseTable { if values.isEmpty { return } - database.rs_deleteRowsWhereKey(key, inValues: values, tableName: name) } @@ -75,5 +77,38 @@ public extension DatabaseTable { let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) return numberWithCountResultSet(resultSet) } + + // MARK: Mapping + + func mapResultSet(_ resultSet: FMResultSet, _ callback: (_ resultSet: FMResultSet) -> T?) -> [T] { + + var objects = [T]() + while resultSet.next() { + if let obj = callback(resultSet) { + objects += [obj] + } + } + return objects + } +} + +public extension FMResultSet { + + public func flatMap(_ callback: (_ row: FMResultSet) -> T?) -> [T] { + + var objects = [T]() + while next() { + if let obj = callback(self) { + objects += [obj] + } + } + close() + return objects + } + + public func mapToSet(_ callback: (_ row: FMResultSet) -> T?) -> Set { + + return Set(flatMap(callback)) + } } diff --git a/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj b/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj index 302060ecd..0bbf36588 100755 --- a/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj +++ b/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ 84419B061B5ABFF700C26BB2 /* FMResultSet+RSExtras.m in Sources */ = {isa = PBXBuildFile; fileRef = 84419B041B5ABFF700C26BB2 /* FMResultSet+RSExtras.m */; }; 844D97411F2D32F300CEDDEA /* ObjectCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844D97401F2D32F300CEDDEA /* ObjectCache.swift */; }; 849BF8C61C94FB8E0071D1DA /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 849BF8C51C94FB8E0071D1DA /* libsqlite3.tbd */; }; + 84ABC1D11F364B07000DCC55 /* LookupTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ABC1D01F364B07000DCC55 /* LookupTable.swift */; }; + 84ABC1D21F364B07000DCC55 /* LookupTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ABC1D01F364B07000DCC55 /* LookupTable.swift */; }; 84DDF1961C94FC45005E6CF5 /* FMDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 84DDF18B1C94FC45005E6CF5 /* FMDatabase.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84DDF1971C94FC45005E6CF5 /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 84DDF18C1C94FC45005E6CF5 /* FMDatabase.m */; }; 84DDF1981C94FC45005E6CF5 /* FMDatabaseAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 84DDF18D1C94FC45005E6CF5 /* FMDatabaseAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -71,6 +73,7 @@ 84419B041B5ABFF700C26BB2 /* FMResultSet+RSExtras.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FMResultSet+RSExtras.m"; path = "RSDatabase/FMResultSet+RSExtras.m"; sourceTree = ""; }; 844D97401F2D32F300CEDDEA /* ObjectCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectCache.swift; path = RSDatabase/ObjectCache.swift; sourceTree = ""; }; 849BF8C51C94FB8E0071D1DA /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; + 84ABC1D01F364B07000DCC55 /* LookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LookupTable.swift; path = RSDatabase/LookupTable.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 = ""; }; 84DDF18D1C94FC45005E6CF5 /* FMDatabaseAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabaseAdditions.h; sourceTree = ""; }; @@ -156,6 +159,7 @@ 84419AD81B5ABD7400C26BB2 /* NSString+RSDatabase.h */, 84419AD91B5ABD7400C26BB2 /* NSString+RSDatabase.m */, 840405DA1F1C158C00DF0296 /* DatabaseTable.swift */, + 84ABC1D01F364B07000DCC55 /* LookupTable.swift */, 844D97401F2D32F300CEDDEA /* ObjectCache.swift */, 84DDF18A1C94FC45005E6CF5 /* FMDB */, 84F22C5A1B52E0D9000060CE /* Info.plist */, @@ -350,6 +354,7 @@ files = ( 8400AC001E0CFC0700AA7C57 /* RSDatabaseQueue.m in Sources */, 8400AC061E0CFC0700AA7C57 /* NSString+RSDatabase.m in Sources */, + 84ABC1D21F364B07000DCC55 /* LookupTable.swift in Sources */, 8400AC0C1E0CFC3100AA7C57 /* FMResultSet.m in Sources */, 840405DC1F1C15EA00DF0296 /* DatabaseTable.swift in Sources */, 8400AC021E0CFC0700AA7C57 /* FMDatabase+RSExtras.m in Sources */, @@ -367,6 +372,7 @@ 84419AD71B5ABD6D00C26BB2 /* FMDatabase+RSExtras.m in Sources */, 84419ADB1B5ABD7400C26BB2 /* NSString+RSDatabase.m in Sources */, 840405DB1F1C158C00DF0296 /* DatabaseTable.swift in Sources */, + 84ABC1D11F364B07000DCC55 /* LookupTable.swift in Sources */, 84DDF1971C94FC45005E6CF5 /* FMDatabase.m in Sources */, 84DDF1A01C94FC45005E6CF5 /* FMResultSet.m in Sources */, 84419B061B5ABFF700C26BB2 /* FMResultSet+RSExtras.m in Sources */, diff --git a/Frameworks/RSDatabase/RSDatabase/LookupTable.swift b/Frameworks/RSDatabase/RSDatabase/LookupTable.swift new file mode 100644 index 000000000..167e9723c --- /dev/null +++ b/Frameworks/RSDatabase/RSDatabase/LookupTable.swift @@ -0,0 +1,73 @@ +// +// LookupTable.swift +// RSDatabase +// +// Created by Brent Simmons on 8/5/17. +// Copyright © 2017 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +// Implement a lookup table for a many-to-many relationship. +// Example: CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID)); +// authorID is primaryKey; articleID is foreignKey. + +public struct LookupTable { + + let name: String + let primaryKey: String + let foreignKey: String + + public init(name: String, primaryKey: String, foreignKey: String) { + + self.name = name + self.primaryKey = primaryKey + self.foreignKey = foreignKey + } + + public func fetchLookupValues(_ foreignIDs: Set, database: FMDatabase) -> Set { + + guard let resultSet = database.rs_selectRowsWhereKey(foreignKey, inValues: Array(foreignIDs), tableName: name) else { + return Set() + } + return lookupValuesWithResultSet(resultSet) + } +} + +private extension LookupTable { + + func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set { + + return resultSet.mapToSet(lookupValueWithRow) + } + + func lookupValueWithRow(_ resultSet: FMResultSet) -> LookupValue? { + + guard let primaryID = resultSet.string(forColumn: primaryKey) else { + return nil + } + guard let foreignID = resultSet.string(forColumn: foreignKey) else { + return nil + } + return LookupValue(primaryID: primaryID, foreignID: foreignID) + } +} + +public struct LookupValue: Hashable { + + public let primaryID: String + public let foreignID: String + public let hashValue: Int + + init(primaryID: String, foreignID: String) { + + self.primaryID = primaryID + self.foreignID = foreignID + self.hashValue = "\(primaryID)\(foreignID)".hashValue + } + + static public func ==(lhs: LookupValue, rhs: LookupValue) -> Bool { + + return lhs.primaryID == rhs.primaryID && lhs.foreignID == rhs.foreignID + } +}