db: collapse paged library queries into one builder per entity

Replace the thirty hand-written order variants with one query builder per
entity plus a default and a downloaded @RawQuery entry point. The
repository's sort dispatch trees and the duplicated positional filters in
LibraryPaging are gone, and the SQL now lives in multi-line strings with
named parameters.

The generated statements are identical to the previous ones modulo
whitespace: the downloaded variants keep the same CASE/download conditions,
and the observed entities match what Room inferred for the old queries. A
small helper rewrites the named parameters to RoomRawQuery's positional
form, including the repeated :sourceId bindings.
This commit is contained in:
austinried
2026-11-03 01:03:45 +00:00
parent 29935b7bee
commit 8d6f7e2341
7 changed files with 472 additions and 674 deletions
@@ -10,15 +10,21 @@ import androidx.room3.Transaction
import androidx.room3.Upsert import androidx.room3.Upsert
import androidx.room3.paging.PagingSourceDaoReturnTypeConverter import androidx.room3.paging.PagingSourceDaoReturnTypeConverter
import com.subtracks.data.model.Album import com.subtracks.data.model.Album
import com.subtracks.data.model.AlbumArtistRef
import com.subtracks.data.model.AlbumSearch
import com.subtracks.data.model.AlbumSongItem import com.subtracks.data.model.AlbumSongItem
import com.subtracks.data.model.Artist import com.subtracks.data.model.Artist
import com.subtracks.data.model.ArtistBio import com.subtracks.data.model.ArtistBio
import com.subtracks.data.model.ArtistSearch
import com.subtracks.data.model.Disc import com.subtracks.data.model.Disc
import com.subtracks.data.model.DiscKey import com.subtracks.data.model.DiscKey
import com.subtracks.data.model.Playlist import com.subtracks.data.model.Playlist
import com.subtracks.data.model.PlaylistSearch
import com.subtracks.data.model.PlaylistSong import com.subtracks.data.model.PlaylistSong
import com.subtracks.data.model.PlaylistSongItem import com.subtracks.data.model.PlaylistSongItem
import com.subtracks.data.model.Song import com.subtracks.data.model.Song
import com.subtracks.data.model.SongArtistRef
import com.subtracks.data.model.SongDownload
import com.subtracks.data.model.Source import com.subtracks.data.model.Source
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -36,20 +42,11 @@ internal const val DOWNLOADED_SONG =
"EXISTS (SELECT 1 FROM song_downloads sd WHERE sd.sourceId = playlist_songs.sourceId " + "EXISTS (SELECT 1 FROM song_downloads sd WHERE sd.sourceId = playlist_songs.sourceId " +
"AND sd.songId = playlist_songs.songId AND sd.status = 'Completed')" "AND sd.songId = playlist_songs.songId AND sd.status = 'Completed')"
internal const val ALBUMS_FILTER = internal const val ALBUM_ORDER_BY_NAME = "sortName COLLATE NOCASE, id"
"FROM albums WHERE sourceId = :sourceId " + internal const val ALBUM_ORDER_BY_NAME_REVERSED = "sortName COLLATE NOCASE DESC, id DESC"
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " + internal const val ALBUM_ORDER_BY_ARTIST = "albumArtistSort COLLATE NOCASE, year, sortName COLLATE NOCASE, id"
"AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM songs " + internal const val ALBUM_ORDER_BY_ARTIST_REVERSED =
"JOIN song_downloads sd ON sd.sourceId = songs.sourceId AND sd.songId = songs.id " + "albumArtistSort COLLATE NOCASE DESC, year DESC, sortName COLLATE NOCASE DESC, id DESC"
"WHERE songs.sourceId = albums.sourceId AND songs.albumId = albums.id AND sd.status = 'Completed')) " +
"AND (:search = '' " +
"OR (length(:search) >= 3 AND rowid IN (SELECT rowid FROM album_search " +
"WHERE album_search MATCH '\"' || replace(:search, '\"', '\"\"') || '\"')) " +
"OR (length(:search) < 3 AND (instr(lower(name), lower(:search)) > 0 " +
"OR instr(lower(albumArtist), lower(:search)) > 0 " +
"OR instr(lower(sortName), lower(:search)) > 0 " +
"OR instr(lower(albumArtistSort), lower(:search)) > 0))) "
internal const val ALBUM_ORDER_BY_YEAR = "year DESC, sortName COLLATE NOCASE, id" internal const val ALBUM_ORDER_BY_YEAR = "year DESC, sortName COLLATE NOCASE, id"
internal const val ALBUM_ORDER_BY_YEAR_REVERSED = "year ASC, sortName COLLATE NOCASE DESC, id DESC" internal const val ALBUM_ORDER_BY_YEAR_REVERSED = "year ASC, sortName COLLATE NOCASE DESC, id DESC"
internal const val ALBUM_ORDER_BY_ADDED = "created DESC, sortName COLLATE NOCASE, id" internal const val ALBUM_ORDER_BY_ADDED = "created DESC, sortName COLLATE NOCASE, id"
@@ -154,7 +151,7 @@ internal const val DOWNLOADED_ALBUM_ROWS =
"JOIN song_downloads sd ON sd.sourceId = songs.sourceId AND sd.songId = songs.id " + "JOIN song_downloads sd ON sd.sourceId = songs.sourceId AND sd.songId = songs.id " +
"WHERE song_artists.sourceId = :sourceId AND sd.status = 'Completed'" "WHERE song_artists.sourceId = :sourceId AND sd.status = 'Completed'"
private const val DOWNLOADED_ARTIST_ALBUMS = internal const val DOWNLOADED_ARTIST_ALBUMS =
"SELECT albumId AS id FROM ($DOWNLOADED_ALBUM_ROWS) WHERE artistId = artists.id" "SELECT albumId AS id FROM ($DOWNLOADED_ALBUM_ROWS) WHERE artistId = artists.id"
private const val DOWNLOADED_COVER_ALBUM_ROWS = private const val DOWNLOADED_COVER_ALBUM_ROWS =
@@ -300,33 +297,13 @@ internal const val DOWNLOADED_ARTIST_ART_UPDATE_SQL =
"FROM desired WHERE artists.rowid = desired.rowid AND " + "FROM desired WHERE artists.rowid = desired.rowid AND " +
"artists.downloadedCoverArts != desired.downloadedCoverArts" "artists.downloadedCoverArts != desired.downloadedCoverArts"
internal const val ARTISTS_FILTER = internal const val ARTIST_COLUMNS_TAIL =
"FROM artists WHERE sourceId = :sourceId " + "artists.starred, artists.coverArt, artists.artistImageUrl, artists.coverArts, " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " + "artists.downloadedCoverArts, artists.playCount, artists.played, artists.songCount, " +
"AND $ARTIST_CREDITED " + "artists.sortName, artists.credited"
"AND (:downloadedFilter = 0 OR EXISTS (" +
"SELECT 1 FROM ($ARTIST_DOWNLOAD_ROWS) dl " +
"WHERE dl.id = artists.id AND dl.status = 'Completed')) " +
"AND (:search = '' " +
"OR (length(:search) >= 3 AND rowid IN (SELECT rowid FROM artist_search " +
"WHERE artist_search MATCH '\"' || replace(:search, '\"', '\"\"') || '\"')) " +
"OR (length(:search) < 3 AND (instr(lower(name), lower(:search)) > 0 " +
"OR instr(lower(sortName), lower(:search)) > 0))) "
internal const val ARTIST_COLUMNS = internal const val ARTIST_COLUMNS =
"artists.sourceId, artists.id, artists.name, artists.albumCount, artists.starred, " + "artists.sourceId, artists.id, artists.name, artists.albumCount, $ARTIST_COLUMNS_TAIL"
"artists.coverArt, artists.artistImageUrl, artists.coverArts, artists.downloadedCoverArts, " +
"artists.playCount, artists.played, artists.songCount, artists.sortName, artists.credited"
// With the Downloaded filter on, count the albums that actually have a download so the number under
// an artist matches the albums its detail screen lists.
internal const val ARTISTS_SELECT =
"SELECT artists.sourceId, artists.id, artists.name, " +
"CASE WHEN :downloadedFilter = 1 THEN (" +
"SELECT COUNT(*) FROM ($DOWNLOADED_ARTIST_ALBUMS)" +
") ELSE artists.albumCount END AS albumCount, " +
"artists.starred, artists.coverArt, artists.artistImageUrl, artists.coverArts, artists.downloadedCoverArts, " +
"artists.playCount, artists.played, artists.songCount, artists.sortName, artists.credited "
internal const val ARTIST_ORDER_BY_ALBUM_COUNT = "albumCount DESC, sortName COLLATE NOCASE, id" internal const val ARTIST_ORDER_BY_ALBUM_COUNT = "albumCount DESC, sortName COLLATE NOCASE, id"
internal const val ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED = "albumCount ASC, sortName COLLATE NOCASE DESC, id DESC" internal const val ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED = "albumCount ASC, sortName COLLATE NOCASE DESC, id DESC"
@@ -346,27 +323,6 @@ private const val SONG_NAME_SORT_KEY_MODE =
"(CASE :sortMode WHEN 'Name' THEN songs.title WHEN 'NonLatin' THEN (CASE WHEN songs.title GLOB '*[A-Za-z]*' THEN songs.title ELSE songs.sortName END) ELSE songs.sortName END) COLLATE NOCASE" "(CASE :sortMode WHEN 'Name' THEN songs.title WHEN 'NonLatin' THEN (CASE WHEN songs.title GLOB '*[A-Za-z]*' THEN songs.title ELSE songs.sortName END) ELSE songs.sortName END) COLLATE NOCASE"
internal const val SONG_SEARCH_ORDER_SORT_MODE = "$SONG_NAME_SORT_KEY_MODE, songs.id" internal const val SONG_SEARCH_ORDER_SORT_MODE = "$SONG_NAME_SORT_KEY_MODE, songs.id"
internal const val PLAYLISTS_FILTER =
"FROM playlists WHERE sourceId = :sourceId " +
"AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM playlist_songs ps " +
"JOIN song_downloads sd ON sd.sourceId = ps.sourceId AND sd.songId = ps.songId " +
"WHERE ps.sourceId = playlists.sourceId AND ps.playlistId = playlists.id AND sd.status = 'Completed')) " +
"AND (:search = '' " +
"OR (length(:search) >= 3 AND rowid IN (SELECT rowid FROM playlist_search " +
"WHERE playlist_search MATCH '\"' || replace(:search, '\"', '\"\"') || '\"')) " +
"OR (length(:search) < 3 AND instr(lower(name), lower(:search)) > 0)) "
// With the Downloaded filter on, count the songs that actually have a download so the number under
// a playlist matches the songs its detail screen lists.
internal const val PLAYLISTS_SELECT =
"SELECT playlists.sourceId, playlists.id, playlists.name, playlists.comment, playlists.coverArt, " +
"CASE WHEN :downloadedFilter = 1 THEN (" +
"SELECT COUNT(DISTINCT ps.songId) FROM playlist_songs ps " +
"JOIN song_downloads sd ON sd.sourceId = ps.sourceId AND sd.songId = ps.songId AND sd.status = 'Completed' " +
"WHERE ps.sourceId = playlists.sourceId AND ps.playlistId = playlists.id " +
") ELSE playlists.songCount END AS songCount, " +
"playlists.created, playlists.changed, playlists.duration, playlists.owner, playlists.shared "
internal const val PLAYLIST_ORDER_BY_NAME = "name COLLATE NOCASE, id" internal const val PLAYLIST_ORDER_BY_NAME = "name COLLATE NOCASE, id"
internal const val PLAYLIST_ORDER_BY_NAME_REVERSED = "name COLLATE NOCASE DESC, id DESC" internal const val PLAYLIST_ORDER_BY_NAME_REVERSED = "name COLLATE NOCASE DESC, id DESC"
internal const val PLAYLIST_ORDER_BY_ADDED = "created DESC, name COLLATE NOCASE, id" internal const val PLAYLIST_ORDER_BY_ADDED = "created DESC, name COLLATE NOCASE, id"
@@ -804,245 +760,27 @@ interface LibraryDao {
@RawQuery(observedEntities = [Playlist::class]) @RawQuery(observedEntities = [Playlist::class])
fun playlistsPaging(query: RoomRawQuery): PagingSource<Int, Playlist> fun playlistsPaging(query: RoomRawQuery): PagingSource<Int, Playlist>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_NAME_SORT_MODE") // These observe every table the statement reads, matching what Room inferred for the
fun albumsByName( // hand-written downloaded queries, so download and library writes invalidate as before.
sourceId: Long, @RawQuery(observedEntities = [Album::class, Song::class, SongDownload::class, AlbumSearch::class])
starredFilter: Int, fun downloadedAlbumsPaging(query: RoomRawQuery): PagingSource<Int, Album>
search: String,
downloadedFilter: Int = 0,
sortMode: String = "",
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_NAME_SORT_MODE_REVERSED") @RawQuery(
fun albumsByNameReversed( observedEntities = [
sourceId: Long, Album::class,
starredFilter: Int, Song::class,
search: String, SongDownload::class,
downloadedFilter: Int = 0, AlbumArtistRef::class,
sortMode: String = "", SongArtistRef::class,
): PagingSource<Int, Album> Artist::class,
Source::class,
ArtistSearch::class,
],
)
fun downloadedArtistsPaging(query: RoomRawQuery): PagingSource<Int, Artist>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ARTIST_SORT_MODE") @RawQuery(observedEntities = [PlaylistSong::class, SongDownload::class, Playlist::class, PlaylistSearch::class])
fun albumsByArtist( fun downloadedPlaylistsPaging(query: RoomRawQuery): PagingSource<Int, Playlist>
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
sortMode: String = "",
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ARTIST_SORT_MODE_REVERSED")
fun albumsByArtistReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
sortMode: String = "",
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_YEAR")
fun albumsByYear(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_YEAR_REVERSED")
fun albumsByYearReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ADDED")
fun albumsByRecentlyAdded(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_ADDED_REVERSED")
fun albumsByRecentlyAddedReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_STARRED")
fun albumsByStarred(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_STARRED_REVERSED")
fun albumsByStarredReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_FREQUENT")
fun albumsByFrequent(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_FREQUENT_REVERSED")
fun albumsByFrequentReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_RECENT")
fun albumsByRecent(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("SELECT * $ALBUMS_FILTER ORDER BY $ALBUM_ORDER_BY_RECENT_REVERSED")
fun albumsByRecentReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Album>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_NAME_SORT_MODE")
fun artistsByName(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
sortMode: String = "",
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_NAME_SORT_MODE_REVERSED")
fun artistsByNameReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
sortMode: String = "",
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_ALBUM_COUNT")
fun artistsByAlbumCount(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_ALBUM_COUNT_REVERSED")
fun artistsByAlbumCountReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_STARRED")
fun artistsByStarred(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_STARRED_REVERSED")
fun artistsByStarredReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_FREQUENT")
fun artistsByFrequent(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_FREQUENT_REVERSED")
fun artistsByFrequentReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_RECENT")
fun artistsByRecent(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("$ARTISTS_SELECT $ARTISTS_FILTER ORDER BY $ARTIST_ORDER_BY_RECENT_REVERSED")
fun artistsByRecentReversed(
sourceId: Long,
starredFilter: Int,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME")
fun playlistsByName(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME_REVERSED")
fun playlistsByNameReversed(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED")
fun playlistsByAdded(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED_REVERSED")
fun playlistsByAddedReversed(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED")
fun playlistsByUpdated(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED_REVERSED")
fun playlistsByUpdatedReversed(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("SELECT * FROM albums WHERE sourceId = :sourceId AND played IS NOT NULL ORDER BY played DESC, sortName COLLATE NOCASE, id") @Query("SELECT * FROM albums WHERE sourceId = :sourceId AND played IS NOT NULL ORDER BY played DESC, sortName COLLATE NOCASE, id")
fun homeRecentlyPlayedAlbums(sourceId: Long): PagingSource<Int, Album> fun homeRecentlyPlayedAlbums(sourceId: Long): PagingSource<Int, Album>
@@ -7,49 +7,161 @@ import com.subtracks.data.prefs.ArtistSort
import com.subtracks.data.prefs.PlaylistSort import com.subtracks.data.prefs.PlaylistSort
import com.subtracks.data.prefs.SortNameMode import com.subtracks.data.prefs.SortNameMode
// These filters mirror the download-aware ones in LibraryDao but never mention song_downloads, so // The default filters never mention song_downloads, so the paging sources used without the
// the resulting PagingSource is not invalidated by download writes (including progress updates). // Downloaded filter are not invalidated by download writes (including progress updates). The
internal const val ALBUMS_FILTER_BASE = // downloaded variants are the same statements the hand-written queries used.
"FROM albums WHERE sourceId = ? " + internal const val ALBUMS_FILTER = """
"AND (? = 0 OR (? = 1 AND starred IS NOT NULL) OR (? = 2 AND starred IS NULL)) " + FROM albums WHERE sourceId = :sourceId
"AND (? = '' " + AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL))
"OR (length(?) >= 3 AND rowid IN (SELECT rowid FROM album_search " + AND (:search = ''
"WHERE album_search MATCH '\"' || replace(?, '\"', '\"\"') || '\"')) " + OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM album_search
"OR (length(?) < 3 AND (instr(lower(name), lower(?)) > 0 " + WHERE album_search MATCH '"' || replace(:search, '"', '""') || '"'))
"OR instr(lower(albumArtist), lower(?)) > 0 " + OR (length(:search) < $SEARCH_MIN_LENGTH AND (instr(lower(name), lower(:search)) > 0
"OR instr(lower(sortName), lower(?)) > 0 " + OR instr(lower(albumArtist), lower(:search)) > 0
"OR instr(lower(albumArtistSort), lower(?)) > 0))) " OR instr(lower(sortName), lower(:search)) > 0
OR instr(lower(albumArtistSort), lower(:search)) > 0)))
"""
internal const val ARTISTS_FILTER_BASE = internal const val ALBUMS_FILTER_DOWNLOADED = """
"FROM artists WHERE sourceId = ? " + FROM albums WHERE sourceId = :sourceId
"AND (? = 0 OR (? = 1 AND starred IS NOT NULL) OR (? = 2 AND starred IS NULL)) " + AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL))
"AND $ARTIST_CREDITED " + AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM songs
"AND (? = '' " + JOIN song_downloads sd ON sd.sourceId = songs.sourceId AND sd.songId = songs.id
"OR (length(?) >= 3 AND rowid IN (SELECT rowid FROM artist_search " + WHERE songs.sourceId = albums.sourceId AND songs.albumId = albums.id AND sd.status = 'Completed'))
"WHERE artist_search MATCH '\"' || replace(?, '\"', '\"\"') || '\"')) " + AND (:search = ''
"OR (length(?) < 3 AND (instr(lower(name), lower(?)) > 0 " + OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM album_search
"OR instr(lower(sortName), lower(?)) > 0))) " WHERE album_search MATCH '"' || replace(:search, '"', '""') || '"'))
OR (length(:search) < $SEARCH_MIN_LENGTH AND (instr(lower(name), lower(:search)) > 0
OR instr(lower(albumArtist), lower(:search)) > 0
OR instr(lower(sortName), lower(:search)) > 0
OR instr(lower(albumArtistSort), lower(:search)) > 0)))
"""
internal const val PLAYLISTS_FILTER_BASE = internal const val ARTISTS_FILTER = """
"FROM playlists WHERE sourceId = ? " + FROM artists WHERE sourceId = :sourceId
"AND (? = '' " + AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL))
"OR (length(?) >= 3 AND rowid IN (SELECT rowid FROM playlist_search " + AND $ARTIST_CREDITED
"WHERE playlist_search MATCH '\"' || replace(?, '\"', '\"\"') || '\"')) " + AND (:search = ''
"OR (length(?) < 3 AND instr(lower(name), lower(?)) > 0)) " OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM artist_search
WHERE artist_search MATCH '"' || replace(:search, '"', '""') || '"'))
OR (length(:search) < $SEARCH_MIN_LENGTH AND (instr(lower(name), lower(:search)) > 0
OR instr(lower(sortName), lower(:search)) > 0)))
"""
internal fun albumsDefaultQuery( internal const val ARTISTS_FILTER_DOWNLOADED = """
FROM artists WHERE sourceId = :sourceId
AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL))
AND $ARTIST_CREDITED
AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM ($ARTIST_DOWNLOAD_ROWS) dl
WHERE dl.id = artists.id AND dl.status = 'Completed'))
AND (:search = ''
OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM artist_search
WHERE artist_search MATCH '"' || replace(:search, '"', '""') || '"'))
OR (length(:search) < $SEARCH_MIN_LENGTH AND (instr(lower(name), lower(:search)) > 0
OR instr(lower(sortName), lower(:search)) > 0)))
"""
// With the Downloaded filter on, count the albums that actually have a download so the number under
// an artist matches the albums its detail screen lists.
internal const val ARTIST_COLUMNS_DOWNLOADED = """
artists.sourceId, artists.id, artists.name,
CASE WHEN :downloadedFilter = 1 THEN (SELECT COUNT(*) FROM ($DOWNLOADED_ARTIST_ALBUMS)) ELSE artists.albumCount END AS albumCount,
$ARTIST_COLUMNS_TAIL
"""
internal const val PLAYLISTS_FILTER = """
FROM playlists WHERE sourceId = :sourceId
AND (:search = ''
OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM playlist_search
WHERE playlist_search MATCH '"' || replace(:search, '"', '""') || '"'))
OR (length(:search) < $SEARCH_MIN_LENGTH AND instr(lower(name), lower(:search)) > 0))
"""
internal const val PLAYLISTS_FILTER_DOWNLOADED = """
FROM playlists WHERE sourceId = :sourceId
AND (:downloadedFilter = 0 OR EXISTS (SELECT 1 FROM playlist_songs ps
JOIN song_downloads sd ON sd.sourceId = ps.sourceId AND sd.songId = ps.songId
WHERE ps.sourceId = playlists.sourceId AND ps.playlistId = playlists.id AND sd.status = 'Completed'))
AND (:search = ''
OR (length(:search) >= $SEARCH_MIN_LENGTH AND rowid IN (SELECT rowid FROM playlist_search
WHERE playlist_search MATCH '"' || replace(:search, '"', '""') || '"'))
OR (length(:search) < $SEARCH_MIN_LENGTH AND instr(lower(name), lower(:search)) > 0))
"""
// With the Downloaded filter on, count the songs that actually have a download so the number under
// a playlist matches the songs its detail screen lists.
internal const val PLAYLISTS_COLUMNS_DOWNLOADED = """
playlists.sourceId, playlists.id, playlists.name, playlists.comment, playlists.coverArt,
CASE WHEN :downloadedFilter = 1 THEN (SELECT COUNT(DISTINCT ps.songId)
FROM playlist_songs ps
JOIN song_downloads sd ON sd.sourceId = ps.sourceId AND sd.songId = ps.songId AND sd.status = 'Completed'
WHERE ps.sourceId = playlists.sourceId AND ps.playlistId = playlists.id
) ELSE playlists.songCount END AS songCount,
playlists.created, playlists.changed, playlists.duration, playlists.owner, playlists.shared
"""
internal fun albumsPageQuery(
sourceId: Long, sourceId: Long,
starredFilter: Int, starredFilter: Int,
search: String, search: String,
sort: AlbumSort, sort: AlbumSort,
descending: Boolean, descending: Boolean,
downloaded: Boolean,
sortNameMode: SortNameMode = SortNameMode.SortName, sortNameMode: SortNameMode = SortNameMode.SortName,
): RoomRawQuery { ): RoomRawQuery =
val order = albumOrder(sort, descending, sortNameMode) rawQuery(
return RoomRawQuery("SELECT * $ALBUMS_FILTER_BASE ORDER BY $order") { stmt -> sql = "SELECT * ${if (downloaded) ALBUMS_FILTER_DOWNLOADED else ALBUMS_FILTER} ORDER BY ${albumOrder(sort, descending, sortNameMode)}",
stmt.bindAlbumFilters(sourceId, starredFilter, search) bindings =
} mapOf(
} "sourceId" to sourceId,
"starredFilter" to starredFilter,
"downloadedFilter" to if (downloaded) 1 else 0,
"search" to search,
),
)
internal fun artistsPageQuery(
sourceId: Long,
starredFilter: Int,
search: String,
sort: ArtistSort,
descending: Boolean,
downloaded: Boolean,
sortNameMode: SortNameMode = SortNameMode.SortName,
): RoomRawQuery =
rawQuery(
sql =
"SELECT ${if (downloaded) ARTIST_COLUMNS_DOWNLOADED else ARTIST_COLUMNS} " +
"${if (downloaded) ARTISTS_FILTER_DOWNLOADED else ARTISTS_FILTER} " +
"ORDER BY ${artistOrder(sort, descending, sortNameMode)}",
bindings =
mapOf(
"sourceId" to sourceId,
"starredFilter" to starredFilter,
"downloadedFilter" to if (downloaded) 1 else 0,
"search" to search,
),
)
internal fun playlistsPageQuery(
sourceId: Long,
search: String,
sort: PlaylistSort,
descending: Boolean,
downloaded: Boolean,
): RoomRawQuery =
rawQuery(
sql =
"SELECT ${if (downloaded) PLAYLISTS_COLUMNS_DOWNLOADED else "*"} " +
"${if (downloaded) PLAYLISTS_FILTER_DOWNLOADED else PLAYLISTS_FILTER} " +
"ORDER BY ${playlistOrder(sort, descending)}",
bindings =
mapOf(
"sourceId" to sourceId,
"downloadedFilter" to if (downloaded) 1 else 0,
"search" to search,
),
)
internal fun rediscoverAlbumsQuery( internal fun rediscoverAlbumsQuery(
sourceId: Long, sourceId: Long,
@@ -57,70 +169,71 @@ internal fun rediscoverAlbumsQuery(
search: String, search: String,
cutoff: Long, cutoff: Long,
): RoomRawQuery = ): RoomRawQuery =
RoomRawQuery( rawQuery(
"SELECT * $ALBUMS_FILTER_BASE AND played IS NOT NULL AND played < ? " + sql =
"SELECT * $ALBUMS_FILTER AND played IS NOT NULL AND played < :cutoff " +
"ORDER BY played ASC, sortName COLLATE NOCASE, id", "ORDER BY played ASC, sortName COLLATE NOCASE, id",
) { stmt -> bindings =
stmt.bindAlbumFilters(sourceId, starredFilter, search) mapOf(
stmt.bindLong(13, cutoff) "sourceId" to sourceId,
"starredFilter" to starredFilter,
"search" to search,
"cutoff" to cutoff,
),
)
// RoomRawQuery binds by position, so the named fragments are rewritten once here instead of
// every caller carrying an index for each repeated :sourceId.
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++
}
}
return RoomRawQuery(positional) { statement ->
names.forEachIndexed { index, name -> statement.bind(index + 1, bindings.getValue(name)) }
}
} }
private fun SQLiteStatement.bindAlbumFilters( private fun SQLiteStatement.bind(
sourceId: Long, index: Int,
starredFilter: Int, value: Any?,
search: String,
) { ) {
bindLong(1, sourceId) when (value) {
bindInt(2, starredFilter) null -> bindNull(index)
bindInt(3, starredFilter) is Int -> bindLong(index, value.toLong())
bindInt(4, starredFilter) is Long -> bindLong(index, value)
bindText(5, search) is String -> bindText(index, value)
bindText(6, search) else -> error("Unsupported SQL parameter: ${value::class.qualifiedName}")
bindText(7, search)
bindText(8, search)
bindText(9, search)
bindText(10, search)
bindText(11, search)
bindText(12, search)
}
internal fun artistsDefaultQuery(
sourceId: Long,
starredFilter: Int,
search: String,
sort: ArtistSort,
descending: Boolean,
sortNameMode: SortNameMode = SortNameMode.SortName,
): RoomRawQuery {
val order = artistOrder(sort, descending, sortNameMode)
return RoomRawQuery("SELECT $ARTIST_COLUMNS $ARTISTS_FILTER_BASE ORDER BY $order") { stmt ->
stmt.bindLong(1, sourceId)
stmt.bindInt(2, starredFilter)
stmt.bindInt(3, starredFilter)
stmt.bindInt(4, starredFilter)
stmt.bindText(5, search)
stmt.bindText(6, search)
stmt.bindText(7, search)
stmt.bindText(8, search)
stmt.bindText(9, search)
stmt.bindText(10, search)
}
}
internal fun playlistsDefaultQuery(
sourceId: Long,
search: String,
sort: PlaylistSort,
descending: Boolean,
): RoomRawQuery {
val order = playlistOrder(sort, descending)
return RoomRawQuery("SELECT * $PLAYLISTS_FILTER_BASE ORDER BY $order") { stmt ->
stmt.bindLong(1, sourceId)
stmt.bindText(2, search)
stmt.bindText(3, search)
stmt.bindText(4, search)
stmt.bindText(5, search)
stmt.bindText(6, search)
} }
} }
@@ -7,9 +7,9 @@ import androidx.paging.PagingSource
import com.subtracks.R import com.subtracks.R
import com.subtracks.UiMessage import com.subtracks.UiMessage
import com.subtracks.data.db.SubtracksDatabase import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.db.albumsDefaultQuery import com.subtracks.data.db.albumsPageQuery
import com.subtracks.data.db.artistsDefaultQuery import com.subtracks.data.db.artistsPageQuery
import com.subtracks.data.db.playlistsDefaultQuery import com.subtracks.data.db.playlistsPageQuery
import com.subtracks.data.db.rediscoverAlbumsQuery import com.subtracks.data.db.rediscoverAlbumsQuery
import com.subtracks.data.model.Album import com.subtracks.data.model.Album
import com.subtracks.data.model.AlbumSongItem import com.subtracks.data.model.AlbumSongItem
@@ -195,68 +195,8 @@ class LibraryRepository(
userPreferences.sortNameMode().flatMapLatest { mode -> userPreferences.sortNameMode().flatMapLatest { mode ->
pager(40, enablePlaceholders = true) { pager(40, enablePlaceholders = true) {
val dao = db.libraryDao() val dao = db.libraryDao()
if (!downloaded) { val query = albumsPageQuery(sourceId, starred.ordinal, search, sort, descending, downloaded, mode)
dao.albumsPaging(albumsDefaultQuery(sourceId, starred.ordinal, search, sort, descending, mode)) if (downloaded) dao.downloadedAlbumsPaging(query) else dao.albumsPaging(query)
} else {
val downloadedFilter = 1
when (sort) {
AlbumSort.Name -> {
if (descending) {
dao.albumsByNameReversed(sourceId, starred.ordinal, search, downloadedFilter, mode.name)
} else {
dao.albumsByName(sourceId, starred.ordinal, search, downloadedFilter, mode.name)
}
}
AlbumSort.Artist -> {
if (descending) {
dao.albumsByArtistReversed(sourceId, starred.ordinal, search, downloadedFilter, mode.name)
} else {
dao.albumsByArtist(sourceId, starred.ordinal, search, downloadedFilter, mode.name)
}
}
AlbumSort.Year -> {
if (descending) {
dao.albumsByYearReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByYear(sourceId, starred.ordinal, search, downloadedFilter)
}
}
AlbumSort.Added -> {
if (descending) {
dao.albumsByRecentlyAddedReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByRecentlyAdded(sourceId, starred.ordinal, search, downloadedFilter)
}
}
AlbumSort.Starred -> {
if (descending) {
dao.albumsByStarredReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByStarred(sourceId, starred.ordinal, search, downloadedFilter)
}
}
AlbumSort.Frequent -> {
if (descending) {
dao.albumsByFrequentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByFrequent(sourceId, starred.ordinal, search, downloadedFilter)
}
}
AlbumSort.Recent -> {
if (descending) {
dao.albumsByRecentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.albumsByRecent(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
}
} }
} }
@@ -271,52 +211,8 @@ class LibraryRepository(
userPreferences.sortNameMode().flatMapLatest { mode -> userPreferences.sortNameMode().flatMapLatest { mode ->
pager(60) { pager(60) {
val dao = db.libraryDao() val dao = db.libraryDao()
if (!downloaded) { val query = artistsPageQuery(sourceId, starred.ordinal, search, sort, descending, downloaded, mode)
dao.artistsPaging(artistsDefaultQuery(sourceId, starred.ordinal, search, sort, descending, mode)) if (downloaded) dao.downloadedArtistsPaging(query) else dao.artistsPaging(query)
} else {
val downloadedFilter = 1
when (sort) {
ArtistSort.Name -> {
if (descending) {
dao.artistsByNameReversed(sourceId, starred.ordinal, search, downloadedFilter, mode.name)
} else {
dao.artistsByName(sourceId, starred.ordinal, search, downloadedFilter, mode.name)
}
}
ArtistSort.AlbumCount -> {
if (descending) {
dao.artistsByAlbumCountReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByAlbumCount(sourceId, starred.ordinal, search, downloadedFilter)
}
}
ArtistSort.Starred -> {
if (descending) {
dao.artistsByStarredReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByStarred(sourceId, starred.ordinal, search, downloadedFilter)
}
}
ArtistSort.Frequent -> {
if (descending) {
dao.artistsByFrequentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByFrequent(sourceId, starred.ordinal, search, downloadedFilter)
}
}
ArtistSort.Recent -> {
if (descending) {
dao.artistsByRecentReversed(sourceId, starred.ordinal, search, downloadedFilter)
} else {
dao.artistsByRecent(sourceId, starred.ordinal, search, downloadedFilter)
}
}
}
}
} }
} }
@@ -329,36 +225,8 @@ class LibraryRepository(
): Flow<PagingData<Playlist>> = ): Flow<PagingData<Playlist>> =
pager(40) { pager(40) {
val dao = db.libraryDao() val dao = db.libraryDao()
if (!downloaded) { val query = playlistsPageQuery(sourceId, search, sort, descending, downloaded)
dao.playlistsPaging(playlistsDefaultQuery(sourceId, search, sort, descending)) if (downloaded) dao.downloadedPlaylistsPaging(query) else dao.playlistsPaging(query)
} else {
val downloadedFilter = 1
when (sort) {
PlaylistSort.Name -> {
if (descending) {
dao.playlistsByNameReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByName(sourceId, search, downloadedFilter)
}
}
PlaylistSort.Added -> {
if (descending) {
dao.playlistsByAddedReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByAdded(sourceId, search, downloadedFilter)
}
}
PlaylistSort.Updated -> {
if (descending) {
dao.playlistsByUpdatedReversed(sourceId, search, downloadedFilter)
} else {
dao.playlistsByUpdated(sourceId, search, downloadedFilter)
}
}
}
}
} }
fun album( fun album(
@@ -67,17 +67,17 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("Alpha", "Beta", "Gamma"), listOf("Alpha", "Beta", "Gamma"),
dao.albumsByName(sourceId, starredFilter = 0, search = "").page().map { it.name }, dao.albumPages(sourceId, starredFilter = 0, search = "").page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Gamma", "Beta", "Alpha"), listOf("Gamma", "Beta", "Alpha"),
dao.albumsByNameReversed(sourceId, starredFilter = 0, search = "").page().map { it.name }, dao.albumPages(sourceId, starredFilter = 0, search = "", descending = true).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha", "Beta", "Gamma"), listOf("Alpha", "Beta", "Gamma"),
dao.albumsByYear(sourceId, starredFilter = 0, search = "").page().map { it.name }, dao.albumPages(sourceId, starredFilter = 0, search = "", sort = AlbumSort.Year).page().map { it.name },
) )
assertEquals(listOf("Alpha"), dao.albumsByName(sourceId, starredFilter = 1, search = "").page().map { it.name }) assertEquals(listOf("Alpha"), dao.albumPages(sourceId, starredFilter = 1, search = "").page().map { it.name })
} }
@Test @Test
@@ -95,14 +95,14 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("Beta", "Gamma", "Alpha"), listOf("Beta", "Gamma", "Alpha"),
dao.albumsByRecentlyAdded(sourceId, starredFilter = 0, search = "").page().map { it.name }, dao.albumPages(sourceId, starredFilter = 0, search = "", sort = AlbumSort.Added).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha", "Gamma", "Beta"), listOf("Alpha", "Gamma", "Beta"),
dao.albumsByStarred(sourceId, starredFilter = 0, search = "").page().map { it.name }, dao.albumPages(sourceId, starredFilter = 0, search = "", sort = AlbumSort.Starred).page().map { it.name },
) )
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, starredFilter = 0, search = "gam").page().map { it.name }) assertEquals(listOf("Gamma"), dao.albumPages(sourceId, starredFilter = 0, search = "gam").page().map { it.name })
assertEquals(listOf("Beta"), dao.albumsByName(sourceId, starredFilter = 2, search = "").page().map { it.name }) assertEquals(listOf("Beta"), dao.albumPages(sourceId, starredFilter = 2, search = "").page().map { it.name })
} }
@Test @Test
@@ -148,19 +148,19 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("Beta", "Alpha", "Delta", "Gamma"), listOf("Beta", "Alpha", "Delta", "Gamma"),
dao.albumsByStarred(sourceId, 0, "").page().map { it.name }, dao.albumPages(sourceId, 0, "", sort = AlbumSort.Starred).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha", "Beta", "Gamma", "Delta"), listOf("Alpha", "Beta", "Gamma", "Delta"),
dao.albumsByStarredReversed(sourceId, 0, "").page().map { it.name }, dao.albumPages(sourceId, 0, "", sort = AlbumSort.Starred, descending = true).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Beta", "Alpha", "Delta", "Gamma"), listOf("Beta", "Alpha", "Delta", "Gamma"),
dao.artistsByStarred(sourceId, 0, "").page().map { it.name }, dao.artistPages(sourceId, 0, "", sort = ArtistSort.Starred).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha", "Beta", "Gamma", "Delta"), listOf("Alpha", "Beta", "Gamma", "Delta"),
dao.artistsByStarredReversed(sourceId, 0, "").page().map { it.name }, dao.artistPages(sourceId, 0, "", sort = ArtistSort.Starred, descending = true).page().map { it.name },
) )
} }
@@ -206,19 +206,19 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("Two", "One", "Three"), listOf("Two", "One", "Three"),
dao.albumsByFrequent(sourceId, 0, "").page().map { it.name }, dao.albumPages(sourceId, 0, "", sort = AlbumSort.Frequent).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Two", "One", "Three"), listOf("Two", "One", "Three"),
dao.albumsByRecent(sourceId, 0, "").page().map { it.name }, dao.albumPages(sourceId, 0, "", sort = AlbumSort.Recent).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Many", "Few"), listOf("Many", "Few"),
dao.artistsByFrequent(sourceId, 0, "").page().map { it.name }, dao.artistPages(sourceId, 0, "", sort = ArtistSort.Frequent).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Many", "Few"), listOf("Many", "Few"),
dao.artistsByRecent(sourceId, 0, "").page().map { it.name }, dao.artistPages(sourceId, 0, "", sort = ArtistSort.Recent).page().map { it.name },
) )
} }
@@ -268,12 +268,17 @@ class LibraryDaoTest {
val pairs = val pairs =
listOf( listOf(
dao.albumsByName(sourceId, 0, "") to dao.albumsByNameReversed(sourceId, 0, ""), dao.albumPages(sourceId, 0, "") to dao.albumPages(sourceId, 0, "", descending = true),
dao.albumsByArtist(sourceId, 0, "") to dao.albumsByArtistReversed(sourceId, 0, ""), dao.albumPages(sourceId, 0, "", sort = AlbumSort.Artist) to
dao.albumsByYear(sourceId, 0, "") to dao.albumsByYearReversed(sourceId, 0, ""), dao.albumPages(sourceId, 0, "", sort = AlbumSort.Artist, descending = true),
dao.albumsByRecentlyAdded(sourceId, 0, "") to dao.albumsByRecentlyAddedReversed(sourceId, 0, ""), dao.albumPages(sourceId, 0, "", sort = AlbumSort.Year) to
dao.albumsByFrequent(sourceId, 0, "") to dao.albumsByFrequentReversed(sourceId, 0, ""), dao.albumPages(sourceId, 0, "", sort = AlbumSort.Year, descending = true),
dao.albumsByRecent(sourceId, 0, "") to dao.albumsByRecentReversed(sourceId, 0, ""), dao.albumPages(sourceId, 0, "", sort = AlbumSort.Added) to
dao.albumPages(sourceId, 0, "", sort = AlbumSort.Added, descending = true),
dao.albumPages(sourceId, 0, "", sort = AlbumSort.Frequent) to
dao.albumPages(sourceId, 0, "", sort = AlbumSort.Frequent, descending = true),
dao.albumPages(sourceId, 0, "", sort = AlbumSort.Recent) to
dao.albumPages(sourceId, 0, "", sort = AlbumSort.Recent, descending = true),
) )
for ((base, reversed) in pairs) { for ((base, reversed) in pairs) {
val ids = base.page().map { it.id } val ids = base.page().map { it.id }
@@ -303,23 +308,23 @@ class LibraryDaoTest {
val artistPairs = val artistPairs =
listOf( listOf(
dao.artistsByName(sourceId, 0, "").page().map { it.id } to dao.artistPages(sourceId, 0, "").page().map { it.id } to
dao.artistsByNameReversed(sourceId, 0, "").page().map { it.id }, dao.artistPages(sourceId, 0, "", descending = true).page().map { it.id },
dao.artistsByAlbumCount(sourceId, 0, "").page().map { it.id } to dao.artistPages(sourceId, 0, "", sort = ArtistSort.AlbumCount).page().map { it.id } to
dao.artistsByAlbumCountReversed(sourceId, 0, "").page().map { it.id }, dao.artistPages(sourceId, 0, "", sort = ArtistSort.AlbumCount, descending = true).page().map { it.id },
dao.artistsByFrequent(sourceId, 0, "").page().map { it.id } to dao.artistPages(sourceId, 0, "", sort = ArtistSort.Frequent).page().map { it.id } to
dao.artistsByFrequentReversed(sourceId, 0, "").page().map { it.id }, dao.artistPages(sourceId, 0, "", sort = ArtistSort.Frequent, descending = true).page().map { it.id },
dao.artistsByRecent(sourceId, 0, "").page().map { it.id } to dao.artistPages(sourceId, 0, "", sort = ArtistSort.Recent).page().map { it.id } to
dao.artistsByRecentReversed(sourceId, 0, "").page().map { it.id }, dao.artistPages(sourceId, 0, "", sort = ArtistSort.Recent, descending = true).page().map { it.id },
) )
val playlistPairs = val playlistPairs =
listOf( listOf(
dao.playlistsByName(sourceId, "").page().map { it.id } to dao.playlistPages(sourceId, "").page().map { it.id } to
dao.playlistsByNameReversed(sourceId, "").page().map { it.id }, dao.playlistPages(sourceId, "", descending = true).page().map { it.id },
dao.playlistsByAdded(sourceId, "").page().map { it.id } to dao.playlistPages(sourceId, "", sort = PlaylistSort.Added).page().map { it.id } to
dao.playlistsByAddedReversed(sourceId, "").page().map { it.id }, dao.playlistPages(sourceId, "", sort = PlaylistSort.Added, descending = true).page().map { it.id },
dao.playlistsByUpdated(sourceId, "").page().map { it.id } to dao.playlistPages(sourceId, "", sort = PlaylistSort.Updated).page().map { it.id } to
dao.playlistsByUpdatedReversed(sourceId, "").page().map { it.id }, dao.playlistPages(sourceId, "", sort = PlaylistSort.Updated, descending = true).page().map { it.id },
) )
for ((base, reversed) in artistPairs + playlistPairs) { for ((base, reversed) in artistPairs + playlistPairs) {
assertEquals(base.reversed(), reversed) assertEquals(base.reversed(), reversed)
@@ -357,11 +362,13 @@ class LibraryDaoTest {
ALBUM_ORDER_BY_RECENT_REVERSED to "index_albums_recent", ALBUM_ORDER_BY_RECENT_REVERSED to "index_albums_recent",
) )
for ((order, index) in orders) { for ((order, index) in orders) {
val plan = plan("SELECT * $ALBUMS_FILTER ORDER BY $order LIMIT 20 OFFSET 0") for (filter in listOf(ALBUMS_FILTER, ALBUMS_FILTER_DOWNLOADED)) {
val plan = plan("SELECT * $filter ORDER BY $order LIMIT 20 OFFSET 0")
assertTrue(plan, plan.contains("USING INDEX $index")) assertTrue(plan, plan.contains("USING INDEX $index"))
assertFalse(plan, plan.contains("TEMP B-TREE")) assertFalse(plan, plan.contains("TEMP B-TREE"))
} }
} }
}
@Test @Test
fun artistOrdersAreIndexBacked() = fun artistOrdersAreIndexBacked() =
@@ -382,11 +389,13 @@ class LibraryDaoTest {
ARTIST_ORDER_BY_RECENT_REVERSED to "index_artists_recent", ARTIST_ORDER_BY_RECENT_REVERSED to "index_artists_recent",
) )
for ((order, index) in orders) { for ((order, index) in orders) {
val plan = plan("SELECT * $ARTISTS_FILTER ORDER BY $order LIMIT 20 OFFSET 0") 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")) assertTrue(plan, plan.contains("USING INDEX $index"))
assertFalse(plan, plan.contains("TEMP B-TREE")) assertFalse(plan, plan.contains("TEMP B-TREE"))
} }
} }
}
@Test @Test
fun playlistOrdersAreIndexBacked() = fun playlistOrdersAreIndexBacked() =
@@ -401,11 +410,13 @@ class LibraryDaoTest {
PLAYLIST_ORDER_BY_UPDATED_REVERSED to "index_playlists_updated", PLAYLIST_ORDER_BY_UPDATED_REVERSED to "index_playlists_updated",
) )
for ((order, index) in orders) { for ((order, index) in orders) {
val plan = plan("SELECT * $PLAYLISTS_FILTER ORDER BY $order LIMIT 20 OFFSET 0") 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")) assertTrue(plan, plan.contains("USING INDEX $index"))
assertFalse(plan, plan.contains("TEMP B-TREE")) assertFalse(plan, plan.contains("TEMP B-TREE"))
} }
} }
}
@Test @Test
fun anUnindexedAlbumOrderStillShowsATempSort() = fun anUnindexedAlbumOrderStillShowsATempSort() =
@@ -456,17 +467,17 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("Downloaded"), listOf("Downloaded"),
dao.albumsByName(sourceId, 0, "", downloadedFilter = 1).page().map { it.name }, dao.albumPages(sourceId, 0, "", downloaded = true).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("ar-1"), listOf("ar-1"),
dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page().map { it.id }, dao.artistPages(sourceId, 0, "", downloaded = true).page().map { it.id },
) )
assertEquals( assertEquals(
listOf("pl-1"), listOf("pl-1"),
dao.playlistsByName(sourceId, "", downloadedFilter = 1).page().map { it.id }, dao.playlistPages(sourceId, "", downloaded = true).page().map { it.id },
) )
assertEquals(3, dao.albumsByName(sourceId, 0, "").page().size) assertEquals(3, dao.albumPages(sourceId, 0, "").page().size)
} }
@Test @Test
@@ -628,7 +639,7 @@ class LibraryDaoTest {
dao.recomputeArtistArt(sourceId) dao.recomputeArtistArt(sourceId)
val artists = dao.artistsByName(sourceId, 0, "").page().associateBy { it.id } val artists = dao.artistPages(sourceId, 0, "").page().associateBy { it.id }
assertEquals("own-art", artists.getValue("ar-own").coverArt) assertEquals("own-art", artists.getValue("ar-own").coverArt)
assertNull(artists.getValue("ar-album").coverArt) assertNull(artists.getValue("ar-album").coverArt)
@@ -648,7 +659,7 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("album-ref-art"), listOf("album-ref-art"),
dao dao
.artistsPaging(artistsDefaultQuery(sourceId, 0, "", ArtistSort.Name, false)) .artistPages(sourceId, 0, "", ArtistSort.Name, false)
.page() .page()
.first { it.id == "ar-album-ref" } .first { it.id == "ar-album-ref" }
.coverArts, .coverArts,
@@ -904,7 +915,7 @@ class LibraryDaoTest {
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed)) db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
assertEquals( assertEquals(
listOf("Album Artist"), listOf("Album Artist"),
dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page().map { it.name }, dao.artistPages(sourceId, 0, "", downloaded = true).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Album Artist"), listOf("Album Artist"),
@@ -913,7 +924,7 @@ class LibraryDaoTest {
db.downloadDao().upsert(SongDownload(sourceId, "s2", DownloadStatus.Completed)) db.downloadDao().upsert(SongDownload(sourceId, "s2", DownloadStatus.Completed))
val filtered = dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page() val filtered = dao.artistPages(sourceId, 0, "", downloaded = true).page()
assertEquals(listOf("Album Artist", "Guest"), filtered.map { it.name }) assertEquals(listOf("Album Artist", "Guest"), filtered.map { it.name })
assertEquals(listOf(1L, 1L), filtered.map { it.albumCount }) assertEquals(listOf(1L, 1L), filtered.map { it.albumCount })
@@ -961,14 +972,14 @@ class LibraryDaoTest {
) )
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed)) db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
val filtered = dao.artistsByName(sourceId, 0, "", downloadedFilter = 1).page() val filtered = dao.artistPages(sourceId, 0, "", downloaded = true).page()
assertEquals(1, filtered.size) assertEquals(1, filtered.size)
assertEquals(1L, filtered.first().albumCount) assertEquals(1L, filtered.first().albumCount)
assertEquals( assertEquals(
5L, 5L,
dao dao
.artistsByName(sourceId, 0, "") .artistPages(sourceId, 0, "")
.page() .page()
.first { it.id == "ar-1" } .first { it.id == "ar-1" }
.albumCount, .albumCount,
@@ -1006,7 +1017,7 @@ class LibraryDaoTest {
// ar-1 has two downloaded albums, ar-2 one, so the downloaded order is ar-1 first even // ar-1 has two downloaded albums, ar-2 one, so the downloaded order is ar-1 first even
// though the synced counts would put ar-2 first. // though the synced counts would put ar-2 first.
val filtered = dao.artistsByAlbumCount(sourceId, 0, "", downloadedFilter = 1).page() val filtered = dao.artistPages(sourceId, 0, "", downloaded = true, sort = ArtistSort.AlbumCount).page()
assertEquals(listOf("ar-1", "ar-2"), filtered.map { it.id }) assertEquals(listOf("ar-1", "ar-2"), filtered.map { it.id })
assertEquals(listOf(2L, 1L), filtered.map { it.albumCount }) assertEquals(listOf(2L, 1L), filtered.map { it.albumCount })
} }
@@ -1036,7 +1047,7 @@ class LibraryDaoTest {
) )
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed)) db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
val filtered = dao.playlistsByName(sourceId, "", downloadedFilter = 1).page() val filtered = dao.playlistPages(sourceId, "", downloaded = true).page()
assertEquals(1, filtered.size) assertEquals(1, filtered.size)
assertEquals(1L, filtered.first().songCount) assertEquals(1L, filtered.first().songCount)
assertEquals("austin", filtered.first().owner) assertEquals("austin", filtered.first().owner)
@@ -1045,7 +1056,7 @@ class LibraryDaoTest {
assertEquals( assertEquals(
5L, 5L,
dao dao
.playlistsByName(sourceId, "") .playlistPages(sourceId, "")
.page() .page()
.first { it.id == "pl-1" } .first { it.id == "pl-1" }
.songCount, .songCount,
@@ -1064,11 +1075,11 @@ class LibraryDaoTest {
), ),
) )
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "amm").page().map { it.name }) assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "amm").page().map { it.name })
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "GAM").page().map { it.name }) assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "GAM").page().map { it.name })
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "ga").page().map { it.name }) assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "ga").page().map { it.name })
assertEquals(listOf("Alpha"), dao.albumsByName(sourceId, 0, "Else").page().map { it.name }) assertEquals(listOf("Alpha"), dao.albumPages(sourceId, 0, "Else").page().map { it.name })
assertTrue(dao.albumsByName(sourceId, 0, "zzz").page().isEmpty()) assertTrue(dao.albumPages(sourceId, 0, "zzz").page().isEmpty())
} }
@Test @Test
@@ -1077,17 +1088,17 @@ class LibraryDaoTest {
val sourceId = source() val sourceId = source()
val dao = db.libraryDao() val dao = db.libraryDao()
dao.upsertAlbums(listOf(album(sourceId, "a", "Gamma", year = null, starred = null))) dao.upsertAlbums(listOf(album(sourceId, "a", "Gamma", year = null, starred = null)))
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "gam").page().map { it.name }) assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "gam").page().map { it.name })
dao.setAlbumStar(sourceId, "a", 7) dao.setAlbumStar(sourceId, "a", 7)
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "gam").page().map { it.name }) assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "gam").page().map { it.name })
dao.upsertAlbums(listOf(album(sourceId, "a", "Delta", year = null, starred = null))) dao.upsertAlbums(listOf(album(sourceId, "a", "Delta", year = null, starred = null)))
assertTrue(dao.albumsByName(sourceId, 0, "gam").page().isEmpty()) assertTrue(dao.albumPages(sourceId, 0, "gam").page().isEmpty())
assertEquals(listOf("Delta"), dao.albumsByName(sourceId, 0, "elt").page().map { it.name }) assertEquals(listOf("Delta"), dao.albumPages(sourceId, 0, "elt").page().map { it.name })
dao.deleteAlbums(sourceId, listOf("a")) dao.deleteAlbums(sourceId, listOf("a"))
assertTrue(dao.albumsByName(sourceId, 0, "elt").page().isEmpty()) assertTrue(dao.albumPages(sourceId, 0, "elt").page().isEmpty())
} }
@Test @Test
@@ -1101,8 +1112,8 @@ class LibraryDaoTest {
val second = source() val second = source()
dao.upsertAlbums(listOf(album(second, "a", "Beta", year = null, starred = null))) dao.upsertAlbums(listOf(album(second, "a", "Beta", year = null, starred = null)))
assertEquals(listOf("Beta"), dao.albumsByName(second, 0, "eta").page().map { it.name }) assertEquals(listOf("Beta"), dao.albumPages(second, 0, "eta").page().map { it.name })
assertTrue(dao.albumsByName(second, 0, "gam").page().isEmpty()) assertTrue(dao.albumPages(second, 0, "gam").page().isEmpty())
} }
@Test @Test
@@ -1113,10 +1124,10 @@ class LibraryDaoTest {
dao.upsertArtists(listOf(artist(sourceId, "r1", "The Beatles", albumCount = 1))) dao.upsertArtists(listOf(artist(sourceId, "r1", "The Beatles", albumCount = 1)))
dao.upsertPlaylists(listOf(playlist(sourceId, "l1", "Road Trip", created = 0, changed = 0))) dao.upsertPlaylists(listOf(playlist(sourceId, "l1", "Road Trip", created = 0, changed = 0)))
assertEquals(listOf("r1"), dao.artistsByName(sourceId, 0, "eat").page().map { it.id }) assertEquals(listOf("r1"), dao.artistPages(sourceId, 0, "eat").page().map { it.id })
assertEquals(listOf("l1"), dao.playlistsByName(sourceId, "rip").page().map { it.id }) assertEquals(listOf("l1"), dao.playlistPages(sourceId, "rip").page().map { it.id })
assertTrue(dao.artistsByName(sourceId, 0, "rip").page().isEmpty()) assertTrue(dao.artistPages(sourceId, 0, "rip").page().isEmpty())
assertTrue(dao.playlistsByName(sourceId, "eat").page().isEmpty()) assertTrue(dao.playlistPages(sourceId, "eat").page().isEmpty())
} }
@Test @Test
@@ -1131,10 +1142,10 @@ class LibraryDaoTest {
), ),
) )
assertEquals(listOf("Say \"Hello\""), dao.albumsByName(sourceId, 0, "\"Hel").page().map { it.name }) assertEquals(listOf("Say \"Hello\""), dao.albumPages(sourceId, 0, "\"Hel").page().map { it.name })
assertEquals( assertEquals(
listOf("Say \"Hello\""), listOf("Say \"Hello\""),
dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "\"Hel", AlbumSort.Name, false)).page().map { it.name }, dao.albumPages(sourceId, 0, "\"Hel", AlbumSort.Name, false).page().map { it.name },
) )
} }
@@ -1145,7 +1156,7 @@ class LibraryDaoTest {
val dao = db.libraryDao() val dao = db.libraryDao()
dao.upsertAlbums(listOf(album(sourceId, "a", "Gamma", year = null, starred = null).copy(albumArtist = null))) dao.upsertAlbums(listOf(album(sourceId, "a", "Gamma", year = null, starred = null).copy(albumArtist = null)))
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, 0, "amm").page().map { it.name }) assertEquals(listOf("Gamma"), dao.albumPages(sourceId, 0, "amm").page().map { it.name })
} }
@Test @Test
@@ -1274,35 +1285,35 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("Alpha", "Beta", "Gamma"), listOf("Alpha", "Beta", "Gamma"),
dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Name, false)).page().map { it.name }, dao.albumPages(sourceId, 0, "", AlbumSort.Name, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Gamma", "Beta", "Alpha"), listOf("Gamma", "Beta", "Alpha"),
dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Name, true)).page().map { it.name }, dao.albumPages(sourceId, 0, "", AlbumSort.Name, true).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha", "Beta", "Gamma"), listOf("Alpha", "Beta", "Gamma"),
dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Year, false)).page().map { it.name }, dao.albumPages(sourceId, 0, "", AlbumSort.Year, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Gamma", "Beta", "Alpha"), listOf("Gamma", "Beta", "Alpha"),
dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Artist, false)).page().map { it.name }, dao.albumPages(sourceId, 0, "", AlbumSort.Artist, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha"), listOf("Alpha"),
dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "alp", AlbumSort.Name, false)).page().map { it.name }, dao.albumPages(sourceId, 0, "alp", AlbumSort.Name, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Gamma"), listOf("Gamma"),
dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "am", AlbumSort.Name, false)).page().map { it.name }, dao.albumPages(sourceId, 0, "am", AlbumSort.Name, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha"), listOf("Alpha"),
dao.albumsPaging(albumsDefaultQuery(sourceId, 1, "", AlbumSort.Name, false)).page().map { it.name }, dao.albumPages(sourceId, 1, "", AlbumSort.Name, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Beta", "Gamma"), listOf("Beta", "Gamma"),
dao.albumsPaging(albumsDefaultQuery(sourceId, 2, "", AlbumSort.Name, false)).page().map { it.name }, dao.albumPages(sourceId, 2, "", AlbumSort.Name, false).page().map { it.name },
) )
} }
@@ -1326,23 +1337,23 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("Alpha", "Beta"), listOf("Alpha", "Beta"),
dao.artistsPaging(artistsDefaultQuery(sourceId, 0, "a", ArtistSort.Name, false)).page().map { it.name }, dao.artistPages(sourceId, 0, "a", ArtistSort.Name, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Beta", "Alpha"), listOf("Beta", "Alpha"),
dao.artistsPaging(artistsDefaultQuery(sourceId, 0, "", ArtistSort.AlbumCount, false)).page().map { it.name }, dao.artistPages(sourceId, 0, "", ArtistSort.AlbumCount, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha", "Beta"), listOf("Alpha", "Beta"),
dao.playlistsPaging(playlistsDefaultQuery(sourceId, "a", PlaylistSort.Name, false)).page().map { it.name }, dao.playlistPages(sourceId, "a", PlaylistSort.Name, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Alpha", "Beta"), listOf("Alpha", "Beta"),
dao.playlistsPaging(playlistsDefaultQuery(sourceId, "", PlaylistSort.Added, false)).page().map { it.name }, dao.playlistPages(sourceId, "", PlaylistSort.Added, false).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Beta", "Alpha"), listOf("Beta", "Alpha"),
dao.playlistsPaging(playlistsDefaultQuery(sourceId, "", PlaylistSort.Updated, false)).page().map { it.name }, dao.playlistPages(sourceId, "", PlaylistSort.Updated, false).page().map { it.name },
) )
} }
@@ -1355,12 +1366,12 @@ class LibraryDaoTest {
dao.upsertSongs(listOf(song(sourceId, "s1", "One", starred = null).copy(albumId = "a"))) dao.upsertSongs(listOf(song(sourceId, "s1", "One", starred = null).copy(albumId = "a")))
val defaultInvalidated = AtomicBoolean(false) val defaultInvalidated = AtomicBoolean(false)
val default = dao.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Name, false)) val default = dao.albumPages(sourceId, 0, "", AlbumSort.Name, false)
default.registerInvalidatedCallback { defaultInvalidated.set(true) } default.registerInvalidatedCallback { defaultInvalidated.set(true) }
default.page() default.page()
val downloadedInvalidated = AtomicBoolean(false) val downloadedInvalidated = AtomicBoolean(false)
val downloaded = dao.albumsByName(sourceId, 0, "", downloadedFilter = 1) val downloaded = dao.albumPages(sourceId, 0, "", downloaded = true)
downloaded.registerInvalidatedCallback { downloadedInvalidated.set(true) } downloaded.registerInvalidatedCallback { downloadedInvalidated.set(true) }
downloaded.page() downloaded.page()
@@ -1395,27 +1406,27 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("宇多田ヒカル", "あいみょん"), listOf("宇多田ヒカル", "あいみょん"),
dao.artistsByName(sourceId, 0, "").page().map { it.name }, dao.artistPages(sourceId, 0, "").page().map { it.name },
) )
assertEquals( assertEquals(
listOf("あいみょん", "宇多田ヒカル"), listOf("あいみょん", "宇多田ヒカル"),
dao.artistsByNameReversed(sourceId, 0, "").page().map { it.name }, dao.artistPages(sourceId, 0, "", descending = true).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Zzz", "Aaa"), listOf("Zzz", "Aaa"),
dao.albumsByName(sourceId, 0, "").page().map { it.name }, dao.albumPages(sourceId, 0, "").page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Aaa", "Zzz"), listOf("Aaa", "Zzz"),
dao.albumsByNameReversed(sourceId, 0, "").page().map { it.name }, dao.albumPages(sourceId, 0, "", descending = true).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Aaa", "Zzz"), listOf("Aaa", "Zzz"),
dao.albumsByArtist(sourceId, 0, "").page().map { it.name }, dao.albumPages(sourceId, 0, "", sort = AlbumSort.Artist).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Zzz", "Aaa"), listOf("Zzz", "Aaa"),
dao.albumsByArtistReversed(sourceId, 0, "").page().map { it.name }, dao.albumPages(sourceId, 0, "", sort = AlbumSort.Artist, descending = true).page().map { it.name },
) )
} }
@@ -1445,40 +1456,48 @@ class LibraryDaoTest {
assertEquals( assertEquals(
listOf("宇多田ヒカル", "Zebra", "Beatles", "あいみょん"), listOf("宇多田ヒカル", "Zebra", "Beatles", "あいみょん"),
dao.artistsByName(sourceId, 0, "", sortMode = SortNameMode.SortName.name).page().map { it.name }, dao.artistPages(sourceId, 0, "", sortNameMode = SortNameMode.SortName).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Beatles", "Zebra", "あいみょん", "宇多田ヒカル"), listOf("Beatles", "Zebra", "あいみょん", "宇多田ヒカル"),
dao.artistsByName(sourceId, 0, "", sortMode = SortNameMode.Name.name).page().map { it.name }, dao.artistPages(sourceId, 0, "", sortNameMode = SortNameMode.Name).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("宇多田ヒカル", "あいみょん", "Zebra", "Beatles"), listOf("宇多田ヒカル", "あいみょん", "Zebra", "Beatles"),
dao.artistsByNameReversed(sourceId, 0, "", sortMode = SortNameMode.Name.name).page().map { it.name }, dao
.artistPages(sourceId, 0, "", descending = true, sortNameMode = SortNameMode.Name)
.page()
.map { it.name },
) )
assertEquals( assertEquals(
listOf("宇多田ヒカル", "Beatles", "Zebra", "あいみょん"), listOf("宇多田ヒカル", "Beatles", "Zebra", "あいみょん"),
dao.artistsByName(sourceId, 0, "", sortMode = SortNameMode.NonLatin.name).page().map { it.name }, dao.artistPages(sourceId, 0, "", sortNameMode = SortNameMode.NonLatin).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Aaa", "Beatles", "Zzz"), listOf("Aaa", "Beatles", "Zzz"),
dao.albumsByName(sourceId, 0, "", sortMode = SortNameMode.Name.name).page().map { it.name }, dao.albumPages(sourceId, 0, "", sortNameMode = SortNameMode.Name).page().map { it.name },
) )
assertEquals( assertEquals(
listOf("Aaa", "Zzz", "Beatles"), listOf("Aaa", "Zzz", "Beatles"),
dao.albumsByArtist(sourceId, 0, "", sortMode = SortNameMode.Name.name).page().map { it.name }, dao
.albumPages(sourceId, 0, "", sort = AlbumSort.Artist, sortNameMode = SortNameMode.Name)
.page()
.map { it.name },
) )
assertEquals( assertEquals(
listOf("Beatles", "Zebra", "あいみょん", "宇多田ヒカル"), listOf("Beatles", "Zebra", "あいみょん", "宇多田ヒカル"),
dao dao
.artistsPaging(artistsDefaultQuery(sourceId, 0, "", ArtistSort.Name, false, SortNameMode.Name)) .artistsPaging(
.page() artistsPageQuery(sourceId, 0, "", ArtistSort.Name, false, false, SortNameMode.Name),
).page()
.map { it.name }, .map { it.name },
) )
assertEquals( assertEquals(
listOf("Aaa", "Beatles", "Zzz"), listOf("Aaa", "Beatles", "Zzz"),
dao dao
.albumsPaging(albumsDefaultQuery(sourceId, 0, "", AlbumSort.Name, false, SortNameMode.Name)) .albumsPaging(
.page() albumsPageQuery(sourceId, 0, "", AlbumSort.Name, false, false, SortNameMode.Name),
).page()
.map { it.name }, .map { it.name },
) )
} }
@@ -1507,11 +1526,11 @@ class LibraryDaoTest {
), ),
) )
assertEquals(listOf("宇多田ヒカル"), dao.artistsByName(sourceId, 0, "utada").page().map { it.name }) assertEquals(listOf("宇多田ヒカル"), dao.artistPages(sourceId, 0, "utada").page().map { it.name })
assertEquals(listOf("宇多田ヒカル"), dao.artistsByName(sourceId, 0, "ut").page().map { it.name }) assertEquals(listOf("宇多田ヒカル"), dao.artistPages(sourceId, 0, "ut").page().map { it.name })
assertEquals(listOf("宇多田ヒカル"), dao.searchArtists(sourceId, "utada", 10).first().map { it.name }) assertEquals(listOf("宇多田ヒカル"), dao.searchArtists(sourceId, "utada", 10).first().map { it.name })
assertEquals(listOf("First Love"), dao.albumsByName(sourceId, 0, "first").page().map { it.name }) assertEquals(listOf("First Love"), dao.albumPages(sourceId, 0, "first").page().map { it.name })
assertEquals(listOf("First Love"), dao.albumsByName(sourceId, 0, "fi").page().map { it.name }) assertEquals(listOf("First Love"), dao.albumPages(sourceId, 0, "fi").page().map { it.name })
assertEquals(listOf("First Love"), dao.searchAlbums(sourceId, "first", 10).first().map { it.name }) assertEquals(listOf("First Love"), dao.searchAlbums(sourceId, "first", 10).first().map { it.name })
assertEquals(listOf("Automatic"), dao.searchSongs(sourceId, "utada", 10).first().map { it.song.title }) assertEquals(listOf("Automatic"), dao.searchSongs(sourceId, "utada", 10).first().map { it.song.title })
} }
@@ -0,0 +1,53 @@
package com.subtracks.data.db
import androidx.paging.PagingSource
import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.Playlist
import com.subtracks.data.prefs.AlbumSort
import com.subtracks.data.prefs.ArtistSort
import com.subtracks.data.prefs.PlaylistSort
import com.subtracks.data.prefs.SortNameMode
internal fun LibraryDao.albumPages(
sourceId: Long,
starredFilter: Int = 0,
search: String = "",
sort: AlbumSort = AlbumSort.Name,
descending: Boolean = false,
downloaded: Boolean = false,
sortNameMode: SortNameMode = SortNameMode.SortName,
): PagingSource<Int, Album> =
if (downloaded) {
downloadedAlbumsPaging(albumsPageQuery(sourceId, starredFilter, search, sort, descending, true, sortNameMode))
} else {
albumsPaging(albumsPageQuery(sourceId, starredFilter, search, sort, descending, false, sortNameMode))
}
internal fun LibraryDao.artistPages(
sourceId: Long,
starredFilter: Int = 0,
search: String = "",
sort: ArtistSort = ArtistSort.Name,
descending: Boolean = false,
downloaded: Boolean = false,
sortNameMode: SortNameMode = SortNameMode.SortName,
): PagingSource<Int, Artist> =
if (downloaded) {
downloadedArtistsPaging(artistsPageQuery(sourceId, starredFilter, search, sort, descending, true, sortNameMode))
} else {
artistsPaging(artistsPageQuery(sourceId, starredFilter, search, sort, descending, false, sortNameMode))
}
internal fun LibraryDao.playlistPages(
sourceId: Long,
search: String = "",
sort: PlaylistSort = PlaylistSort.Name,
descending: Boolean = false,
downloaded: Boolean = false,
): PagingSource<Int, Playlist> =
if (downloaded) {
downloadedPlaylistsPaging(playlistsPageQuery(sourceId, search, sort, descending, true))
} else {
playlistsPaging(playlistsPageQuery(sourceId, search, sort, descending, false))
}
@@ -14,6 +14,9 @@ import com.subtracks.data.model.Playlist
import com.subtracks.data.model.PlaylistSong import com.subtracks.data.model.PlaylistSong
import com.subtracks.data.model.Song import com.subtracks.data.model.Song
import com.subtracks.data.model.Source import com.subtracks.data.model.Source
import com.subtracks.data.prefs.AlbumSort
import com.subtracks.data.prefs.ArtistSort
import com.subtracks.data.prefs.PlaylistSort
import com.subtracks.data.source.MusicSource import com.subtracks.data.source.MusicSource
import com.subtracks.data.source.StarType import com.subtracks.data.source.StarType
import com.subtracks.data.sync.SyncService import com.subtracks.data.sync.SyncService
@@ -110,28 +113,31 @@ class QueryTimingMeasurementTest {
private suspend fun measureReads() { private suspend fun measureReads() {
val dao = db.libraryDao() val dao = db.libraryDao()
timed("albums first page (name)") { dao.albumsByName(1, 0, "").firstPage() } timed("albums first page (name)") { dao.albumPages(1, 0, "", sort = AlbumSort.Name, downloaded = true).firstPage() }
timed("albums first page (artist)") { dao.albumsByArtist(1, 0, "").firstPage() } timed("albums first page (name, default)") { dao.albumPages(1, 0, "", sort = AlbumSort.Name).firstPage() }
timed("albums first page (year)") { dao.albumsByYear(1, 0, "").firstPage() } timed("albums first page (artist)") { dao.albumPages(1, 0, "", sort = AlbumSort.Artist, downloaded = true).firstPage() }
timed("albums first page (added)") { dao.albumsByRecentlyAdded(1, 0, "").firstPage() } timed("albums first page (year)") { dao.albumPages(1, 0, "", sort = AlbumSort.Year, downloaded = true).firstPage() }
timed("albums first page (starred)") { dao.albumsByStarred(1, 0, "").firstPage() } timed("albums first page (added)") { dao.albumPages(1, 0, "", sort = AlbumSort.Added, downloaded = true).firstPage() }
timed("albums first page (frequent)") { dao.albumsByFrequent(1, 0, "").firstPage() } timed("albums first page (starred)") { dao.albumPages(1, 0, "", sort = AlbumSort.Starred, downloaded = true).firstPage() }
timed("albums first page (recent)") { dao.albumsByRecent(1, 0, "").firstPage() } timed("albums first page (frequent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Frequent, downloaded = true).firstPage() }
timed("artists first page (name)") { dao.artistsByName(1, 0, "").firstPage() } timed("albums first page (recent)") { dao.albumPages(1, 0, "", sort = AlbumSort.Recent, downloaded = true).firstPage() }
timed("artists first page (frequent)") { dao.artistsByFrequent(1, 0, "").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("artist albums") { dao.albumsForArtist(1, "ar0").first() } timed("artist albums") { dao.albumsForArtist(1, "ar0").first() }
timed("artist name") { dao.artistName(1, "ar0").first() } timed("artist name") { dao.artistName(1, "ar0").first() }
timed("artist songs") { db.queueDao().artistSongs(1, "ar0", 0, 100) } timed("artist songs") { db.queueDao().artistSongs(1, "ar0", 0, 100) }
timed("artist length") { db.queueDao().artistLength(1, "ar0") } timed("artist length") { db.queueDao().artistLength(1, "ar0") }
timed("artist song ids") { db.queueDao().artistSongIds(1, "ar0") } timed("artist song ids") { db.queueDao().artistSongIds(1, "ar0") }
timed("downloaded artists") { dao.downloadedArtists(1).firstPage() } timed("downloaded artists") { dao.downloadedArtists(1).firstPage() }
timed("artists downloaded filter") { dao.artistsByName(1, 0, "", downloadedFilter = 1).firstPage() } timed("artists downloaded filter") { dao.artistPages(1, 0, "", downloaded = true).firstPage() }
timed("artist statuses") { db.downloadDao().artistStatuses(1).first() } timed("artist statuses") { db.downloadDao().artistStatuses(1).first() }
timed("artist status") { db.downloadDao().artistStatus(1, "ar0").first() } timed("artist status") { db.downloadDao().artistStatus(1, "ar0").first() }
timed("playlists first page (name)") { dao.playlistsByName(1, "").firstPage() } timed("playlists first page (name)") { dao.playlistPages(1, "", sort = PlaylistSort.Name, downloaded = true).firstPage() }
timed("albums deep page") { dao.albumsByName(1, 0, "").deepPage(1000) } timed("playlists first page (name, default)") { dao.playlistPages(1, "", sort = PlaylistSort.Name).firstPage() }
timed("albums search FTS") { dao.albumsByName(1, 0, "Album 1").firstPage() } timed("albums deep page") { dao.albumPages(1, 0, "", sort = AlbumSort.Name, downloaded = true).deepPage(1000) }
timed("albums search short") { dao.albumsByName(1, 0, "al").firstPage() } 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("songs search FTS") { dao.searchSongs(1, "Song", 50).first() } timed("songs search FTS") { dao.searchSongs(1, "Song", 50).first() }
timed("home recently played") { dao.recentlyPlayedAlbums(1, 20).first() } timed("home recently played") { dao.recentlyPlayedAlbums(1, 20).first() }
timed("home most played") { dao.mostPlayedAlbums(1, 20).first() } timed("home most played") { dao.mostPlayedAlbums(1, 20).first() }
@@ -10,7 +10,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.db.LoggingSQLiteDriver import com.subtracks.data.db.LoggingSQLiteDriver
import com.subtracks.data.db.SqlExecution import com.subtracks.data.db.SqlExecution
import com.subtracks.data.db.SubtracksDatabase import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.db.artistsDefaultQuery import com.subtracks.data.db.artistPages
import com.subtracks.data.db.artistsPageQuery
import com.subtracks.data.model.Album import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist import com.subtracks.data.model.Artist
import com.subtracks.data.model.ArtistRef import com.subtracks.data.model.ArtistRef
@@ -75,7 +76,7 @@ class SyncServiceTest {
2, 2,
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "") .artistPages(1, 0, "")
.allRows() .allRows()
.size, .size,
) )
@@ -98,7 +99,7 @@ class SyncServiceTest {
listOf("a1"), listOf("a1"),
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "") .artistPages(1, 0, "")
.allRows() .allRows()
.map { it.id }, .map { it.id },
) )
@@ -172,7 +173,7 @@ class SyncServiceTest {
"Old", "Old",
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "") .artistPages(1, 0, "")
.allRows() .allRows()
.single() .single()
.name, .name,
@@ -185,7 +186,7 @@ class SyncServiceTest {
"New", "New",
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "") .artistPages(1, 0, "")
.allRows() .allRows()
.single() .single()
.name, .name,
@@ -1107,16 +1108,16 @@ class SyncServiceTest {
listOf("a1"), listOf("a1"),
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "") .artistPages(1, 0, "")
.allRows() .allRows()
.map { it.id }, .map { it.id },
) )
val query = artistsDefaultQuery(1, 0, "alpha", ArtistSort.Name, false) val query = artistsPageQuery(1, 0, "alpha", ArtistSort.Name, false, false)
assertEquals( assertEquals(
listOf("a1"), listOf("a1"),
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "alpha") .artistPages(1, 0, "alpha")
.allRows() .allRows()
.map { it.id }, .map { it.id },
) )
@@ -1203,7 +1204,7 @@ class SyncServiceTest {
listOf("a1", "combo"), listOf("a1", "combo"),
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "") .artistPages(1, 0, "")
.allRows() .allRows()
.map { it.id }, .map { it.id },
) )
@@ -1216,7 +1217,7 @@ class SyncServiceTest {
listOf("a1"), listOf("a1"),
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "") .artistPages(1, 0, "")
.allRows() .allRows()
.map { it.id }, .map { it.id },
) )
@@ -1434,7 +1435,7 @@ class SyncServiceTest {
SyncService(db, source).sync() SyncService(db, source).sync()
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed)) db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed))
val filtered = db.libraryDao().artistsByName(1, 0, "", downloadedFilter = 1).allRows() val filtered = db.libraryDao().artistPages(1, 0, "", downloaded = true).allRows()
assertEquals(listOf("A", "B"), filtered.map { it.name }) assertEquals(listOf("A", "B"), filtered.map { it.name })
assertEquals(listOf(1L, 1L), filtered.map { it.albumCount }) assertEquals(listOf(1L, 1L), filtered.map { it.albumCount })
assertEquals(ListDownloadStatus(total = 1, downloaded = 1), db.downloadDao().artistStatus(1, "a2").first()) assertEquals(ListDownloadStatus(total = 1, downloaded = 1), db.downloadDao().artistStatus(1, "a2").first())
@@ -1538,7 +1539,7 @@ class SyncServiceTest {
albums.size.toLong(), albums.size.toLong(),
db db
.libraryDao() .libraryDao()
.artistsByName(1, 0, "") .artistPages(1, 0, "")
.allRows() .allRows()
.first { it.id == "a2" } .first { it.id == "a2" }
.albumCount, .albumCount,
@@ -1780,7 +1781,7 @@ class SyncServiceTest {
assertEquals("$artistId downloaded", downloaded.toLong(), status.downloaded) assertEquals("$artistId downloaded", downloaded.toLong(), status.downloaded)
} }
val downloadedArtists = db.libraryDao().artistsByName(1, 0, "", downloadedFilter = 1).allRows() val downloadedArtists = db.libraryDao().artistPages(1, 0, "", downloaded = true).allRows()
for (artist in downloadedArtists) { for (artist in downloadedArtists) {
assertEquals( assertEquals(
"${artist.id} downloaded albums", "${artist.id} downloaded albums",