288 lines
7.8 KiB
Swift
288 lines
7.8 KiB
Swift
//
|
|
// StatusesTable.swift
|
|
// NetNewsWire
|
|
//
|
|
// Created by Brent Simmons on 5/8/16.
|
|
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import RSCore
|
|
import RSDatabase
|
|
import Articles
|
|
|
|
// Article->ArticleStatus is a to-one relationship.
|
|
//
|
|
// CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0);
|
|
|
|
final class StatusesTable: DatabaseTable {
|
|
|
|
let name = DatabaseTableName.statuses
|
|
private let cache = StatusCache()
|
|
private let queue: DatabaseQueue
|
|
|
|
init(queue: DatabaseQueue) {
|
|
self.queue = queue
|
|
}
|
|
|
|
// MARK: - Creating/Updating
|
|
|
|
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) -> [String: ArticleStatus] {
|
|
// Check cache.
|
|
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
|
|
if articleIDsMissingCachedStatus.isEmpty {
|
|
return statusesDictionary(articleIDs)
|
|
}
|
|
|
|
// Check database.
|
|
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database)
|
|
|
|
let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs)
|
|
if !articleIDsNeedingStatus.isEmpty {
|
|
// Create new statuses.
|
|
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, read, database)
|
|
}
|
|
|
|
return statusesDictionary(articleIDs)
|
|
}
|
|
|
|
func existingStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) -> [String: ArticleStatus] {
|
|
// Check cache.
|
|
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
|
|
if articleIDsMissingCachedStatus.isEmpty {
|
|
return statusesDictionary(articleIDs)
|
|
}
|
|
|
|
// Check database.
|
|
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database)
|
|
|
|
return statusesDictionary(articleIDs)
|
|
}
|
|
|
|
// MARK: - Marking
|
|
|
|
@discardableResult
|
|
func mark(_ statuses: Set<ArticleStatus>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) -> Set<ArticleStatus>? {
|
|
// Sets flag in both memory and in database.
|
|
|
|
var updatedStatuses = Set<ArticleStatus>()
|
|
|
|
for status in statuses {
|
|
if status.boolStatus(forKey: statusKey) == flag {
|
|
continue
|
|
}
|
|
status.setBoolStatus(flag, forKey: statusKey)
|
|
updatedStatuses.insert(status)
|
|
}
|
|
|
|
if updatedStatuses.isEmpty {
|
|
return nil
|
|
}
|
|
let articleIDs = updatedStatuses.articleIDs()
|
|
|
|
self.markArticleIDs(articleIDs, statusKey, flag, database)
|
|
|
|
return updatedStatuses
|
|
}
|
|
|
|
func mark(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
|
|
let statusesDictionary = ensureStatusesForArticleIDs(articleIDs, flag, database)
|
|
let statuses = Set(statusesDictionary.values)
|
|
mark(statuses, statusKey, flag, database)
|
|
}
|
|
|
|
// MARK: - Fetching
|
|
|
|
func fetchUnreadArticleIDs() throws -> Set<String> {
|
|
return try fetchArticleIDs("select articleID from statuses where read=0;")
|
|
}
|
|
|
|
func fetchStarredArticleIDs() throws -> Set<String> {
|
|
return try fetchArticleIDs("select articleID from statuses where starred=1;")
|
|
}
|
|
|
|
func fetchArticleIDsForStatusesWithoutArticlesNewerThan(_ cutoffDate: Date, _ completion: @escaping ArticleIDsCompletionBlock) {
|
|
queue.runInDatabase { databaseResult in
|
|
|
|
var error: DatabaseError?
|
|
var articleIDs = Set<String>()
|
|
|
|
func makeDatabaseCall(_ database: FMDatabase) {
|
|
let sql = "select articleID from statuses s where (starred=1 or dateArrived>?) and not exists (select 1 from articles a where a.articleID = s.articleID);"
|
|
if let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) {
|
|
articleIDs = resultSet.mapToSet(self.articleIDWithRow)
|
|
}
|
|
}
|
|
|
|
switch databaseResult {
|
|
case .success(let database):
|
|
makeDatabaseCall(database)
|
|
case .failure(let databaseError):
|
|
error = databaseError
|
|
}
|
|
|
|
if let error = error {
|
|
DispatchQueue.main.async {
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
else {
|
|
DispatchQueue.main.async {
|
|
completion(.success(articleIDs))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func fetchArticleIDs(_ sql: String) throws -> Set<String> {
|
|
var error: DatabaseError?
|
|
var articleIDs = Set<String>()
|
|
queue.runInDatabaseSync { databaseResult in
|
|
switch databaseResult {
|
|
case .success(let database):
|
|
if let resultSet = database.executeQuery(sql, withArgumentsIn: nil) {
|
|
articleIDs = resultSet.mapToSet(self.articleIDWithRow)
|
|
}
|
|
case .failure(let databaseError):
|
|
error = databaseError
|
|
}
|
|
}
|
|
|
|
if let error = error {
|
|
throw(error)
|
|
}
|
|
return articleIDs
|
|
}
|
|
|
|
func articleIDWithRow(_ row: FMResultSet) -> String? {
|
|
return row.string(forColumn: DatabaseKey.articleID)
|
|
}
|
|
|
|
func statusWithRow(_ row: FMResultSet) -> ArticleStatus? {
|
|
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else {
|
|
return nil
|
|
}
|
|
return statusWithRow(row, articleID: articleID)
|
|
}
|
|
|
|
func statusWithRow(_ row: FMResultSet, articleID: String) ->ArticleStatus? {
|
|
if let cachedStatus = cache[articleID] {
|
|
return cachedStatus
|
|
}
|
|
|
|
guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else {
|
|
return nil
|
|
}
|
|
|
|
let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row)
|
|
cache.addStatusIfNotCached(articleStatus)
|
|
|
|
return articleStatus
|
|
}
|
|
|
|
func statusesDictionary(_ articleIDs: Set<String>) -> [String: ArticleStatus] {
|
|
var d = [String: ArticleStatus]()
|
|
|
|
for articleID in articleIDs {
|
|
if let articleStatus = cache[articleID] {
|
|
d[articleID] = articleStatus
|
|
}
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
// MARK: - Cleanup
|
|
|
|
func removeStatuses(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
|
deleteRowsWhere(key: DatabaseKey.articleID, equalsAnyValue: Array(articleIDs), in: database)
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private extension StatusesTable {
|
|
|
|
// MARK: - Cache
|
|
|
|
func articleIDsWithNoCachedStatus(_ articleIDs: Set<String>) -> Set<String> {
|
|
return Set(articleIDs.filter { cache[$0] == nil })
|
|
}
|
|
|
|
// MARK: - Creating
|
|
|
|
func saveStatuses(_ statuses: Set<ArticleStatus>, _ database: FMDatabase) {
|
|
let statusArray = statuses.map { $0.databaseDictionary()! }
|
|
self.insertRows(statusArray, insertType: .orIgnore, in: database)
|
|
}
|
|
|
|
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) {
|
|
let now = Date()
|
|
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, read: read, dateArrived: now) })
|
|
cache.addIfNotCached(statuses)
|
|
|
|
saveStatuses(statuses, database)
|
|
}
|
|
|
|
func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
|
guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else {
|
|
return
|
|
}
|
|
|
|
let statuses = resultSet.mapToSet(self.statusWithRow)
|
|
self.cache.addIfNotCached(statuses)
|
|
}
|
|
|
|
// MARK: - Marking
|
|
|
|
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
|
|
updateRowsWithValue(NSNumber(value: flag), valueKey: statusKey.rawValue, whereKey: DatabaseKey.articleID, matches: Array(articleIDs), database: database)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private final class StatusCache {
|
|
|
|
// Serial database queue only.
|
|
|
|
var dictionary = [String: ArticleStatus]()
|
|
var cachedStatuses: Set<ArticleStatus> {
|
|
return Set(dictionary.values)
|
|
}
|
|
|
|
func add(_ statuses: Set<ArticleStatus>) {
|
|
// Replaces any cached statuses.
|
|
for status in statuses {
|
|
self[status.articleID] = status
|
|
}
|
|
}
|
|
|
|
func addStatusIfNotCached(_ status: ArticleStatus) {
|
|
addIfNotCached(Set([status]))
|
|
}
|
|
|
|
func addIfNotCached(_ statuses: Set<ArticleStatus>) {
|
|
// Does not replace already cached statuses.
|
|
|
|
for status in statuses {
|
|
let articleID = status.articleID
|
|
if let _ = self[articleID] {
|
|
continue
|
|
}
|
|
self[articleID] = status
|
|
}
|
|
}
|
|
|
|
subscript(_ articleID: String) -> ArticleStatus? {
|
|
get {
|
|
return dictionary[articleID]
|
|
}
|
|
set {
|
|
dictionary[articleID] = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
|