diff --git a/Frameworks/Data/Author.swift b/Frameworks/Data/Author.swift index b24099363..7f354b06f 100644 --- a/Frameworks/Data/Author.swift +++ b/Frameworks/Data/Author.swift @@ -11,7 +11,7 @@ import RSCore public struct Author: Hashable { - public let databaseID: String // calculated + public let authorID: String // calculated public let name: String? public let url: String? public let avatarURL: String? diff --git a/Frameworks/Database/AttachmentsTable.swift b/Frameworks/Database/AttachmentsTable.swift index df94e41a0..f7a39b4d3 100644 --- a/Frameworks/Database/AttachmentsTable.swift +++ b/Frameworks/Database/AttachmentsTable.swift @@ -34,14 +34,12 @@ import Data final class AttachmentsTable: DatabaseTable { let name: String - let queue: RSDatabaseQueue private let cacheByArticleID = ObjectCache(keyPathForID: \Attachment.articleID) private let cacheByDatabaseID = ObjectCache(keyPathForID: \Attachment.databaseID) - init(name: String, queue: RSDatabaseQueue) { + init(name: String) { self.name = name - self.queue = queue } private var cachedAttachments = [String: Attachment]() // Attachment.databaseID key diff --git a/Frameworks/Database/AuthorsTable.swift b/Frameworks/Database/AuthorsTable.swift index 4a9c35cb4..cfa4a6b10 100644 --- a/Frameworks/Database/AuthorsTable.swift +++ b/Frameworks/Database/AuthorsTable.swift @@ -17,45 +17,27 @@ import Data // CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID)); -final class AuthorsTable: DatabaseTable { - +struct AuthorsTable: DatabaseTable { + let name: String - let queue: RSDatabaseQueue private let cache = ObjectCache(keyPathForID: \Author.databaseID) - private var articleIDToAuthorsCache = [String: Set]() - private let authorsLookupTable = LookupTable(name: DatabaseTableName.authorsLookup, primaryKey: DatabaseKey.authorID, foreignKey: DatabaseKey.articleID) - init(name: String, queue: RSDatabaseQueue) { + init(name: String) { self.name = name - self.queue = queue + } + + // MARK: DatabaseTable Methods + + func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject] { + + + } + + func save(_ objects: [DatabaseObject], in database: FMDatabase) { + <#code#> } - func attachAuthors(_ articles: Set
, _ database: FMDatabase) { - - attachCachedAuthors(articles) - - let articlesMissingAuthors = articlesNeedingAuthors(articles) - if articlesMissingAuthors.isEmpty { - return - } - - let articleIDs = Set(articlesMissingAuthors.map { $0.databaseID }) - let authorTable = fetchAuthorsForArticleIDs(articleIDs, database) - - for article in articlesMissingAuthors { - - let articleID = article.databaseID - - if let authors = authorTable?[articleID] { - articleIDsWithNoAuthors.remove(articleID) - article.authors = Array(authors) - } - else { - articleIDsWithNoAuthors.insert(articleID) - } - } - } } private extension AuthorsTable { diff --git a/Frameworks/Database/Constants.swift b/Frameworks/Database/Constants.swift index dc5baeb58..1a643b679 100644 --- a/Frameworks/Database/Constants.swift +++ b/Frameworks/Database/Constants.swift @@ -63,3 +63,8 @@ public struct DatabaseKey { static let emailAddress = "emailAddress" } +public struct RelationshipName { + + static let authors = "authors" + static let tags = "tags" +} diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 2cf56b796..30e396c6b 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -24,16 +24,17 @@ typealias ArticleResultBlock = (Set
) -> Void final class Database { - fileprivate let queue: RSDatabaseQueue + private let queue: RSDatabaseQueue private let databaseFile: String private let articlesTable: ArticlesTable private let authorsTable: AuthorsTable + private let authorsLookupTable: DatabaseLookupTable private let attachmentsTable: AttachmentsTable private let statusesTable: StatusesTable - private let tagsTable: TagsTable - fileprivate var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! - fileprivate let minimumNumberOfArticles = 10 - fileprivate weak var delegate: AccountDelegate? + private let tagsLookupTable: DatabaseLookupTable + private var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! + private let minimumNumberOfArticles = 10 + private weak var delegate: AccountDelegate? init(databaseFile: String, delegate: AccountDelegate) { @@ -42,11 +43,15 @@ final class Database { self.queue = RSDatabaseQueue(filepath: databaseFile, excludeFromBackup: false) self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, queue: queue) - self.authorsTable = AuthorsTable(name: DatabaseTableName.authors, queue: queue) - self.attachmentsTable = AttachmentsTable(name: DatabaseTableName.attachments, queue: queue) - self.statusesTable = StatusesTable(name: DatabaseTableName.statuses, queue: queue) - self.tagsTable = TagsTable(name: DatabaseTableName.tags, queue: queue) + self.attachmentsTable = AttachmentsTable(name: DatabaseTableName.attachments) + self.statusesTable = StatusesTable(name: DatabaseTableName.statuses) + + self.authorsTable = AuthorsTable(name: DatabaseTableName.authors) + self.authorsLookupTable = DatabaseLookupTable(name: DatabaseTableName.authorsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.authorID, relatedTable: authorsTable, relationshipName: RelationshipName.authors) + let tagsTable = TagsTable(name: DatabaseTableName.tags) + self.tagsLookupTable = DatabaseLookupTable(name: DatabaseTableName.tags, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.tagName, relatedTable: tagsTable, relationshipName: RelationshipName.tags) + let createStatementsPath = Bundle(for: type(of: self)).path(forResource: "CreateStatements", ofType: "sql")! let createStatements = try! NSString(contentsOfFile: createStatementsPath, encoding: String.Encoding.utf8.rawValue) queue.createTables(usingStatements: createStatements as String) diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index 2f2177e2f..c583966b9 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 846146271F0ABC7B00870CB3 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846146241F0ABC7400870CB3 /* RSParser.framework */; }; 84BB4BA21F119C5400858766 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84BB4B981F119C4900858766 /* RSCore.framework */; }; 84BB4BA91F11A32800858766 /* TagsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB4BA81F11A32800858766 /* TagsTable.swift */; }; + 84D0DEA11F4A429800073503 /* String+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D0DEA01F4A429800073503 /* String+Database.swift */; }; 84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156E91F0AB80500F8CC05 /* Database.swift */; }; 84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */; }; 84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */; }; @@ -124,6 +125,7 @@ 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; 84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = ""; }; 84BB4BA81F11A32800858766 /* TagsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsTable.swift; sourceTree = ""; }; + 84D0DEA01F4A429800073503 /* String+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "String+Database.swift"; path = "Extensions/String+Database.swift"; sourceTree = ""; }; 84E156E81F0AB75600F8CC05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84E156E91F0AB80500F8CC05 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticlesTable.swift; sourceTree = ""; }; @@ -212,6 +214,7 @@ 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */, 84F20F901F1810DD00D8E682 /* Author+Database.swift */, 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */, + 84D0DEA01F4A429800073503 /* String+Database.swift */, 845580711F0AEE49003CCFA1 /* AccountInfo.swift */, ); name = Extensions; @@ -468,6 +471,7 @@ 84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */, 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */, 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */, + 84D0DEA11F4A429800073503 /* String+Database.swift in Sources */, 843CB9961F34174100EE6581 /* Author+Database.swift in Sources */, 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */, 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */, diff --git a/Frameworks/Database/Extensions/Author+Database.swift b/Frameworks/Database/Extensions/Author+Database.swift index 5ba5805d9..86d2db4d5 100644 --- a/Frameworks/Database/Extensions/Author+Database.swift +++ b/Frameworks/Database/Extensions/Author+Database.swift @@ -12,13 +12,22 @@ import RSDatabase extension Author { - init?(databaseID: String, row: FMResultSet) { + init?(authorID: String, row: FMResultSet) { let name = row.string(forColumn: DatabaseKey.name) let url = row.string(forColumn: DatabaseKey.url) let avatarURL = row.string(forColumn: DatabaseKey.avatarURL) let emailAddress = row.string(forColumn: DatabaseKey.emailAddress) - self.init(databaseID: databaseID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress) + self.init(authorID: authorID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress) + } +} + +extension Author: DatabaseObject { + + var databaseID: String { + get { + return authorID + } } } diff --git a/Frameworks/Database/Extensions/String+Database.swift b/Frameworks/Database/Extensions/String+Database.swift new file mode 100644 index 000000000..13804d2ee --- /dev/null +++ b/Frameworks/Database/Extensions/String+Database.swift @@ -0,0 +1,22 @@ +// +// String+Database.swift +// Database +// +// Created by Brent Simmons on 8/20/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSDatabase + +// A tag is a String. +// Extending tag to conform to DatabaseObject means extending String to conform to DatabaseObject. + +extension String: DatabaseObject { + + var databaseID: String { + get { + return self + } + } +} diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index e76b80cbf..5055ad852 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -18,13 +18,11 @@ import Data final class StatusesTable: DatabaseTable { let name: String - let queue: RSDatabaseQueue private let cache = ObjectCache(keyPathForID: \ArticleStatus.articleID) - init(name: String, queue: RSDatabaseQueue) { + init(name: String) { self.name = name - self.queue = queue } func markArticles(_ articles: Set
, statusKey: String, flag: Bool) { diff --git a/Frameworks/Database/TagsTable.swift b/Frameworks/Database/TagsTable.swift index 329f8936f..8ddcd636a 100644 --- a/Frameworks/Database/TagsTable.swift +++ b/Frameworks/Database/TagsTable.swift @@ -11,225 +11,33 @@ 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. +// Since a tag is just a String, the tags table and the lookup table are the same table. +// All the heavy lifting is done in DatabaseLookupTable. // // 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 - -final class TagsTable: DatabaseTable { - +struct TagsTable: DatabaseTable { + let name: String - let queue: RSDatabaseQueue - let lookupTable: LookupTable - init(name: String, queue: RSDatabaseQueue) { + init(name: String) { self.name = name - self.queue = queue - self.lookupTable = LookupTable(name: DatabaseTableName.tags, primaryKey: DatabaseKey.tagName, foreignKey: DatabaseKey.articleID) } - func attachTags(_ articles: Set
, _ database: FMDatabase) { - - guard let lookupTableDictionary = lookupTable.fetchLookupTableDictionary(articleIDs, database) else { - return - } - - for article in articles { - if let lookupValues = lookupTableDictionary[article.databaseID] { - article.tags = lookupValues.tags() - } - } + // MARK: DatabaseTable Methods + + func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject] { + + // A tag is a string, and it is its own databaseID. + return databaseIDs.map{ $0 as DatabaseObject } + } + + func save(_ objects: [DatabaseObject], in database: FMDatabase) { + + // Nothing to do, since tags are saved in the lookup table, not in a separate table. } -// func saveTagsForArticles(_ articles: Set
) { -// -// var articlesToSaveTags = Set
() -// var articlesToRemoveTags = Set
() -// -// articles.forEach { (oneArticle) in -// -// if articleTagsMatchCache(oneArticle) { -// return -// } -// if let tags = oneArticle.tags { -// articlesToSaveTags.insert(oneArticle) -// } -// else { -// articlesToRemoveTags.insert(oneArticle) -// } -// } -// -// if !articlesToSaveTags.isEmpty { -// updateTagsForArticles(articlesToSaveTags) -// } -// -// if !articlesToRemoveTags.isEmpty { -// removeArticleFromTags(articlesToRemoveTags) -// } -// } -} - -private extension TagsTable { - -// func cacheTagsForArticle(_ article: Article, tags: TagNameSet) { -// -// articleIDsWithNoTags.remove(article.articleID) -// articleIDCache[article.articleID] = tags -// } -// -// func cachedTagsForArticleID(_ articleID: String) -> TagNameSet? { -// -// return articleIDsCache[articleID] -// } -// -// func articleTagsMatchCache(_ article: Article) -> Bool { -// -// if let tags = article.tags { -// return tags == articleIDCache[article.articleID] -// } -// return articleIDIsKnowToHaveNoTags(article.articleID) -// } -// -// func articleIDIsKnownToHaveNoTags(_ articleID: String) -> Bool { -// -// return articleIDsWithNoTags.contains(articleID) -// } -// -// func removeTagsFromCacheForArticleID(_ articleID: String) { -// -// articleIDsCache[oneArticleID] = nil -// articleIDsWithNoTags.insert(oneArticleID) -// } -// -// func removeArticleFromTags(_ articles: Set
) { -// -// var articleIDsToRemove = [String]() -// -// articles.forEach { (oneArticle) in -// let oneArticleID = oneArticle.articleID -// if articleIDIsKnownToHaveNoTags(oneArticle) { -// return -// } -// articleIDsToRemove += oneArticleID -// removeTagsFromCacheForArticleID(oneArticleID) -// } -// -// if !articleIDsToRemove.isEmpty { -// queue.update { (database) in -// database.rs_deleteRowsWhereKey(DatabaseKey.articleID, inValues: articleIDsToRemove, tableName: DatabaseTableName.tags) -// } -// } -// } -// -// typealias TagsTable = [String: TagNameSet] // [articleID: Set] -// -// func updateTagsForArticles(_ articles: Set
) { -// -// var tagsForArticleIDs = TagsTable() -// articles.forEach { (oneArticle) -// if let tags = oneArticle.tags { -// cacheTagsForArticle(oneArticle, tags) -// tagsForArticleIDs[oneArticle.articleID] = oneArticle.tags -// } -// else { -// assertionFailure("article must have tags") -// } -// } -// -// if tagsForArticleIDs.isEmpty { // Shouldn’t be empty -// return -// } -// let articleIDs = tagsForArticleIDs.keys -// -// queue.update { (database) in -// -// let existingTags = self.fetchTagsForArticleIDs(articleIDs, database: database) -// self.syncIncomingAndExistingTags(incomingTags: tagsForArticleIDs, existingTags: existingTags, database: database) -// } -// } -// -// func syncIncomingAndExistingTags(incomingTags: TagsTable, existingTags: TagsTable, database: database) { -// -// for (oneArticleID, oneTagNames) in incomingTags { -// if let existingTagNames = existingTags[oneArticleID] { -// syncIncomingAndExistingTagsForArticleID(oneArticleID, incomingTagNames: oneTagNames, existingTagNames: existingTagNames, database: database) -// } -// else { -// saveIncomingTagsForArticleID(oneArticleID, tagNames: oneTagNames, database: database) -// } -// } -// } -// -// func saveIncomingTagsForArticleID(_ articleID: String, tagNames: TagNameSet, database: FMDatabase) { -// -// // No existing tags in database. Simple save. -// -// for oneTagName in tagNames { -// let oneDictionary = [DatabaseTableName.articleID: articleID, DatabaseTableName.tagName: oneTagName] -// database.rs_insertRow(with: oneDictionary, insertType: .OrIgnore, tableName: DatabaseTableName.tags) -// } -// } -// -// func syncingIncomingAndExistingTagsForArticleID(_ articleID: String, incomingTagNames: TagNameSet, existingTagNames: TagNameSet, database: FMDatabase) { -// -// if incomingTagNames == existingTagNames { -// return -// } -// -// var tagsToRemove = TagNameSet() -// for oneExistingTagName in existingTagNames { -// if !incomingTagNames.contains(oneExistingTagName) { -// tagsToRemove.insert(oneExistingTagName) -// } -// } -// -// var tagsToAdd = TagNameSet() -// for oneIncomingTagName in incomingTagNames { -// if !existingTagNames.contains(oneIncomingTagName) { -// tagsToAdd.insert(oneIncomingTagName) -// } -// } -// -// if !tagsToRemove.isEmpty { -// let placeholders = NSString.rs_SQLValueListWithPlaceholders -// let sql = "delete from \(DatabaseTableName.tags) where \(DatabaseKey.articleID) = ? and \(DatabaseKey.tagName) in " -// database.executeUpdate(sql, withArgumentsIn: [articleID, ]) -// } -// } -// -// func fetchTagsForArticleIDs(_ articleIDs: Set, database: FMDatabase) -> TagsTable { -// -// var tagSpecifiers = TagsTable() -// -// guard let rs = database.rs_selectRowsWhereKey(DatabaseKey.articleID, inValues: Array(articleIDs), tableName: DatabaseTableName.tags) else { -// return tagSpecifiers -// } -// -// while rs.next() { -// -// guard let oneTagName = rs.string(forColumn: DatabaseKey.tagName), let oneArticleID = rs.string(forColumn: DatabaseKey.articleID) else { -// continue -// } -// if tagSpecifiers[oneArticleID] == nil { -// tagSpecifiers[oneArticleID] = Set([oneTagName]) -// } -// else { -// tagSpecifiers[oneArticleID]!.insert(oneTagName) -// } -// } -// -// return tagSpecifiers -// } -} - -private extension Set where Element == LookupValue { - - func tags() -> Set { - - return Set(flatMap{ $0.primaryID }) - } } diff --git a/Frameworks/RSDatabase/DatabaseTable.swift b/Frameworks/RSDatabase/DatabaseTable.swift index 5e0946032..6303e45fa 100644 --- a/Frameworks/RSDatabase/DatabaseTable.swift +++ b/Frameworks/RSDatabase/DatabaseTable.swift @@ -8,9 +8,9 @@ import Foundation -public protocol DatabaseTable: class { +public protocol DatabaseTable { - var name: String {get} + var name: String { get } func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject] func save(_ objects: [DatabaseObject], in database: FMDatabase) diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift b/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift index 0dec93042..b107bf00d 100644 --- a/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift +++ b/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift @@ -18,7 +18,7 @@ public final class DatabaseLookupTable { private let objectIDKey: String private let relatedObjectIDKey: String private let relationshipName: String - private weak var relatedTable: DatabaseTable? + private let relatedTable: DatabaseTable private let cache: DatabaseLookupTableCache public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseTable, relationshipName: String) { @@ -124,11 +124,6 @@ private extension DatabaseLookupTable { // Save the actual related objects. - guard let relatedTable = relatedTable else { - assertionFailure("updateRelationships: relatedTable unexpectedly disappeared.") - return - } - let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objectsNeedingUpdate) if relatedObjectsToSave.isEmpty { assertionFailure("updateRelationships: expected related objects to save. This should be unreachable.") diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift b/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift index 66e460766..b7f51d95d 100644 --- a/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift +++ b/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift @@ -16,6 +16,18 @@ public protocol DatabaseObject { func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? } +public extension DatabaseObject { + + func setRelatedObjects(_ objects: [DatabaseObject], name: String) { + // Do nothing + } + + func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? { + + return nil + } +} + extension Array where Element == DatabaseObject { func dictionary() -> [String: DatabaseObject] { diff --git a/ToDo.ooutline b/ToDo.ooutline index d2847926a..fab1f61b1 100644 Binary files a/ToDo.ooutline and b/ToDo.ooutline differ