summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/data/Daos.kt
blob: 8fe2ec2a5397c8729280a7e7b5677427038a5fa8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package org.terst.feedreader.data

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface FeedDao {
    @Query("SELECT * FROM feeds ORDER BY title ASC")
    fun observeAll(): Flow<List<FeedEntity>>

    @Query("SELECT * FROM feeds ORDER BY title ASC")
    suspend fun getAll(): List<FeedEntity>

    @Query("SELECT * FROM feeds WHERE id = :id")
    suspend fun getById(id: String): FeedEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(feed: FeedEntity)

    @Update
    suspend fun update(feed: FeedEntity)

    @Query("DELETE FROM feeds WHERE id = :id")
    suspend fun delete(id: String)
}

data class FeedUnreadCount(val feedId: String, val count: Int)

// A projection excluding feedContentHtml/extractedContent -- the potentially
// large per-article content blobs that list views (row title/date/thumbnail)
// never need. Selecting them for every row of a big result set is what was
// crashing the Unread view once real feed volume (107 feeds worth of
// articles) hit it: Android's per-row/CursorWindow limits are sized for
// "many small rows," not "many rows each carrying a full article's HTML."
data class ArticleListItem(
    val id: String,
    override val feedId: String?,
    override val title: String,
    val link: String,
    val author: String?,
    val publishedAt: Long?,
    val imageUrl: String?,
    val isRead: Boolean,
    val isSaved: Boolean,
    val stashArtifactId: String?,
    val createdAt: Long,
    val feedTitle: String?  // via LEFT JOIN feeds -- null for standalone share-sheet saves with no feedId
) : Titled

private const val ARTICLE_LIST_COLUMNS =
    "articles.id, articles.feedId, articles.title, articles.link, articles.author, articles.publishedAt, " +
        "articles.imageUrl, articles.isRead, articles.isSaved, articles.stashArtifactId, articles.createdAt, " +
        "feeds.title AS feedTitle"

private const val ARTICLE_LIST_FROM = "articles LEFT JOIN feeds ON articles.feedId = feeds.id"

@Dao
interface ArticleDao {
    // Unread-first, newest-first -- the default "what's new" view across every feed.
    @Query("SELECT $ARTICLE_LIST_COLUMNS FROM $ARTICLE_LIST_FROM WHERE articles.isRead = 0 ORDER BY articles.publishedAt DESC")
    fun observeUnread(): Flow<List<ArticleListItem>>

    @Query("SELECT feedId, COUNT(*) as count FROM articles WHERE isRead = 0 AND feedId IS NOT NULL GROUP BY feedId")
    fun observeUnreadCounts(): Flow<List<FeedUnreadCount>>

    @Query("SELECT $ARTICLE_LIST_COLUMNS FROM $ARTICLE_LIST_FROM WHERE articles.feedId = :feedId ORDER BY articles.publishedAt DESC")
    fun observeByFeed(feedId: String): Flow<List<ArticleListItem>>

    @Query("SELECT $ARTICLE_LIST_COLUMNS FROM $ARTICLE_LIST_FROM WHERE articles.isSaved = 1 ORDER BY articles.createdAt DESC")
    fun observeSaved(): Flow<List<ArticleListItem>>

    @Query("SELECT * FROM articles WHERE id = :id")
    suspend fun getById(id: String): ArticleEntity?

    // IGNORE, not REPLACE: a re-fetch of a feed re-parses items we already have:
    // this keeps the existing row (read state, extracted content, saved state)
    // untouched instead of clobbering it with the freshly-parsed feed data.
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertNew(articles: List<ArticleEntity>): List<Long>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(article: ArticleEntity)

    @Query("UPDATE articles SET isRead = :read WHERE id = :id")
    suspend fun setRead(id: String, read: Boolean)

    // Used for mark-as-read-on-scroll in the article list -- batched per scroll-settle
    // rather than one query per row.
    @Query("UPDATE articles SET isRead = 1 WHERE id IN (:ids)")
    suspend fun markReadBatch(ids: List<String>)

    @Query("UPDATE articles SET isRead = 1 WHERE feedId = :feedId")
    suspend fun markAllReadForFeed(feedId: String)

    // Run when unsubscribing from a feed. Saved articles are deliberately kept
    // regardless (isSaved is a separate, durable "keep forever" concept,
    // independent of subscription) -- everything else is deleted so it doesn't
    // linger forever as an orphaned row cluttering the cross-feed Unread view.
    @Query("DELETE FROM articles WHERE feedId = :feedId AND isSaved = 0")
    suspend fun deleteUnsavedByFeed(feedId: String)

    @Query("UPDATE articles SET extractedContent = :content WHERE id = :id")
    suspend fun setExtractedContent(id: String, content: String)

    @Query("UPDATE articles SET isSaved = :saved WHERE id = :id")
    suspend fun setSaved(id: String, saved: Boolean)

    @Query("UPDATE articles SET stashArtifactId = :artifactId WHERE id = :id")
    suspend fun setStashArtifactId(id: String, artifactId: String)

    @Delete
    suspend fun delete(article: ArticleEntity)
}

@Dao
interface FilterRuleDao {
    @Query("SELECT * FROM filter_rules ORDER BY createdAt DESC")
    fun observeAll(): Flow<List<FilterRuleEntity>>

    @Query("SELECT * FROM filter_rules")
    suspend fun getAll(): List<FilterRuleEntity>

    @Insert
    suspend fun insert(rule: FilterRuleEntity): Long

    @Query("DELETE FROM filter_rules WHERE id = :id")
    suspend fun delete(id: Long)
}