fix(playlists): unblock Discover by removing SELECT DISTINCT + ORDER BY plan error
The cross-user bucket query combined SELECT DISTINCT with ORDER BY md5(...), which Postgres rejects at plan time (SQLSTATE 42P10). buildDiscoverCandidates returned that error on first bucket failure, so the whole Discover playlist was skipped every nightly run — even though the random bucket pool was healthy. Switched to GROUP BY so the md5 ordering expression no longer needs to appear in the select list, and hardened the function so future single- bucket failures degrade gracefully via slot redistribution instead of taking out the whole playlist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
const listCrossUserLikedTracksForDiscover = `-- name: ListCrossUserLikedTracksForDiscover :many
|
||||
SELECT DISTINCT t.id, t.album_id, t.artist_id
|
||||
SELECT t.id, t.album_id, t.artist_id
|
||||
FROM general_likes gl
|
||||
JOIN tracks t ON t.id = gl.track_id
|
||||
WHERE gl.user_id != $1
|
||||
@@ -30,6 +30,7 @@ SELECT DISTINCT t.id, t.album_id, t.artist_id
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
GROUP BY t.id, t.album_id, t.artist_id
|
||||
ORDER BY md5(t.id::text || $2::text)
|
||||
LIMIT 60
|
||||
`
|
||||
@@ -50,6 +51,11 @@ type ListCrossUserLikedTracksForDiscoverRow struct {
|
||||
// rows (the caller's slot redistribution rolls the deficit into the
|
||||
// other two buckets).
|
||||
// $1 = user_id, $2 = date string for md5 ordering.
|
||||
//
|
||||
// GROUP BY (not SELECT DISTINCT) so the md5 ORDER BY expression need
|
||||
// not appear in the select list — Postgres rejects DISTINCT + ORDER BY
|
||||
// by-expression at plan time (SQLSTATE 42P10), which previously caused
|
||||
// the entire Discover build to fail silently.
|
||||
func (q *Queries) ListCrossUserLikedTracksForDiscover(ctx context.Context, arg ListCrossUserLikedTracksForDiscoverParams) ([]ListCrossUserLikedTracksForDiscoverRow, error) {
|
||||
rows, err := q.db.Query(ctx, listCrossUserLikedTracksForDiscover, arg.UserID, arg.Column2)
|
||||
if err != nil {
|
||||
|
||||
@@ -52,7 +52,12 @@ SELECT t.id, t.album_id, t.artist_id
|
||||
-- rows (the caller's slot redistribution rolls the deficit into the
|
||||
-- other two buckets).
|
||||
-- $1 = user_id, $2 = date string for md5 ordering.
|
||||
SELECT DISTINCT t.id, t.album_id, t.artist_id
|
||||
--
|
||||
-- GROUP BY (not SELECT DISTINCT) so the md5 ORDER BY expression need
|
||||
-- not appear in the select list — Postgres rejects DISTINCT + ORDER BY
|
||||
-- by-expression at plan time (SQLSTATE 42P10), which previously caused
|
||||
-- the entire Discover build to fail silently.
|
||||
SELECT t.id, t.album_id, t.artist_id
|
||||
FROM general_likes gl
|
||||
JOIN tracks t ON t.id = gl.track_id
|
||||
WHERE gl.user_id != $1
|
||||
@@ -70,6 +75,7 @@ SELECT DISTINCT t.id, t.album_id, t.artist_id
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
GROUP BY t.id, t.album_id, t.artist_id
|
||||
ORDER BY md5(t.id::text || $2::text)
|
||||
LIMIT 60;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package playlists
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
@@ -34,24 +34,34 @@ type discoverTrack struct {
|
||||
// Returns up to discoverTotalSlots track IDs in the order they should
|
||||
// appear in the playlist (round-robin interleaved across buckets).
|
||||
// Empty slice when the library has no eligible tracks at all.
|
||||
func buildDiscoverCandidates(ctx context.Context, q *dbq.Queries, userID pgtype.UUID, dateStr string) ([]rankedCandidate, error) {
|
||||
//
|
||||
// Individual bucket query failures are logged and treated as empty —
|
||||
// the redistribution algorithm rolls the deficit into the surviving
|
||||
// buckets so one broken bucket can't silently kill the whole playlist.
|
||||
func buildDiscoverCandidates(ctx context.Context, q *dbq.Queries, logger *slog.Logger, userID pgtype.UUID, dateStr string) ([]rankedCandidate, error) {
|
||||
dormantRows, err := q.ListDormantArtistTracksForDiscover(ctx, dbq.ListDormantArtistTracksForDiscoverParams{
|
||||
UserID: userID, Column2: dateStr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dormant bucket: %w", err)
|
||||
logger.Warn("discover: dormant bucket failed; continuing with empty pool",
|
||||
"user_id", uuidStringPL(userID), "err", err)
|
||||
dormantRows = nil
|
||||
}
|
||||
crossUserRows, err := q.ListCrossUserLikedTracksForDiscover(ctx, dbq.ListCrossUserLikedTracksForDiscoverParams{
|
||||
UserID: userID, Column2: dateStr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cross-user bucket: %w", err)
|
||||
logger.Warn("discover: cross-user bucket failed; continuing with empty pool",
|
||||
"user_id", uuidStringPL(userID), "err", err)
|
||||
crossUserRows = nil
|
||||
}
|
||||
randomRows, err := q.ListRandomUnheardTracksForDiscover(ctx, dbq.ListRandomUnheardTracksForDiscoverParams{
|
||||
UserID: userID, Column2: dateStr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("random bucket: %w", err)
|
||||
logger.Warn("discover: random bucket failed; continuing with empty pool",
|
||||
"user_id", uuidStringPL(userID), "err", err)
|
||||
randomRows = nil
|
||||
}
|
||||
|
||||
// Adapt sqlc-generated rows into the internal struct, then apply
|
||||
|
||||
@@ -294,7 +294,10 @@ func BuildSystemPlaylists(ctx context.Context, pool *pgxpool.Pool, logger *slog.
|
||||
}
|
||||
|
||||
// 4. Discover: surface unheard tracks, biased toward dormant artists.
|
||||
discoverTracks, derr := buildDiscoverCandidates(ctx, q, userID, dateStr)
|
||||
// Individual bucket failures are logged inside buildDiscoverCandidates
|
||||
// and redistribute as deficit; the function only returns an error for
|
||||
// unrecoverable conditions, so a remaining derr here is genuine.
|
||||
discoverTracks, derr := buildDiscoverCandidates(ctx, q, logger, userID, dateStr)
|
||||
if derr != nil {
|
||||
logger.Warn("system playlist: discover candidates load failed; skipping",
|
||||
"user_id", uuidStringPL(userID), "err", derr)
|
||||
|
||||
Reference in New Issue
Block a user