refactor: push down more data operations to SQL

This commit is contained in:
Harsh Shandilya
2025-12-25 19:47:47 +05:30
parent 3ab2eb8b66
commit 39c12f2173
5 changed files with 131 additions and 28 deletions
@@ -39,8 +39,11 @@ class SavedPostsWidget() : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val posts =
(context.applicationContext as ClawApplication).appGraph.savedPostsRepository.savedPosts
val postWindow = posts.first().take(50).map(UIPost::fromSavedPost).toImmutableList()
(context.applicationContext as ClawApplication)
.appGraph
.savedPostsRepository
.getRecentPosts(50)
val postWindow = posts.first().map(UIPost::fromSavedPost).toImmutableList()
provideContent {
GlanceTheme(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) GlanceTheme.colors
@@ -40,7 +40,6 @@ import java.io.OutputStream
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.collectLatest
@@ -87,9 +86,18 @@ class ClawViewModel(
)
.flow
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
val savedPostsCount = savedPosts.map { it.size.toLong() }
val savedPostsCount = savedPostsRepository.savedPosts.map { it.size.toLong() }
val savedPostsByMonth
get() = savedPosts.map(::groupSavedPosts)
get() =
savedPostsRepository.savedPostsSortedByDate.map { posts ->
posts
.map(UIPost.Companion::fromSavedPost)
.groupBy { post ->
val time = post.createdAt.toLocalDateTime()
"${time.month.name.lowercase().capitalize(Locale.current)} ${time.year}"
}
.toImmutableMap()
}
var searchQuery by mutableStateOf("")
@@ -101,27 +109,6 @@ class ClawViewModel(
viewModelScope.launch { readPostsRepository.readPosts.collectLatest { _readPosts = it } }
}
private fun groupSavedPosts(items: List<UIPost>): ImmutableMap<String, List<UIPost>> {
val sorted =
items.sortedWith { post1, post2 ->
val post1Date = post1.createdAt.toLocalDateTime()
val post2Date = post2.createdAt.toLocalDateTime()
if (post2Date.isBefore(post1Date)) {
-1
} else if (post2Date.isAfter(post1Date)) {
1
} else {
0
}
}
return sorted
.groupBy { post ->
val time = post.createdAt.toLocalDateTime()
"${time.month.name.lowercase().capitalize(Locale.current)} ${time.year}"
}
.toImmutableMap()
}
fun toggleSave(post: UIPost) {
viewModelScope.launch {
savedPostsRepository.toggleSave(post)
@@ -17,7 +17,6 @@ import dev.msfjarvis.claw.model.toSavedPost
import dev.zacsweers.metro.Inject
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
@Inject
@@ -27,12 +26,19 @@ class SavedPostsRepository(
@DatabaseWriteDispatcher private val writeDispatcher: CoroutineDispatcher,
) {
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(readDispatcher)
val savedPostsSortedByDate =
savedPostQueries.selectAllPostsSortedByDate().asFlow().mapToList(readDispatcher)
fun getPostsFromLastNDays(days: Long) =
savedPostQueries.selectPostsFromLastNDays(days.toString()).asFlow().mapToList(readDispatcher)
fun getRecentPosts(limit: Long) =
savedPostQueries.selectRecentPosts(limit).asFlow().mapToList(readDispatcher)
suspend fun toggleSave(post: UIPost) {
if (savedPosts.firstOrNull().orEmpty().any { it.shortId == post.shortId }) {
val exists =
withContext(readDispatcher) { savedPostQueries.postExists(post.shortId).executeAsOne() }
if (exists) {
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
withContext(writeDispatcher) { savedPostQueries.deletePost(post.shortId) }
} else {
@@ -42,3 +42,21 @@ deletePost:
DELETE
FROM SavedPost
WHERE shortId = ?;
postExists:
SELECT EXISTS(
SELECT 1
FROM SavedPost
WHERE shortId = ?
);
selectRecentPosts:
SELECT *
FROM SavedPost
ORDER BY datetime(createdAt) DESC
LIMIT :limit;
selectAllPostsSortedByDate:
SELECT *
FROM SavedPost
ORDER BY datetime(createdAt) DESC;
@@ -137,6 +137,95 @@ class SavedPostQueriesTest {
assertThat(postsFromLast30Days.map { it.shortId }).doesNotContain("old_1")
}
@Test
fun `postExists returns true when post exists`() {
val post = createTestData(1).first()
postQueries.insertOrReplacePost(post)
val exists = postQueries.postExists("test_id_1").executeAsOne()
assertThat(exists).isTrue()
}
@Test
fun `postExists returns false when post does not exist`() {
val exists = postQueries.postExists("nonexistent_id").executeAsOne()
assertThat(exists).isFalse()
}
@Test
fun `selectRecentPosts returns correct number of posts`() {
val posts = createTestData(10)
posts.forEach { postQueries.insertOrReplacePost(it) }
val recentPosts = postQueries.selectRecentPosts(5).executeAsList()
assertThat(recentPosts).hasSize(5)
}
@Test
fun `selectRecentPosts returns all posts when limit exceeds count`() {
val posts = createTestData(3)
posts.forEach { postQueries.insertOrReplacePost(it) }
val recentPosts = postQueries.selectRecentPosts(10).executeAsList()
assertThat(recentPosts).hasSize(3)
}
@Test
fun `selectRecentPosts returns posts in descending date order`() {
val now = Instant.now()
val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
fun dateOffset(daysToSubtract: Long): String {
return formatter.format(
now.minus(daysToSubtract, ChronoUnit.DAYS).atOffset(java.time.ZoneOffset.UTC)
)
}
val oldest = createPostWithDate(id = "oldest", createdAt = dateOffset(30))
val middle = createPostWithDate(id = "middle", createdAt = dateOffset(15))
val newest = createPostWithDate(id = "newest", createdAt = dateOffset(1))
postQueries.insertOrReplacePost(oldest)
postQueries.insertOrReplacePost(middle)
postQueries.insertOrReplacePost(newest)
val recentPosts = postQueries.selectRecentPosts(10).executeAsList()
assertThat(recentPosts.map { it.shortId })
.containsExactly("newest", "middle", "oldest")
.inOrder()
}
@Test
fun `selectAllPostsSortedByDate returns posts in descending date order`() {
val now = Instant.now()
val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
fun dateOffset(daysToSubtract: Long): String {
return formatter.format(
now.minus(daysToSubtract, ChronoUnit.DAYS).atOffset(java.time.ZoneOffset.UTC)
)
}
val post1 = createPostWithDate(id = "post_1", createdAt = dateOffset(20))
val post2 = createPostWithDate(id = "post_2", createdAt = dateOffset(5))
val post3 = createPostWithDate(id = "post_3", createdAt = dateOffset(10))
val post4 = createPostWithDate(id = "post_4", createdAt = dateOffset(1))
postQueries.insertOrReplacePost(post1)
postQueries.insertOrReplacePost(post2)
postQueries.insertOrReplacePost(post3)
postQueries.insertOrReplacePost(post4)
val sortedPosts = postQueries.selectAllPostsSortedByDate().executeAsList()
assertThat(sortedPosts.map { it.shortId })
.containsExactly("post_4", "post_2", "post_3", "post_1")
.inOrder()
}
private fun createTestData(count: Int): ArrayList<SavedPost> {
val posts = arrayListOf<SavedPost>()