Attach authors.
This commit is contained in:
parent
c30e7eeb99
commit
4503f771da
|
@ -238,15 +238,7 @@ private extension AttachmentsTable {
|
|||
|
||||
func attachmentsWithResultSet(_ resultSet: FMResultSet) -> Set<Attachment> {
|
||||
|
||||
var attachments = Set<Attachment>()
|
||||
|
||||
while (resultSet.next()) {
|
||||
if let oneAttachment = attachmentWithRow(resultSet) {
|
||||
attachments.insert(oneAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
return attachments
|
||||
return resultSet.mapToSet(attachmentWithRow)
|
||||
}
|
||||
|
||||
func attachmentWithRow(_ row: FMResultSet) -> Attachment? {
|
||||
|
|
|
@ -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<Author>(keyPathForID: \Author.databaseID)
|
||||
private var articleIDToAuthorsCache = [String: Set<Author>]()
|
||||
private var articleIDsWithNoAuthors = Set<String>()
|
||||
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<Article>, _ 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<Article>) {
|
||||
|
||||
for article in articles {
|
||||
if let authors = articleIDToAuthorsCache[article.databaseID] {
|
||||
article.authors = Array(authors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func articlesMissingAuthors(_ articles: Set<Article>) -> Set<Article> {
|
||||
|
||||
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<String>, _ database: FMDatabase) -> [String: Set<Author>]? {
|
||||
|
||||
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<LookupValue>) -> [String: Set<Author>] {
|
||||
|
||||
var authorTable = [String: Set<Author>]()
|
||||
|
||||
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<Author> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<Article>, statusKey: statusKey, flag: flag)
|
||||
statusesTable.markArticles(articles as! Set<Article>, 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<Article>()
|
||||
}
|
||||
|
||||
func articlesWithResultSet(_ resultSet: FMResultSet) -> Set<Article> {
|
||||
func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set<Article> {
|
||||
|
||||
var fetchedArticles = Set<Article>()
|
||||
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)
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<String>, _ 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<String>, _ database: FMDatabase) -> Set<ArticleStatus> {
|
||||
func fetchStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) -> Set<ArticleStatus>? {
|
||||
|
||||
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<ArticleStatus>()
|
||||
return articleStatusesWithResultSet(resultSet)
|
||||
}
|
||||
|
||||
func articleStatusesWithResultSet(_ resultSet: FMResultSet) -> Set<ArticleStatus> {
|
||||
|
||||
var statuses = Set<ArticleStatus>()
|
||||
|
||||
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
|
||||
|
|
|
@ -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<String>
|
||||
|
||||
|
|
|
@ -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<T>(_ 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<T>(_ callback: (_ row: FMResultSet) -> T?) -> [T] {
|
||||
|
||||
var objects = [T]()
|
||||
while next() {
|
||||
if let obj = callback(self) {
|
||||
objects += [obj]
|
||||
}
|
||||
}
|
||||
close()
|
||||
return objects
|
||||
}
|
||||
|
||||
public func mapToSet<T>(_ callback: (_ row: FMResultSet) -> T?) -> Set<T> {
|
||||
|
||||
return Set(flatMap(callback))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
844D97401F2D32F300CEDDEA /* ObjectCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectCache.swift; path = RSDatabase/ObjectCache.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 /* LookupTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LookupTable.swift; path = RSDatabase/LookupTable.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>"; };
|
||||
84DDF18D1C94FC45005E6CF5 /* FMDatabaseAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabaseAdditions.h; sourceTree = "<group>"; };
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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<String>, database: FMDatabase) -> Set<LookupValue> {
|
||||
|
||||
guard let resultSet = database.rs_selectRowsWhereKey(foreignKey, inValues: Array(foreignIDs), tableName: name) else {
|
||||
return Set<LookupValue>()
|
||||
}
|
||||
return lookupValuesWithResultSet(resultSet)
|
||||
}
|
||||
}
|
||||
|
||||
private extension LookupTable {
|
||||
|
||||
func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set<LookupValue> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue