From 3ef560e414ceb3862c9618802b5e4b6fe4a1b572 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 27 Apr 2026 08:01:50 -0400 Subject: [PATCH] feat(db): add LoadRadioCandidates query Single SELECT that joins tracks with general_likes (for is_liked) and an aggregated LATERAL subquery on play_events (for last_played_at, play_count, skip_count). Excludes seed + tracks played in the last N hours. Drives the M3 weighted shuffle scoring. --- internal/db/dbq/recommendation.sql.go | 98 ++++++++++++++++++++++++++ internal/db/queries/recommendation.sql | 29 ++++++++ 2 files changed, 127 insertions(+) create mode 100644 internal/db/dbq/recommendation.sql.go create mode 100644 internal/db/queries/recommendation.sql diff --git a/internal/db/dbq/recommendation.sql.go b/internal/db/dbq/recommendation.sql.go new file mode 100644 index 00000000..535b4bea --- /dev/null +++ b/internal/db/dbq/recommendation.sql.go @@ -0,0 +1,98 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: recommendation.sql + +package dbq + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const loadRadioCandidates = `-- name: LoadRadioCandidates :many +SELECT + 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, + (l.user_id IS NOT NULL)::bool AS is_liked, + pe.last_played_at::timestamptz AS last_played_at, + pe.play_count, + pe.skip_count +FROM tracks t +LEFT JOIN general_likes l ON l.user_id = $1 AND l.track_id = t.id +LEFT JOIN LATERAL ( + SELECT + max(started_at) AS last_played_at, + count(*) AS play_count, + count(*) FILTER (WHERE was_skipped) AS skip_count + FROM play_events + WHERE user_id = $1 AND track_id = t.id +) pe ON true +WHERE t.id <> $2 + AND NOT EXISTS ( + SELECT 1 FROM play_events + WHERE user_id = $1 AND track_id = t.id + AND started_at > now() - $3 * interval '1 hour' + ) +` + +type LoadRadioCandidatesParams struct { + UserID pgtype.UUID + ID pgtype.UUID + Column3 interface{} +} + +type LoadRadioCandidatesRow struct { + Track Track + IsLiked bool + LastPlayedAt pgtype.Timestamptz + PlayCount int64 + SkipCount int64 +} + +// Returns all tracks except the seed and any played by the user within +// the last $3 hours, joined with the stats needed for scoring: +// +// is_liked — boolean from general_likes for this user +// last_played_at — max(play_events.started_at) for this user/track +// play_count — count of play_events for this user/track +// skip_count — play_events where was_skipped=true +func (q *Queries) LoadRadioCandidates(ctx context.Context, arg LoadRadioCandidatesParams) ([]LoadRadioCandidatesRow, error) { + rows, err := q.db.Query(ctx, loadRadioCandidates, arg.UserID, arg.ID, arg.Column3) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LoadRadioCandidatesRow + for rows.Next() { + var i LoadRadioCandidatesRow + if err := rows.Scan( + &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.IsLiked, + &i.LastPlayedAt, + &i.PlayCount, + &i.SkipCount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/db/queries/recommendation.sql b/internal/db/queries/recommendation.sql new file mode 100644 index 00000000..6eea7a75 --- /dev/null +++ b/internal/db/queries/recommendation.sql @@ -0,0 +1,29 @@ +-- name: LoadRadioCandidates :many +-- Returns all tracks except the seed and any played by the user within +-- the last $3 hours, joined with the stats needed for scoring: +-- is_liked — boolean from general_likes for this user +-- last_played_at — max(play_events.started_at) for this user/track +-- play_count — count of play_events for this user/track +-- skip_count — play_events where was_skipped=true +SELECT + sqlc.embed(t), + (l.user_id IS NOT NULL)::bool AS is_liked, + pe.last_played_at::timestamptz AS last_played_at, + pe.play_count, + pe.skip_count +FROM tracks t +LEFT JOIN general_likes l ON l.user_id = $1 AND l.track_id = t.id +LEFT JOIN LATERAL ( + SELECT + max(started_at) AS last_played_at, + count(*) AS play_count, + count(*) FILTER (WHERE was_skipped) AS skip_count + FROM play_events + WHERE user_id = $1 AND track_id = t.id +) pe ON true +WHERE t.id <> $2 + AND NOT EXISTS ( + SELECT 1 FROM play_events + WHERE user_id = $1 AND track_id = t.id + AND started_at > now() - $3 * interval '1 hour' + );