470 lines
15 KiB
Swift
470 lines
15 KiB
Swift
//
|
|
// Database.swift
|
|
// Evergreen
|
|
//
|
|
// Created by Brent Simmons on 7/20/15.
|
|
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import RSCore
|
|
import RSDatabase
|
|
import RSParser
|
|
import Data
|
|
|
|
private let sqlLogging = false
|
|
|
|
private func logSQL(_ sql: String) {
|
|
if sqlLogging {
|
|
print("SQL: \(sql)")
|
|
}
|
|
}
|
|
|
|
typealias ArticleResultBlock = (Set<Article>) -> Void
|
|
|
|
final class Database {
|
|
|
|
private let queue: RSDatabaseQueue
|
|
private let databaseFile: String
|
|
private let articlesTable: ArticlesTable
|
|
private let statusesTable: StatusesTable
|
|
private let authorsLookupTable: DatabaseLookupTable
|
|
private let attachmentsLookupTable: DatabaseLookupTable
|
|
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) {
|
|
|
|
self.delegate = delegate
|
|
self.databaseFile = databaseFile
|
|
self.queue = RSDatabaseQueue(filepath: databaseFile, excludeFromBackup: false)
|
|
|
|
self.articlesTable = ArticlesTable(name: DatabaseTableName.articles)
|
|
self.statusesTable = StatusesTable(name: DatabaseTableName.statuses)
|
|
|
|
let 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 attachmentsTable = AttachmentsTable(name: DatabaseTableName.attachments)
|
|
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)
|
|
queue.createTables(usingStatements: createStatements as String)
|
|
queue.vacuumIfNeeded()
|
|
}
|
|
|
|
// MARK: - Fetching Articles
|
|
|
|
func fetchArticlesForFeed(_ feed: Feed) -> Set<Article> {
|
|
|
|
return articlesTable.fetchArticlesForFeed(feed)
|
|
|
|
// return Set<Article>() // TODO
|
|
// var fetchedArticles = Set<Article>()
|
|
// 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) {
|
|
|
|
articlesTable.fetchArticlesForFeedAsync(feed, resultBlock)
|
|
|
|
// 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<Article> {
|
|
|
|
return articlesTable.fetchUnreadArticlesForFeeds(folder.flattenedFeeds())
|
|
// return fetchUnreadArticlesForFeedIDs(folder.flattenedFeedIDs())
|
|
}
|
|
|
|
// MARK: - Unread Counts
|
|
|
|
typealias UnreadCountTable = [String: Int] // feedID: unreadCount
|
|
typealias UnreadCountCompletionBlock = (UnreadCountTable) -> Void //feedID: unreadCount
|
|
|
|
func fetchUnreadCounts(for feeds: Set<Feed>, 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)
|
|
// }
|
|
}
|
|
|
|
// MARK: - Status
|
|
|
|
func markArticles(_ articles: Set<Article>, statusKey: ArticleStatusKey, flag: Bool) {
|
|
|
|
// statusesTable.markArticles(articles, statusKey: statusKey, flag: flag)
|
|
}
|
|
}
|
|
|
|
// 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<Article> {
|
|
//
|
|
// if feedIDs.isEmpty {
|
|
// return Set<Article>()
|
|
// }
|
|
//
|
|
// var fetchedArticles = Set<Article>()
|
|
// 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<NSDictionary>, newArticles: Set<Article>) {
|
|
//
|
|
// 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<NSDictionary> {
|
|
//
|
|
// var articleChanges = Set<NSDictionary>()
|
|
//
|
|
// 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<ParsedItem>, feedID: String) -> Set<Article> {
|
|
//
|
|
// return Set(parsedArticles.map { Article(account: account, feedID: feedID, parsedArticle: $0) })
|
|
// }
|
|
//
|
|
// func articlesWithParsedArticles(_ parsedArticles: Set<ParsedItem>, feedID: String) -> Set<Article> {
|
|
//
|
|
// var localArticles = Set<Article>()
|
|
//
|
|
// 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<Article> {
|
|
//
|
|
// 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<ParsedItem> {
|
|
//
|
|
// var result = Set<ParsedItem>()
|
|
//
|
|
// 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<Article> {
|
|
//
|
|
// let sql = "select * from articles where \(whereClause);"
|
|
// logSQL(sql)
|
|
//
|
|
// if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) {
|
|
// return articlesWithResultSet(resultSet, database)
|
|
// }
|
|
//
|
|
// return Set<Article>()
|
|
// }
|
|
//
|
|
// func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set<Article> {
|
|
//
|
|
// 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<Article> {
|
|
//
|
|
// 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<Article>, feedCounts: [String: Int]) -> Set<Article> {
|
|
//
|
|
// var articlesSet = Set<Article>()
|
|
//
|
|
// for oneArticle in articles {
|
|
// if let feedCount = feedCounts[oneArticle.feedID], articleShouldAppearToUser(oneArticle, feedCount) {
|
|
// articlesSet.insert(oneArticle)
|
|
// }
|
|
//
|
|
// }
|
|
//
|
|
// return articlesSet
|
|
// }
|
|
//
|
|
//
|
|
// func feedIDsFromArticles(_ articles: Set<Article>) -> Set<String> {
|
|
//
|
|
// return Set(articles.map { $0.feedID })
|
|
// }
|
|
//
|
|
// func deletePossibleOldArticles(_ articles: Set<Article>) {
|
|
//
|
|
// let feedIDs = feedIDsFromArticles(articles)
|
|
// if feedIDs.isEmpty {
|
|
// return
|
|
// }
|
|
// }
|
|
}
|