Continue work on tags. Build broken.
This commit is contained in:
parent
d02013cb3a
commit
c79580b87c
|
@ -26,7 +26,7 @@ public final class Article: Hashable {
|
|||
public var datePublished: Date?
|
||||
public var dateModified: Date?
|
||||
public var authors: [Author]?
|
||||
public var tags: [String]?
|
||||
public var tags: Set<String>?
|
||||
public var attachments: [Attachment]?
|
||||
public var accountInfo: [String: Any]? //If account needs to store more data
|
||||
|
||||
|
@ -39,7 +39,7 @@ public final class Article: Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
init(account: Account, feedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, bannerImageURL: String?, datePublished: Date?, dateModified: Date?, authors: [Author]?, tags: [String]?, attachments: [Attachment]?, accountInfo: AccountInfo?) {
|
||||
init(account: Account, feedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, bannerImageURL: String?, datePublished: Date?, dateModified: Date?, authors: [Author]?, tags: Set<String>?, attachments: [Attachment]?, accountInfo: AccountInfo?) {
|
||||
|
||||
self.account = account
|
||||
self.feedID = feedID
|
||||
|
|
|
@ -84,8 +84,6 @@
|
|||
844BEE961F0AB4F8004AB7CD /* DisplayNameProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayNameProvider.swift; sourceTree = "<group>"; };
|
||||
844BEE971F0AB4F8004AB7CD /* UnreadCountProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountProvider.swift; sourceTree = "<group>"; };
|
||||
844BEE9C1F0AB512004AB7CD /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
|
||||
84BB4B7C1F1177AD00858766 /* ArticleCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleCache.swift; sourceTree = "<group>"; };
|
||||
84BB4B801F1178E400858766 /* StatusesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesManager.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -120,7 +118,6 @@
|
|||
844BEE841F0AB4DB004AB7CD /* ArticleStatus.swift */,
|
||||
844BEE861F0AB4E3004AB7CD /* BatchUpdates.swift */,
|
||||
844BEE881F0AB4E7004AB7CD /* Notifications.swift */,
|
||||
84BB4B7B1F1177AD00858766 /* Database */,
|
||||
844BEE8A1F0AB4EF004AB7CD /* OPML */,
|
||||
844BEE931F0AB4F8004AB7CD /* Protocols */,
|
||||
844BEE761F0AB444004AB7CD /* Info.plist */,
|
||||
|
@ -188,15 +185,6 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
84BB4B7B1F1177AD00858766 /* Database */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
84BB4B7C1F1177AD00858766 /* ArticleCache.swift */,
|
||||
84BB4B801F1178E400858766 /* StatusesManager.swift */,
|
||||
);
|
||||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
|
|
@ -12,6 +12,7 @@ public struct DatabaseTableName {
|
|||
|
||||
static let articles = "articles"
|
||||
static let statuses = "statuses"
|
||||
static let tags = "tags"
|
||||
}
|
||||
|
||||
public struct DatabaseKey {
|
||||
|
@ -42,10 +43,13 @@ public struct DatabaseKey {
|
|||
static let starred = "starred"
|
||||
static let userDeleted = "userDeleted"
|
||||
static let dateArrived = "dateArrived"
|
||||
|
||||
|
||||
// Attachment
|
||||
static let mimeType = "mimeType"
|
||||
static let sizeInBytes = "sizeInBytes"
|
||||
static let durationInSeconds = "durationInSeconds"
|
||||
|
||||
// Tag
|
||||
static let tagName = "tagName"
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
CREATE TABLE if not EXISTS articles (articleID TEXT NOT NULL PRIMARY KEY, feedID TEXT NOT NULL, uniqueID TEXT, title TEXT, contentHTML TEXT, contentText TEXT, url TEXT, externalURL TEXT, summary TEXT, imageURL TEXT, bannerImageURL TEXT, datePublished DATE, dateModified DATE, authors BLOB, tags BLOB, attachments BLOB, accountInfo BLOB);
|
||||
CREATE TABLE if not EXISTS articles (articleID TEXT NOT NULL PRIMARY KEY, feedID TEXT NOT NULL, uniqueID TEXT, title TEXT, contentHTML TEXT, contentText TEXT, url TEXT, externalURL TEXT, summary TEXT, imageURL TEXT, bannerImageURL TEXT, datePublished DATE, dateModified DATE, accountInfo BLOB);
|
||||
|
||||
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);
|
||||
|
||||
CREATE TABLE if not EXISTS authors (authorID 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));
|
||||
|
||||
CREATE TABLE if not EXISTS tags(tagName TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(tagName, articleID));
|
||||
|
||||
CREATE TABLE if not EXISTS attachments(articleID TEXT NOT NULL, url TEXT NOT NULL, mimeType TEXT, title TEXT, sizeInBytes INTEGER, durationInSeconds INTEGER, PRIMARY KEY(articleID, url));
|
||||
|
||||
CREATE INDEX if not EXISTS feedIndex on articles (feedID);
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */; };
|
||||
846146271F0ABC7B00870CB3 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846146241F0ABC7400870CB3 /* RSParser.framework */; };
|
||||
84BB4BA21F119C5400858766 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84BB4B981F119C4900858766 /* RSCore.framework */; };
|
||||
84BB4BA41F119D4A00858766 /* Author+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB4BA31F119D4A00858766 /* Author+Database.swift */; };
|
||||
84BB4BA91F11A32800858766 /* TagsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB4BA81F11A32800858766 /* TagsManager.swift */; };
|
||||
84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156E91F0AB80500F8CC05 /* Database.swift */; };
|
||||
84E156EC1F0AB80E00F8CC05 /* ArticlesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156EB1F0AB80E00F8CC05 /* ArticlesManager.swift */; };
|
||||
84E156EE1F0AB81400F8CC05 /* StatusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156ED1F0AB81400F8CC05 /* StatusesManager.swift */; };
|
||||
|
@ -118,6 +120,8 @@
|
|||
8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Attachment+Database.swift"; path = "Extensions/Attachment+Database.swift"; sourceTree = "<group>"; };
|
||||
8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
|
||||
84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
|
||||
84BB4BA31F119D4A00858766 /* Author+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Author+Database.swift"; path = "Extensions/Author+Database.swift"; sourceTree = "<group>"; };
|
||||
84BB4BA81F11A32800858766 /* TagsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsManager.swift; sourceTree = "<group>"; };
|
||||
84E156E81F0AB75600F8CC05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
84E156E91F0AB80500F8CC05 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
||||
84E156EB1F0AB80E00F8CC05 /* ArticlesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticlesManager.swift; sourceTree = "<group>"; };
|
||||
|
@ -157,6 +161,7 @@
|
|||
845580661F0AEBCD003CCFA1 /* Constants.swift */,
|
||||
84E156EB1F0AB80E00F8CC05 /* ArticlesManager.swift */,
|
||||
84E156ED1F0AB81400F8CC05 /* StatusesManager.swift */,
|
||||
84BB4BA81F11A32800858766 /* TagsManager.swift */,
|
||||
8461462A1F0AC44100870CB3 /* Extensions */,
|
||||
84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */,
|
||||
84E156E81F0AB75600F8CC05 /* Info.plist */,
|
||||
|
@ -200,6 +205,7 @@
|
|||
845580751F0AF670003CCFA1 /* Article+Database.swift */,
|
||||
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */,
|
||||
8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */,
|
||||
84BB4BA31F119D4A00858766 /* Author+Database.swift */,
|
||||
845580711F0AEE49003CCFA1 /* PropertyListTransformer.swift */,
|
||||
);
|
||||
name = Extensions;
|
||||
|
@ -227,10 +233,10 @@
|
|||
84E156FB1F0AB83A00F8CC05 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
84E156F11F0AB83600F8CC05 /* Data.xcodeproj */,
|
||||
84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */,
|
||||
8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */,
|
||||
84E157001F0AB89B00F8CC05 /* RSDatabase.xcodeproj */,
|
||||
84E156F11F0AB83600F8CC05 /* Data.xcodeproj */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -458,6 +464,8 @@
|
|||
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */,
|
||||
845580721F0AEE49003CCFA1 /* PropertyListTransformer.swift in Sources */,
|
||||
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */,
|
||||
84BB4BA41F119D4A00858766 /* Author+Database.swift in Sources */,
|
||||
84BB4BA91F11A32800858766 /* TagsManager.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// Author+Database.swift
|
||||
// Database
|
||||
//
|
||||
// Created by Brent Simmons on 7/8/17.
|
||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Author {
|
||||
|
||||
private static let
|
||||
convenience init?(databaseDictionary d: [String: Any]) {
|
||||
|
||||
guard let url = d[DatabaseKey.url] as? String else {
|
||||
return nil
|
||||
}
|
||||
let mimeType = d[DatabaseKey.mimeType] as? String
|
||||
let title = d[DatabaseKey.title] as? String
|
||||
let durationInSeconds = d[DatabaseKey.durationInSeconds] as? Int
|
||||
|
||||
self.init(url: url, mimeType: mimeType, title: title, durationInSeconds: durationInSeconds)
|
||||
|
||||
|
||||
|
||||
self.init(name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress)
|
||||
}
|
||||
|
||||
class func attachments(with plist: [Any]) -> [Attachment]? {
|
||||
|
||||
return plist.flatMap{ (oneDictionary) -> Attachment? in
|
||||
if let d = oneDictionary as? [String: Any] {
|
||||
return Attachment(databaseDictionary: d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -43,7 +43,18 @@ struct PropertyListTransformer {
|
|||
}
|
||||
return Attachment.attachments(with: plist)
|
||||
}
|
||||
|
||||
|
||||
static func authorsWithRow(_ row: FMResultSet) -> [Author]? {
|
||||
|
||||
guard let d = row.data(forColumn: DatabaseKey.authors) else {
|
||||
return nil
|
||||
}
|
||||
guard let plist = propertyList(withData: d) as? [Any] else {
|
||||
return nil
|
||||
}
|
||||
return Author.authors(with: plist)
|
||||
}
|
||||
|
||||
static func propertyListWithRow(_ row: FMResultSet, column: String) -> Any? {
|
||||
|
||||
guard let rawData = row.data(forColumn: column) else {
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
//
|
||||
// TagsManager.swift
|
||||
// Database
|
||||
//
|
||||
// Created by Brent Simmons on 7/8/17.
|
||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSDatabase
|
||||
|
||||
// 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.
|
||||
|
||||
final class TagsManager {
|
||||
|
||||
private var articleIDCache = [String: <String>]() // articleID: tag
|
||||
private var articleIDsWithNoTags = Set<String>()
|
||||
|
||||
private let queue: RSDatabaseQueue
|
||||
|
||||
init(queue: RSDatabaseQueue) {
|
||||
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
func saveTagsForArticles(_ articles: Set<Article>) {
|
||||
|
||||
var articlesToSaveTags = Set<Article>()
|
||||
var articlesToRemoveTags = Set<Article>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias TagNameSet = Set<String>
|
||||
|
||||
private extension TagsManager {
|
||||
|
||||
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<Article>) {
|
||||
|
||||
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<tagName>]
|
||||
|
||||
func updateTagsForArticles(_ articles: Set<Article>) {
|
||||
|
||||
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<String>, 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), oneArticleID = rs.string(forColumn: DatabaseKey.articleID) else {
|
||||
continue
|
||||
}
|
||||
if tagSpecifiers[oneArticleID] == nil {
|
||||
tagSpecifiers[oneArticleID] = Set([oneTagName])
|
||||
}
|
||||
else {
|
||||
tagSpecifiers[oneArticleID]!.insert(oneTagName)
|
||||
}
|
||||
}
|
||||
|
||||
return tagSpecifiers
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue