db: cover downloaded paging invalidation and filters

Add tests for the downloaded artist and playlist paging sources now that
their observed entities are hand-picked, and assert the downloaded
search/starred filters directly. The downloaded artist plan test uses the
real select list and documents the computed album count's in-memory sort,
which the hand-written query also had.
This commit is contained in:
austinried
2026-11-03 01:03:45 +00:00
parent 8d6f7e2341
commit 18c24ca566
2 changed files with 173 additions and 10 deletions
@@ -219,8 +219,9 @@ internal fun rawQuery(
index++
}
}
val values = names.map(bindings::getValue)
return RoomRawQuery(positional) { statement ->
names.forEachIndexed { index, name -> statement.bind(index + 1, bindings.getValue(name)) }
values.forEachIndexed { index, value -> statement.bind(index + 1, value) }
}
}
@@ -389,10 +389,41 @@ class LibraryDaoTest {
ARTIST_ORDER_BY_RECENT_REVERSED to "index_artists_recent",
)
for ((order, index) in orders) {
for (filter in listOf(ARTISTS_FILTER, ARTISTS_FILTER_DOWNLOADED)) {
val plan = plan("SELECT * $filter ORDER BY $order LIMIT 20 OFFSET 0")
assertTrue(plan, plan.contains("USING INDEX $index"))
assertFalse(plan, plan.contains("TEMP B-TREE"))
val plan = plan("SELECT $ARTIST_COLUMNS $ARTISTS_FILTER ORDER BY $order LIMIT 20 OFFSET 0")
assertTrue(plan, plan.contains("USING INDEX $index"))
assertFalse(plan, plan.contains("TEMP B-TREE"))
}
}
@Test
fun downloadedArtistOrdersUseTheIndexesTheirSortColumnsHave() =
runTest {
val orders =
listOf(
ARTIST_ORDER_BY_NAME to "index_artists_name",
ARTIST_ORDER_BY_NAME_REVERSED to "index_artists_name",
ARTIST_ORDER_BY_ALBUM_COUNT to "index_artists_albumCount",
ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED to "index_artists_albumCount",
ARTIST_ORDER_BY_STARRED to "index_artists_starred",
ARTIST_ORDER_BY_STARRED_REVERSED to "index_artists_starred",
ARTIST_ORDER_BY_FREQUENT to "index_artists_frequent",
ARTIST_ORDER_BY_FREQUENT_REVERSED to "index_artists_frequent",
ARTIST_ORDER_BY_RECENT to "index_artists_recent",
ARTIST_ORDER_BY_RECENT_REVERSED to "index_artists_recent",
)
for ((order, index) in orders) {
val plan =
topLevelPlan(
"SELECT $ARTIST_COLUMNS_DOWNLOADED $ARTISTS_FILTER_DOWNLOADED " +
"ORDER BY $order LIMIT 20 OFFSET 0",
)
if (order == ARTIST_ORDER_BY_ALBUM_COUNT || order == ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED) {
// The displayed count comes from song_downloads, so this order cannot use
// index_artists_albumCount; the hand-written statement sorted in memory too.
assertTrue("$order\n$plan", plan.contains("USE TEMP B-TREE FOR ORDER BY"))
} else {
assertTrue("$order\n$plan", plan.contains("USING INDEX $index"))
assertFalse("$order\n$plan", plan.contains("USE TEMP B-TREE FOR ORDER BY"))
}
}
}
@@ -410,11 +441,17 @@ class LibraryDaoTest {
PLAYLIST_ORDER_BY_UPDATED_REVERSED to "index_playlists_updated",
)
for ((order, index) in orders) {
for (filter in listOf(PLAYLISTS_FILTER, PLAYLISTS_FILTER_DOWNLOADED)) {
val plan = plan("SELECT * $filter ORDER BY $order LIMIT 20 OFFSET 0")
assertTrue(plan, plan.contains("USING INDEX $index"))
assertFalse(plan, plan.contains("TEMP B-TREE"))
}
val defaultPlan = plan("SELECT * $PLAYLISTS_FILTER ORDER BY $order LIMIT 20 OFFSET 0")
assertTrue(defaultPlan, defaultPlan.contains("USING INDEX $index"))
assertFalse(defaultPlan, defaultPlan.contains("TEMP B-TREE"))
val downloadedPlan =
topLevelPlan(
"SELECT $PLAYLISTS_COLUMNS_DOWNLOADED $PLAYLISTS_FILTER_DOWNLOADED " +
"ORDER BY $order LIMIT 20 OFFSET 0",
)
assertTrue(downloadedPlan, downloadedPlan.contains("USING INDEX $index"))
assertFalse(downloadedPlan, downloadedPlan.contains("USE TEMP B-TREE FOR ORDER BY"))
}
}
@@ -480,6 +517,74 @@ class LibraryDaoTest {
assertEquals(3, dao.albumPages(sourceId, 0, "").page().size)
}
@Test
fun downloadedFiltersHonourSearchAndStarred() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertArtists(
listOf(
artist(sourceId, "ar-1", "Artist", albumCount = 2).copy(starred = 5),
artist(sourceId, "ar-2", "Other", albumCount = 1),
),
)
dao.upsertAlbums(
listOf(
album(sourceId, "al-1", "Downloaded One", year = 2000, starred = 5),
album(sourceId, "al-2", "Downloaded Two", year = 2001, starred = null),
album(sourceId, "al-3", "Missing", year = 2002, starred = 5),
),
)
dao.upsertSongs(
listOf(
song(sourceId, "s1", "One", starred = null),
song(sourceId, "s2", "Two", starred = null).copy(albumId = "al-2"),
song(sourceId, "s3", "Three", starred = null).copy(albumId = "al-3", artistId = "ar-2"),
),
)
dao.upsertPlaylists(
listOf(
playlist(sourceId, "pl-1", "Has download", created = 0, changed = 0),
playlist(sourceId, "pl-2", "No download", created = 1, changed = 1),
),
)
dao.upsertPlaylistSongs(
listOf(
PlaylistSong(sourceId, "pl-1", "s1", 0),
PlaylistSong(sourceId, "pl-2", "s2", 0),
),
)
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
assertEquals(
listOf("Downloaded One"),
dao.albumPages(sourceId, 0, "one", downloaded = true).page().map { it.name },
)
assertTrue(dao.albumPages(sourceId, 0, "missing", downloaded = true).page().isEmpty())
assertEquals(
listOf("Downloaded One"),
dao.albumPages(sourceId, 1, "", downloaded = true).page().map { it.name },
)
assertTrue(dao.albumPages(sourceId, 2, "", downloaded = true).page().isEmpty())
assertEquals(
listOf("ar-1"),
dao.artistPages(sourceId, 0, "art", downloaded = true).page().map { it.id },
)
assertTrue(dao.artistPages(sourceId, 0, "other", downloaded = true).page().isEmpty())
assertEquals(
listOf("ar-1"),
dao.artistPages(sourceId, 1, "", downloaded = true).page().map { it.id },
)
assertTrue(dao.artistPages(sourceId, 2, "", downloaded = true).page().isEmpty())
assertEquals(
listOf("pl-1"),
dao.playlistPages(sourceId, "has", downloaded = true).page().map { it.id },
)
assertTrue(dao.playlistPages(sourceId, "no", downloaded = true).page().isEmpty())
}
@Test
fun offlineListQueriesReturnOnlyCompletedDownloads() =
runTest {
@@ -1270,6 +1375,17 @@ class LibraryDaoTest {
}
}
private suspend fun topLevelPlan(sql: String): String =
db.useReaderConnection { connection ->
connection.usePrepared("EXPLAIN QUERY PLAN $sql") { statement ->
val details = mutableListOf<String>()
while (statement.step()) {
if (statement.getLong(1) == 0L) details += statement.getText(3)
}
details.joinToString("\n")
}
}
@Test
fun defaultPagingQueriesMatchTheDownloadFreeFilters() =
runTest {
@@ -1384,6 +1500,52 @@ class LibraryDaoTest {
awaitUntil("the default list observes album writes") { defaultInvalidated.get() }
}
@Test
fun downloadWritesInvalidateTheDownloadedArtistAndPlaylistPagingSources() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertArtists(listOf(artist(sourceId, "ar-1", "Artist", albumCount = 1)))
dao.upsertAlbums(listOf(album(sourceId, "al-1", "Album", year = null, starred = null)))
dao.upsertSongs(listOf(song(sourceId, "s1", "One", starred = null)))
dao.upsertPlaylists(listOf(playlist(sourceId, "pl-1", "Playlist", created = 0, changed = 0)))
dao.upsertPlaylistSongs(listOf(PlaylistSong(sourceId, "pl-1", "s1", 0)))
val artistInvalidated = AtomicBoolean(false)
val artists = dao.artistPages(sourceId, downloaded = true)
artists.registerInvalidatedCallback { artistInvalidated.set(true) }
artists.page()
val playlistInvalidated = AtomicBoolean(false)
val playlists = dao.playlistPages(sourceId, downloaded = true)
playlists.registerInvalidatedCallback { playlistInvalidated.set(true) }
playlists.page()
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
awaitUntil("the downloaded artists observe download writes") { artistInvalidated.get() }
awaitUntil("the downloaded playlists observe download writes") { playlistInvalidated.get() }
}
@Test
fun songWritesInvalidateTheDownloadedArtistPagingSource() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertArtists(listOf(artist(sourceId, "ar-1", "Artist", albumCount = 1)))
dao.upsertAlbums(listOf(album(sourceId, "al-1", "Album", year = null, starred = null)))
dao.upsertSongs(listOf(song(sourceId, "s1", "One", starred = null)))
val invalidated = AtomicBoolean(false)
val artists = dao.artistPages(sourceId, downloaded = true)
artists.registerInvalidatedCallback { invalidated.set(true) }
artists.page()
dao.upsertSongs(listOf(song(sourceId, "s2", "Two", starred = null)))
awaitUntil("the downloaded artists observe song writes") { invalidated.get() }
}
@Test
fun sortNamesDriveArtistAndAlbumOrder() =
runTest {