mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
db: bind named parameters directly and cover the repository dispatch
SQLite gives a repeated named parameter one index, so rawQuery can pass the SQL through unchanged and bind the distinct names in first-appearance order instead of rewriting them to positional placeholders. Use named arguments for the repository dispatch, since the adjacent descending/downloaded booleans were easy to transpose, and add a repository test that collects the downloaded album grid. Fix the query-timing labels so the unqualified rows measure the same default path as before.
This commit is contained in:
@@ -186,45 +186,20 @@ internal fun rediscoverAlbumsQuery(
|
||||
),
|
||||
)
|
||||
|
||||
// RoomRawQuery binds by position, so the named fragments are rewritten once here instead of
|
||||
// every caller carrying an index for each repeated :sourceId.
|
||||
// RoomRawQuery takes a positional bind function, but SQLite gives a repeated named parameter one
|
||||
// index, so the SQL can keep its names and only the first-appearance order needs binding.
|
||||
internal fun rawQuery(
|
||||
sql: String,
|
||||
bindings: Map<String, Any?>,
|
||||
): RoomRawQuery {
|
||||
val names = mutableListOf<String>()
|
||||
val positional =
|
||||
buildString(sql.length) {
|
||||
var index = 0
|
||||
var quote: Char? = null
|
||||
while (index < sql.length) {
|
||||
val char = sql[index]
|
||||
if (quote != null) {
|
||||
append(char)
|
||||
if (char == quote) quote = null
|
||||
index++
|
||||
continue
|
||||
}
|
||||
if (char == '\'' || char == '"') {
|
||||
quote = char
|
||||
append(char)
|
||||
index++
|
||||
continue
|
||||
}
|
||||
if (char == ':' && sql.getOrNull(index + 1).let { it != null && (it.isLetter() || it == '_') }) {
|
||||
var end = index + 1
|
||||
while (end < sql.length && (sql[end].isLetterOrDigit() || sql[end] == '_')) end++
|
||||
names += sql.substring(index + 1, end)
|
||||
append('?')
|
||||
index = end
|
||||
continue
|
||||
}
|
||||
append(char)
|
||||
index++
|
||||
}
|
||||
}
|
||||
val values = names.map(bindings::getValue)
|
||||
return RoomRawQuery(positional) { statement ->
|
||||
val values =
|
||||
Regex(":(\\w+)")
|
||||
.findAll(sql)
|
||||
.map { it.groupValues[1] }
|
||||
.distinct()
|
||||
.map(bindings::getValue)
|
||||
.toList()
|
||||
return RoomRawQuery(sql) { statement ->
|
||||
values.forEachIndexed { index, value -> statement.bind(index + 1, value) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,16 @@ class LibraryRepository(
|
||||
userPreferences.sortNameMode().flatMapLatest { mode ->
|
||||
pager(40, enablePlaceholders = true) {
|
||||
val dao = db.libraryDao()
|
||||
val query = albumsPageQuery(sourceId, starred.ordinal, search, sort, descending, downloaded, mode)
|
||||
val query =
|
||||
albumsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starred.ordinal,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = downloaded,
|
||||
sortNameMode = mode,
|
||||
)
|
||||
if (downloaded) dao.downloadedAlbumsPaging(query) else dao.albumsPaging(query)
|
||||
}
|
||||
}
|
||||
@@ -211,7 +220,16 @@ class LibraryRepository(
|
||||
userPreferences.sortNameMode().flatMapLatest { mode ->
|
||||
pager(60) {
|
||||
val dao = db.libraryDao()
|
||||
val query = artistsPageQuery(sourceId, starred.ordinal, search, sort, descending, downloaded, mode)
|
||||
val query =
|
||||
artistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starred.ordinal,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = downloaded,
|
||||
sortNameMode = mode,
|
||||
)
|
||||
if (downloaded) dao.downloadedArtistsPaging(query) else dao.artistsPaging(query)
|
||||
}
|
||||
}
|
||||
@@ -225,7 +243,14 @@ class LibraryRepository(
|
||||
): Flow<PagingData<Playlist>> =
|
||||
pager(40) {
|
||||
val dao = db.libraryDao()
|
||||
val query = playlistsPageQuery(sourceId, search, sort, descending, downloaded)
|
||||
val query =
|
||||
playlistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = downloaded,
|
||||
)
|
||||
if (downloaded) dao.downloadedPlaylistsPaging(query) else dao.playlistsPaging(query)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,29 @@ internal fun LibraryDao.albumPages(
|
||||
sortNameMode: SortNameMode = SortNameMode.SortName,
|
||||
): PagingSource<Int, Album> =
|
||||
if (downloaded) {
|
||||
downloadedAlbumsPaging(albumsPageQuery(sourceId, starredFilter, search, sort, descending, true, sortNameMode))
|
||||
downloadedAlbumsPaging(
|
||||
albumsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starredFilter,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = true,
|
||||
sortNameMode = sortNameMode,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
albumsPaging(albumsPageQuery(sourceId, starredFilter, search, sort, descending, false, sortNameMode))
|
||||
albumsPaging(
|
||||
albumsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starredFilter,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = false,
|
||||
sortNameMode = sortNameMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun LibraryDao.artistPages(
|
||||
@@ -34,9 +54,29 @@ internal fun LibraryDao.artistPages(
|
||||
sortNameMode: SortNameMode = SortNameMode.SortName,
|
||||
): PagingSource<Int, Artist> =
|
||||
if (downloaded) {
|
||||
downloadedArtistsPaging(artistsPageQuery(sourceId, starredFilter, search, sort, descending, true, sortNameMode))
|
||||
downloadedArtistsPaging(
|
||||
artistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starredFilter,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = true,
|
||||
sortNameMode = sortNameMode,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
artistsPaging(artistsPageQuery(sourceId, starredFilter, search, sort, descending, false, sortNameMode))
|
||||
artistsPaging(
|
||||
artistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
starredFilter = starredFilter,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = false,
|
||||
sortNameMode = sortNameMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun LibraryDao.playlistPages(
|
||||
@@ -47,7 +87,23 @@ internal fun LibraryDao.playlistPages(
|
||||
downloaded: Boolean = false,
|
||||
): PagingSource<Int, Playlist> =
|
||||
if (downloaded) {
|
||||
downloadedPlaylistsPaging(playlistsPageQuery(sourceId, search, sort, descending, true))
|
||||
downloadedPlaylistsPaging(
|
||||
playlistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = true,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
playlistsPaging(playlistsPageQuery(sourceId, search, sort, descending, false))
|
||||
playlistsPaging(
|
||||
playlistsPageQuery(
|
||||
sourceId = sourceId,
|
||||
search = search,
|
||||
sort = sort,
|
||||
descending = descending,
|
||||
downloaded = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -113,17 +113,21 @@ class QueryTimingMeasurementTest {
|
||||
|
||||
private suspend fun measureReads() {
|
||||
val dao = db.libraryDao()
|
||||
timed("albums first page (name)") { dao.albumPages(1, 0, "", sort = AlbumSort.Name, downloaded = true).firstPage() }
|
||||
timed("albums first page (name, default)") { dao.albumPages(1, 0, "", sort = AlbumSort.Name).firstPage() }
|
||||
timed("albums first page (artist)") { dao.albumPages(1, 0, "", sort = AlbumSort.Artist, downloaded = true).firstPage() }
|
||||
timed("albums first page (year)") { dao.albumPages(1, 0, "", sort = AlbumSort.Year, downloaded = true).firstPage() }
|
||||
timed("albums first page (added)") { dao.albumPages(1, 0, "", sort = AlbumSort.Added, downloaded = true).firstPage() }
|
||||
timed("albums first page (starred)") { dao.albumPages(1, 0, "", sort = AlbumSort.Starred, downloaded = true).firstPage() }
|
||||
timed("albums first page (frequent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Frequent, downloaded = true).firstPage() }
|
||||
timed("albums first page (recent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Recent, downloaded = true).firstPage() }
|
||||
timed("artists first page (name)") { dao.artistPages(1, 0, "", sort = ArtistSort.Name, downloaded = true).firstPage() }
|
||||
timed("artists first page (name, default)") { dao.artistPages(1, 0, "", sort = ArtistSort.Name).firstPage() }
|
||||
timed("artists first page (frequent)") { dao.artistPages(1, 0, "", sort = ArtistSort.Frequent, downloaded = true).firstPage() }
|
||||
timed("albums first page (name)") { dao.albumPages(1, 0, "", sort = AlbumSort.Name).firstPage() }
|
||||
timed("albums first page (name, downloaded)") {
|
||||
dao.albumPages(1, 0, "", sort = AlbumSort.Name, downloaded = true).firstPage()
|
||||
}
|
||||
timed("albums first page (artist)") { dao.albumPages(1, 0, "", sort = AlbumSort.Artist).firstPage() }
|
||||
timed("albums first page (year)") { dao.albumPages(1, 0, "", sort = AlbumSort.Year).firstPage() }
|
||||
timed("albums first page (added)") { dao.albumPages(1, 0, "", sort = AlbumSort.Added).firstPage() }
|
||||
timed("albums first page (starred)") { dao.albumPages(1, 0, "", sort = AlbumSort.Starred).firstPage() }
|
||||
timed("albums first page (frequent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Frequent).firstPage() }
|
||||
timed("albums first page (recent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Recent).firstPage() }
|
||||
timed("artists first page (name)") { dao.artistPages(1, 0, "", sort = ArtistSort.Name).firstPage() }
|
||||
timed("artists first page (name, downloaded)") {
|
||||
dao.artistPages(1, 0, "", sort = ArtistSort.Name, downloaded = true).firstPage()
|
||||
}
|
||||
timed("artists first page (frequent)") { dao.artistPages(1, 0, "", sort = ArtistSort.Frequent).firstPage() }
|
||||
timed("artist albums") { dao.albumsForArtist(1, "ar0").first() }
|
||||
timed("artist name") { dao.artistName(1, "ar0").first() }
|
||||
timed("artist songs") { db.queueDao().artistSongs(1, "ar0", 0, 100) }
|
||||
@@ -133,11 +137,13 @@ class QueryTimingMeasurementTest {
|
||||
timed("artists downloaded filter") { dao.artistPages(1, 0, "", downloaded = true).firstPage() }
|
||||
timed("artist statuses") { db.downloadDao().artistStatuses(1).first() }
|
||||
timed("artist status") { db.downloadDao().artistStatus(1, "ar0").first() }
|
||||
timed("playlists first page (name)") { dao.playlistPages(1, "", sort = PlaylistSort.Name, downloaded = true).firstPage() }
|
||||
timed("playlists first page (name, default)") { dao.playlistPages(1, "", sort = PlaylistSort.Name).firstPage() }
|
||||
timed("albums deep page") { dao.albumPages(1, 0, "", sort = AlbumSort.Name, downloaded = true).deepPage(1000) }
|
||||
timed("albums search FTS") { dao.albumPages(1, 0, "Album 1", sort = AlbumSort.Name, downloaded = true).firstPage() }
|
||||
timed("albums search short") { dao.albumPages(1, 0, "al", sort = AlbumSort.Name, downloaded = true).firstPage() }
|
||||
timed("playlists first page (name)") { dao.playlistPages(1, "", sort = PlaylistSort.Name).firstPage() }
|
||||
timed("playlists first page (name, downloaded)") {
|
||||
dao.playlistPages(1, "", sort = PlaylistSort.Name, downloaded = true).firstPage()
|
||||
}
|
||||
timed("albums deep page") { dao.albumPages(1, 0, "", sort = AlbumSort.Name).deepPage(1000) }
|
||||
timed("albums search FTS") { dao.albumPages(1, 0, "Album 1", sort = AlbumSort.Name).firstPage() }
|
||||
timed("albums search short") { dao.albumPages(1, 0, "al", sort = AlbumSort.Name).firstPage() }
|
||||
timed("songs search FTS") { dao.searchSongs(1, "Song", 50).first() }
|
||||
timed("home recently played") { dao.recentlyPlayedAlbums(1, 20).first() }
|
||||
timed("home most played") { dao.mostPlayedAlbums(1, 20).first() }
|
||||
|
||||
@@ -208,6 +208,42 @@ class LibraryRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theDownloadedAlbumGridOnlyPresentsDownloadedRows() =
|
||||
runBlocking {
|
||||
val sourceId = sourceRepository.addSource("nav", "http://localhost/", "u", "p", false)
|
||||
db.libraryDao().upsertAlbums(listOf(seedAlbum(sourceId, 1), seedAlbum(sourceId, 2)))
|
||||
db.libraryDao().upsertSongs(
|
||||
listOf(
|
||||
seedSong(sourceId).copy(id = "s1", albumId = "al-0001"),
|
||||
seedSong(sourceId).copy(id = "s2", albumId = "al-0002"),
|
||||
),
|
||||
)
|
||||
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
|
||||
|
||||
val presenterDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
val presenter = RecordingPresenter(presenterDispatcher)
|
||||
val job =
|
||||
launch {
|
||||
repository
|
||||
.albums(
|
||||
sourceId = sourceId,
|
||||
sort = AlbumSort.Name,
|
||||
descending = false,
|
||||
starred = StarredFilter.Any,
|
||||
search = "",
|
||||
downloaded = true,
|
||||
).collect { presenter.collectFrom(it) }
|
||||
}
|
||||
try {
|
||||
awaitRefreshes(presenter, 1)
|
||||
assertEquals(1, presented(presenter, presenterDispatcher))
|
||||
} finally {
|
||||
job.cancelAndJoin()
|
||||
presenterDispatcher.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun artistAlbumsSeparatesAlbumCreditsFromTrackAppearances() =
|
||||
runBlocking {
|
||||
|
||||
Reference in New Issue
Block a user