From 71a9bd8deef712d7dc247d61a68f8b3bb950c97f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 30 Apr 2026 16:54:03 -0400 Subject: [PATCH] fix(db): drop redundant quarantine user index + tighten ListQuarantineForUser - Composite PK (user_id, track_id) already serves WHERE user_id queries; the secondary (user_id, created_at DESC) index just amplified writes. - ListQuarantineForUser now selects only the joined fields the SPA card actually renders (~10 columns) rather than embedding three full structs (~35 columns); halves wire/DB bandwidth before consumers exist. - max(q.created_at)::timestamptz cast emits pgtype.Timestamptz instead of interface{} so handlers can read latest_at without a type-assert. --- internal/db/dbq/lidarr_quarantine.sql.go | 74 ++++++++----------- .../0011_lidarr_quarantine.down.sql | 1 - .../migrations/0011_lidarr_quarantine.up.sql | 5 +- internal/db/queries/lidarr_quarantine.sql | 21 ++++-- 4 files changed, 50 insertions(+), 51 deletions(-) diff --git a/internal/db/dbq/lidarr_quarantine.sql.go b/internal/db/dbq/lidarr_quarantine.sql.go index 5942194f..eac2ac6d 100644 --- a/internal/db/dbq/lidarr_quarantine.sql.go +++ b/internal/db/dbq/lidarr_quarantine.sql.go @@ -69,8 +69,8 @@ SELECT al.title AS album_title, al.id AS album_id, al.mbid AS lidarr_album_mbid, - count(q.user_id)::int AS report_count, - max(q.created_at) AS latest_at + count(q.user_id)::int AS report_count, + max(q.created_at)::timestamptz AS latest_at FROM lidarr_quarantine q JOIN tracks t ON t.id = q.track_id JOIN albums al ON al.id = t.album_id @@ -87,7 +87,7 @@ type ListAdminQuarantineQueueRow struct { AlbumID pgtype.UUID LidarrAlbumMbid *string ReportCount int32 - LatestAt interface{} + LatestAt pgtype.Timestamptz } // Aggregated admin queue. One row per track. The handler post-processes @@ -162,9 +162,14 @@ func (q *Queries) ListQuarantineActions(ctx context.Context, limit int32) ([]Lid const listQuarantineForUser = `-- name: ListQuarantineForUser :many SELECT q.user_id, q.track_id, q.reason, q.notes, q.created_at, - t.id, t.title, t.album_id, t.artist_id, t.track_number, t.disc_number, t.duration_ms, t.file_path, t.file_size, t.file_format, t.bitrate, t.mbid, t.genre, t.added_at, t.updated_at, - al.id, al.title, al.sort_title, al.artist_id, al.release_date, al.mbid, al.cover_art_path, al.created_at, al.updated_at, - ar.id, ar.name, ar.sort_name, ar.mbid, ar.created_at, ar.updated_at + t.id AS track_id_join, + t.title AS track_title, + t.duration_ms AS track_duration_ms, + al.id AS album_id, + al.title AS album_title, + al.cover_art_path AS album_cover_art_path, + ar.id AS artist_id, + ar.name AS artist_name FROM lidarr_quarantine q JOIN tracks t ON t.id = q.track_id JOIN albums al ON al.id = t.album_id @@ -174,14 +179,21 @@ ORDER BY q.created_at DESC ` type ListQuarantineForUserRow struct { - LidarrQuarantine LidarrQuarantine - Track Track - Album Album - Artist Artist + LidarrQuarantine LidarrQuarantine + TrackIDJoin pgtype.UUID + TrackTitle string + TrackDurationMs int32 + AlbumID pgtype.UUID + AlbumTitle string + AlbumCoverArtPath *string + ArtistID pgtype.UUID + ArtistName string } -// Caller's own quarantines joined with track + album + artist for full -// detail. Drives /library/hidden. +// Caller's own quarantines plus just the joined fields the SPA's +// /library/hidden actually renders. Embedding the full tracks/albums/artists +// structs would drag ~28 columns of file_path / sort_* / mbid / etc. that +// the row card never uses. func (q *Queries) ListQuarantineForUser(ctx context.Context, userID pgtype.UUID) ([]ListQuarantineForUserRow, error) { rows, err := q.db.Query(ctx, listQuarantineForUser, userID) if err != nil { @@ -197,36 +209,14 @@ func (q *Queries) ListQuarantineForUser(ctx context.Context, userID pgtype.UUID) &i.LidarrQuarantine.Reason, &i.LidarrQuarantine.Notes, &i.LidarrQuarantine.CreatedAt, - &i.Track.ID, - &i.Track.Title, - &i.Track.AlbumID, - &i.Track.ArtistID, - &i.Track.TrackNumber, - &i.Track.DiscNumber, - &i.Track.DurationMs, - &i.Track.FilePath, - &i.Track.FileSize, - &i.Track.FileFormat, - &i.Track.Bitrate, - &i.Track.Mbid, - &i.Track.Genre, - &i.Track.AddedAt, - &i.Track.UpdatedAt, - &i.Album.ID, - &i.Album.Title, - &i.Album.SortTitle, - &i.Album.ArtistID, - &i.Album.ReleaseDate, - &i.Album.Mbid, - &i.Album.CoverArtPath, - &i.Album.CreatedAt, - &i.Album.UpdatedAt, - &i.Artist.ID, - &i.Artist.Name, - &i.Artist.SortName, - &i.Artist.Mbid, - &i.Artist.CreatedAt, - &i.Artist.UpdatedAt, + &i.TrackIDJoin, + &i.TrackTitle, + &i.TrackDurationMs, + &i.AlbumID, + &i.AlbumTitle, + &i.AlbumCoverArtPath, + &i.ArtistID, + &i.ArtistName, ); err != nil { return nil, err } diff --git a/internal/db/migrations/0011_lidarr_quarantine.down.sql b/internal/db/migrations/0011_lidarr_quarantine.down.sql index beaf999f..bd086c54 100644 --- a/internal/db/migrations/0011_lidarr_quarantine.down.sql +++ b/internal/db/migrations/0011_lidarr_quarantine.down.sql @@ -2,7 +2,6 @@ DROP INDEX IF EXISTS lidarr_quarantine_actions_created_idx; DROP INDEX IF EXISTS lidarr_quarantine_actions_track_idx; DROP TABLE IF EXISTS lidarr_quarantine_actions; DROP TYPE IF EXISTS lidarr_quarantine_action_kind; -DROP INDEX IF EXISTS lidarr_quarantine_user_idx; DROP INDEX IF EXISTS lidarr_quarantine_track_idx; DROP TABLE IF EXISTS lidarr_quarantine; DROP TYPE IF EXISTS lidarr_quarantine_reason; diff --git a/internal/db/migrations/0011_lidarr_quarantine.up.sql b/internal/db/migrations/0011_lidarr_quarantine.up.sql index ff4c177e..8ff73259 100644 --- a/internal/db/migrations/0011_lidarr_quarantine.up.sql +++ b/internal/db/migrations/0011_lidarr_quarantine.up.sql @@ -21,8 +21,11 @@ CREATE TABLE lidarr_quarantine ( PRIMARY KEY (user_id, track_id) ); +-- Only the track_id index is non-redundant: the composite PK +-- (user_id, track_id) already serves WHERE user_id = $1 lookups, so a +-- separate (user_id, created_at DESC) index would just amplify writes +-- without paying for itself at household-scale row counts. CREATE INDEX lidarr_quarantine_track_idx ON lidarr_quarantine (track_id); -CREATE INDEX lidarr_quarantine_user_idx ON lidarr_quarantine (user_id, created_at DESC); CREATE TYPE lidarr_quarantine_action_kind AS ENUM ( 'resolved', 'deleted_file', 'deleted_via_lidarr' diff --git a/internal/db/queries/lidarr_quarantine.sql b/internal/db/queries/lidarr_quarantine.sql index 7e4c54ed..8c43fc9c 100644 --- a/internal/db/queries/lidarr_quarantine.sql +++ b/internal/db/queries/lidarr_quarantine.sql @@ -17,13 +17,20 @@ DELETE FROM lidarr_quarantine RETURNING user_id, track_id, reason, notes, created_at; -- name: ListQuarantineForUser :many --- Caller's own quarantines joined with track + album + artist for full --- detail. Drives /library/hidden. +-- Caller's own quarantines plus just the joined fields the SPA's +-- /library/hidden actually renders. Embedding the full tracks/albums/artists +-- structs would drag ~28 columns of file_path / sort_* / mbid / etc. that +-- the row card never uses. SELECT sqlc.embed(q), - sqlc.embed(t), - sqlc.embed(al), - sqlc.embed(ar) + t.id AS track_id_join, + t.title AS track_title, + t.duration_ms AS track_duration_ms, + al.id AS album_id, + al.title AS album_title, + al.cover_art_path AS album_cover_art_path, + ar.id AS artist_id, + ar.name AS artist_name FROM lidarr_quarantine q JOIN tracks t ON t.id = q.track_id JOIN albums al ON al.id = t.album_id @@ -42,8 +49,8 @@ SELECT al.title AS album_title, al.id AS album_id, al.mbid AS lidarr_album_mbid, - count(q.user_id)::int AS report_count, - max(q.created_at) AS latest_at + count(q.user_id)::int AS report_count, + max(q.created_at)::timestamptz AS latest_at FROM lidarr_quarantine q JOIN tracks t ON t.id = q.track_id JOIN albums al ON al.id = t.album_id