From 9ddaaf5f5dd4ccbb2825464fe33e04b3a543811c Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 20 Aug 2017 21:23:17 -0700 Subject: [PATCH] Decide on preliminary public API for Database.swift. Stub-out everything. --- Frameworks/Data/Article.swift | 12 +- Frameworks/Data/Author.swift | 2 +- Frameworks/Database/Constants.swift | 1 + Frameworks/Database/Database.swift | 744 +++++++++--------- .../Database.xcodeproj/project.pbxproj | 4 + .../Database/Extensions/Feed+Database.swift | 18 + ToDo.ooutline | Bin 2454 -> 2643 bytes 7 files changed, 405 insertions(+), 376 deletions(-) create mode 100644 Frameworks/Database/Extensions/Feed+Database.swift diff --git a/Frameworks/Data/Article.swift b/Frameworks/Data/Article.swift index d8ae01422..4f3b0a25d 100644 --- a/Frameworks/Data/Article.swift +++ b/Frameworks/Data/Article.swift @@ -39,7 +39,7 @@ public final class Article: Hashable { } } - init(account: Account, databaseID: String?, 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?, attachments: [Attachment]?, accountInfo: AccountInfo?) { + init(account: Account, articleID: String?, 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?, attachments: [Attachment]?, accountInfo: AccountInfo?) { self.account = account self.feedID = feedID @@ -59,16 +59,14 @@ public final class Article: Hashable { self.attachments = attachments self.accountInfo = accountInfo - let _databaseID: String - if let databaseID = databaseID { - _databaseID = databaseID + if let articleID = articleID { + self.articleID = articleID } else { - _databaseID = databaseIDWithString("\(feedID) \(uniqueID)") + self.articleID = databaseIDWithString("\(feedID) \(uniqueID)") } - self.databaseID = _databaseID - self.hashValue = account.hashValue ^ _databaseID.hashValue + self.hashValue = account.hashValue ^ self.articleID.hashValue } public class func ==(lhs: Article, rhs: Article) -> Bool { diff --git a/Frameworks/Data/Author.swift b/Frameworks/Data/Author.swift index 658a09922..67fb137a3 100644 --- a/Frameworks/Data/Author.swift +++ b/Frameworks/Data/Author.swift @@ -44,6 +44,6 @@ public struct Author: Hashable { public static func ==(lhs: Author, rhs: Author) -> Bool { - return lhs.hashValue == rhs.hashValue && lhs.databaseID == rhs.databaseID + return lhs.hashValue == rhs.hashValue && lhs.authorID == rhs.authorID } } diff --git a/Frameworks/Database/Constants.swift b/Frameworks/Database/Constants.swift index 22fa5b7e5..a85a78b4a 100644 --- a/Frameworks/Database/Constants.swift +++ b/Frameworks/Database/Constants.swift @@ -69,4 +69,5 @@ public struct RelationshipName { static let authors = "authors" static let tags = "tags" + static let attachments = "attachments" } diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index fb77c17dd..a2fbf20c1 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -51,7 +51,7 @@ final class Database { self.tagsLookupTable = DatabaseLookupTable(name: DatabaseTableName.tags, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.tagName, relatedTable: tagsTable, relationshipName: RelationshipName.tags) let attachmentsTable = AttachmentsTable(name: DatabaseTableName.attachments) - self.attachmentsLookupTable = DatabaseLookupTable(name: DatabaseTableName.tags, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.tagName, relatedTable: tagsTable, relationshipName: RelationshipName.tags) + self.attachmentsLookupTable = DatabaseLookupTable(name: DatabaseTableName.attachmentsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.attachmentID, relatedTable: attachmentsTable, relationshipName: RelationshipName.attachments) let createStatementsPath = Bundle(for: type(of: self)).path(forResource: "CreateStatements", ofType: "sql")! let createStatements = try! NSString(contentsOfFile: createStatementsPath, encoding: String.Encoding.utf8.rawValue) @@ -59,399 +59,407 @@ final class Database { queue.vacuumIfNeeded() } - // MARK: Fetching Articles + // MARK: - Fetching Articles func fetchArticlesForFeed(_ feed: Feed) -> Set
{ - var fetchedArticles = Set
() - let feedID = feed.feedID - - queue.fetchSync { (database: FMDatabase!) -> Void in - - fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database) - } - - let articles = articleCache.uniquedArticles(fetchedArticles, statusesTable: statusesTable) - return filteredArticles(articles, feedCounts: [feed.feedID: fetchedArticles.count]) + return Set
() // TODO +// var fetchedArticles = Set
() +// let feedID = feed.feedID +// +// queue.fetchSync { (database: FMDatabase!) -> Void in +// +// fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database) +// } +// +// let articles = articleCache.uniquedArticles(fetchedArticles, statusesTable: statusesTable) +// return filteredArticles(articles, feedCounts: [feed.feedID: fetchedArticles.count]) } func fetchArticlesForFeedAsync(_ feed: Feed, _ resultBlock: @escaping ArticleResultBlock) { - let feedID = feed.feedID - - queue.fetch { (database: FMDatabase!) -> Void in - - let fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database) - - DispatchQueue.main.async() { () -> Void in - - let articles = self.articleCache.uniquedArticles(fetchedArticles, statusesTable: self.statusesTable) - let filteredArticles = self.filteredArticles(articles, feedCounts: [feed.feedID: fetchedArticles.count]) - resultBlock(filteredArticles) - } - } - } - - func feedIDCountDictionariesWithResultSet(_ resultSet: FMResultSet) -> [String: Int] { - - var counts = [String: Int]() - - while (resultSet.next()) { - - if let oneFeedID = resultSet.string(forColumnIndex: 0) { - let count = resultSet.int(forColumnIndex: 1) - counts[oneFeedID] = Int(count) - } - } - - return counts - } - - func countsForAllFeeds(_ database: FMDatabase) -> [String: Int] { - - let sql = "select distinct feedID, count(*) as count from articles group by feedID;" - - if let resultSet = database.executeQuery(sql, withArgumentsIn: []) { - return feedIDCountDictionariesWithResultSet(resultSet) - } - - return [String: Int]() - } - - func countsForFeedIDs(_ feedIDs: [String], _ database: FMDatabase) -> [String: Int] { - - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! - let sql = "select distinct feedID, count(*) from articles where feedID in \(placeholders) group by feedID;" - logSQL(sql) - - if let resultSet = database.executeQuery(sql, withArgumentsIn: feedIDs) { - return feedIDCountDictionariesWithResultSet(resultSet) - } - - return [String: Int]() - +// let feedID = feed.feedID +// +// queue.fetch { (database: FMDatabase!) -> Void in +// +// let fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database) +// +// DispatchQueue.main.async() { () -> Void in +// +// let articles = self.articleCache.uniquedArticles(fetchedArticles, statusesTable: self.statusesTable) +// let filteredArticles = self.filteredArticles(articles, feedCounts: [feed.feedID: fetchedArticles.count]) +// resultBlock(filteredArticles) +// } +// } } func fetchUnreadArticlesForFolder(_ folder: Folder) -> Set
{ - return fetchUnreadArticlesForFeedIDs(folder.flattenedFeedIDs()) + return Set
() // TODO +// return fetchUnreadArticlesForFeedIDs(folder.flattenedFeedIDs()) } - - func fetchUnreadArticlesForFeedIDs(_ feedIDs: [String]) -> Set
{ - - if feedIDs.isEmpty { - return Set
() - } - - var fetchedArticles = Set
() - var counts = [String: Int]() - - queue.fetchSync { (database: FMDatabase!) -> Void in - - counts = self.countsForFeedIDs(feedIDs, database) - - // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read = 0 - - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! - let sql = "select * from articles natural join statuses where feedID in \(placeholders) and read=0;" - logSQL(sql) - - if let resultSet = database.executeQuery(sql, withArgumentsIn: feedIDs) { - fetchedArticles = self.articlesWithResultSet(resultSet) - } - } - - let articles = articleCache.uniquedArticles(fetchedArticles, statusesTable: statusesTable) - return filteredArticles(articles, feedCounts: counts) - } - - typealias UnreadCountCompletionBlock = ([String: Int]) -> Void //feedID: unreadCount - - func updateUnreadCounts(for feedIDs: Set, completion: @escaping UnreadCountCompletionBlock) { - - queue.fetch { (database: FMDatabase!) -> Void in - - var unreadCounts = [String: Int]() - for oneFeedID in feedIDs { - unreadCounts[oneFeedID] = self.unreadCount(oneFeedID, database) - } - - DispatchQueue.main.async() { - completion(unreadCounts) - } - } - } - - // MARK: Updating Articles + // MARK: - Unread Counts + + typealias UnreadCountTable = [String: Int] // feedID: unreadCount + typealias UnreadCountCompletionBlock = (UnreadCountTable) -> Void //feedID: unreadCount + + func fetchUnreadCounts(for feeds: Set, completion: @escaping UnreadCountCompletionBlock) { + +// let feedIDs = feeds.feedIDs() +// +// queue.fetch { (database: FMDatabase!) -> Void in +// +// var unreadCounts = UnreadCountTable() +// for oneFeedID in feedIDs { +// unreadCounts[oneFeedID] = self.unreadCount(oneFeedID, database) +// } +// +// DispatchQueue.main.async() { +// completion(unreadCounts) +// } +// } + } + + // MARK: - Updating Articles func updateFeedWithParsedFeed(_ feed: Feed, parsedFeed: ParsedFeed, completionHandler: @escaping RSVoidCompletionBlock) { - if parsedFeed.items.isEmpty { - completionHandler() - return - } - - let parsedArticlesDictionary = self.articlesDictionary(parsedFeed.items as NSSet) as! [String: ParsedItem] - - fetchArticlesForFeedAsync(feed) { (articles) -> Void in - - let articlesDictionary = self.articlesDictionary(articles as NSSet) as! [String: Article] - self.updateArticles(articlesDictionary, parsedArticles: parsedArticlesDictionary, feed: feed, completionHandler: completionHandler) - } +// if parsedFeed.items.isEmpty { +// completionHandler() +// return +// } +// +// let parsedArticlesDictionary = self.articlesDictionary(parsedFeed.items as NSSet) as! [String: ParsedItem] +// +// fetchArticlesForFeedAsync(feed) { (articles) -> Void in +// +// let articlesDictionary = self.articlesDictionary(articles as NSSet) as! [String: Article] +// self.updateArticles(articlesDictionary, parsedArticles: parsedArticlesDictionary, feed: feed, completionHandler: completionHandler) +// } } - // MARK: Status + // MARK: - Status - func markArticles(_ articles: NSSet, statusKey: ArticleStatusKey, flag: Bool) { + func markArticles(_ articles: Set
, statusKey: ArticleStatusKey, flag: Bool) { - statusesTable.markArticles(articles as! Set
, statusKey: statusKey, flag: flag) +// statusesTable.markArticles(articles, statusKey: statusKey, flag: flag) } } -// MARK: Private +// MARK: - Private private extension Database { + + // func feedIDCountDictionariesWithResultSet(_ resultSet: FMResultSet) -> [String: Int] { + // + // var counts = [String: Int]() + // + // while (resultSet.next()) { + // + // if let oneFeedID = resultSet.string(forColumnIndex: 0) { + // let count = resultSet.int(forColumnIndex: 1) + // counts[oneFeedID] = Int(count) + // } + // } + // + // return counts + // } + + // func countsForAllFeeds(_ database: FMDatabase) -> [String: Int] { + // + // let sql = "select distinct feedID, count(*) as count from articles group by feedID;" + // + // if let resultSet = database.executeQuery(sql, withArgumentsIn: []) { + // return feedIDCountDictionariesWithResultSet(resultSet) + // } + // + // return [String: Int]() + // } + + // func countsForFeedIDs(_ feedIDs: [String], _ database: FMDatabase) -> [String: Int] { + // + // let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! + // let sql = "select distinct feedID, count(*) from articles where feedID in \(placeholders) group by feedID;" + // logSQL(sql) + // + // if let resultSet = database.executeQuery(sql, withArgumentsIn: feedIDs) { + // return feedIDCountDictionariesWithResultSet(resultSet) + // } + // + // return [String: Int]() + // + // } + + + // func fetchUnreadArticlesForFeedIDs(_ feedIDs: [String]) -> Set
{ + // + // if feedIDs.isEmpty { + // return Set
() + // } + // + // var fetchedArticles = Set
() + // var counts = [String: Int]() + // + // queue.fetchSync { (database: FMDatabase!) -> Void in + // + // counts = self.countsForFeedIDs(feedIDs, database) + // + // // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read = 0 + // + // let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! + // let sql = "select * from articles natural join statuses where feedID in \(placeholders) and read=0;" + // logSQL(sql) + // + // if let resultSet = database.executeQuery(sql, withArgumentsIn: feedIDs) { + // fetchedArticles = self.articlesWithResultSet(resultSet) + // } + // } + // + // let articles = articleCache.uniquedArticles(fetchedArticles, statusesTable: statusesTable) + // return filteredArticles(articles, feedCounts: counts) + // } + // MARK: Saving Articles - func saveUpdatedAndNewArticles(_ articleChanges: Set, newArticles: Set
) { - - if articleChanges.isEmpty && newArticles.isEmpty { - return - } - - statusesTable.assertNoMissingStatuses(newArticles) - articleCache.cacheArticles(newArticles) - - let newArticleDictionaries = newArticles.map { (oneArticle) in - return oneArticle.databaseDictionary() - } - - queue.update { (database: FMDatabase!) -> Void in - - if !articleChanges.isEmpty { - - for oneDictionary in articleChanges { - - let oneArticleDictionary = oneDictionary.mutableCopy() as! NSMutableDictionary - let articleID = oneArticleDictionary[DatabaseKey.articleID]! - oneArticleDictionary.removeObject(forKey: DatabaseKey.articleID) - - let _ = database.rs_updateRows(with: oneArticleDictionary as [NSObject: AnyObject], whereKey: DatabaseKey.articleID, equalsValue: articleID, tableName: DatabaseTableName.articles) - } - - } - if !newArticleDictionaries.isEmpty { - - for oneNewArticleDictionary in newArticleDictionaries { - let _ = database.rs_insertRow(with: oneNewArticleDictionary as [NSObject: AnyObject], insertType: RSDatabaseInsertOrReplace, tableName: DatabaseTableName.articles) - } - } - } - } - - // MARK: Updating Articles - - func updateArticles(_ articles: [String: Article], parsedArticles: [String: ParsedItem], feed: Feed, completionHandler: @escaping RSVoidCompletionBlock) { - - statusesTable.ensureStatusesForParsedArticles(Set(parsedArticles.values)) { - - let articleChanges = self.updateExistingArticles(articles, parsedArticles) - let newArticles = self.createNewArticles(articles, parsedArticles: parsedArticles, feedID: feed.feedID) - - self.saveUpdatedAndNewArticles(articleChanges, newArticles: newArticles) - - completionHandler() - } - } - - func articlesDictionary(_ articles: NSSet) -> [String: AnyObject] { - - var d = [String: AnyObject]() - for oneArticle in articles { - let oneArticleID = (oneArticle as AnyObject).value(forKey: DatabaseKey.articleID) as! String - d[oneArticleID] = oneArticle as AnyObject - } - return d - } - - func updateExistingArticles(_ articles: [String: Article], _ parsedArticles: [String: ParsedItem]) -> Set { - - var articleChanges = Set() - - for oneArticle in articles.values { - if let oneParsedArticle = parsedArticles[oneArticle.articleID] { - if let oneArticleChanges = oneArticle.updateWithParsedArticle(oneParsedArticle) { - articleChanges.insert(oneArticleChanges) - } - } - } - - return articleChanges - } - - // MARK: Creating Articles - - func createNewArticlesWithParsedArticles(_ parsedArticles: Set, feedID: String) -> Set
{ - - return Set(parsedArticles.map { Article(account: account, feedID: feedID, parsedArticle: $0) }) - } - - func articlesWithParsedArticles(_ parsedArticles: Set, feedID: String) -> Set
{ - - var localArticles = Set
() - - for oneParsedArticle in parsedArticles { - let oneLocalArticle = Article(account: self.account, feedID: feedID, parsedArticle: oneParsedArticle) - localArticles.insert(oneLocalArticle) - } - - return localArticles - } - - func createNewArticles(_ existingArticles: [String: Article], parsedArticles: [String: ParsedItem], feedID: String) -> Set
{ - - let newParsedArticles = parsedArticlesMinusExistingArticles(parsedArticles, existingArticles: existingArticles) - let newArticles = createNewArticlesWithParsedArticles(newParsedArticles, feedID: feedID) - - statusesTable.attachCachedUniqueStatuses(newArticles) - - return newArticles - } - - func parsedArticlesMinusExistingArticles(_ parsedArticles: [String: ParsedItem], existingArticles: [String: Article]) -> Set { - - var result = Set() - - for oneParsedArticle in parsedArticles.values { - - if let _ = existingArticles[oneParsedArticle.databaseID] { - continue - } - result.insert(oneParsedArticle) - } - - return result - } - - // MARK: Fetching Articles - - func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set
{ - - let sql = "select * from articles where \(whereClause);" - logSQL(sql) - - if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) { - return articlesWithResultSet(resultSet, database) - } - - return Set
() - } - - func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set
{ - - let fetchedArticles = resultSet.mapToSet { Article(account: self.account, row: $0) } - - statusesTable.attachStatuses(fetchedArticles, database) - authorsTable.attachAuthors(fetchedArticles, database) - tagsTable.attachTags(fetchedArticles, database) - attachmentsTable.attachAttachments(fetchedArticles, database) - - return fetchedArticles - } - - func fetchArticlesForFeedID(_ feedID: String, database: FMDatabase) -> Set
{ - - return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject]) - } - - // MARK: Unread counts - - func numberOfArticles(_ feedID: String, _ database: FMDatabase) -> Int { - - let sql = "select count(*) from articles where feedID = ?;" - logSQL(sql) - - return numberWithSQLAndParameters(sql, parameters: [feedID], database) - } - - func unreadCount(_ feedID: String, _ database: FMDatabase) -> Int { - - let totalNumberOfArticles = numberOfArticles(feedID, database) - - if totalNumberOfArticles <= minimumNumberOfArticles { - return unreadCountIgnoringCutoffDate(feedID, database) - } - return unreadCountRespectingCutoffDate(feedID, database) - } - - func unreadCountIgnoringCutoffDate(_ feedID: String, _ database: FMDatabase) -> Int { - - let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and userDeleted=0;" - logSQL(sql) - - return numberWithSQLAndParameters(sql, parameters: [feedID], database) - } - - func unreadCountRespectingCutoffDate(_ feedID: String, _ database: FMDatabase) -> Int { - - let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and userDeleted=0 and (starred=1 or dateArrived>?);" - logSQL(sql) - - return numberWithSQLAndParameters(sql, parameters: [feedID, articleArrivalCutoffDate], database) - } - - // MARK: Filtering out old articles - - func articleIsOlderThanCutoffDate(_ article: Article) -> Bool { - - if let dateArrived = article.status?.dateArrived { - return dateArrived < articleArrivalCutoffDate - } - return false - } - - func articleShouldBeSavedForever(_ article: Article) -> Bool { - - return article.status.starred - } - - func articleShouldAppearToUser(_ article: Article, _ numberOfArticlesInFeed: Int) -> Bool { - - if numberOfArticlesInFeed <= minimumNumberOfArticles { - return true - } - return articleShouldBeSavedForever(article) || !articleIsOlderThanCutoffDate(article) - } - - private static let minimumNumberOfArticlesInFeed = 10 - - func filteredArticles(_ articles: Set
, feedCounts: [String: Int]) -> Set
{ - - var articlesSet = Set
() - - for oneArticle in articles { - if let feedCount = feedCounts[oneArticle.feedID], articleShouldAppearToUser(oneArticle, feedCount) { - articlesSet.insert(oneArticle) - } - - } - - return articlesSet - } - - - func feedIDsFromArticles(_ articles: Set
) -> Set { - - return Set(articles.map { $0.feedID }) - } - - func deletePossibleOldArticles(_ articles: Set
) { - - let feedIDs = feedIDsFromArticles(articles) - if feedIDs.isEmpty { - return - } - } +// func saveUpdatedAndNewArticles(_ articleChanges: Set, newArticles: Set
) { +// +// if articleChanges.isEmpty && newArticles.isEmpty { +// return +// } +// +// statusesTable.assertNoMissingStatuses(newArticles) +// articleCache.cacheArticles(newArticles) +// +// let newArticleDictionaries = newArticles.map { (oneArticle) in +// return oneArticle.databaseDictionary() +// } +// +// queue.update { (database: FMDatabase!) -> Void in +// +// if !articleChanges.isEmpty { +// +// for oneDictionary in articleChanges { +// +// let oneArticleDictionary = oneDictionary.mutableCopy() as! NSMutableDictionary +// let articleID = oneArticleDictionary[DatabaseKey.articleID]! +// oneArticleDictionary.removeObject(forKey: DatabaseKey.articleID) +// +// let _ = database.rs_updateRows(with: oneArticleDictionary as [NSObject: AnyObject], whereKey: DatabaseKey.articleID, equalsValue: articleID, tableName: DatabaseTableName.articles) +// } +// +// } +// if !newArticleDictionaries.isEmpty { +// +// for oneNewArticleDictionary in newArticleDictionaries { +// let _ = database.rs_insertRow(with: oneNewArticleDictionary as [NSObject: AnyObject], insertType: RSDatabaseInsertOrReplace, tableName: DatabaseTableName.articles) +// } +// } +// } +// } +// +// // MARK: Updating Articles +// +// func updateArticles(_ articles: [String: Article], parsedArticles: [String: ParsedItem], feed: Feed, completionHandler: @escaping RSVoidCompletionBlock) { +// +// statusesTable.ensureStatusesForParsedArticles(Set(parsedArticles.values)) { +// +// let articleChanges = self.updateExistingArticles(articles, parsedArticles) +// let newArticles = self.createNewArticles(articles, parsedArticles: parsedArticles, feedID: feed.feedID) +// +// self.saveUpdatedAndNewArticles(articleChanges, newArticles: newArticles) +// +// completionHandler() +// } +// } +// +// func articlesDictionary(_ articles: NSSet) -> [String: AnyObject] { +// +// var d = [String: AnyObject]() +// for oneArticle in articles { +// let oneArticleID = (oneArticle as AnyObject).value(forKey: DatabaseKey.articleID) as! String +// d[oneArticleID] = oneArticle as AnyObject +// } +// return d +// } +// +// func updateExistingArticles(_ articles: [String: Article], _ parsedArticles: [String: ParsedItem]) -> Set { +// +// var articleChanges = Set() +// +// for oneArticle in articles.values { +// if let oneParsedArticle = parsedArticles[oneArticle.articleID] { +// if let oneArticleChanges = oneArticle.updateWithParsedArticle(oneParsedArticle) { +// articleChanges.insert(oneArticleChanges) +// } +// } +// } +// +// return articleChanges +// } +// +// // MARK: Creating Articles +// +// func createNewArticlesWithParsedArticles(_ parsedArticles: Set, feedID: String) -> Set
{ +// +// return Set(parsedArticles.map { Article(account: account, feedID: feedID, parsedArticle: $0) }) +// } +// +// func articlesWithParsedArticles(_ parsedArticles: Set, feedID: String) -> Set
{ +// +// var localArticles = Set
() +// +// for oneParsedArticle in parsedArticles { +// let oneLocalArticle = Article(account: self.account, feedID: feedID, parsedArticle: oneParsedArticle) +// localArticles.insert(oneLocalArticle) +// } +// +// return localArticles +// } +// +// func createNewArticles(_ existingArticles: [String: Article], parsedArticles: [String: ParsedItem], feedID: String) -> Set
{ +// +// let newParsedArticles = parsedArticlesMinusExistingArticles(parsedArticles, existingArticles: existingArticles) +// let newArticles = createNewArticlesWithParsedArticles(newParsedArticles, feedID: feedID) +// +// statusesTable.attachCachedUniqueStatuses(newArticles) +// +// return newArticles +// } +// +// func parsedArticlesMinusExistingArticles(_ parsedArticles: [String: ParsedItem], existingArticles: [String: Article]) -> Set { +// +// var result = Set() +// +// for oneParsedArticle in parsedArticles.values { +// +// if let _ = existingArticles[oneParsedArticle.databaseID] { +// continue +// } +// result.insert(oneParsedArticle) +// } +// +// return result +// } +// +// // MARK: Fetching Articles +// +// func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]?) -> Set
{ +// +// let sql = "select * from articles where \(whereClause);" +// logSQL(sql) +// +// if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) { +// return articlesWithResultSet(resultSet, database) +// } +// +// return Set
() +// } +// +// func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set
{ +// +// let fetchedArticles = resultSet.mapToSet { Article(account: self.account, row: $0) } +// +// statusesTable.attachStatuses(fetchedArticles, database) +// authorsTable.attachAuthors(fetchedArticles, database) +// tagsTable.attachTags(fetchedArticles, database) +// attachmentsTable.attachAttachments(fetchedArticles, database) +// +// return fetchedArticles +// } +// +// func fetchArticlesForFeedID(_ feedID: String, database: FMDatabase) -> Set
{ +// +// return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject]) +// } +// +// // MARK: Unread counts +// +// func numberOfArticles(_ feedID: String, _ database: FMDatabase) -> Int { +// +// let sql = "select count(*) from articles where feedID = ?;" +// logSQL(sql) +// +// return numberWithSQLAndParameters(sql, parameters: [feedID], database) +// } +// +// func unreadCount(_ feedID: String, _ database: FMDatabase) -> Int { +// +// let totalNumberOfArticles = numberOfArticles(feedID, database) +// +// if totalNumberOfArticles <= minimumNumberOfArticles { +// return unreadCountIgnoringCutoffDate(feedID, database) +// } +// return unreadCountRespectingCutoffDate(feedID, database) +// } +// +// func unreadCountIgnoringCutoffDate(_ feedID: String, _ database: FMDatabase) -> Int { +// +// let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and userDeleted=0;" +// logSQL(sql) +// +// return numberWithSQLAndParameters(sql, parameters: [feedID], database) +// } +// +// func unreadCountRespectingCutoffDate(_ feedID: String, _ database: FMDatabase) -> Int { +// +// let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and userDeleted=0 and (starred=1 or dateArrived>?);" +// logSQL(sql) +// +// return numberWithSQLAndParameters(sql, parameters: [feedID, articleArrivalCutoffDate], database) +// } +// +// // MARK: Filtering out old articles +// +// func articleIsOlderThanCutoffDate(_ article: Article) -> Bool { +// +// if let dateArrived = article.status?.dateArrived { +// return dateArrived < articleArrivalCutoffDate +// } +// return false +// } +// +// func articleShouldBeSavedForever(_ article: Article) -> Bool { +// +// return article.status.starred +// } +// +// func articleShouldAppearToUser(_ article: Article, _ numberOfArticlesInFeed: Int) -> Bool { +// +// if numberOfArticlesInFeed <= minimumNumberOfArticles { +// return true +// } +// return articleShouldBeSavedForever(article) || !articleIsOlderThanCutoffDate(article) +// } +// +// private static let minimumNumberOfArticlesInFeed = 10 +// +// func filteredArticles(_ articles: Set
, feedCounts: [String: Int]) -> Set
{ +// +// var articlesSet = Set
() +// +// for oneArticle in articles { +// if let feedCount = feedCounts[oneArticle.feedID], articleShouldAppearToUser(oneArticle, feedCount) { +// articlesSet.insert(oneArticle) +// } +// +// } +// +// return articlesSet +// } +// +// +// func feedIDsFromArticles(_ articles: Set
) -> Set { +// +// return Set(articles.map { $0.feedID }) +// } +// +// func deletePossibleOldArticles(_ articles: Set
) { +// +// let feedIDs = feedIDsFromArticles(articles) +// if feedIDs.isEmpty { +// return +// } +// } } diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index c583966b9..c2daa7101 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; }; 8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */; }; 846146271F0ABC7B00870CB3 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846146241F0ABC7400870CB3 /* RSParser.framework */; }; + 846FB36B1F4A937B00EAB81D /* Feed+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846FB36A1F4A937B00EAB81D /* Feed+Database.swift */; }; 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 */; }; @@ -123,6 +124,7 @@ 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ArticleStatus+Database.swift"; path = "Extensions/ArticleStatus+Database.swift"; sourceTree = ""; }; 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Attachment+Database.swift"; path = "Extensions/Attachment+Database.swift"; sourceTree = ""; }; 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; + 846FB36A1F4A937B00EAB81D /* Feed+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Feed+Database.swift"; path = "Extensions/Feed+Database.swift"; 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 = ""; }; @@ -210,6 +212,7 @@ isa = PBXGroup; children = ( 845580771F0AF678003CCFA1 /* Folder+Database.swift */, + 846FB36A1F4A937B00EAB81D /* Feed+Database.swift */, 845580751F0AF670003CCFA1 /* Article+Database.swift */, 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */, 84F20F901F1810DD00D8E682 /* Author+Database.swift */, @@ -472,6 +475,7 @@ 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */, 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */, 84D0DEA11F4A429800073503 /* String+Database.swift in Sources */, + 846FB36B1F4A937B00EAB81D /* Feed+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/Feed+Database.swift b/Frameworks/Database/Extensions/Feed+Database.swift new file mode 100644 index 000000000..baf961f3b --- /dev/null +++ b/Frameworks/Database/Extensions/Feed+Database.swift @@ -0,0 +1,18 @@ +// +// Feed+Database.swift +// Database +// +// Created by Brent Simmons on 8/20/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import Data + +extension Set where Element == Feed { + + func feedIDs() -> Set { + + return Set(map { $0.feedID }) + } +} diff --git a/ToDo.ooutline b/ToDo.ooutline index fab1f61b1cd9f4959b931c8d464316595c95cf62..0c83d3e5a8b4b3828b45acee24abcb4b41c89485 100644 GIT binary patch literal 2643 zcmZ{mS5Om(7KW2h0vZs4(hO2%fe?uchMrJXIsyVpQHc;F5Tytxp<_TPq99#rkf!t! zA{{BKh=_=Qp>ODIDUncv%ifo}cW3T752wv}`)9u2(v*$;GynkL0yMojZCX*cn`8|J z03yx<0FdLYiPjY;= zYi`IfF{C>sosveGZRwv3d=y947QCsK|FdVygR&eI8V-ZWTJ=Lh)B)(H(pWvB!I&HO zrX`@-Y~cyb%l-9^_jEMK-q2g|k=#}SH`hci4>an#ANiD>8=uetb}^#3eJSdDBFXL< z_Z71jSUKx1h*N95&h?T{%TGODyLlIUNxeHe@G}BO4!9_I?-ZZUmM)*cMf*Ws9WDb# zqz<<}*a}7%ru}(Az3}OU>6aC!Ie)8(c-~BrD~PT^Sz+@Q*PDBP=$M)3nOciSCn+x? zB*~EY+1~}nP=Ah~405PM$5F|2vJl;QWfkt2qj8^4 zm)g}Ov45gqE&(P{z-2m6trWwP!j&FmS(V2FeQsnreco?BPndQNu6wE`B`UbA zW9LVn`P;}QvsFfnTFN_KWn1)ML45MISdmBYvI|rICyihGHK^SU9A0ENo#JbF-(@+6 zd?PT3*IBL{DP`lSLU6{{;EtMC7-T)yMS_gP_QNmX-@;PU#W=b^es0>|Qo2%02%FxY zjo}rU^A~%HSIHkp{Y|pvWE!3fYq=!7o~av9=zPfw7-Kr1UXd(R2qTnNa#dn%s9eEP zHVgBQBDl*qRfc={Q%D*@w!l@kL|?;)k!sry#BK~6rDjn?3B_dou@5i4O8T^#6dLOY z@@!}B9{CpBKpgObA8-Oo?Ld_1oEE85Wrcj!8xS)f@+KJ?;2wUdU)?qA_Em=~uVbr} zg*fVQ7DGJUNI8o`7bENS=G~GgYL+LXqkd_?HD7+raNaF8W?1;8^+)5%jco{NSf^&Q zr_-Z+QdPvp5yG6}Cb3?HxZ4LixxZXiXe+QVcc>FYEC-Rh7cZ2}f5{es*zCN^sH~e3 z*G+n-8>hS0f_(e1`SMEd+}w^Y2#rCx4>;CACQl+*ErXUn53jbR-!dAkG(9xQlr7-HpdLIvOb1TxrrFIIr$EE945~H|w|;w$Eq&vR z?8eYuEBgNX;?$n$?xFh0s9z|VF7GL*TNARZ$<*YDg1}j})jSDnvzffyxNaXC(X3J% z@i#>xom(4IIa;yE7NCh-Ri`th#yH<#Hb1xT=wbxeLSOmz!#`HMV?Uhno))W#?#&Hb zxV@;WBO;ZqPHp7_TTLa7I`RvJ?Wb@_MfE5;n0TMb_kn(ME|Tm>V=t^9XEv{V94R+u zg6eL2_T3{UJ^6sc>Gcbw0;HLI?f6;9l|--zg-V=ixO|o-z#oJa?YVpZd?>u=DyMo% zRz~7DJ>FevlcZ+u%n%r9#AwH5cNM4}sR=w_s$c|1kb)d&Oy(s)@#416o@jfFg_wh< zGsp;<7P2#n$sM77{bU!r#J~->+%$Be#0}~DL8FICJ%KG_7pdpiX!O@&lx*c5E9p<& zTEQmbN(s5UUt{H8q#7ea-j`gGUvkRGhbs`|5!Q}~nBhpd>*p2RB*vmkpUy_tf*;F{ zzCw+9QgH7XLpmaOwiF?&eR3(0WfU|yKW;DHuq3(I?|ctk3=7;bK<%<=lyk~yJ0PCuZ{On6!4xt^)n zvKSla-rJ&&xO+1;e~x3{bVhuS>KMsL`%O^7`vlqlNpuj0Hw3}lE}D19RT5zBOr#0) zmt-`(Yx{PiytHAA16S@9gNYNuoTJ~<5>IeB3ms&g9aKiQXq)LD&L57}3f$ZydeW?^ zq^xiHnSlaWuCh1p$Zplj^s_HlWra3XG#b=dTP>s>7G`y#ZN~r06~@7L$65k`OYCd6 zmL!vYrlu!Tbg!iAh)SDLwV-OM*<$h%e>l9IMu%7n2-OEskbSk=Y5rgV&EgK0-nL6R zJ1$JK*6GJ5s~fGd;zt~o{{!E_O}+0jpkw&50{|zF;p>WX_w%~{ANIyu-}D_t@&zwk z$NzvyqEB@CAv4G-=T#7c?)br0zes;d%7+^19^tP&Ve2$uknz%NpYOvaYSR`^|4581 zyy{A!Y~I?PI4JVALP`v%@og)XEfDPi>Ajd*09A=W9P;8 zj(DSXS1ES%`+cn)2azh+<`OeA!Uj!wRz7M@kTN-xC56W-4l0~J=bzk}{1*B6w`B7f z=7A!<8fB$S;a7d8>Bac6AgHFG zyhc$%sQRVe+%)ypCQNI@E@|Y TH09v@`v^Kt-{bm``|s&5wYKOP delta 2414 zcmV-!36b{G6qXZzP)h>@6aWAK2ms}K6ie|jQK`@d003_x000aC003ieZggdCbaO6v zZEV$9TT|OO6n^io@c3!>LGdkd!cLOTc3BFgTWEon_JvVw#SxJ&$Z{_I^()zyuK^MQ z%+Qy{l8*G9Pv1F562tc^pC}7NV;lw})#&J|f&wRWao~-A)T_%QFi_u*-wnku;D;_E z%8a7vNS!ej{iSKk<+2m{0rqH^L>(vewE+4kj-Z3IGv0odFoFX_HEthfzrmu{p@F9= zY#pJI3ZsZ%2Qt3C7^Et(GebTUok3izLlJ?i!0Uw6(}Gxw8BH9PP^59h0$)D2!e^*h zZH;cx%iw^2a0;fF@c#qg;&?t%rvx%}{7zAZFz`Pg^k+6n2^oO z@(#rbNl1&Ov#)G=zCik^EQKdpzUHn_Y8=G)H`)k)CF{sR1c^UE91{*0@+c(fBtFjk z^5JTv+P$7-t7F6556%){Z^kwPl3n*gG8&MKt~%~L84hO12P_0Z$mNx)^bfAtih`?} z{j)$!!=(faC*cAq!d{7>D}s0n@VW%A>$*DDpA0{O6!Mv)8GrT*qjrc6qB8$&}H(o_aJ zSenw(;fPiw*)V26>o0C@DS(?Xy&)LHfLHBP> zT0^tZN;&$`jJ$6Kk(El+yg6bsj?SBbyeQ>ZYkfs{y;9v`XvMhBow)!LxD9BcS0XA0 zZCz9qxDQ0h9bXaAbhqo^iuUK^tkqaM4GqOYE=Y&O`7D(t1-m@duj1>^k$%1~JPGW7 z7(r376z&l}dqo7UK}b1x0j2<$&QJ-wG{BnYoU*@gDJ`d5Mbw+9ikPxv_H5m@IZyJV z&r|QD_^5@qmM-(avgy%fZ0V1W%+C~Kt1&dERFYod>mCN(E|}5}KbLFsy2+r~V$gUa z3>t%dX_6FlF!=elNlULqOZTnNV(guUjFZ#er=w5c=u49pyG2XyjnHzicMM(i&*ADI zK3@6-D?B_&da`O4ID0EzHlISa;K<0y>G!*R)6U4rjvbt$2l(09ZQpio{xhe4m-hO+ z?RL8=^nm>^zK3KJ@?%_=;zYtSqFBn{~{#7B)t38TYW!8AKN6fYtZ1ON$B?V z-awMjw+FTU#oN36N`cml}Y!^#IR!BvQ+Nl=TKh6o=Ll-b(0`?s!q-5O_qFF+e?YulDL z5zNLz2N9wyAjRS#K+q@|Ji-JCYQN`wCoXRkPekH@X{ZU~O8iz_#3mnR?)Bku>?IDr@yD64v#0ryx<;|8Nc+rcq~0Fg9bbKPZw@_PZej*m5|@3~ z*2U-bcW`De)bx3>=y5N9qc7H{<piM{1h-Xc)3Q=g9C`KHgE?(&DK@Un_T z`QPj?*P*FyL*dihyI{#AR;E1Mqhvy`qx^k-@~o~-Z|47olRKaF&$e*YctKYW(NsMD zzmT6H0Qf4QW_0a8=I5(x_yRE{iInWmOyT5~$8fy2^Vu(do?(ik*7)83P)h>@6aWAK z2ms}K6iaL?(F^JU007Mc000XB003oVX>@OLb1ryoY>iS|Z__Xoe($fa{EVAkwyHu+ zC#Go>gs27^3|=PJJ|td!!*-e$#DC|cO*+N|+FNwa`L6NoY-=>xK#0oOoDTZ~O27&y zl`V35bA8Ewru1yyn>A<5I0>4R0lww5Ov%5@vbwJO&RA6hSNXnhCbQf?^jtu;#QCyH zT3HAg;@#n!fmF$b$Tq`%umvS)=OL%ud#wad=%YENB#IIYZyhw#yctZH9E-Bc!Dx!r26WHnFIFpwpj|>F5QSOiz9_Cbk;!H&rJ!p-Q#Tu(;U^y;j zPInL~X@Gli)^&K0IjkF}=rt+{D;`L#WGZty8jNYyu3A%#GQ`$mWD4n5=cT;8Sj3_d zEze(n=Ut%Y`2KeM@!jqdTTV8VluAly9dw%G5yq>G;bE84J-oBOmK<$j| z7yH&0YxVWb=*mP?hOOsTcK_-q{)A-~L4!y+AX&5e7d2eHy8AF#i<{90A(kpz5oCK