-- name: UpsertAlbum :one INSERT INTO albums (title, sort_title, artist_id, release_date, mbid, cover_art_path) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (mbid) WHERE mbid IS NOT NULL DO UPDATE SET title = EXCLUDED.title, sort_title = EXCLUDED.sort_title, artist_id = EXCLUDED.artist_id, release_date = EXCLUDED.release_date, cover_art_path = EXCLUDED.cover_art_path, updated_at = now() RETURNING *; -- name: GetAlbumByID :one SELECT * FROM albums WHERE id = $1; -- name: GetAlbumWithArtist :one -- Combined fetch for /api/albums/{id}: returns the album row + the -- joined artist name in a single round trip. Replaces a sequential -- GetAlbumByID + GetArtistByID pair on the hot detail-page path. SELECT sqlc.embed(albums), artists.name AS artist_name FROM albums JOIN artists ON artists.id = albums.artist_id WHERE albums.id = $1; -- name: ListAlbumsByArtistWithTrackCount :many -- Replaces the N+1 pattern in handleGetArtist (1 + N CountTracksByAlbum -- per album). Returns each album joined with its track count via a -- correlated subquery — single round trip regardless of album count. SELECT sqlc.embed(albums), (SELECT count(*) FROM tracks t WHERE t.album_id = albums.id)::bigint AS track_count FROM albums WHERE albums.artist_id = $1 ORDER BY release_date NULLS LAST, sort_title; -- name: GetAlbumByArtistAndTitle :one -- Scanner uses this for the no-mbid dedupe path: resolve-or-create. SELECT * FROM albums WHERE artist_id = $1 AND title = $2 LIMIT 1; -- name: ListAlbumsByArtist :many SELECT * FROM albums WHERE artist_id = $1 ORDER BY release_date NULLS LAST, sort_title; -- name: ListAlbumsAlphaByName :many SELECT * FROM albums ORDER BY sort_title LIMIT $1 OFFSET $2; -- name: ListAlbumsAlphaByArtist :many -- Sorted by the owning artist's sort_name. Needed by Subsonic's -- alphabeticalByArtist album list type. SELECT sqlc.embed(albums), artists.sort_name AS artist_sort_name FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY artists.sort_name, albums.sort_title LIMIT $1 OFFSET $2; -- name: ListAlbumsNewest :many SELECT * FROM albums ORDER BY created_at DESC LIMIT $1 OFFSET $2; -- name: ListAlbumsRandom :many SELECT * FROM albums ORDER BY random() LIMIT $1; -- name: ListAlbumsByGenre :many -- Album "belongs to" a genre if any of its tracks carry that genre. SELECT DISTINCT ON (albums.id) albums.* FROM albums JOIN tracks ON tracks.album_id = albums.id WHERE tracks.genre = $1 ORDER BY albums.id, albums.sort_title LIMIT $2 OFFSET $3; -- name: SearchAlbums :many SELECT * FROM albums WHERE title ILIKE '%' || $1 || '%' ORDER BY sort_title LIMIT $2 OFFSET $3; -- name: CountAlbumsMatching :one SELECT COUNT(*) FROM albums WHERE title ILIKE '%' || $1::text || '%'; -- name: ListRecentlyAddedAlbumsWithArtist :many -- M6a: recently-added albums joined with artist_name + artist_id for the -- home-page section. created_at is the row-insert timestamp from the -- scanner — the closest proxy to "added to library". Stable secondary -- ordering by id keeps pagination tie-break deterministic. SELECT sqlc.embed(albums), artists.name AS artist_name FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY albums.created_at DESC, albums.id LIMIT $1; -- name: ListAlbumsAlphaWithArtist :many -- M6a: alpha-sorted album list joined with artist_name. Used by -- /api/library/albums for the wrapping-grid page. Stable id-tiebreak. SELECT sqlc.embed(albums), artists.name AS artist_name FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY albums.sort_title, albums.id LIMIT $1 OFFSET $2; -- name: CountAlbums :one -- M6a: total album count for the /api/library/albums envelope. SELECT COUNT(*) FROM albums; -- name: CountAlbumsByArtist :one -- Used by request-progress reporting to count how many albums have been -- ingested for a matched artist while a request is still in flight. SELECT COUNT(*) FROM albums WHERE artist_id = $1; -- name: DeleteAlbumIfEmpty :one -- M7 #372: deletes the album row only when it has no remaining tracks. -- Used after a track delete to tidy up orphaned albums. RETURNING gives -- us the artist_id so the service can chain into the artist-empty check. -- Returns no rows when the album still has tracks; the service treats -- pgx.ErrNoRows as "album not orphaned, skip cascade". DELETE FROM albums a WHERE a.id = $1 AND NOT EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id) RETURNING a.id, a.artist_id; -- name: SetAlbumMbidIfNull :exec -- M7 #379: heal MBID on existing album rows during rescan. Idempotent — -- only updates when the existing mbid is NULL. UPDATE albums SET mbid = $2, updated_at = now() WHERE id = $1 AND mbid IS NULL; -- name: ListAlbumsMissingMbidWithTrack :many -- One-shot MBID backfill: returns each album where mbid IS NULL alongside -- one of its tracks' file_path so the worker can re-read tags. LIMIT -- supplied by caller for batching/progress purposes. SELECT a.id AS album_id, a.artist_id AS artist_id, a.title AS title, t.file_path AS track_file_path FROM albums a JOIN LATERAL ( SELECT file_path FROM tracks WHERE album_id = a.id ORDER BY disc_number NULLS LAST, track_number NULLS LAST, id LIMIT 1 ) t ON true WHERE a.mbid IS NULL ORDER BY a.created_at ASC LIMIT $1; -- name: GetAlbumCoverageRollup :one -- M7 coverage-gauge: library-wide cover-art coverage rollup for the -- admin gauge. Single sequential scan with FILTER aggregates — -- sub-millisecond on realistic libraries. pending_no_mbid is a SUBSET -- of pending; the UI surfaces it separately so operators can see how -- many "pending" albums are blocked on missing MBID and won't be -- helped by another scan. -- -- Invariant: with_art + pending + settled = total. (pending_no_mbid is -- not part of the sum — it's a subset of pending, surfaced separately.) -- -- The IN list below ('sidecar','embedded','mbcaa','theaudiodb') must stay in sync -- with the cover_art_source CHECK constraint in migration -- 0016_album_cover_source.up.sql. If a new source value is added there -- without updating this query, with_art will silently undercount. SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE cover_art_source IN ('sidecar','embedded','mbcaa','theaudiodb')) AS with_art, COUNT(*) FILTER (WHERE cover_art_source IS NULL) AS pending, COUNT(*) FILTER (WHERE cover_art_source = 'none') AS settled, COUNT(*) FILTER (WHERE cover_art_source IS NULL AND mbid IS NULL) AS pending_no_mbid FROM albums; -- name: SetAlbumCoverWithVersion :exec -- M7 cover-sources: records a successful or settled enrichment with the -- current sources_version stamp. cover_art_path is NULL when source is -- 'none' (no file written). Atomic; called per album per pass. UPDATE albums SET cover_art_path = $2, cover_art_source = $3, cover_art_sources_version = $4, updated_at = now() WHERE id = $1; -- name: GetAlbumsByIDs :many -- Batched lookup used by /api/library/sync to hydrate upsert payloads -- (#357). Mirror of GetArtistsByIDs. SELECT * FROM albums WHERE id = ANY($1::uuid[]);