2017-07-03 20:20:14 +02:00
//
2017-07-29 21:08:10 +02:00
// S t a t u s e s T a b l e . s w i f t
2018-08-29 07:18:24 +02:00
// N e t N e w s W i r e
2017-07-03 19:40:48 +02:00
//
// C r e a t e d b y B r e n t S i m m o n s o n 5 / 8 / 1 6 .
// C o p y r i g h t © 2 0 1 6 R a n c h e r o S o f t w a r e , L L C . A l l r i g h t s r e s e r v e d .
//
import Foundation
import RSCore
import RSDatabase
2020-07-30 02:48:54 +02:00
import RSDatabaseObjC
2018-07-24 03:29:08 +02:00
import Articles
2017-07-03 19:40:48 +02:00
2017-08-06 21:37:47 +02:00
// A r t i c l e - > A r t i c l e S t a t u s i s a t o - o n e r e l a t i o n s h i p .
//
2020-04-13 02:12:36 +02:00
// C R E A T E T A B L E i f n o t E X I S T S s t a t u s e s ( a r t i c l e I D T E X T N O T N U L L P R I M A R Y K E Y , r e a d B O O L N O T N U L L D E F A U L T 0 , s t a r r e d B O O L N O T N U L L D E F A U L T 0 , d a t e A r r i v e d D A T E N O T N U L L D E F A U L T 0 ) ;
2017-08-06 21:37:47 +02:00
2017-07-29 21:08:10 +02:00
final class StatusesTable : DatabaseTable {
2017-07-30 20:22:21 +02:00
2017-09-02 23:19:42 +02:00
let name = DatabaseTableName . statuses
2017-09-05 03:29:02 +02:00
private let cache = StatusCache ( )
2019-11-30 06:49:44 +01:00
private let queue : DatabaseQueue
2017-09-05 17:53:45 +02:00
2019-11-30 06:49:44 +01:00
init ( queue : DatabaseQueue ) {
2017-09-05 17:53:45 +02:00
self . queue = queue
}
2017-09-14 22:32:06 +02:00
2019-07-09 07:09:28 +02:00
// MARK: - C r e a t i n g / U p d a t i n g
2017-09-03 01:08:02 +02:00
2020-04-10 22:19:33 +02:00
func ensureStatusesForArticleIDs ( _ articleIDs : Set < String > , _ read : Bool , _ database : FMDatabase ) -> ( [ String : ArticleStatus ] , Set < String > ) {
2017-09-03 01:08:02 +02:00
// C h e c k c a c h e .
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus ( articleIDs )
if articleIDsMissingCachedStatus . isEmpty {
2020-04-10 22:19:33 +02:00
return ( statusesDictionary ( articleIDs ) , Set < String > ( ) )
2017-09-03 01:08:02 +02:00
}
2017-09-05 17:53:45 +02:00
2017-09-03 01:08:02 +02:00
// C h e c k d a t a b a s e .
2017-09-19 07:00:35 +02:00
fetchAndCacheStatusesForArticleIDs ( articleIDsMissingCachedStatus , database )
2017-09-05 17:53:45 +02:00
2017-09-19 07:00:35 +02:00
let articleIDsNeedingStatus = self . articleIDsWithNoCachedStatus ( articleIDs )
if ! articleIDsNeedingStatus . isEmpty {
// C r e a t e n e w s t a t u s e s .
2019-05-13 15:32:03 +02:00
self . createAndSaveStatusesForArticleIDs ( articleIDsNeedingStatus , read , database )
2017-09-03 01:08:02 +02:00
}
2017-09-19 07:00:35 +02:00
2020-04-10 22:19:33 +02:00
return ( statusesDictionary ( articleIDs ) , articleIDsNeedingStatus )
2017-09-03 01:08:02 +02:00
}
2019-12-12 07:28:01 +01:00
func existingStatusesForArticleIDs ( _ articleIDs : Set < String > , _ database : FMDatabase ) -> [ String : ArticleStatus ] {
// C h e c k c a c h e .
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus ( articleIDs )
if articleIDsMissingCachedStatus . isEmpty {
return statusesDictionary ( articleIDs )
}
// C h e c k d a t a b a s e .
fetchAndCacheStatusesForArticleIDs ( articleIDsMissingCachedStatus , database )
return statusesDictionary ( articleIDs )
}
2019-07-09 07:09:28 +02:00
// MARK: - M a r k i n g
2017-09-05 03:29:02 +02:00
2019-07-09 06:09:16 +02:00
@ discardableResult
2019-05-22 00:59:33 +02:00
func mark ( _ statuses : Set < ArticleStatus > , _ statusKey : ArticleStatus . Key , _ flag : Bool , _ database : FMDatabase ) -> Set < ArticleStatus > ? {
2017-09-16 20:04:29 +02:00
// S e t s f l a g i n b o t h m e m o r y a n d i n d a t a b a s e .
var updatedStatuses = Set < ArticleStatus > ( )
for status in statuses {
if status . boolStatus ( forKey : statusKey ) = = flag {
continue
}
2017-09-18 22:17:30 +02:00
status . setBoolStatus ( flag , forKey : statusKey )
updatedStatuses . insert ( status )
2017-09-16 20:04:29 +02:00
}
if updatedStatuses . isEmpty {
2017-10-09 06:06:25 +02:00
return nil
2017-09-16 20:04:29 +02:00
}
2017-09-19 07:00:35 +02:00
let articleIDs = updatedStatuses . articleIDs ( )
2019-05-22 00:59:33 +02:00
self . markArticleIDs ( articleIDs , statusKey , flag , database )
2017-10-09 06:06:25 +02:00
return updatedStatuses
2017-09-05 03:29:02 +02:00
}
2020-04-10 23:25:58 +02:00
func markAndFetchNew ( _ articleIDs : Set < String > , _ statusKey : ArticleStatus . Key , _ flag : Bool , _ database : FMDatabase ) -> Set < String > {
2020-04-10 22:19:33 +02:00
let ( statusesDictionary , newStatusIDs ) = ensureStatusesForArticleIDs ( articleIDs , flag , database )
2019-12-17 07:45:59 +01:00
let statuses = Set ( statusesDictionary . values )
mark ( statuses , statusKey , flag , database )
2020-04-10 23:25:58 +02:00
return newStatusIDs
2019-12-17 07:45:59 +01:00
}
2019-07-09 07:09:28 +02:00
// MARK: - F e t c h i n g
2017-09-05 03:29:02 +02:00
2019-12-16 07:09:27 +01:00
func fetchUnreadArticleIDs ( ) throws -> Set < String > {
2020-04-13 02:12:36 +02:00
return try fetchArticleIDs ( " select articleID from statuses where read=0; " )
2019-05-14 13:20:53 +02:00
}
2019-12-16 07:09:27 +01:00
func fetchStarredArticleIDs ( ) throws -> Set < String > {
2020-04-13 02:12:36 +02:00
return try fetchArticleIDs ( " select articleID from statuses where starred=1; " )
2019-05-14 13:20:53 +02:00
}
2019-12-17 22:28:48 +01:00
func fetchArticleIDsForStatusesWithoutArticlesNewerThan ( _ cutoffDate : Date , _ completion : @ escaping ArticleIDsCompletionBlock ) {
queue . runInDatabase { databaseResult in
var error : DatabaseError ?
var articleIDs = Set < String > ( )
func makeDatabaseCall ( _ database : FMDatabase ) {
2020-04-13 02:12:36 +02:00
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); "
2019-12-17 22:28:48 +01:00
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 ) )
}
}
}
}
2019-05-17 21:56:27 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticleIDs ( _ sql : String ) throws -> Set < String > {
2019-12-16 07:37:45 +01:00
var error : DatabaseError ?
2019-08-30 17:39:52 +02:00
var articleIDs = Set < String > ( )
2019-12-16 07:09:27 +01:00
queue . runInDatabaseSync { databaseResult in
switch databaseResult {
case . success ( let database ) :
if let resultSet = database . executeQuery ( sql , withArgumentsIn : nil ) {
articleIDs = resultSet . mapToSet ( self . articleIDWithRow )
}
2019-12-16 07:37:45 +01:00
case . failure ( let databaseError ) :
error = databaseError
2019-05-14 13:20:53 +02:00
}
2019-12-16 07:09:27 +01:00
}
if let error = error {
throw ( error )
2019-05-14 13:20:53 +02:00
}
2019-08-30 17:39:52 +02:00
return articleIDs
2019-05-14 13:20:53 +02:00
}
func articleIDWithRow ( _ row : FMResultSet ) -> String ? {
return row . string ( forColumn : DatabaseKey . articleID )
}
2017-09-05 03:29:02 +02:00
func statusWithRow ( _ row : FMResultSet ) -> ArticleStatus ? {
guard let articleID = row . string ( forColumn : DatabaseKey . articleID ) else {
return nil
}
2019-09-28 21:18:08 +02:00
return statusWithRow ( row , articleID : articleID )
}
func statusWithRow ( _ row : FMResultSet , articleID : String ) -> ArticleStatus ? {
2017-09-19 07:00:35 +02:00
if let cachedStatus = cache [ articleID ] {
return cachedStatus
}
2019-09-28 21:18:08 +02:00
2017-09-05 03:29:02 +02:00
guard let dateArrived = row . date ( forColumn : DatabaseKey . dateArrived ) else {
return nil
2017-08-06 21:37:47 +02:00
}
2017-09-05 03:29:02 +02:00
let articleStatus = ArticleStatus ( articleID : articleID , dateArrived : dateArrived , row : row )
2017-09-19 07:00:35 +02:00
cache . addStatusIfNotCached ( articleStatus )
2019-09-28 21:18:08 +02:00
2017-09-05 03:29:02 +02:00
return articleStatus
2017-07-03 19:40:48 +02:00
}
2017-09-20 22:29:21 +02:00
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
}
2019-10-25 07:28:26 +02:00
// MARK: - C l e a n u p
func removeStatuses ( _ articleIDs : Set < String > , _ database : FMDatabase ) {
deleteRowsWhere ( key : DatabaseKey . articleID , equalsAnyValue : Array ( articleIDs ) , in : database )
}
2017-09-14 22:32:06 +02:00
}
// MARK: - P r i v a t e
private extension StatusesTable {
2017-08-01 03:39:42 +02:00
2019-07-09 07:09:28 +02:00
// MARK: - C a c h e
2017-09-05 17:53:45 +02:00
2017-09-03 01:08:02 +02:00
func articleIDsWithNoCachedStatus ( _ articleIDs : Set < String > ) -> Set < String > {
return Set ( articleIDs . filter { cache [ $0 ] = = nil } )
}
2019-07-09 07:09:28 +02:00
// MARK: - C r e a t i n g
2017-07-03 19:40:48 +02:00
2017-09-19 07:00:35 +02:00
func saveStatuses ( _ statuses : Set < ArticleStatus > , _ database : FMDatabase ) {
let statusArray = statuses . map { $0 . databaseDictionary ( ) ! }
self . insertRows ( statusArray , insertType : . orIgnore , in : database )
2017-08-27 00:37:15 +02:00
}
2019-05-13 15:32:03 +02:00
func createAndSaveStatusesForArticleIDs ( _ articleIDs : Set < String > , _ read : Bool , _ database : FMDatabase ) {
2017-08-27 00:37:15 +02:00
let now = Date ( )
2019-05-13 15:32:03 +02:00
let statuses = Set ( articleIDs . map { ArticleStatus ( articleID : $0 , read : read , dateArrived : now ) } )
2017-09-05 17:53:45 +02:00
cache . addIfNotCached ( statuses )
2017-09-19 07:00:35 +02:00
saveStatuses ( statuses , database )
2017-08-27 00:37:15 +02:00
}
2017-07-03 19:40:48 +02:00
2017-09-19 07:00:35 +02:00
func fetchAndCacheStatusesForArticleIDs ( _ articleIDs : Set < String > , _ database : FMDatabase ) {
guard let resultSet = self . selectRowsWhere ( key : DatabaseKey . articleID , inValues : Array ( articleIDs ) , in : database ) else {
return
2017-09-03 01:08:02 +02:00
}
2017-09-19 07:00:35 +02:00
let statuses = resultSet . mapToSet ( self . statusWithRow )
self . cache . addIfNotCached ( statuses )
2017-09-05 03:29:02 +02:00
}
2017-09-16 20:04:29 +02:00
2019-07-09 07:09:28 +02:00
// MARK: - M a r k i n g
2017-09-16 20:04:29 +02:00
2017-10-09 06:06:25 +02:00
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 )
2017-09-16 20:04:29 +02:00
}
2017-09-05 03:29:02 +02:00
}
2017-09-19 07:00:35 +02:00
// MARK: -
2017-09-05 03:29:02 +02:00
private final class StatusCache {
2017-09-18 22:17:30 +02:00
// S e r i a l d a t a b a s e q u e u e o n l y .
2017-09-05 03:29:02 +02:00
var dictionary = [ String : ArticleStatus ] ( )
2017-11-20 01:28:26 +01:00
var cachedStatuses : Set < ArticleStatus > {
2018-02-14 22:14:25 +01:00
return Set ( dictionary . values )
2017-11-20 01:28:26 +01:00
}
2017-09-05 03:29:02 +02:00
func add ( _ statuses : Set < ArticleStatus > ) {
// R e p l a c e s a n y c a c h e d s t a t u s e s .
for status in statuses {
self [ status . articleID ] = status
}
}
2017-09-19 07:00:35 +02:00
func addStatusIfNotCached ( _ status : ArticleStatus ) {
addIfNotCached ( Set ( [ status ] ) )
}
2017-09-05 17:53:45 +02:00
func addIfNotCached ( _ statuses : Set < ArticleStatus > ) {
// D o e s n o t r e p l a c e a l r e a d y c a c h e d s t a t u s e s .
for status in statuses {
let articleID = status . articleID
if let _ = self [ articleID ] {
continue
2017-09-05 03:29:02 +02:00
}
2017-09-05 17:53:45 +02:00
self [ articleID ] = status
2017-09-05 03:29:02 +02:00
}
}
2017-11-20 01:28:26 +01:00
2017-09-09 21:57:24 +02:00
subscript ( _ articleID : String ) -> ArticleStatus ? {
2017-09-05 03:29:02 +02:00
get {
2017-10-08 10:54:37 +02:00
return dictionary [ articleID ]
2017-09-05 03:29:02 +02:00
}
set {
2017-10-08 10:54:37 +02:00
dictionary [ articleID ] = newValue
2017-09-05 03:29:02 +02:00
}
2017-09-03 01:08:02 +02:00
}
2017-07-03 19:40:48 +02:00
}
2017-09-05 03:29:02 +02:00