perf(library): resolve album and genre file paths in one query
"Play this artist" awaited `GetAlbumTracks` inside a for loop — 13 sequential round trips for a 12-album artist — and every one of the four sites doing that asked for whole track rows to read `FilePath` off them. Five genres cost 6 MB across the IPC. `GetFilePathsByAlbums(ids, libraryID)` and `GetFilePathsByGenres(names, libraryID)` answer once and carry only the paths. Measured at 50 000 tracks: an artist 13 calls / 74.2 kB -> 2 / 19.2 kB, twenty albums 20 / 117.5 kB / 7.8 ms -> 1 / 26.0 kB / 1.7 ms, five genres 5 / 6 014 kB / 213 ms -> 1 / 1 291 kB / 32.6 ms, with the returned path lists identical. They return the paths grouped by album id or genre name rather than flattened, because the caller owns the order — an album list is sorted by name, not by id, and a flattened result would silently reorder a queue — and because the album drag cache stores them per album. A libraryID of 0 means "every library", matching an unset filter.
This commit is contained in:
@@ -295,3 +295,26 @@ LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
|
||||
LEFT JOIN file_types ft ON af.file_type_id = ft.id
|
||||
WHERE rgr.release_group_id = ? AND af.library_id = ?
|
||||
ORDER BY rgr.disc_number, rgr.track_number;
|
||||
|
||||
-- "Play this artist" and "play these albums" wanted file paths and asked
|
||||
-- for whole track rows to get them, one round trip per album (perf.m2).
|
||||
-- These answer the same question in one query and carry only what the
|
||||
-- caller uses; the release group id comes back so the caller can keep
|
||||
-- its own album ordering.
|
||||
|
||||
-- name: GetFilePathsByReleaseGroups :many
|
||||
SELECT rgr.release_group_id, af.file_path
|
||||
FROM release_group_recordings rgr
|
||||
JOIN recordings r ON rgr.recording_id = r.id
|
||||
JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE rgr.release_group_id IN (sqlc.slice('release_group_ids'))
|
||||
ORDER BY rgr.disc_number, rgr.track_number;
|
||||
|
||||
-- name: GetFilePathsByReleaseGroupsByLibrary :many
|
||||
SELECT rgr.release_group_id, af.file_path
|
||||
FROM release_group_recordings rgr
|
||||
JOIN recordings r ON rgr.recording_id = r.id
|
||||
JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE rgr.release_group_id IN (sqlc.slice('release_group_ids'))
|
||||
AND af.library_id = ?
|
||||
ORDER BY rgr.disc_number, rgr.track_number;
|
||||
|
||||
@@ -125,3 +125,26 @@ JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE af.library_id = ?
|
||||
GROUP BY g.id, g.name
|
||||
ORDER BY g.name;
|
||||
|
||||
-- Same as GetFilePathsByReleaseGroups, for "play these genres" (perf.m2):
|
||||
-- one query instead of one per genre, and file paths instead of whole
|
||||
-- track rows, which was 6 MB over the IPC for five genres.
|
||||
|
||||
-- name: GetFilePathsByGenres :many
|
||||
SELECT g.name AS genre_name, af.file_path
|
||||
FROM genres g
|
||||
JOIN recording_genres rg ON g.id = rg.genre_id
|
||||
JOIN recordings r ON rg.recording_id = r.id
|
||||
JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE g.name IN (sqlc.slice('genre_names'))
|
||||
ORDER BY r.name;
|
||||
|
||||
-- name: GetFilePathsByGenresByLibrary :many
|
||||
SELECT g.name AS genre_name, af.file_path
|
||||
FROM genres g
|
||||
JOIN recording_genres rg ON g.id = rg.genre_id
|
||||
JOIN recordings r ON rg.recording_id = r.id
|
||||
JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE g.name IN (sqlc.slice('genre_names'))
|
||||
AND af.library_id = ?
|
||||
ORDER BY r.name;
|
||||
|
||||
@@ -907,6 +907,113 @@ func (q *Queries) GetAudioFilesNeedingMetadata(ctx context.Context) ([]AudioFile
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getFilePathsByReleaseGroups = `-- name: GetFilePathsByReleaseGroups :many
|
||||
|
||||
SELECT rgr.release_group_id, af.file_path
|
||||
FROM release_group_recordings rgr
|
||||
JOIN recordings r ON rgr.recording_id = r.id
|
||||
JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE rgr.release_group_id IN (/*SLICE:release_group_ids*/?)
|
||||
ORDER BY rgr.disc_number, rgr.track_number
|
||||
`
|
||||
|
||||
type GetFilePathsByReleaseGroupsRow struct {
|
||||
ReleaseGroupID int64
|
||||
FilePath string
|
||||
}
|
||||
|
||||
// "Play this artist" and "play these albums" wanted file paths and asked
|
||||
// for whole track rows to get them, one round trip per album (perf.m2).
|
||||
// These answer the same question in one query and carry only what the
|
||||
// caller uses; the release group id comes back so the caller can keep
|
||||
// its own album ordering.
|
||||
func (q *Queries) GetFilePathsByReleaseGroups(ctx context.Context, releaseGroupIds []int64) ([]GetFilePathsByReleaseGroupsRow, error) {
|
||||
query := getFilePathsByReleaseGroups
|
||||
var queryParams []interface{}
|
||||
if len(releaseGroupIds) > 0 {
|
||||
for _, v := range releaseGroupIds {
|
||||
queryParams = append(queryParams, v)
|
||||
}
|
||||
query = strings.Replace(query, "/*SLICE:release_group_ids*/?", strings.Repeat(",?", len(releaseGroupIds))[1:], 1)
|
||||
} else {
|
||||
query = strings.Replace(query, "/*SLICE:release_group_ids*/?", "NULL", 1)
|
||||
}
|
||||
rows, err := q.db.QueryContext(ctx, query, queryParams...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFilePathsByReleaseGroupsRow
|
||||
for rows.Next() {
|
||||
var i GetFilePathsByReleaseGroupsRow
|
||||
if err := rows.Scan(&i.ReleaseGroupID, &i.FilePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getFilePathsByReleaseGroupsByLibrary = `-- name: GetFilePathsByReleaseGroupsByLibrary :many
|
||||
SELECT rgr.release_group_id, af.file_path
|
||||
FROM release_group_recordings rgr
|
||||
JOIN recordings r ON rgr.recording_id = r.id
|
||||
JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE rgr.release_group_id IN (/*SLICE:release_group_ids*/?)
|
||||
AND af.library_id = ?
|
||||
ORDER BY rgr.disc_number, rgr.track_number
|
||||
`
|
||||
|
||||
type GetFilePathsByReleaseGroupsByLibraryParams struct {
|
||||
ReleaseGroupIds []int64
|
||||
LibraryID int64
|
||||
}
|
||||
|
||||
type GetFilePathsByReleaseGroupsByLibraryRow struct {
|
||||
ReleaseGroupID int64
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func (q *Queries) GetFilePathsByReleaseGroupsByLibrary(ctx context.Context, arg GetFilePathsByReleaseGroupsByLibraryParams) ([]GetFilePathsByReleaseGroupsByLibraryRow, error) {
|
||||
query := getFilePathsByReleaseGroupsByLibrary
|
||||
var queryParams []interface{}
|
||||
if len(arg.ReleaseGroupIds) > 0 {
|
||||
for _, v := range arg.ReleaseGroupIds {
|
||||
queryParams = append(queryParams, v)
|
||||
}
|
||||
query = strings.Replace(query, "/*SLICE:release_group_ids*/?", strings.Repeat(",?", len(arg.ReleaseGroupIds))[1:], 1)
|
||||
} else {
|
||||
query = strings.Replace(query, "/*SLICE:release_group_ids*/?", "NULL", 1)
|
||||
}
|
||||
queryParams = append(queryParams, arg.LibraryID)
|
||||
rows, err := q.db.QueryContext(ctx, query, queryParams...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFilePathsByReleaseGroupsByLibraryRow
|
||||
for rows.Next() {
|
||||
var i GetFilePathsByReleaseGroupsByLibraryRow
|
||||
if err := rows.Scan(&i.ReleaseGroupID, &i.FilePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLibraryMaxModifiedAt = `-- name: GetLibraryMaxModifiedAt :one
|
||||
SELECT CAST(COALESCE(MAX(modified_at), 0) AS INTEGER) FROM audio_files
|
||||
WHERE library_id = ?
|
||||
|
||||
@@ -8,6 +8,7 @@ package sqlcgen
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const countGenreReferences = `-- name: CountGenreReferences :one
|
||||
@@ -148,6 +149,113 @@ func (q *Queries) GetAllGenresWithCountsByLibrary(ctx context.Context, libraryID
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getFilePathsByGenres = `-- name: GetFilePathsByGenres :many
|
||||
|
||||
SELECT g.name AS genre_name, af.file_path
|
||||
FROM genres g
|
||||
JOIN recording_genres rg ON g.id = rg.genre_id
|
||||
JOIN recordings r ON rg.recording_id = r.id
|
||||
JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE g.name IN (/*SLICE:genre_names*/?)
|
||||
ORDER BY r.name
|
||||
`
|
||||
|
||||
type GetFilePathsByGenresRow struct {
|
||||
GenreName string
|
||||
FilePath string
|
||||
}
|
||||
|
||||
// Same as GetFilePathsByReleaseGroups, for "play these genres" (perf.m2):
|
||||
// one query instead of one per genre, and file paths instead of whole
|
||||
// track rows, which was 6 MB over the IPC for five genres.
|
||||
func (q *Queries) GetFilePathsByGenres(ctx context.Context, genreNames []string) ([]GetFilePathsByGenresRow, error) {
|
||||
query := getFilePathsByGenres
|
||||
var queryParams []interface{}
|
||||
if len(genreNames) > 0 {
|
||||
for _, v := range genreNames {
|
||||
queryParams = append(queryParams, v)
|
||||
}
|
||||
query = strings.Replace(query, "/*SLICE:genre_names*/?", strings.Repeat(",?", len(genreNames))[1:], 1)
|
||||
} else {
|
||||
query = strings.Replace(query, "/*SLICE:genre_names*/?", "NULL", 1)
|
||||
}
|
||||
rows, err := q.db.QueryContext(ctx, query, queryParams...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFilePathsByGenresRow
|
||||
for rows.Next() {
|
||||
var i GetFilePathsByGenresRow
|
||||
if err := rows.Scan(&i.GenreName, &i.FilePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getFilePathsByGenresByLibrary = `-- name: GetFilePathsByGenresByLibrary :many
|
||||
SELECT g.name AS genre_name, af.file_path
|
||||
FROM genres g
|
||||
JOIN recording_genres rg ON g.id = rg.genre_id
|
||||
JOIN recordings r ON rg.recording_id = r.id
|
||||
JOIN audio_files af ON af.recording_id = r.id
|
||||
WHERE g.name IN (/*SLICE:genre_names*/?)
|
||||
AND af.library_id = ?
|
||||
ORDER BY r.name
|
||||
`
|
||||
|
||||
type GetFilePathsByGenresByLibraryParams struct {
|
||||
GenreNames []string
|
||||
LibraryID int64
|
||||
}
|
||||
|
||||
type GetFilePathsByGenresByLibraryRow struct {
|
||||
GenreName string
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func (q *Queries) GetFilePathsByGenresByLibrary(ctx context.Context, arg GetFilePathsByGenresByLibraryParams) ([]GetFilePathsByGenresByLibraryRow, error) {
|
||||
query := getFilePathsByGenresByLibrary
|
||||
var queryParams []interface{}
|
||||
if len(arg.GenreNames) > 0 {
|
||||
for _, v := range arg.GenreNames {
|
||||
queryParams = append(queryParams, v)
|
||||
}
|
||||
query = strings.Replace(query, "/*SLICE:genre_names*/?", strings.Repeat(",?", len(arg.GenreNames))[1:], 1)
|
||||
} else {
|
||||
query = strings.Replace(query, "/*SLICE:genre_names*/?", "NULL", 1)
|
||||
}
|
||||
queryParams = append(queryParams, arg.LibraryID)
|
||||
rows, err := q.db.QueryContext(ctx, query, queryParams...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFilePathsByGenresByLibraryRow
|
||||
for rows.Next() {
|
||||
var i GetFilePathsByGenresByLibraryRow
|
||||
if err := rows.Scan(&i.GenreName, &i.FilePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getGenresByRecordingID = `-- name: GetGenresByRecordingID :many
|
||||
SELECT g.id, g.name
|
||||
FROM genres g
|
||||
|
||||
Reference in New Issue
Block a user