The release-group MBID backfill queried `release_groups`, which plan 013
renamed to `albums`. It failed on its first statement on every launch
since e7748f1 and the pass returned quietly having done nothing:
W/yellowjacket: msg="release-group mbid backfill: query failed"
explore.error="SQL logic error: no such table: release_groups (1)"
What it does is resolve a release-level MBID (MUSICBRAINZ_ALBUMID, which
many taggers write instead of MUSICBRAINZ_RELEASEGROUPID) into the
release-group MBID everything else on the album page is keyed by. A scan
cannot afford a live MusicBrainz call, so `library.updateMBIDs` stashes
the release MBID in `pending_release_mbid` and defers to this. With this
broken the marker was written by every scan and resolved by nothing, so
those albums were untagged as far as the catalog is concerned,
permanently.
**The fix is to call the queries plan 013 already wrote.**
`GetAlbumsWithPendingReleaseMBID` and `ResolveAlbumPendingReleaseMBID`
have been in sql/queries/albums.sql since that change, generated and
never called -- the writer of the marker was repointed at `albums` and
the reader was not. So this is not a missed rename so much as a call
site left behind, and thirty lines of raw SQL and hand-rolled scanning
become three.
That is also the durable half. These two were the last raw-SQL
references to a schema table in the tree, and being raw is exactly why
013 missed them: sqlc reads sql/schemas/ and cannot generate against a
table that is not declared, which is what made every other statement in
the repo immune to the same rename.
Three smaller things.
**The LIMIT came back.** The raw statement bounded a run at
releaseGroupMBIDBackfillMaxPerRun and 013's sqlc replacement had no
LIMIT at all, so switching over as-written would have swapped a dead
pass for an unbounded one -- each row is a live MusicBrainz lookup on a
1 req/s limiter shared with every page the user can open.
**The UPDATE goes through the writer.** `ReadQueries` is a query-only
pool and an UPDATE issued on it fails at runtime with "attempt to write
a readonly database".
**The query is its own method so its failure is assertable.**
A test of the pass as a whole cannot see this bug, because a query
error and an empty library are the same early return -- which is the
whole reason it survived. `pendingReleaseMBIDs` returns the error, and
the test reproduces the device's exact message against the old
statement.
Verified on the reference device: the warning is gone from logcat.
Closes #189
521 lines
15 KiB
Go
521 lines
15 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: albums.sql
|
|
|
|
package sqlcgen
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"strings"
|
|
)
|
|
|
|
const deleteAlbum = `-- name: DeleteAlbum :exec
|
|
DELETE FROM albums WHERE id = ?
|
|
`
|
|
|
|
func (q *Queries) DeleteAlbum(ctx context.Context, id int64) error {
|
|
_, err := q.db.ExecContext(ctx, deleteAlbum, id)
|
|
return err
|
|
}
|
|
|
|
const deleteAllAlbums = `-- name: DeleteAllAlbums :exec
|
|
DELETE FROM albums
|
|
`
|
|
|
|
func (q *Queries) DeleteAllAlbums(ctx context.Context) error {
|
|
_, err := q.db.ExecContext(ctx, deleteAllAlbums)
|
|
return err
|
|
}
|
|
|
|
const getAlbum = `-- name: GetAlbum :one
|
|
SELECT id, name, artist_credit, artist_id, mbid, year, original_year, cover_art_id, pending_release_mbid FROM albums WHERE id = ? LIMIT 1
|
|
`
|
|
|
|
func (q *Queries) GetAlbum(ctx context.Context, id int64) (Album, error) {
|
|
row := q.db.QueryRowContext(ctx, getAlbum, id)
|
|
var i Album
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.ArtistCredit,
|
|
&i.ArtistID,
|
|
&i.Mbid,
|
|
&i.Year,
|
|
&i.OriginalYear,
|
|
&i.CoverArtID,
|
|
&i.PendingReleaseMbid,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAlbumCompleteness = `-- name: GetAlbumCompleteness :one
|
|
SELECT
|
|
-- Distinct (disc, track) pairs: this app detects duplicates, and
|
|
-- counting two files of track 3 twice would report a short album as
|
|
-- complete. A file with no track number falls back to its own id,
|
|
-- because three untagged files are three tracks, not one.
|
|
CAST(COUNT(DISTINCT CAST(COALESCE(a.disc_number, 1) AS TEXT) || ':' ||
|
|
COALESCE(CAST(a.track_number AS TEXT), 'f' || a.id)
|
|
) AS INTEGER) AS owned,
|
|
CAST(COALESCE((
|
|
SELECT SUM(per_disc.total)
|
|
FROM (
|
|
SELECT MAX(b.total_tracks) AS total
|
|
FROM audio_files b
|
|
WHERE b.album_id = ?1 AND b.total_tracks IS NOT NULL
|
|
GROUP BY COALESCE(b.disc_number, 1)
|
|
) per_disc
|
|
), 0) AS INTEGER) AS expected,
|
|
CAST((
|
|
SELECT COUNT(*) = 0 FROM audio_files c
|
|
WHERE c.album_id = ?1 AND c.total_tracks IS NULL
|
|
) AS INTEGER) AS known
|
|
FROM audio_files a
|
|
WHERE a.album_id = ?1
|
|
`
|
|
|
|
type GetAlbumCompletenessRow struct {
|
|
Owned int64
|
|
Expected int64
|
|
Known int64
|
|
}
|
|
|
|
// "Do I have all of this album", answered from the tags on disk.
|
|
//
|
|
// The expectation is a **sum over discs**, not one number: totals are
|
|
// declared per disc ("5/12" on disc 2 means 12 tracks on disc 2), so a
|
|
// multi-disc album's expectation is the sum of each disc's declared
|
|
// total. A disc whose files declared nothing leaves the whole album
|
|
// unknowable rather than being covered by the discs that did -- which is
|
|
// what `known` reports.
|
|
//
|
|
// Owned counts DISTINCT track numbers: this app detects duplicates, and
|
|
// counting two files of track 3 twice would report a short album as
|
|
// complete.
|
|
func (q *Queries) GetAlbumCompleteness(ctx context.Context, albumID sql.NullInt64) (GetAlbumCompletenessRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getAlbumCompleteness, albumID)
|
|
var i GetAlbumCompletenessRow
|
|
err := row.Scan(&i.Owned, &i.Expected, &i.Known)
|
|
return i, err
|
|
}
|
|
|
|
const getAlbums = `-- name: GetAlbums :many
|
|
SELECT
|
|
al.id,
|
|
al.name,
|
|
COALESCE(al.original_year, al.year) AS year,
|
|
COALESCE(al.year, 0) AS release_year,
|
|
al.mbid,
|
|
al.artist_credit AS artist_name,
|
|
CAST(COALESCE(ar.mbid, '') AS TEXT) AS artist_mbid,
|
|
COALESCE(ca.file_path, '') AS cover_art_path
|
|
FROM albums al
|
|
LEFT JOIN artists ar ON ar.id = al.artist_id
|
|
LEFT JOIN cover_art ca ON ca.id = al.cover_art_id
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM audio_files af
|
|
WHERE af.album_id = al.id
|
|
AND af.library_id = COALESCE(NULLIF(CAST(?1 AS INTEGER), 0), af.library_id)
|
|
)
|
|
ORDER BY al.name
|
|
`
|
|
|
|
type GetAlbumsRow struct {
|
|
ID int64
|
|
Name string
|
|
Year sql.NullInt64
|
|
ReleaseYear int64
|
|
Mbid sql.NullString
|
|
ArtistName string
|
|
ArtistMbid string
|
|
CoverArtPath string
|
|
}
|
|
|
|
func (q *Queries) GetAlbums(ctx context.Context, libraryID int64) ([]GetAlbumsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getAlbums, libraryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetAlbumsRow
|
|
for rows.Next() {
|
|
var i GetAlbumsRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Year,
|
|
&i.ReleaseYear,
|
|
&i.Mbid,
|
|
&i.ArtistName,
|
|
&i.ArtistMbid,
|
|
&i.CoverArtPath,
|
|
); 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 getAlbumsByArtistName = `-- name: GetAlbumsByArtistName :many
|
|
SELECT
|
|
al.id,
|
|
al.name,
|
|
COALESCE(al.original_year, al.year) AS year,
|
|
COALESCE(al.year, 0) AS release_year,
|
|
al.mbid,
|
|
al.artist_credit AS artist_name,
|
|
CAST(COALESCE(ar.mbid, '') AS TEXT) AS artist_mbid,
|
|
COALESCE(ca.file_path, '') AS cover_art_path
|
|
FROM albums al
|
|
LEFT JOIN artists ar ON ar.id = al.artist_id
|
|
LEFT JOIN cover_art ca ON ca.id = al.cover_art_id
|
|
WHERE (al.artist_credit = ?1 OR ar.name = ?1)
|
|
AND EXISTS (
|
|
SELECT 1 FROM audio_files af
|
|
WHERE af.album_id = al.id
|
|
AND af.library_id = COALESCE(NULLIF(CAST(?2 AS INTEGER), 0), af.library_id)
|
|
)
|
|
ORDER BY year, al.name
|
|
`
|
|
|
|
type GetAlbumsByArtistNameParams struct {
|
|
Artist string
|
|
LibraryID int64
|
|
}
|
|
|
|
type GetAlbumsByArtistNameRow struct {
|
|
ID int64
|
|
Name string
|
|
Year sql.NullInt64
|
|
ReleaseYear int64
|
|
Mbid sql.NullString
|
|
ArtistName string
|
|
ArtistMbid string
|
|
CoverArtPath string
|
|
}
|
|
|
|
func (q *Queries) GetAlbumsByArtistName(ctx context.Context, arg GetAlbumsByArtistNameParams) ([]GetAlbumsByArtistNameRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getAlbumsByArtistName, arg.Artist, arg.LibraryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetAlbumsByArtistNameRow
|
|
for rows.Next() {
|
|
var i GetAlbumsByArtistNameRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Year,
|
|
&i.ReleaseYear,
|
|
&i.Mbid,
|
|
&i.ArtistName,
|
|
&i.ArtistMbid,
|
|
&i.CoverArtPath,
|
|
); 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 getAlbumsCompleteness = `-- name: GetAlbumsCompleteness :many
|
|
WITH per_disc AS (
|
|
SELECT
|
|
album_id AS album_id,
|
|
COUNT(DISTINCT COALESCE(CAST(track_number AS TEXT), 'f' || id))
|
|
AS owned_on_disc,
|
|
MAX(total_tracks) AS disc_total,
|
|
SUM(CASE WHEN total_tracks IS NULL THEN 1 ELSE 0 END)
|
|
AS discs_without_a_total
|
|
FROM audio_files
|
|
WHERE album_id IN (/*SLICE:album_ids*/?)
|
|
GROUP BY album_id, COALESCE(disc_number, 1)
|
|
)
|
|
SELECT
|
|
CAST(album_id AS INTEGER) AS album_id,
|
|
CAST(SUM(owned_on_disc) AS INTEGER) AS owned,
|
|
CAST(COALESCE(SUM(disc_total), 0) AS INTEGER) AS expected,
|
|
CAST(SUM(discs_without_a_total) = 0 AS INTEGER) AS known
|
|
FROM per_disc
|
|
GROUP BY album_id
|
|
`
|
|
|
|
type GetAlbumsCompletenessRow struct {
|
|
AlbumID int64
|
|
Owned int64
|
|
Expected int64
|
|
Known int64
|
|
}
|
|
|
|
// The same question as GetAlbumCompleteness, asked of a screenful of
|
|
// albums at once.
|
|
//
|
|
// A card grid cannot afford one query per card, and the answer it wants
|
|
// is the one thing a badge cannot guess: an album held 9 tracks of 12
|
|
// must show the count, never a bare tick. So this is one query for the
|
|
// whole grid, asked only of the cards that have a local album id.
|
|
//
|
|
// It is two grouping levels rather than the single-album form's
|
|
// correlated subqueries, because a correlated subquery in the FROM
|
|
// clause is not something SQLite will reliably do -- and because the
|
|
// slice may only be spelled once, or sqlc expands it twice with
|
|
// independently numbered placeholders.
|
|
//
|
|
// The per-disc level is where the meaning is, and it is the same
|
|
// meaning as the single-album query. `owned` counts DISTINCT track
|
|
// numbers within a disc (this app detects duplicates, and counting two
|
|
// files of track 3 twice would report a short album as complete), with
|
|
// a file that declares no track number falling back to its own id
|
|
// because three untagged files are three tracks and not one.
|
|
// `expected` takes each disc's declared total and sums over discs,
|
|
// since a total is declared per disc and a release total written on
|
|
// every file of a two-disc album would double its expectation. A disc
|
|
// whose files declared nothing contributes a NULL that SUM ignores,
|
|
// and `known` is what says the album is therefore unanswerable.
|
|
func (q *Queries) GetAlbumsCompleteness(ctx context.Context, albumIds []sql.NullInt64) ([]GetAlbumsCompletenessRow, error) {
|
|
query := getAlbumsCompleteness
|
|
var queryParams []interface{}
|
|
if len(albumIds) > 0 {
|
|
for _, v := range albumIds {
|
|
queryParams = append(queryParams, v)
|
|
}
|
|
query = strings.Replace(query, "/*SLICE:album_ids*/?", strings.Repeat(",?", len(albumIds))[1:], 1)
|
|
} else {
|
|
query = strings.Replace(query, "/*SLICE:album_ids*/?", "NULL", 1)
|
|
}
|
|
rows, err := q.db.QueryContext(ctx, query, queryParams...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetAlbumsCompletenessRow
|
|
for rows.Next() {
|
|
var i GetAlbumsCompletenessRow
|
|
if err := rows.Scan(
|
|
&i.AlbumID,
|
|
&i.Owned,
|
|
&i.Expected,
|
|
&i.Known,
|
|
); 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 getAlbumsWithPendingReleaseMBID = `-- name: GetAlbumsWithPendingReleaseMBID :many
|
|
SELECT id, pending_release_mbid FROM albums
|
|
WHERE pending_release_mbid IS NOT NULL AND pending_release_mbid != ''
|
|
AND (mbid IS NULL OR mbid = '')
|
|
LIMIT ?
|
|
`
|
|
|
|
type GetAlbumsWithPendingReleaseMBIDRow struct {
|
|
ID int64
|
|
PendingReleaseMbid sql.NullString
|
|
}
|
|
|
|
func (q *Queries) GetAlbumsWithPendingReleaseMBID(ctx context.Context, limit int64) ([]GetAlbumsWithPendingReleaseMBIDRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getAlbumsWithPendingReleaseMBID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []GetAlbumsWithPendingReleaseMBIDRow
|
|
for rows.Next() {
|
|
var i GetAlbumsWithPendingReleaseMBIDRow
|
|
if err := rows.Scan(&i.ID, &i.PendingReleaseMbid); 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 getEmptyAlbumIDs = `-- name: GetEmptyAlbumIDs :many
|
|
SELECT id FROM albums al
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM audio_files af WHERE af.album_id = al.id
|
|
)
|
|
`
|
|
|
|
// Albums with no file left behind them. Under the old schema this was
|
|
// one of three orphan sweeps that had to run by hand and did not;
|
|
// audio_files is the only thing that can leave an album empty now, so
|
|
// this is the whole of it.
|
|
func (q *Queries) GetEmptyAlbumIDs(ctx context.Context) ([]int64, error) {
|
|
rows, err := q.db.QueryContext(ctx, getEmptyAlbumIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []int64
|
|
for rows.Next() {
|
|
var id int64
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const resolveAlbumPendingReleaseMBID = `-- name: ResolveAlbumPendingReleaseMBID :exec
|
|
UPDATE albums
|
|
SET mbid = ?, pending_release_mbid = NULL
|
|
WHERE id = ? AND (mbid IS NULL OR mbid = '')
|
|
`
|
|
|
|
type ResolveAlbumPendingReleaseMBIDParams struct {
|
|
Mbid sql.NullString
|
|
ID int64
|
|
}
|
|
|
|
// Clears the pending marker once the release-group MBID it stood in for
|
|
// has been resolved. Guarded so a real MBID is never overwritten.
|
|
func (q *Queries) ResolveAlbumPendingReleaseMBID(ctx context.Context, arg ResolveAlbumPendingReleaseMBIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, resolveAlbumPendingReleaseMBID, arg.Mbid, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const setAlbumCoverArt = `-- name: SetAlbumCoverArt :exec
|
|
UPDATE albums SET cover_art_id = ? WHERE id = ?
|
|
`
|
|
|
|
type SetAlbumCoverArtParams struct {
|
|
CoverArtID sql.NullInt64
|
|
ID int64
|
|
}
|
|
|
|
func (q *Queries) SetAlbumCoverArt(ctx context.Context, arg SetAlbumCoverArtParams) error {
|
|
_, err := q.db.ExecContext(ctx, setAlbumCoverArt, arg.CoverArtID, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const setAlbumMBID = `-- name: SetAlbumMBID :exec
|
|
UPDATE albums SET mbid = ? WHERE id = ?
|
|
`
|
|
|
|
type SetAlbumMBIDParams struct {
|
|
Mbid sql.NullString
|
|
ID int64
|
|
}
|
|
|
|
func (q *Queries) SetAlbumMBID(ctx context.Context, arg SetAlbumMBIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, setAlbumMBID, arg.Mbid, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const setAlbumOriginalYear = `-- name: SetAlbumOriginalYear :exec
|
|
UPDATE albums SET original_year = ? WHERE id = ?
|
|
`
|
|
|
|
type SetAlbumOriginalYearParams struct {
|
|
OriginalYear sql.NullInt64
|
|
ID int64
|
|
}
|
|
|
|
func (q *Queries) SetAlbumOriginalYear(ctx context.Context, arg SetAlbumOriginalYearParams) error {
|
|
_, err := q.db.ExecContext(ctx, setAlbumOriginalYear, arg.OriginalYear, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const setAlbumPendingReleaseMBID = `-- name: SetAlbumPendingReleaseMBID :exec
|
|
UPDATE albums SET pending_release_mbid = ? WHERE id = ?
|
|
`
|
|
|
|
type SetAlbumPendingReleaseMBIDParams struct {
|
|
PendingReleaseMbid sql.NullString
|
|
ID int64
|
|
}
|
|
|
|
func (q *Queries) SetAlbumPendingReleaseMBID(ctx context.Context, arg SetAlbumPendingReleaseMBIDParams) error {
|
|
_, err := q.db.ExecContext(ctx, setAlbumPendingReleaseMBID, arg.PendingReleaseMbid, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const upsertAlbum = `-- name: UpsertAlbum :one
|
|
|
|
INSERT INTO albums (name, artist_credit, artist_id, year, cover_art_id)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(name, artist_credit) DO UPDATE SET
|
|
artist_id = COALESCE(excluded.artist_id, albums.artist_id),
|
|
year = COALESCE(excluded.year, albums.year),
|
|
cover_art_id = COALESCE(excluded.cover_art_id, albums.cover_art_id)
|
|
RETURNING id, name, artist_credit, artist_id, mbid, year, original_year, cover_art_id, pending_release_mbid
|
|
`
|
|
|
|
type UpsertAlbumParams struct {
|
|
Name string
|
|
ArtistCredit string
|
|
ArtistID sql.NullInt64
|
|
Year sql.NullInt64
|
|
CoverArtID sql.NullInt64
|
|
}
|
|
|
|
// Queries over albums (formerly release_groups).
|
|
//
|
|
// The two-copy pattern is gone here too: one query answers both the
|
|
// whole-library and the single-library case. The `fallback_ac`
|
|
// subquery every album read used to carry -- "if the album has no album
|
|
// artist credit, borrow one from any of its recordings" -- is gone with
|
|
// it, because the album carries its own credit text now.
|
|
func (q *Queries) UpsertAlbum(ctx context.Context, arg UpsertAlbumParams) (Album, error) {
|
|
row := q.db.QueryRowContext(ctx, upsertAlbum,
|
|
arg.Name,
|
|
arg.ArtistCredit,
|
|
arg.ArtistID,
|
|
arg.Year,
|
|
arg.CoverArtID,
|
|
)
|
|
var i Album
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.ArtistCredit,
|
|
&i.ArtistID,
|
|
&i.Mbid,
|
|
&i.Year,
|
|
&i.OriginalYear,
|
|
&i.CoverArtID,
|
|
&i.PendingReleaseMbid,
|
|
)
|
|
return i, err
|
|
}
|