2017-07-03 20:20:14 +02:00
//
2017-07-29 21:08:10 +02:00
// A r t i c l 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 20:20:14 +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 / 9 / 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
2017-08-23 22:23:12 +02:00
import RSCore
2017-07-29 21:29:05 +02:00
import RSDatabase
2017-08-23 22:23:12 +02:00
import RSParser
2018-07-24 03:29:08 +02:00
import Articles
2017-07-03 20:20:14 +02:00
2017-08-21 22:31:14 +02:00
final class ArticlesTable : DatabaseTable {
let name : String
2017-09-05 17:53:45 +02:00
private let accountID : String
2019-11-30 06:49:44 +01:00
private let queue : DatabaseQueue
2017-09-05 17:53:45 +02:00
private let statusesTable : StatusesTable
2017-08-23 22:23:12 +02:00
private let authorsLookupTable : DatabaseLookupTable
2020-03-30 03:51:03 +02:00
private let retentionStyle : ArticlesDatabase . RetentionStyle
2019-12-28 07:47:02 +01:00
private var articlesCache = [ String : Article ] ( )
2017-09-05 03:29:02 +02:00
2019-02-25 00:34:10 +01:00
private lazy var searchTable : SearchTable = {
return SearchTable ( queue : queue , articlesTable : self )
} ( )
2017-08-31 22:35:48 +02:00
// TODO: u p d a t e a r t i c l e C u t o f f D a t e a s t i m e p a s s e s a n d b a s e d o n u s e r p r e f e r e n c e s .
2020-01-30 07:47:01 +01:00
let articleCutoffDate = Date ( ) . bySubtracting ( days : 90 )
2017-08-31 22:35:48 +02:00
2019-07-06 05:06:31 +02:00
private typealias ArticlesFetchMethod = ( FMDatabase ) -> Set < Article >
2020-03-30 03:51:03 +02:00
init ( name : String , accountID : String , queue : DatabaseQueue , retentionStyle : ArticlesDatabase . RetentionStyle ) {
2017-07-29 21:29:05 +02:00
2017-08-21 22:31:14 +02:00
self . name = name
2017-09-05 17:53:45 +02:00
self . accountID = accountID
2017-08-27 00:37:15 +02:00
self . queue = queue
2017-09-10 03:46:58 +02:00
self . statusesTable = StatusesTable ( queue : queue )
2020-03-30 03:51:03 +02:00
self . retentionStyle = retentionStyle
2017-08-23 22:23:12 +02:00
let authorsTable = AuthorsTable ( name : DatabaseTableName . authors )
self . authorsLookupTable = DatabaseLookupTable ( name : DatabaseTableName . authorsLookup , objectIDKey : DatabaseKey . articleID , relatedObjectIDKey : DatabaseKey . authorID , relatedTable : authorsTable , relationshipName : RelationshipName . authors )
2017-08-21 22:31:14 +02:00
}
2017-07-29 21:29:05 +02:00
2019-07-09 06:35:29 +02:00
// MARK: - F e t c h i n g A r t i c l e s f o r F e e d
2017-08-21 22:31:14 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticles ( _ webFeedID : String ) throws -> Set < Article > {
return try fetchArticles { self . fetchArticlesForFeedID ( webFeedID , withLimits : true , $0 ) }
2019-07-06 05:06:31 +02:00
}
2017-08-27 00:37:15 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticlesAsync ( _ webFeedID : String , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticlesAsync ( { self . fetchArticlesForFeedID ( webFeedID , withLimits : true , $0 ) } , completion )
2019-07-06 05:06:31 +02:00
}
2017-08-27 00:37:15 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticles ( _ webFeedIDs : Set < String > ) throws -> Set < Article > {
return try fetchArticles { self . fetchArticles ( webFeedIDs , $0 ) }
2019-11-22 17:21:30 +01:00
}
2019-12-16 07:09:27 +01:00
func fetchArticlesAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticlesAsync ( { self . fetchArticles ( webFeedIDs , $0 ) } , completion )
2019-11-22 17:21:30 +01:00
}
2019-07-09 06:35:29 +02:00
// MARK: - F e t c h i n g A r t i c l e s b y a r t i c l e I D
2019-07-06 05:06:31 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticles ( articleIDs : Set < String > ) throws -> Set < Article > {
return try fetchArticles { self . fetchArticles ( articleIDs : articleIDs , $0 ) }
2019-05-14 13:20:53 +02:00
}
2017-08-27 00:37:15 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticlesAsync ( articleIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
return fetchArticlesAsync ( { self . fetchArticles ( articleIDs : articleIDs , $0 ) } , completion )
2019-07-06 05:06:31 +02:00
}
2017-08-27 00:37:15 +02:00
2019-07-09 06:35:29 +02:00
// MARK: - F e t c h i n g U n r e a d A r t i c l e s
2019-07-06 05:06:31 +02:00
2019-12-16 07:09:27 +01:00
func fetchUnreadArticles ( _ webFeedIDs : Set < String > ) throws -> Set < Article > {
return try fetchArticles { self . fetchUnreadArticles ( webFeedIDs , $0 ) }
2019-07-06 05:06:31 +02:00
}
2019-12-16 07:09:27 +01:00
func fetchUnreadArticlesAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticlesAsync ( { self . fetchUnreadArticles ( webFeedIDs , $0 ) } , completion )
2019-07-06 05:06:31 +02:00
}
2019-07-09 06:35:29 +02:00
// MARK: - F e t c h i n g T o d a y A r t i c l e s
2019-07-06 05:06:31 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticlesSince ( _ webFeedIDs : Set < String > , _ cutoffDate : Date ) throws -> Set < Article > {
return try fetchArticles { self . fetchArticlesSince ( webFeedIDs , cutoffDate , $0 ) }
2017-08-21 22:31:14 +02:00
}
2017-09-01 22:31:27 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticlesSinceAsync ( _ webFeedIDs : Set < String > , _ cutoffDate : Date , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticlesAsync ( { self . fetchArticlesSince ( webFeedIDs , cutoffDate , $0 ) } , completion )
2019-07-06 05:06:31 +02:00
}
2018-02-11 02:37:47 +01:00
2019-07-09 06:35:29 +02:00
// MARK: - F e t c h i n g S t a r r e d A r t i c l e s
2018-02-11 21:07:55 +01:00
2019-12-16 07:09:27 +01:00
func fetchStarredArticles ( _ webFeedIDs : Set < String > ) throws -> Set < Article > {
return try fetchArticles { self . fetchStarredArticles ( webFeedIDs , $0 ) }
2018-02-11 21:07:55 +01:00
}
2019-12-16 07:09:27 +01:00
func fetchStarredArticlesAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticlesAsync ( { self . fetchStarredArticles ( webFeedIDs , $0 ) } , completion )
2019-07-06 05:06:31 +02:00
}
2019-07-09 06:35:29 +02:00
// MARK: - F e t c h i n g S e a r c h A r t i c l e s
2019-07-06 05:06:31 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticlesMatching ( _ searchString : String ) throws -> Set < Article > {
2019-02-25 04:22:16 +01:00
var articles : Set < Article > = Set < Article > ( )
2019-12-16 07:37:45 +01:00
var error : DatabaseError ? = nil
2019-12-16 21:49:46 +01:00
2019-12-16 07:09:27 +01:00
queue . runInDatabaseSync { ( databaseResult ) in
switch databaseResult {
case . success ( let database ) :
articles = self . fetchArticlesMatching ( searchString , database )
2019-12-16 07:37:45 +01:00
case . failure ( let databaseError ) :
error = databaseError
2019-12-16 07:09:27 +01:00
}
2019-12-09 22:06:03 +01:00
}
2019-12-16 21:49:46 +01:00
2019-12-16 07:09:27 +01:00
if let error = error {
throw ( error )
2019-02-19 07:29:43 +01:00
}
2019-08-31 22:53:47 +02:00
return articles
}
2019-12-16 07:09:27 +01:00
func fetchArticlesMatching ( _ searchString : String , _ webFeedIDs : Set < String > ) throws -> Set < Article > {
var articles = try fetchArticlesMatching ( searchString )
2019-11-15 03:11:41 +01:00
articles = articles . filter { webFeedIDs . contains ( $0 . webFeedID ) }
2019-02-25 04:22:16 +01:00
return articles
2019-02-19 07:29:43 +01:00
}
2019-12-16 07:09:27 +01:00
func fetchArticlesMatchingWithArticleIDs ( _ searchString : String , _ articleIDs : Set < String > ) throws -> Set < Article > {
var articles = try fetchArticlesMatching ( searchString )
2019-08-31 22:53:47 +02:00
articles = articles . filter { articleIDs . contains ( $0 . articleID ) }
return articles
}
2019-12-16 07:09:27 +01:00
func fetchArticlesMatchingAsync ( _ searchString : String , _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticlesAsync ( { self . fetchArticlesMatching ( searchString , webFeedIDs , $0 ) } , completion )
2019-07-06 05:06:31 +02:00
}
2019-12-16 07:09:27 +01:00
func fetchArticlesMatchingWithArticleIDsAsync ( _ searchString : String , _ articleIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticlesAsync ( { self . fetchArticlesMatchingWithArticleIDs ( searchString , articleIDs , $0 ) } , completion )
2019-08-31 22:53:47 +02:00
}
2019-07-06 05:06:31 +02:00
2019-07-09 06:35:29 +02:00
// MARK: - F e t c h i n g A r t i c l e s f o r I n d e x e r
2019-07-06 05:06:31 +02:00
2019-02-25 00:34:10 +01:00
func fetchArticleSearchInfos ( _ articleIDs : Set < String > , in database : FMDatabase ) -> Set < ArticleSearchInfo > ? {
let parameters = articleIDs . map { $0 as AnyObject }
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( articleIDs . count ) ) !
let sql = " select articleID, title, contentHTML, contentText, summary, searchRowID from articles where articleID in \( placeholders ) ; " ;
if let resultSet = database . executeQuery ( sql , withArgumentsIn : parameters ) {
return resultSet . mapToSet { ( row ) -> ArticleSearchInfo ? in
let articleID = row . string ( forColumn : DatabaseKey . articleID ) !
let title = row . string ( forColumn : DatabaseKey . title )
let contentHTML = row . string ( forColumn : DatabaseKey . contentHTML )
let contentText = row . string ( forColumn : DatabaseKey . contentText )
let summary = row . string ( forColumn : DatabaseKey . summary )
let searchRowIDObject = row . object ( forColumnName : DatabaseKey . searchRowID )
var searchRowID : Int ? = nil
if searchRowIDObject != nil && ! ( searchRowIDObject is NSNull ) {
searchRowID = Int ( row . longLongInt ( forColumn : DatabaseKey . searchRowID ) )
}
return ArticleSearchInfo ( articleID : articleID , title : title , contentHTML : contentHTML , contentText : contentText , summary : summary , searchRowID : searchRowID )
}
}
return nil
}
2019-07-09 06:35:29 +02:00
// MARK: - U p d a t i n g
2019-10-14 04:02:56 +02:00
2020-03-30 08:20:01 +02:00
func update ( _ parsedItems : Set < ParsedItem > , _ webFeedID : String , _ completion : @ escaping UpdateArticlesCompletionBlock ) {
precondition ( retentionStyle = = . feedBased )
if parsedItems . isEmpty {
callUpdateArticlesCompletionBlock ( nil , nil , completion )
return
}
// 1 . E n s u r e s t a t u s e s f o r a l l t h e i n c o m i n g a r t i c l e s .
// 2 . C r e a t e i n c o m i n g a r t i c l e s w i t h p a r s e d I t e m s .
// 3 . I g n o r e i n c o m i n g a r t i c l e s t h a t a r e u s e r D e l e t e d
// 4 . F e t c h a l l a r t i c l e s f o r t h e f e e d .
// 5 . C r e a t e a r r a y o f A r t i c l e s n o t i n d a t a b a s e a n d s a v e t h e m .
// 6 . C r e a t e a r r a y o f u p d a t e d A r t i c l e s a n d s a v e w h a t ’ s c h a n g e d .
// 7 . C a l l b a c k w i t h n e w a n d u p d a t e d A r t i c l e s .
// 8 . D e l e t e A r t i c l e s i n d a t a b a s e n o l o n g e r p r e s e n t i n t h e f e e d .
// 9 . U p d a t e s e a r c h i n d e x .
self . queue . runInTransaction { ( databaseResult ) in
func makeDatabaseCalls ( _ database : FMDatabase ) {
let articleIDs = parsedItems . articleIDs ( )
2020-04-10 22:19:33 +02:00
let ( statusesDictionary , _ ) = self . statusesTable . ensureStatusesForArticleIDs ( articleIDs , false , database ) // 1
2020-03-30 08:20:01 +02:00
assert ( statusesDictionary . count = = articleIDs . count )
let allIncomingArticles = Article . articlesWithParsedItems ( parsedItems , webFeedID , self . accountID , statusesDictionary ) // 2
let incomingArticles = Set ( allIncomingArticles . filter { ! ( $0 . status . userDeleted ) } ) // 3
if incomingArticles . isEmpty {
self . callUpdateArticlesCompletionBlock ( nil , nil , completion )
return
}
let fetchedArticles = self . fetchArticlesForFeedID ( webFeedID , withLimits : false , database ) // 4
let fetchedArticlesDictionary = fetchedArticles . dictionary ( )
let newArticles = self . findAndSaveNewArticles ( incomingArticles , fetchedArticlesDictionary , database ) // 5
let updatedArticles = self . findAndSaveUpdatedArticles ( incomingArticles , fetchedArticlesDictionary , database ) // 6
self . callUpdateArticlesCompletionBlock ( newArticles , updatedArticles , completion ) // 7
self . addArticlesToCache ( newArticles )
self . addArticlesToCache ( updatedArticles )
// 8 . D e l e t e a r t i c l e s n o l o n g e r i n f e e d .
let articleIDsToDelete = fetchedArticles . articleIDs ( ) . filter { ! ( articleIDs . contains ( $0 ) ) }
if ! articleIDsToDelete . isEmpty {
self . removeArticles ( articleIDsToDelete , database )
self . removeArticleIDsFromCache ( articleIDsToDelete )
}
// 9 . U p d a t e s e a r c h i n d e x .
if let newArticles = newArticles {
self . searchTable . indexNewArticles ( newArticles , database )
}
if let updatedArticles = updatedArticles {
self . searchTable . indexUpdatedArticles ( updatedArticles , database )
}
}
switch databaseResult {
case . success ( let database ) :
makeDatabaseCalls ( database )
case . failure ( let databaseError ) :
DispatchQueue . main . async {
completion ( . failure ( databaseError ) )
}
}
}
}
2020-03-23 03:25:53 +01:00
func update ( _ webFeedIDsAndItems : [ String : Set < ParsedItem > ] , _ read : Bool , _ completion : @ escaping UpdateArticlesCompletionBlock ) {
2020-03-30 08:20:01 +02:00
precondition ( retentionStyle = = . syncSystem )
2020-03-23 03:25:53 +01:00
if webFeedIDsAndItems . isEmpty {
2019-12-16 07:09:27 +01:00
callUpdateArticlesCompletionBlock ( nil , nil , completion )
2017-09-02 23:19:42 +02:00
return
}
2017-09-19 07:00:35 +02:00
// 1 . E n s u r e s t a t u s e s f o r a l l t h e i n c o m i n g a r t i c l e s .
// 2 . C r e a t e i n c o m i n g a r t i c l e s w i t h p a r s e d I t e m s .
2017-09-11 15:46:32 +02:00
// 3 . I g n o r e i n c o m i n g a r t i c l e s t h a t a r e u s e r D e l e t e d | | ( ! s t a r r e d a n d r e a l l y o l d )
// 4 . F e t c h a l l a r t i c l e s f o r t h e f e e d .
2017-09-06 22:33:04 +02:00
// 5 . C r e a t e a r r a y o f A r t i c l e s n o t i n d a t a b a s e a n d s a v e t h e m .
// 6 . C r e a t e a r r a y o f u p d a t e d A r t i c l e s a n d s a v e w h a t ’ s c h a n g e d .
// 7 . C a l l b a c k w i t h n e w a n d u p d a t e d A r t i c l e s .
2019-02-25 00:34:10 +01:00
// 8 . U p d a t e s e a r c h i n d e x .
2019-12-16 07:09:27 +01:00
self . queue . runInTransaction { ( databaseResult ) in
2019-12-16 21:49:46 +01:00
func makeDatabaseCalls ( _ database : FMDatabase ) {
2020-03-23 03:25:53 +01:00
var articleIDs = Set < String > ( )
for ( _ , parsedItems ) in webFeedIDsAndItems {
articleIDs . formUnion ( parsedItems . articleIDs ( ) )
}
2020-04-10 22:19:33 +02:00
let ( statusesDictionary , _ ) = self . statusesTable . ensureStatusesForArticleIDs ( articleIDs , read , database ) // 1
2019-12-16 21:49:46 +01:00
assert ( statusesDictionary . count = = articleIDs . count )
2019-10-14 04:02:56 +02:00
2020-03-23 03:25:53 +01:00
let allIncomingArticles = Article . articlesWithWebFeedIDsAndItems ( webFeedIDsAndItems , self . accountID , statusesDictionary ) // 2
2019-12-16 21:49:46 +01:00
if allIncomingArticles . isEmpty {
self . callUpdateArticlesCompletionBlock ( nil , nil , completion )
return
}
2019-10-14 04:02:56 +02:00
2019-12-16 21:49:46 +01:00
let incomingArticles = self . filterIncomingArticles ( allIncomingArticles ) // 3
if incomingArticles . isEmpty {
self . callUpdateArticlesCompletionBlock ( nil , nil , completion )
return
}
2017-09-19 07:00:35 +02:00
2019-12-16 21:49:46 +01:00
let incomingArticleIDs = incomingArticles . articleIDs ( )
let fetchedArticles = self . fetchArticles ( articleIDs : incomingArticleIDs , database ) // 4
let fetchedArticlesDictionary = fetchedArticles . dictionary ( )
2019-10-14 04:02:56 +02:00
2019-12-16 21:49:46 +01:00
let newArticles = self . findAndSaveNewArticles ( incomingArticles , fetchedArticlesDictionary , database ) // 5
let updatedArticles = self . findAndSaveUpdatedArticles ( incomingArticles , fetchedArticlesDictionary , database ) // 6
2019-10-14 04:02:56 +02:00
2019-12-16 21:49:46 +01:00
self . callUpdateArticlesCompletionBlock ( newArticles , updatedArticles , completion ) // 7
2019-02-25 00:34:10 +01:00
2019-12-29 06:19:02 +01:00
self . addArticlesToCache ( newArticles )
self . addArticlesToCache ( updatedArticles )
2019-12-16 21:49:46 +01:00
// 8 . U p d a t e s e a r c h i n d e x .
if let newArticles = newArticles {
self . searchTable . indexNewArticles ( newArticles , database )
}
if let updatedArticles = updatedArticles {
self . searchTable . indexUpdatedArticles ( updatedArticles , database )
}
2019-02-25 00:34:10 +01:00
}
2019-12-16 21:49:46 +01:00
switch databaseResult {
case . success ( let database ) :
makeDatabaseCalls ( database )
case . failure ( let databaseError ) :
DispatchQueue . main . async {
completion ( . failure ( databaseError ) )
}
2019-02-25 00:34:10 +01:00
}
2017-09-02 23:19:42 +02:00
}
2017-08-23 22:23:12 +02:00
}
2017-09-01 22:31:27 +02:00
2019-07-09 06:35:29 +02:00
// MARK: - U n r e a d C o u n t s
2017-08-23 22:23:12 +02:00
2019-12-16 07:09:27 +01:00
func fetchUnreadCount ( _ webFeedIDs : Set < String > , _ since : Date , _ completion : @ escaping SingleUnreadCountCompletionBlock ) {
2017-11-19 21:44:17 +01:00
// G e t u n r e a d c o u n t f o r t o d a y , f o r i n s t a n c e .
2019-12-16 07:09:27 +01:00
if webFeedIDs . isEmpty {
completion ( . success ( 0 ) )
2017-11-25 22:11:19 +01:00
return
}
2019-12-16 07:09:27 +01:00
queue . runInDatabase { databaseResult in
2019-12-16 21:49:46 +01:00
func makeDatabaseCalls ( _ database : FMDatabase ) {
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( webFeedIDs . count ) ) !
let sql = " select count(*) from articles natural join statuses where feedID in \( placeholders ) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and read=0 and userDeleted=0; "
2017-11-19 21:44:17 +01:00
2019-12-16 21:49:46 +01:00
var parameters = [ Any ] ( )
parameters += Array ( webFeedIDs ) as [ Any ]
parameters += [ since ] as [ Any ]
parameters += [ since ] as [ Any ]
2017-11-19 21:44:17 +01:00
2019-12-16 21:49:46 +01:00
let unreadCount = self . numberWithSQLAndParameters ( sql , parameters , in : database )
2017-11-19 21:44:17 +01:00
2019-12-16 21:49:46 +01:00
DispatchQueue . main . async {
completion ( . success ( unreadCount ) )
}
}
switch databaseResult {
case . success ( let database ) :
makeDatabaseCalls ( database )
case . failure ( let databaseError ) :
DispatchQueue . main . async {
completion ( . failure ( databaseError ) )
}
2017-11-19 21:44:17 +01:00
}
}
}
2019-12-16 07:09:27 +01:00
func fetchStarredAndUnreadCount ( _ webFeedIDs : Set < String > , _ completion : @ escaping SingleUnreadCountCompletionBlock ) {
if webFeedIDs . isEmpty {
completion ( . success ( 0 ) )
2017-11-25 22:11:19 +01:00
return
}
2019-12-16 07:09:27 +01:00
queue . runInDatabase { databaseResult in
2019-12-16 21:49:46 +01:00
func makeDatabaseCalls ( _ database : FMDatabase ) {
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( webFeedIDs . count ) ) !
let sql = " select count(*) from articles natural join statuses where feedID in \( placeholders ) and read=0 and starred=1 and userDeleted=0; "
let parameters = Array ( webFeedIDs ) as [ Any ]
let unreadCount = self . numberWithSQLAndParameters ( sql , parameters , in : database )
2019-12-16 07:09:27 +01:00
DispatchQueue . main . async {
2019-12-16 21:49:46 +01:00
completion ( . success ( unreadCount ) )
2019-12-16 07:09:27 +01:00
}
}
2019-12-16 21:49:46 +01:00
switch databaseResult {
case . success ( let database ) :
makeDatabaseCalls ( database )
case . failure ( let databaseError ) :
DispatchQueue . main . async {
completion ( . failure ( databaseError ) )
}
2017-11-20 00:40:02 +01:00
}
}
}
2019-07-09 06:35:29 +02:00
// MARK: - S t a t u s e s
2017-08-23 22:23:12 +02:00
2019-12-16 07:09:27 +01:00
func fetchUnreadArticleIDsAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleIDsCompletionBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticleIDsAsync ( . read , false , webFeedIDs , completion )
2019-12-08 07:23:44 +01:00
}
2019-12-08 05:57:23 +01:00
2019-12-16 07:09:27 +01:00
func fetchStarredArticleIDsAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleIDsCompletionBlock ) {
2019-12-15 02:01:34 +01:00
fetchArticleIDsAsync ( . starred , true , webFeedIDs , completion )
2019-12-08 05:57:23 +01:00
}
2019-12-16 07:09:27 +01:00
func fetchStarredArticleIDs ( ) throws -> Set < String > {
return try statusesTable . fetchStarredArticleIDs ( )
2019-05-14 13:20:53 +02:00
}
2019-12-17 22:28:48 +01:00
func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate ( _ completion : @ escaping ArticleIDsCompletionBlock ) {
statusesTable . fetchArticleIDsForStatusesWithoutArticlesNewerThan ( articleCutoffDate , completion )
}
2019-12-16 07:09:27 +01:00
func mark ( _ articles : Set < Article > , _ statusKey : ArticleStatus . Key , _ flag : Bool ) throws -> Set < ArticleStatus > ? {
2019-05-22 00:59:33 +02:00
var statuses : Set < ArticleStatus > ?
2019-12-16 07:37:45 +01:00
var error : DatabaseError ?
2019-12-16 07:09:27 +01:00
self . queue . runInTransactionSync { databaseResult in
switch databaseResult {
case . success ( let database ) :
statuses = self . statusesTable . mark ( articles . statuses ( ) , statusKey , flag , database )
2019-12-16 07:37:45 +01:00
case . failure ( let databaseError ) :
error = databaseError
2019-12-16 07:09:27 +01:00
}
2019-12-09 22:06:03 +01:00
}
2019-12-16 07:09:27 +01:00
if let error = error {
throw error
2019-05-22 00:59:33 +02:00
}
return statuses
2017-08-23 22:23:12 +02:00
}
2017-11-20 01:28:26 +01:00
2020-04-10 22:19:33 +02:00
func markAndFetchNew ( _ articleIDs : Set < String > , _ statusKey : ArticleStatus . Key , _ flag : Bool , _ completion : @ escaping ArticleStatusesResultBlock ) {
2019-12-17 07:45:59 +01:00
queue . runInTransaction { databaseResult in
switch databaseResult {
case . success ( let database ) :
2020-04-10 22:19:33 +02:00
let newStatusIDs = self . statusesTable . markAndFetchNew ( articleIDs , statusKey , flag , database )
2019-12-17 07:56:38 +01:00
DispatchQueue . main . async {
2020-04-10 22:19:33 +02:00
completion ( . success ( newStatusIDs ) )
2019-12-17 07:56:38 +01:00
}
2019-12-17 07:45:59 +01:00
case . failure ( let databaseError ) :
2019-12-17 07:56:38 +01:00
DispatchQueue . main . async {
2020-04-10 22:19:33 +02:00
completion ( . failure ( databaseError ) )
2019-12-17 07:56:38 +01:00
}
2019-12-17 07:45:59 +01:00
}
}
}
2020-01-10 07:27:29 +01:00
func createStatusesIfNeeded ( _ articleIDs : Set < String > , _ completion : @ escaping DatabaseCompletionBlock ) {
queue . runInTransaction { databaseResult in
switch databaseResult {
case . success ( let database ) :
let _ = self . statusesTable . ensureStatusesForArticleIDs ( articleIDs , true , database )
DispatchQueue . main . async {
completion ( nil )
}
case . failure ( let databaseError ) :
DispatchQueue . main . async {
completion ( databaseError )
}
}
}
}
2019-07-09 06:35:29 +02:00
// MARK: - I n d e x i n g
2019-02-25 00:34:10 +01:00
func indexUnindexedArticles ( ) {
2019-12-16 07:09:27 +01:00
queue . runInDatabase { databaseResult in
2019-12-16 21:49:46 +01:00
func makeDatabaseCalls ( _ database : FMDatabase ) {
let sql = " select articleID from articles where searchRowID is null limit 500; "
guard let resultSet = database . executeQuery ( sql , withArgumentsIn : nil ) else {
return
}
let articleIDs = resultSet . mapToSet { $0 . string ( forColumn : DatabaseKey . articleID ) }
if articleIDs . isEmpty {
return
}
self . searchTable . ensureIndexedArticles ( articleIDs , database )
DispatchQueue . main . async {
self . indexUnindexedArticles ( )
}
2019-02-25 00:34:10 +01:00
}
2019-12-16 21:49:46 +01:00
if let database = databaseResult . database {
makeDatabaseCalls ( database )
2019-02-25 00:34:10 +01:00
}
}
}
2019-10-13 00:06:21 +02:00
// MARK: - C a c h e s
func emptyCaches ( ) {
2019-11-30 06:49:44 +01:00
queue . runInDatabase { _ in
2019-12-28 07:47:02 +01:00
self . articlesCache = [ String : Article ] ( )
2019-10-13 00:06:21 +02:00
}
}
2019-10-25 07:28:26 +02:00
// MARK: - C l e a n u p
2020-01-09 07:07:03 +01:00
// / D e l e t e a r t i c l e s t h a t w e w o n ’ t s h o w i n t h e U I a n y l o n g e r
// / — t h e i r a r r i v a l d a t e i s b e f o r e o u r 9 0 - d a y r e c e n c y w i n d o w .
// / K e e p a l l s t a r r e d a r t i c l e s , n o m a t t e r t h e i r a g e .
func deleteOldArticles ( ) {
queue . runInTransaction { databaseResult in
func makeDatabaseCalls ( _ database : FMDatabase ) {
let sql = " delete from articles where articleID in (select articleID from articles natural join statuses where dateArrived<? and starred=0); "
let parameters = [ self . articleCutoffDate ] as [ Any ]
database . executeUpdate ( sql , withArgumentsIn : parameters )
}
if let database = databaseResult . database {
makeDatabaseCalls ( database )
}
}
}
2019-10-25 07:28:26 +02:00
// / D e l e t e a r t i c l e s f r o m f e e d s t h a t a r e n o l o n g e r i n t h e c u r r e n t s e t o f s u b s c r i b e d - t o f e e d s .
// / T h i s d e l e t e s f r o m t h e a r t i c l e s a n d a r t i c l e S t a t u s e s t a b l e s ,
// / a n d , v i a a t r i g g e r , i t a l s o d e l e t e s f r o m t h e s e a r c h i n d e x .
2019-11-15 03:11:41 +01:00
func deleteArticlesNotInSubscribedToFeedIDs ( _ webFeedIDs : Set < String > ) {
2019-12-16 07:09:27 +01:00
if webFeedIDs . isEmpty {
2019-10-25 07:28:26 +02:00
return
}
2019-12-16 07:09:27 +01:00
queue . runInDatabase { databaseResult in
2019-12-16 21:49:46 +01:00
func makeDatabaseCalls ( _ database : FMDatabase ) {
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( webFeedIDs . count ) ) !
let sql = " select articleID from articles where feedID not in \( placeholders ) ; "
let parameters = Array ( webFeedIDs ) as [ Any ]
guard let resultSet = database . executeQuery ( sql , withArgumentsIn : parameters ) else {
return
}
let articleIDs = resultSet . mapToSet { $0 . string ( forColumn : DatabaseKey . articleID ) }
if articleIDs . isEmpty {
return
}
self . removeArticles ( articleIDs , database )
self . statusesTable . removeStatuses ( articleIDs , database )
2019-10-25 07:28:26 +02:00
}
2019-12-16 21:49:46 +01:00
if let database = databaseResult . database {
makeDatabaseCalls ( database )
2019-10-25 07:28:26 +02:00
}
}
}
2017-08-21 22:31:14 +02:00
}
2017-07-29 21:29:05 +02:00
2017-09-02 23:19:42 +02:00
// MARK: - P r i v a t e
2017-08-27 00:37:15 +02:00
private extension ArticlesTable {
2019-07-09 06:35:29 +02:00
// MARK: - F e t c h i n g
2017-08-27 00:37:15 +02:00
2019-12-16 07:09:27 +01:00
private func fetchArticles ( _ fetchMethod : @ escaping ArticlesFetchMethod ) throws -> Set < Article > {
2019-07-06 05:06:31 +02:00
var articles = Set < Article > ( )
2019-12-16 07:37:45 +01:00
var error : DatabaseError ? = nil
2019-12-16 07:09:27 +01:00
queue . runInDatabaseSync { databaseResult in
switch databaseResult {
case . success ( let database ) :
articles = fetchMethod ( database )
2019-12-16 07:37:45 +01:00
case . failure ( let databaseError ) :
error = databaseError
2019-12-16 07:09:27 +01:00
}
2019-12-09 22:06:03 +01:00
}
2019-12-16 07:09:27 +01:00
if let error = error {
throw ( error )
2019-07-06 05:06:31 +02:00
}
return articles
}
2019-12-16 07:09:27 +01:00
private func fetchArticlesAsync ( _ fetchMethod : @ escaping ArticlesFetchMethod , _ completion : @ escaping ArticleSetResultBlock ) {
queue . runInDatabase { databaseResult in
2019-12-16 21:49:46 +01:00
switch databaseResult {
case . success ( let database ) :
let articles = fetchMethod ( database )
2019-12-16 07:09:27 +01:00
DispatchQueue . main . async {
2019-12-16 21:49:46 +01:00
completion ( . success ( articles ) )
}
case . failure ( let databaseError ) :
DispatchQueue . main . async {
completion ( . failure ( databaseError ) )
2019-12-16 07:09:27 +01:00
}
2019-07-06 05:06:31 +02:00
}
}
}
2017-08-27 00:37:15 +02:00
func articlesWithResultSet ( _ resultSet : FMResultSet , _ database : FMDatabase ) -> Set < Article > {
2019-12-28 07:47:02 +01:00
var cachedArticles = Set < Article > ( )
var fetchedArticles = Set < Article > ( )
2017-09-20 22:29:21 +02:00
2019-12-28 07:47:02 +01:00
while resultSet . next ( ) {
2017-09-14 22:32:06 +02:00
2019-12-28 07:47:02 +01:00
guard let articleID = resultSet . string ( forColumn : DatabaseKey . articleID ) else {
2019-09-28 21:18:08 +02:00
assertionFailure ( " Expected articleID. " )
2019-12-28 07:47:02 +01:00
continue
2017-09-19 22:36:13 +02:00
}
2019-12-28 07:47:02 +01:00
if let article = articlesCache [ articleID ] {
cachedArticles . insert ( article )
continue
2019-09-28 22:51:33 +02:00
}
2019-09-28 21:18:08 +02:00
// T h e r e s u l t S e t i s a r e s u l t o f a J O I N q u e r y w i t h t h e s t a t u s e s t a b l e ,
// s o w e c a n g e t t h e s t a t u s e s a t t h e s a m e t i m e a n d a v o i d a d d i t i o n a l d a t a b a s e l o o k u p s .
guard let status = statusesTable . statusWithRow ( resultSet , articleID : articleID ) else {
assertionFailure ( " Expected status. " )
2019-12-28 07:47:02 +01:00
continue
2017-09-19 22:36:13 +02:00
}
2019-12-28 07:47:02 +01:00
guard let article = Article ( accountID : accountID , row : resultSet , status : status ) else {
continue
2017-09-19 22:36:13 +02:00
}
2019-12-28 07:47:02 +01:00
fetchedArticles . insert ( article )
}
resultSet . close ( )
if fetchedArticles . isEmpty {
return cachedArticles
}
2017-09-19 22:36:13 +02:00
2019-12-28 07:47:02 +01:00
// F e t c h a u t h o r s f o r n o n - c a c h e d a r t i c l e s . ( A r t i c l e s f r o m t h e c a c h e a l r e a d y h a v e a u t h o r s . )
let fetchedArticleIDs = fetchedArticles . articleIDs ( )
let authorsMap = authorsLookupTable . fetchRelatedObjects ( for : fetchedArticleIDs , in : database )
let articlesWithFetchedAuthors = fetchedArticles . map { ( article ) -> Article in
if let authors = authorsMap ? . authors ( for : article . articleID ) {
return article . byAdding ( authors )
}
return article
}
2017-09-21 22:25:14 +02:00
2019-12-28 07:47:02 +01:00
// A d d f e t c h e d A r t i c l e s t o c a c h e , n o w t h a t t h e y h a v e a t t a c h e d a u t h o r s .
for article in articlesWithFetchedAuthors {
articlesCache [ article . articleID ] = article
2017-09-19 22:36:13 +02:00
}
2019-12-28 07:47:02 +01:00
return cachedArticles . union ( articlesWithFetchedAuthors )
2017-09-19 22:36:13 +02:00
}
2017-09-05 02:10:02 +02:00
func fetchArticlesWithWhereClause ( _ database : FMDatabase , whereClause : String , parameters : [ AnyObject ] , withLimits : Bool ) -> Set < Article > {
2017-08-31 22:35:48 +02:00
// D o n ’ t f e t c h a r t i c l e s t h a t s h o u l d n ’ t a p p e a r i n t h e U I . T h e r u l e s :
// * M u s t n o t b e d e l e t e d .
// * M u s t b e e i t h e r 1 ) s t a r r e d o r 2 ) d a t e A r r i v e d m u s t b e n e w e r t h a n c u t o f f d a t e .
2018-02-11 02:37:47 +01:00
if withLimits {
2019-12-29 07:20:13 +01:00
let sql = " select * from articles natural join statuses where \( whereClause ) and userDeleted=0 and (starred=1 or dateArrived>?); "
return articlesWithSQL ( sql , parameters + [ articleCutoffDate as AnyObject ] , database )
2018-02-11 02:37:47 +01:00
}
else {
let sql = " select * from articles natural join statuses where \( whereClause ) ; "
return articlesWithSQL ( sql , parameters , database )
}
2017-08-27 22:03:15 +02:00
}
2020-02-02 00:01:47 +01:00
// f u n c f e t c h U n r e a d C o u n t ( _ w e b F e e d I D : S t r i n g , _ d a t a b a s e : F M D a t a b a s e ) - > I n t {
// / / C o u n t o n l y t h e a r t i c l e s t h a t w o u l d a p p e a r i n t h e U I .
// / / * M u s t b e u n r e a d .
// / / * M u s t n o t b e d e l e t e d .
// / / * M u s t b e e i t h e r 1 ) s t a r r e d o r 2 ) d a t e A r r i v e d m u s t b e n e w e r t h a n c u t o f f d a t e .
//
// l e t s q l = " s e l e c t c o u n t ( * ) f r o m a r t i c l e s n a t u r a l j o i n s t a t u s e s w h e r e f e e d I D = ? a n d r e a d = 0 a n d u s e r D e l e t e d = 0 a n d ( s t a r r e d = 1 o r d a t e A r r i v e d > ? ) ; "
// r e t u r n n u m b e r W i t h S Q L A n d P a r a m e t e r s ( s q l , [ w e b F e e d I D , a r t i c l e C u t o f f D a t e ] , i n : d a t a b a s e )
// }
2017-08-31 22:35:48 +02:00
2019-02-19 07:29:43 +01:00
func fetchArticlesMatching ( _ searchString : String , _ database : FMDatabase ) -> Set < Article > {
2019-02-25 03:37:13 +01:00
let sql = " select rowid from search where search match ?; "
let sqlSearchString = sqliteSearchString ( with : searchString )
let searchStringParameters = [ sqlSearchString ]
guard let resultSet = database . executeQuery ( sql , withArgumentsIn : searchStringParameters ) else {
return Set < Article > ( )
}
2019-03-03 21:02:26 +01:00
let searchRowIDs = resultSet . mapToSet { $0 . longLongInt ( forColumnIndex : 0 ) }
2019-02-25 03:37:13 +01:00
if searchRowIDs . isEmpty {
return Set < Article > ( )
}
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( searchRowIDs . count ) ) !
2019-03-03 21:11:16 +01:00
let whereClause = " searchRowID in \( placeholders ) "
2019-02-25 03:37:13 +01:00
let parameters : [ AnyObject ] = Array ( searchRowIDs ) as [ AnyObject ]
2019-03-03 21:05:34 +01:00
return fetchArticlesWithWhereClause ( database , whereClause : whereClause , parameters : parameters , withLimits : true )
2019-02-25 03:37:13 +01:00
}
func sqliteSearchString ( with searchString : String ) -> String {
var s = " "
searchString . enumerateSubstrings ( in : searchString . startIndex . . < searchString . endIndex , options : . byWords ) { ( word , range , enclosingRange , stop ) in
guard let word = word else {
return
}
s += word
if s != " AND " && s != " OR " {
s += " * "
}
s += " "
}
return s
2019-02-19 07:29:43 +01:00
}
2018-02-11 21:07:55 +01:00
2017-08-27 22:03:15 +02:00
func articlesWithSQL ( _ sql : String , _ parameters : [ AnyObject ] , _ database : FMDatabase ) -> Set < Article > {
guard let resultSet = database . executeQuery ( sql , withArgumentsIn : parameters ) else {
return Set < Article > ( )
}
return articlesWithResultSet ( resultSet , database )
2017-08-27 00:37:15 +02:00
}
2017-09-02 23:19:42 +02:00
2019-12-16 07:09:27 +01:00
func fetchArticleIDsAsync ( _ statusKey : ArticleStatus . Key , _ value : Bool , _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleIDsCompletionBlock ) {
guard ! webFeedIDs . isEmpty else {
completion ( . success ( Set < String > ( ) ) )
2019-12-09 22:06:03 +01:00
return
}
2019-12-16 07:09:27 +01:00
queue . runInDatabase { databaseResult in
2019-12-16 21:49:46 +01:00
func makeDatabaseCalls ( _ database : FMDatabase ) {
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( webFeedIDs . count ) ) !
var sql = " select articleID from articles natural join statuses where feedID in \( placeholders ) and \( statusKey . rawValue ) = "
sql += value ? " 1 " : " 0 "
if statusKey != . userDeleted {
sql += " and userDeleted=0 "
2019-12-16 07:09:27 +01:00
}
2019-12-16 21:49:46 +01:00
sql += " ; "
2019-12-16 07:09:27 +01:00
2019-12-16 21:49:46 +01:00
let parameters = Array ( webFeedIDs ) as [ Any ]
2019-12-08 07:23:44 +01:00
2019-12-16 21:49:46 +01:00
guard let resultSet = database . executeQuery ( sql , withArgumentsIn : parameters ) else {
DispatchQueue . main . async {
completion ( . success ( Set < String > ( ) ) )
}
return
}
2019-12-08 07:23:44 +01:00
2019-12-16 21:49:46 +01:00
let articleIDs = resultSet . mapToSet { $0 . string ( forColumnIndex : 0 ) }
2019-12-08 07:23:44 +01:00
DispatchQueue . main . async {
2019-12-16 21:49:46 +01:00
completion ( . success ( articleIDs ) )
2019-12-08 07:23:44 +01:00
}
}
2019-12-16 21:49:46 +01:00
switch databaseResult {
case . success ( let database ) :
makeDatabaseCalls ( database )
case . failure ( let databaseError ) :
DispatchQueue . main . async {
completion ( . failure ( databaseError ) )
}
2019-12-08 07:23:44 +01:00
}
}
}
2019-12-16 07:09:27 +01:00
func fetchArticles ( _ webFeedIDs : Set < String > , _ database : FMDatabase ) -> Set < Article > {
// s e l e c t * f r o m a r t i c l e s n a t u r a l j o i n s t a t u s e s w h e r e f e e d I D i n ( ' h t t p : / / r a n c h e r o . c o m / x m l / r s s . x m l ' ) a n d r e a d = 0
if webFeedIDs . isEmpty {
return Set < Article > ( )
}
let parameters = webFeedIDs . map { $0 as AnyObject }
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( webFeedIDs . count ) ) !
let whereClause = " feedID in \( placeholders ) "
return fetchArticlesWithWhereClause ( database , whereClause : whereClause , parameters : parameters , withLimits : true )
}
func fetchUnreadArticles ( _ webFeedIDs : Set < String > , _ database : FMDatabase ) -> Set < Article > {
// s e l e c t * f r o m a r t i c l e s n a t u r a l j o i n s t a t u s e s w h e r e f e e d I D i n ( ' h t t p : / / r a n c h e r o . c o m / x m l / r s s . x m l ' ) a n d r e a d = 0
if webFeedIDs . isEmpty {
return Set < Article > ( )
}
let parameters = webFeedIDs . map { $0 as AnyObject }
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( webFeedIDs . count ) ) !
let whereClause = " feedID in \( placeholders ) and read=0 "
return fetchArticlesWithWhereClause ( database , whereClause : whereClause , parameters : parameters , withLimits : true )
}
func fetchArticlesForFeedID ( _ webFeedID : String , withLimits : Bool , _ database : FMDatabase ) -> Set < Article > {
return fetchArticlesWithWhereClause ( database , whereClause : " articles.feedID = ? " , parameters : [ webFeedID as AnyObject ] , withLimits : withLimits )
}
func fetchArticles ( articleIDs : Set < String > , _ database : FMDatabase ) -> Set < Article > {
if articleIDs . isEmpty {
return Set < Article > ( )
}
let parameters = articleIDs . map { $0 as AnyObject }
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( articleIDs . count ) ) !
let whereClause = " articleID in \( placeholders ) "
return fetchArticlesWithWhereClause ( database , whereClause : whereClause , parameters : parameters , withLimits : false )
}
func fetchArticlesSince ( _ webFeedIDs : Set < String > , _ cutoffDate : Date , _ database : FMDatabase ) -> Set < Article > {
// s e l e c t * f r o m a r t i c l e s n a t u r a l j o i n s t a t u s e s w h e r e f e e d I D i n ( ' h t t p : / / r a n c h e r o . c o m / x m l / r s s . x m l ' ) a n d ( d a t e P u b l i s h e d > ? | | ( d a t e P u b l i s h e d i s n u l l a n d d a t e A r r i v e d > ? )
//
// d a t e P u b l i s h e d m a y b e n i l , s o w e f a l l b a c k t o d a t e A r r i v e d .
if webFeedIDs . isEmpty {
return Set < Article > ( )
}
let parameters = webFeedIDs . map { $0 as AnyObject } + [ cutoffDate as AnyObject , cutoffDate as AnyObject ]
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( webFeedIDs . count ) ) !
let whereClause = " feedID in \( placeholders ) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and userDeleted = 0 "
return fetchArticlesWithWhereClause ( database , whereClause : whereClause , parameters : parameters , withLimits : false )
}
func fetchStarredArticles ( _ webFeedIDs : Set < String > , _ database : FMDatabase ) -> Set < Article > {
// s e l e c t * f r o m a r t i c l e s n a t u r a l j o i n s t a t u s e s w h e r e f e e d I D i n ( ' h t t p : / / r a n c h e r o . c o m / x m l / r s s . x m l ' ) a n d s t a r r e d = 1 a n d u s e r D e l e t e d = 0 ;
if webFeedIDs . isEmpty {
return Set < Article > ( )
}
let parameters = webFeedIDs . map { $0 as AnyObject }
let placeholders = NSString . rs_SQLValueList ( withPlaceholders : UInt ( webFeedIDs . count ) ) !
let whereClause = " feedID in \( placeholders ) and starred = 1 and userDeleted = 0 "
return fetchArticlesWithWhereClause ( database , whereClause : whereClause , parameters : parameters , withLimits : false )
}
func fetchArticlesMatching ( _ searchString : String , _ webFeedIDs : Set < String > , _ database : FMDatabase ) -> Set < Article > {
let articles = fetchArticlesMatching ( searchString , database )
// TODO: i n c l u d e t h e f e e d I D s i n t h e S Q L r a t h e r t h a n f i l t e r i n g h e r e .
return articles . filter { webFeedIDs . contains ( $0 . webFeedID ) }
}
func fetchArticlesMatchingWithArticleIDs ( _ searchString : String , _ articleIDs : Set < String > , _ database : FMDatabase ) -> Set < Article > {
let articles = fetchArticlesMatching ( searchString , database )
// TODO: i n c l u d e t h e a r t i c l e I D s i n t h e S Q L r a t h e r t h a n f i l t e r i n g h e r e .
return articles . filter { articleIDs . contains ( $0 . articleID ) }
}
2019-12-08 07:23:44 +01:00
2019-07-09 06:35:29 +02:00
// MARK: - S a v i n g P a r s e d I t e m s
2017-09-11 15:46:32 +02:00
2019-10-14 04:02:56 +02:00
func callUpdateArticlesCompletionBlock ( _ newArticles : Set < Article > ? , _ updatedArticles : Set < Article > ? , _ completion : @ escaping UpdateArticlesCompletionBlock ) {
2019-12-16 07:09:27 +01:00
let newAndUpdatedArticles = NewAndUpdatedArticles ( newArticles : newArticles , updatedArticles : updatedArticles )
2017-09-11 15:46:32 +02:00
DispatchQueue . main . async {
2019-12-16 07:09:27 +01:00
completion ( . success ( newAndUpdatedArticles ) )
2017-09-11 15:46:32 +02:00
}
}
2019-07-09 06:35:29 +02:00
// MARK: - S a v i n g N e w A r t i c l e s
2017-09-05 02:10:02 +02:00
2017-09-11 15:46:32 +02:00
func findNewArticles ( _ incomingArticles : Set < Article > , _ fetchedArticlesDictionary : [ String : Article ] ) -> Set < Article > ? {
let newArticles = Set ( incomingArticles . filter { fetchedArticlesDictionary [ $0 . articleID ] = = nil } )
return newArticles . isEmpty ? nil : newArticles
}
func findAndSaveNewArticles ( _ incomingArticles : Set < Article > , _ fetchedArticlesDictionary : [ String : Article ] , _ database : FMDatabase ) -> Set < Article > ? { // 5
guard let newArticles = findNewArticles ( incomingArticles , fetchedArticlesDictionary ) else {
return nil
}
self . saveNewArticles ( newArticles , database )
return newArticles
}
2017-09-08 22:36:30 +02:00
func saveNewArticles ( _ articles : Set < Article > , _ database : FMDatabase ) {
saveRelatedObjectsForNewArticles ( articles , database )
2017-09-05 02:10:02 +02:00
2017-09-13 22:29:52 +02:00
if let databaseDictionaries = articles . databaseDictionaries ( ) {
insertRows ( databaseDictionaries , insertType : . orReplace , in : database )
}
2017-09-08 22:36:30 +02:00
}
2017-09-05 02:10:02 +02:00
2017-09-08 22:36:30 +02:00
func saveRelatedObjectsForNewArticles ( _ articles : Set < Article > , _ database : FMDatabase ) {
let databaseObjects = articles . databaseObjects ( )
authorsLookupTable . saveRelatedObjects ( for : databaseObjects , in : database )
2017-09-05 02:10:02 +02:00
}
2019-07-09 06:35:29 +02:00
// MARK: - U p d a t i n g E x i s t i n g A r t i c l e s
2017-09-05 02:10:02 +02:00
2017-09-10 20:36:28 +02:00
func articlesWithRelatedObjectChanges < T > ( _ comparisonKeyPath : KeyPath < Article , Set < T > ? > , _ updatedArticles : Set < Article > , _ fetchedArticles : [ String : Article ] ) -> Set < Article > {
2017-09-08 22:36:30 +02:00
return updatedArticles . filter { ( updatedArticle ) -> Bool in
if let fetchedArticle = fetchedArticles [ updatedArticle . articleID ] {
2017-09-09 20:02:02 +02:00
return updatedArticle [ keyPath : comparisonKeyPath ] != fetchedArticle [ keyPath : comparisonKeyPath ]
2017-09-08 22:36:30 +02:00
}
assertionFailure ( " Expected to find matching fetched article. " ) ;
2017-09-05 02:10:02 +02:00
return true
}
}
2017-09-10 20:36:28 +02:00
func updateRelatedObjects < T > ( _ comparisonKeyPath : KeyPath < Article , Set < T > ? > , _ updatedArticles : Set < Article > , _ fetchedArticles : [ String : Article ] , _ lookupTable : DatabaseLookupTable , _ database : FMDatabase ) {
2017-09-09 20:02:02 +02:00
let articlesWithChanges = articlesWithRelatedObjectChanges ( comparisonKeyPath , updatedArticles , fetchedArticles )
2017-09-08 22:36:30 +02:00
if ! articlesWithChanges . isEmpty {
2017-09-09 20:02:02 +02:00
lookupTable . saveRelatedObjects ( for : articlesWithChanges . databaseObjects ( ) , in : database )
2017-09-08 22:36:30 +02:00
}
}
2017-09-03 01:08:02 +02:00
2017-09-09 21:57:24 +02:00
func saveUpdatedRelatedObjects ( _ updatedArticles : Set < Article > , _ fetchedArticles : [ String : Article ] , _ database : FMDatabase ) {
2017-09-09 20:02:02 +02:00
updateRelatedObjects ( \ Article . authors , updatedArticles , fetchedArticles , authorsLookupTable , database )
2017-09-05 02:10:02 +02:00
}
2017-09-11 15:46:32 +02:00
func findUpdatedArticles ( _ incomingArticles : Set < Article > , _ fetchedArticlesDictionary : [ String : Article ] ) -> Set < Article > ? {
let updatedArticles = incomingArticles . filter { ( incomingArticle ) -> Bool in // 6
if let existingArticle = fetchedArticlesDictionary [ incomingArticle . articleID ] {
if existingArticle != incomingArticle {
return true
}
}
return false
}
return updatedArticles . isEmpty ? nil : updatedArticles
}
func findAndSaveUpdatedArticles ( _ incomingArticles : Set < Article > , _ fetchedArticlesDictionary : [ String : Article ] , _ database : FMDatabase ) -> Set < Article > ? { // 6
guard let updatedArticles = findUpdatedArticles ( incomingArticles , fetchedArticlesDictionary ) else {
return nil
}
saveUpdatedArticles ( Set ( updatedArticles ) , fetchedArticlesDictionary , database )
return updatedArticles
}
2017-09-08 22:36:30 +02:00
func saveUpdatedArticles ( _ updatedArticles : Set < Article > , _ fetchedArticles : [ String : Article ] , _ database : FMDatabase ) {
2017-09-09 20:10:15 +02:00
saveUpdatedRelatedObjects ( updatedArticles , fetchedArticles , database )
2017-09-09 21:09:48 +02:00
for updatedArticle in updatedArticles {
2017-09-09 21:24:30 +02:00
saveUpdatedArticle ( updatedArticle , fetchedArticles , database )
2017-09-09 21:09:48 +02:00
}
2017-09-08 22:36:30 +02:00
}
2017-09-09 21:09:48 +02:00
func saveUpdatedArticle ( _ updatedArticle : Article , _ fetchedArticles : [ String : Article ] , _ database : FMDatabase ) {
// O n l y u p d a t e e x a c t l y w h a t h a s c h a n g e d i n t h e A r t i c l e ( i f a n y t h i n g ) .
// U n t e s t e d t h e o r y : t h i s g e t s u s b e t t e r p e r f o r m a n c e a n d l e s s d a t a b a s e f r a g m e n t a t i o n .
2017-09-14 06:41:01 +02:00
guard let fetchedArticle = fetchedArticles [ updatedArticle . articleID ] else {
2017-09-09 21:09:48 +02:00
assertionFailure ( " Expected to find matching fetched article. " ) ;
saveNewArticles ( Set ( [ updatedArticle ] ) , database )
return
}
2017-09-14 06:41:01 +02:00
guard let changesDictionary = updatedArticle . changesFrom ( fetchedArticle ) , changesDictionary . count > 0 else {
2017-09-09 21:09:48 +02:00
// N o t u n e x p e c t e d . T h e r e m a y b e n o c h a n g e s .
return
}
2019-09-28 22:51:33 +02:00
2017-09-09 21:09:48 +02:00
updateRowsWithDictionary ( changesDictionary , whereKey : DatabaseKey . articleID , matches : updatedArticle . articleID , database : database )
2017-09-05 02:10:02 +02:00
}
2019-09-28 22:51:33 +02:00
2019-12-29 06:19:02 +01:00
func addArticlesToCache ( _ articles : Set < Article > ? ) {
guard let articles = articles else {
return
}
for article in articles {
articlesCache [ article . articleID ] = article
2019-09-28 22:51:33 +02:00
}
}
2020-03-30 08:20:01 +02:00
func removeArticleIDsFromCache ( _ articleIDs : Set < String > ) {
for articleID in articleIDs {
articlesCache [ articleID ] = nil
}
}
2019-12-09 07:17:25 +01:00
func articleIsIgnorable ( _ article : Article ) -> Bool {
2017-09-05 02:10:02 +02:00
// I g n o r a b l e a r t i c l e s : e i t h e r u s e r D e l e t e d = = 1 o r ( n o t s t a r r e d a n d a r r i v a l d a t e > 4 m o n t h s ) .
2019-12-09 07:17:25 +01:00
if article . status . userDeleted {
2017-09-05 02:10:02 +02:00
return true
}
2019-12-09 07:17:25 +01:00
if article . status . starred {
2017-09-05 02:10:02 +02:00
return false
}
2019-12-09 07:38:00 +01:00
return article . status . dateArrived < articleCutoffDate
2017-09-05 02:10:02 +02:00
}
2017-09-22 03:14:37 +02:00
func filterIncomingArticles ( _ articles : Set < Article > ) -> Set < Article > {
2017-09-11 15:46:32 +02:00
// D r o p A r t i c l e s t h a t w e c a n i g n o r e .
2020-03-30 08:20:01 +02:00
precondition ( retentionStyle = = . syncSystem )
2019-12-09 07:17:25 +01:00
return Set ( articles . filter { ! articleIsIgnorable ( $0 ) } )
2017-09-05 02:10:02 +02:00
}
2019-10-25 07:28:26 +02:00
func removeArticles ( _ articleIDs : Set < String > , _ database : FMDatabase ) {
deleteRowsWhere ( key : DatabaseKey . articleID , equalsAnyValue : Array ( articleIDs ) , in : database )
}
2017-08-27 00:37:15 +02:00
}
2017-07-29 21:29:05 +02:00
2019-10-14 04:02:56 +02:00
private extension Set where Element = = ParsedItem {
func articleIDs ( ) -> Set < String > {
return Set < String > ( map { $0 . articleID } )
}
}