2017-07-03 19:40:48 +02:00
//
2018-07-24 03:29:08 +02:00
// A r t i c l e s D a t a b a s 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 7 / 2 0 / 1 5 .
// C o p y r i g h t © 2 0 1 5 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
2019-11-30 08:42:11 +01:00
import RSCore
2017-07-03 19:40:48 +02:00
import RSDatabase
2017-07-03 20:20:14 +02:00
import RSParser
2018-07-24 03:29:08 +02:00
import Articles
2017-07-03 19:40:48 +02:00
2019-07-06 05:06:31 +02:00
// T h i s f i l e i s t h e e n t i r e t y o f t h e p u b l i c A P I f o r A r t i c l e s D a t a b a s e . f r a m e w o r k .
2017-09-16 19:38:54 +02:00
// E v e r y t h i n g e l s e i s i m p l e m e n t a t i o n .
2019-07-06 05:06:31 +02:00
// M a i n t h r e a d o n l y .
2019-11-15 03:11:41 +01:00
public typealias UnreadCountDictionary = [ String : Int ] // w e b F e e d I D : u n r e a d C o u n t
2019-12-16 07:37:45 +01:00
public typealias UnreadCountDictionaryCompletionResult = Result < UnreadCountDictionary , DatabaseError >
2019-12-16 07:09:27 +01:00
public typealias UnreadCountDictionaryCompletionBlock = ( UnreadCountDictionaryCompletionResult ) -> Void
2017-07-03 19:40:48 +02:00
2019-12-16 07:37:45 +01:00
public typealias SingleUnreadCountResult = Result < Int , DatabaseError >
2019-12-16 07:09:27 +01:00
public typealias SingleUnreadCountCompletionBlock = ( SingleUnreadCountResult ) -> Void
2017-07-03 19:40:48 +02:00
2019-12-16 07:09:27 +01:00
public struct NewAndUpdatedArticles {
2019-12-17 00:55:37 +01:00
public let newArticles : Set < Article > ?
public let updatedArticles : Set < Article > ?
2019-12-16 07:09:27 +01:00
}
2019-12-16 07:37:45 +01:00
public typealias UpdateArticlesResult = Result < NewAndUpdatedArticles , DatabaseError >
2019-12-16 07:09:27 +01:00
public typealias UpdateArticlesCompletionBlock = ( UpdateArticlesResult ) -> Void
2019-12-16 07:37:45 +01:00
public typealias ArticleSetResult = Result < Set < Article > , DatabaseError >
2019-12-16 07:09:27 +01:00
public typealias ArticleSetResultBlock = ( ArticleSetResult ) -> Void
2019-12-16 07:37:45 +01:00
public typealias ArticleIDsResult = Result < Set < String > , DatabaseError >
2019-12-16 07:09:27 +01:00
public typealias ArticleIDsCompletionBlock = ( ArticleIDsResult ) -> Void
2019-12-16 07:37:45 +01:00
public typealias ArticleStatusesResult = Result < Set < ArticleStatus > , DatabaseError >
2019-12-16 07:09:27 +01:00
public typealias ArticleStatusesResultBlock = ( ArticleStatusesResult ) -> Void
public final class ArticlesDatabase {
2019-11-30 06:49:44 +01:00
2020-03-30 03:51:03 +02:00
public enum RetentionStyle {
case feedBased // L o c a l a n d i C l o u d : a r t i c l e r e t e n t i o n i s d e f i n e d b y c o n t e n t s o f f e e d
case syncSystem // F e e d b i n , F e e d l y , e t c . : a r t i c l e r e t e n t i o n i s d e f i n e d b y e x t e r n a l s y s t e m
}
2017-08-21 22:31:14 +02:00
private let articlesTable : ArticlesTable
2019-11-30 06:49:44 +01:00
private let queue : DatabaseQueue
2020-02-06 06:18:29 +01:00
private let operationQueue = MainThreadOperationQueue ( )
2020-03-30 03:51:03 +02:00
private let retentionStyle : RetentionStyle
2017-08-27 00:37:15 +02:00
2020-03-30 03:51:03 +02:00
public init ( databaseFilePath : String , accountID : String , retentionStyle : RetentionStyle ) {
2019-11-30 06:49:44 +01:00
let queue = DatabaseQueue ( databasePath : databaseFilePath )
self . queue = queue
2020-03-30 03:51:03 +02:00
self . articlesTable = ArticlesTable ( name : DatabaseTableName . articles , accountID : accountID , queue : queue , retentionStyle : retentionStyle )
self . retentionStyle = retentionStyle
2017-08-21 00:56:58 +02:00
2019-12-16 02:26:45 +01:00
try ! queue . runCreateStatements ( ArticlesDatabase . tableCreationStatements )
queue . runInDatabase { databaseResult in
let database = databaseResult . database !
2019-02-23 07:17:05 +01:00
if ! self . articlesTable . containsColumn ( " searchRowID " , in : database ) {
database . executeStatements ( " ALTER TABLE articles add column searchRowID INTEGER; " )
}
2019-03-03 21:30:58 +01:00
database . executeStatements ( " CREATE INDEX if not EXISTS articles_searchRowID on articles(searchRowID); " )
2019-12-04 08:03:15 +01:00
database . executeStatements ( " DROP TABLE if EXISTS tags;DROP INDEX if EXISTS tags_tagName_index;DROP INDEX if EXISTS articles_feedID_index;DROP INDEX if EXISTS statuses_read_index;DROP TABLE if EXISTS attachments;DROP TABLE if EXISTS attachmentsLookup; " )
2017-12-19 03:20:13 +01:00
}
2019-11-30 06:49:44 +01:00
2019-02-25 00:34:10 +01:00
DispatchQueue . main . async {
self . articlesTable . indexUnindexedArticles ( )
}
2017-07-03 19:40:48 +02:00
}
2017-08-21 06:23:17 +02:00
// MARK: - F e t c h i n g A r t i c l e s
2017-07-03 19:40:48 +02:00
2019-12-16 07:09:27 +01:00
public func fetchArticles ( _ webFeedID : String ) throws -> Set < Article > {
return try articlesTable . fetchArticles ( webFeedID )
2017-07-03 19:40:48 +02:00
}
2019-05-14 13:20:53 +02:00
2019-12-16 07:09:27 +01:00
public func fetchArticles ( _ webFeedIDs : Set < String > ) throws -> Set < Article > {
return try articlesTable . fetchArticles ( webFeedIDs )
2019-11-22 17:21:30 +01:00
}
2019-12-16 07:09:27 +01:00
public func fetchArticles ( articleIDs : Set < String > ) throws -> Set < Article > {
return try articlesTable . fetchArticles ( articleIDs : articleIDs )
2019-07-06 05:06:31 +02:00
}
2019-12-16 07:09:27 +01:00
public func fetchUnreadArticles ( _ webFeedIDs : Set < String > ) throws -> Set < Article > {
return try articlesTable . fetchUnreadArticles ( webFeedIDs )
2019-07-06 05:06:31 +02:00
}
2019-12-16 07:09:27 +01:00
public func fetchTodayArticles ( _ webFeedIDs : Set < String > ) throws -> Set < Article > {
return try articlesTable . fetchArticlesSince ( webFeedIDs , todayCutoffDate ( ) )
2019-07-06 05:06:31 +02:00
}
2019-12-16 07:09:27 +01:00
public func fetchStarredArticles ( _ webFeedIDs : Set < String > ) throws -> Set < Article > {
return try articlesTable . fetchStarredArticles ( webFeedIDs )
2019-07-06 05:06:31 +02:00
}
2019-12-16 07:09:27 +01:00
public func fetchArticlesMatching ( _ searchString : String , _ webFeedIDs : Set < String > ) throws -> Set < Article > {
return try articlesTable . fetchArticlesMatching ( searchString , webFeedIDs )
2019-07-06 05:06:31 +02:00
}
2019-12-16 07:09:27 +01:00
public func fetchArticlesMatchingWithArticleIDs ( _ searchString : String , _ articleIDs : Set < String > ) throws -> Set < Article > {
return try articlesTable . fetchArticlesMatchingWithArticleIDs ( searchString , articleIDs )
2019-08-31 22:53:47 +02:00
}
2019-07-06 05:06:31 +02:00
// MARK: - F e t c h i n g A r t i c l e s A s y n c
2019-12-16 07:09:27 +01:00
public func fetchArticlesAsync ( _ webFeedID : String , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchArticlesAsync ( webFeedID , completion )
2019-05-14 13:20:53 +02:00
}
2017-07-03 19:40:48 +02:00
2019-12-16 07:09:27 +01:00
public func fetchArticlesAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchArticlesAsync ( webFeedIDs , completion )
2019-11-22 17:21:30 +01:00
}
2019-12-16 07:09:27 +01:00
public func fetchArticlesAsync ( articleIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchArticlesAsync ( articleIDs : articleIDs , completion )
2017-07-03 19:40:48 +02:00
}
2019-12-16 07:09:27 +01:00
public func fetchUnreadArticlesAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchUnreadArticlesAsync ( webFeedIDs , completion )
2017-07-03 19:40:48 +02:00
}
2017-08-21 06:23:17 +02:00
2019-12-16 07:09:27 +01:00
public func fetchTodayArticlesAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchArticlesSinceAsync ( webFeedIDs , todayCutoffDate ( ) , completion )
2018-02-11 02:37:47 +01:00
}
2019-12-16 07:09:27 +01:00
public func fetchedStarredArticlesAsync ( _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchStarredArticlesAsync ( webFeedIDs , completion )
2018-02-11 21:07:55 +01:00
}
2019-12-16 07:09:27 +01:00
public func fetchArticlesMatchingAsync ( _ searchString : String , _ webFeedIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchArticlesMatchingAsync ( searchString , webFeedIDs , completion )
2019-02-19 07:29:43 +01:00
}
2019-12-16 07:09:27 +01:00
public func fetchArticlesMatchingWithArticleIDsAsync ( _ searchString : String , _ articleIDs : Set < String > , _ completion : @ escaping ArticleSetResultBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchArticlesMatchingWithArticleIDsAsync ( searchString , articleIDs , completion )
2019-08-31 22:53:47 +02:00
}
2017-08-21 06:23:17 +02:00
// MARK: - U n r e a d C o u n t s
2020-02-06 06:18:29 +01:00
2020-02-06 06:23:23 +01:00
// / F e t c h a l l n o n - z e r o u n r e a d c o u n t s .
2020-02-06 06:18:29 +01:00
public func fetchAllUnreadCounts ( _ completion : @ escaping UnreadCountDictionaryCompletionBlock ) {
let operation = FetchAllUnreadCountsOperation ( databaseQueue : queue , cutoffDate : articlesTable . articleCutoffDate )
2020-02-06 06:23:23 +01:00
operationQueue . cancelOperations ( named : operation . name ! )
2020-02-06 06:18:29 +01:00
operation . completionBlock = { operation in
let fetchOperation = operation as ! FetchAllUnreadCountsOperation
completion ( fetchOperation . result )
}
operationQueue . add ( operation )
}
2020-02-06 07:17:32 +01:00
// / F e t c h u n r e a d c o u n t f o r a s i n g l e f e e d .
public func fetchUnreadCount ( _ webFeedID : String , _ completion : @ escaping SingleUnreadCountCompletionBlock ) {
let operation = FetchFeedUnreadCountOperation ( webFeedID : webFeedID , databaseQueue : queue , cutoffDate : articlesTable . articleCutoffDate )
operation . completionBlock = { operation in
let fetchOperation = operation as ! FetchFeedUnreadCountOperation
completion ( fetchOperation . result )
}
operationQueue . add ( operation )
}
// / F e t c h n o n - z e r o u n r e a d c o u n t s f o r g i v e n w e b F e e d I D s .
public func fetchUnreadCounts ( for webFeedIDs : Set < String > , _ completion : @ escaping UnreadCountDictionaryCompletionBlock ) {
let operation = FetchUnreadCountsForFeedsOperation ( webFeedIDs : webFeedIDs , databaseQueue : queue , cutoffDate : articlesTable . articleCutoffDate )
operation . completionBlock = { operation in
let fetchOperation = operation as ! FetchUnreadCountsForFeedsOperation
completion ( fetchOperation . result )
}
operationQueue . add ( operation )
}
2019-12-16 07:09:27 +01:00
public func fetchUnreadCountForToday ( for webFeedIDs : Set < String > , completion : @ escaping SingleUnreadCountCompletionBlock ) {
2019-12-15 02:01:34 +01:00
fetchUnreadCount ( for : webFeedIDs , since : todayCutoffDate ( ) , completion : completion )
2019-07-24 18:27:03 +02:00
}
2019-12-16 07:09:27 +01:00
public func fetchUnreadCount ( for webFeedIDs : Set < String > , since : Date , completion : @ escaping SingleUnreadCountCompletionBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchUnreadCount ( webFeedIDs , since , completion )
2017-11-19 21:44:17 +01:00
}
2019-12-16 07:09:27 +01:00
public func fetchStarredAndUnreadCount ( for webFeedIDs : Set < String > , completion : @ escaping SingleUnreadCountCompletionBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchStarredAndUnreadCount ( webFeedIDs , completion )
2017-11-20 00:40:02 +01:00
}
2017-09-16 19:38:54 +02:00
// MARK: - S a v i n g a n d U p d a t i n g A r t i c l e s
2017-07-03 19:40:48 +02:00
2020-03-30 03:51:03 +02:00
// / U p d a t e a r t i c l e s a n d s a v e n e w o n e s — f o r f e e d - b a s e d s y s t e m s ( l o c a l a n d i C l o u d ) .
2020-03-30 08:20:01 +02:00
public func update ( with parsedItems : Set < ParsedItem > , webFeedID : String , completion : @ escaping UpdateArticlesCompletionBlock ) {
2020-03-30 03:51:03 +02:00
precondition ( retentionStyle = = . feedBased )
2020-03-30 08:20:01 +02:00
articlesTable . update ( parsedItems , webFeedID , completion )
2020-03-30 03:51:03 +02:00
}
// / U p d a t e a r t i c l e s a n d s a v e n e w o n e s — f o r s y n c s y s t e m s ( F e e d b i n , F e e d l y , e t c . ) .
2020-03-23 03:25:53 +01:00
public func update ( webFeedIDsAndItems : [ String : Set < ParsedItem > ] , defaultRead : Bool , completion : @ escaping UpdateArticlesCompletionBlock ) {
2020-03-30 03:51:03 +02:00
precondition ( retentionStyle = = . syncSystem )
2020-03-23 03:25:53 +01:00
articlesTable . update ( webFeedIDsAndItems , defaultRead , completion )
2017-07-03 19:40:48 +02:00
}
2019-10-14 04:02:56 +02:00
2017-08-21 06:23:17 +02:00
// MARK: - S t a t u s
2019-12-08 05:57:23 +01:00
2019-12-08 07:23:44 +01:00
// / F e t c h t h e a r t i c l e I D s o f u n r e a d a r t i c l e s i n f e e d s s p e c i f i e d b y w e b F e e d I D s .
2019-12-16 07:09:27 +01:00
public func fetchUnreadArticleIDsAsync ( webFeedIDs : Set < String > , completion : @ escaping ArticleIDsCompletionBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchUnreadArticleIDsAsync ( webFeedIDs , completion )
2019-05-14 13:20:53 +02:00
}
2019-12-08 07:23:44 +01:00
// / F e t c h t h e a r t i c l e I D s o f s t a r r e d a r t i c l e s i n f e e d s s p e c i f i e d b y w e b F e e d I D s .
2019-12-16 07:09:27 +01:00
public func fetchStarredArticleIDsAsync ( webFeedIDs : Set < String > , completion : @ escaping ArticleIDsCompletionBlock ) {
2019-12-15 02:01:34 +01:00
articlesTable . fetchStarredArticleIDsAsync ( webFeedIDs , completion )
2019-05-14 13:20:53 +02:00
}
2019-12-08 07:23:44 +01:00
2020-01-09 06:24:47 +01:00
// / F e t c h a r t i c l e I D s f o r a r t i c l e s t h a t w e s h o u l d h a v e , b u t d o n ’ t . T h e s e a r t i c l e s a r e n o t u s e r D e l e t e d , a n d t h e y a r e e i t h e r ( s t a r r e d ) o r ( n e w e r t h a n t h e a r t i c l e c u t o f f d a t e ) .
2019-12-18 01:43:08 +01:00
public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate ( _ completion : @ escaping ArticleIDsCompletionBlock ) {
2019-12-17 22:28:48 +01:00
articlesTable . fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate ( completion )
}
2019-12-16 07:09:27 +01:00
public func mark ( _ articles : Set < Article > , statusKey : ArticleStatus . Key , flag : Bool ) throws -> Set < ArticleStatus > ? {
return try articlesTable . mark ( articles , statusKey , flag )
2017-07-03 19:40:48 +02:00
}
2019-10-13 00:06:21 +02:00
2020-04-10 22:19:33 +02:00
public func markAndFetchNew ( articleIDs : Set < String > , statusKey : ArticleStatus . Key , flag : Bool , completion : @ escaping ArticleStatusesResultBlock ) {
articlesTable . markAndFetchNew ( articleIDs , statusKey , flag , completion )
2019-12-17 07:45:59 +01:00
}
2020-01-10 07:27:29 +01:00
// / C r e a t e s t a t u s e s f o r s p e c i f i e d a r t i c l e I D s . F o r e x i s t i n g s t a t u s e s , d o n ’ t d o a n y t h i n g .
// / F o r n e w l y - c r e a t e d s t a t u s e s , m a r k t h e m a s r e a d a n d n o t - s t a r r e d .
public func createStatusesIfNeeded ( articleIDs : Set < String > , completion : @ escaping DatabaseCompletionBlock ) {
articlesTable . createStatusesIfNeeded ( articleIDs , completion )
}
2020-03-30 03:51:03 +02:00
#if os ( iOS )
2020-02-06 07:17:32 +01:00
// MARK: - S u s p e n d a n d R e s u m e ( f o r i O S )
2020-01-28 08:00:48 +01:00
2020-02-06 07:17:32 +01:00
// / C a n c e l c u r r e n t o p e r a t i o n s a n d c l o s e t h e d a t a b a s e .
public func cancelAndSuspend ( ) {
cancelOperations ( )
suspend ( )
2020-02-02 00:01:47 +01:00
}
2019-11-30 06:49:44 +01:00
// / C l o s e t h e d a t a b a s e a n d s t o p r u n n i n g d a t a b a s e c a l l s .
// / A n y p e n d i n g c a l l s w i l l c o m p l e t e f i r s t .
public func suspend ( ) {
2020-02-06 07:17:32 +01:00
operationQueue . suspend ( )
2019-11-30 06:49:44 +01:00
queue . suspend ( )
}
// / O p e n t h e d a t a b a s e a n d a l l o w f o r r u n n i n g d a t a b a s e c a l l s a g a i n .
public func resume ( ) {
queue . resume ( )
2020-02-06 07:17:32 +01:00
operationQueue . resume ( )
2019-11-30 06:49:44 +01:00
}
2020-03-30 03:51:03 +02:00
#endif
2019-10-13 00:06:21 +02:00
// MARK: - C a c h e s
// / C a l l t o f r e e u p s o m e m e m o r y . S h o u l d b e d o n e w h e n t h e a p p i s b a c k g r o u n d e d , f o r i n s t a n c e .
// / T h i s d o e s n o t e m p t y * a l l * c a c h e s — j u s t t h e o n e s t h a t a r e e m p t y - a b l e .
public func emptyCaches ( ) {
articlesTable . emptyCaches ( )
}
2019-10-25 07:28:26 +02:00
// MARK: - C l e a n u p
// T h e s e a r e t o b e u s e d o n l y a t s t a r t u p . T h e s e a r e t o p r e v e n t t h e d a t a b a s e f r o m g r o w i n g f o r e v e r .
// / C a l l s t h e v a r i o u s c l e a n - u p f u n c t i o n s .
2019-11-15 03:11:41 +01:00
public func cleanupDatabaseAtStartup ( subscribedToWebFeedIDs : Set < String > ) {
2020-03-30 08:20:01 +02:00
if retentionStyle = = . syncSystem {
articlesTable . deleteOldArticles ( )
}
2019-11-15 03:11:41 +01:00
articlesTable . deleteArticlesNotInSubscribedToFeedIDs ( subscribedToWebFeedIDs )
2019-10-25 07:28:26 +02:00
}
2017-07-03 19:40:48 +02:00
}
2019-02-23 07:17:05 +01:00
// MARK: - P r i v a t e
private extension ArticlesDatabase {
static let tableCreationStatements = " " "
CREATE TABLE if not EXISTS articles ( articleID TEXT NOT NULL PRIMARY KEY , feedID TEXT NOT NULL , uniqueID TEXT NOT NULL , title TEXT , contentHTML TEXT , contentText TEXT , url TEXT , externalURL TEXT , summary TEXT , imageURL TEXT , bannerImageURL TEXT , datePublished DATE , dateModified DATE , searchRowID INTEGER ) ;
CREATE TABLE if not EXISTS statuses ( articleID TEXT NOT NULL PRIMARY KEY , read BOOL NOT NULL DEFAULT 0 , starred BOOL NOT NULL DEFAULT 0 , userDeleted BOOL NOT NULL DEFAULT 0 , dateArrived DATE NOT NULL DEFAULT 0 ) ;
CREATE TABLE if not EXISTS authors ( authorID TEXT NOT NULL PRIMARY KEY , name TEXT , url TEXT , avatarURL TEXT , emailAddress TEXT ) ;
CREATE TABLE if not EXISTS authorsLookup ( authorID TEXT NOT NULL , articleID TEXT NOT NULL , PRIMARY KEY ( authorID , articleID ) ) ;
CREATE INDEX if not EXISTS articles_feedID_datePublished_articleID on articles ( feedID , datePublished , articleID ) ;
CREATE INDEX if not EXISTS statuses_starred_index on statuses ( starred ) ;
CREATE VIRTUAL TABLE if not EXISTS search using fts4 ( title , body ) ;
CREATE TRIGGER if not EXISTS articles_after_delete_trigger_delete_search_text after delete on articles begin delete from search where rowid = OLD . searchRowID ; end ;
" " "
2019-07-24 18:27:03 +02:00
func todayCutoffDate ( ) -> Date {
2019-07-27 21:30:13 +02:00
// 2 4 h o u r s p r e v i o u s . T h i s i s u s e d b y t h e T o d a y s m a r t f e e d , w h i c h s h o u l d n o t a c t u a l l y e m p t y o u t a t m i d n i g h t .
return Date ( timeIntervalSinceNow : - ( 60 * 60 * 24 ) ) // T h i s d o e s n o t n e e d t o b e m o r e p r e c i s e .
2019-07-24 18:27:03 +02:00
}
2020-02-06 07:17:32 +01:00
// MARK: - O p e r a t i o n s
func cancelOperations ( ) {
operationQueue . cancelAllOperations ( )
}
2019-02-23 07:17:05 +01:00
}