address review: offset fallback for far backward album seeks; pin the playlist keyset

- The NULL-aware backward album predicate is not sargable, so a backward seek
  from a far cursor scans the whole album partition. Fall back to the offset
  query when the anchor is more than twice the offset away; near-cursor
  backward reads still use keyset.
- Cover the playlist keyset path: a DAO test for `playlistSongsBefore` and a
  repository test scanning a playlist forward and backward. Both fail if the
  playlist order key stops driving the seek.
This commit is contained in:
austinried
2026-10-04 00:30:01 +00:00
parent 87dc4499ca
commit 729737895f
2 changed files with 46 additions and 1 deletions
@@ -764,6 +764,11 @@ class QueueRepository(
): List<SongItem>? {
if (!cursor.key.seekable(entry.kind)) return null
val from = cursor.ordinal
// A backward seek from a far cursor is not cheaper than the offset query: the album
// predicate is not sargable (it has to include NULL disc/track), so it scans the partition
// back to `offset` instead of using a range. Fall back when the anchor is more than twice
// the offset away, matching the documented far-jump behaviour.
if (from > offset && from - offset >= offset) return null
return if (offset >= from) {
rowsFrom(entry, cursor.key, offset - from, limit)
} else {
@@ -120,6 +120,44 @@ class QueueKeysetTest {
assertEquals(listOf("n2", "s1"), actual)
}
@Test
fun playlistKeysetBeforeMatchesTheOffsetWindow() =
runTest {
seedLibrary()
val all = allPlaylistRows()
val anchor = all[10]
val expected = all.drop(5).take(5).map { it.song.id }
val actual =
db
.queueDao()
.playlistSongsBefore(1, "pl1", anchor.position, skip = 0, limit = 5)
.asReversed()
.map { it.song.id }
assertEquals(expected, actual)
}
@Test
fun scanningAPlaylistThroughTheRepositoryUsesKeysetSeeks() =
runTest {
seedLibrary()
repository.replace(listOf(repository.playlistEntry(1, "pl1")))
val snapshot = repository.snapshot()
val expected = allPlaylistRows().map { it.song.id }
assertEquals(expected.size, snapshot.size.toInt())
val forward = (0 until snapshot.size).map { repository.itemAt(snapshot, it)?.song?.id }
assertEquals(expected, forward)
assertTrue("forward keyset seeks: ${repository.keysetSeeks}", repository.keysetSeeks >= snapshot.size - 1)
assertEquals(1L, repository.offsetSeeks)
val beforeBackward = repository.keysetSeeks
val backward = (snapshot.size - 1 downTo 0).map { repository.itemAt(snapshot, it)?.song?.id }.asReversed()
assertEquals(expected, backward)
assertTrue("backward keyset seeks", repository.keysetSeeks > beforeBackward)
}
@Test
fun scanningAnAlbumWithNullDiscAndTrackBackwardsResolvesEveryRow() =
runTest {
@@ -176,7 +214,9 @@ class QueueKeysetTest {
val resolved =
(snapshot.size - 1 downTo 0).map { repository.itemAt(snapshot, it)?.song?.id }.asReversed()
assertEquals(expected, resolved)
assertTrue(repository.keysetSeeks >= snapshot.size - 1)
// Most steps are keyset; the first few near the start fall back to OFFSET, which is
// cheaper there.
assertTrue("backward keyset seeks: ${repository.keysetSeeks}", repository.keysetSeeks >= snapshot.size * 9 / 10)
}
@Test