b4801c2dd3
For-You + Songs-Like-X seed selection moves to Go-side daily rotation (next commit). The SQL change just widens the candidate pool: top-5 played tracks instead of single top played track; top-5 artists instead of top-3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
180 lines
6.5 KiB
SQL
180 lines
6.5 KiB
SQL
-- M7 #352 slice 2: system-generated playlist queries.
|
|
|
|
-- name: ListActiveUsersForSystemPlaylists :many
|
|
-- Active = had a play in the last 7 days. The cron iterates this list.
|
|
SELECT u.id FROM users u
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM play_events pe
|
|
WHERE pe.user_id = u.id
|
|
AND pe.started_at > now() - INTERVAL '7 days'
|
|
);
|
|
|
|
-- name: GetSystemPlaylistRun :one
|
|
SELECT user_id, last_run_at, last_run_date, in_flight, last_error
|
|
FROM system_playlist_runs
|
|
WHERE user_id = $1;
|
|
|
|
-- name: TryClaimSystemPlaylistRun :one
|
|
-- Atomic test-and-set on in_flight. Returns the row if we won the claim,
|
|
-- or pgx.ErrNoRows if someone else is already building.
|
|
INSERT INTO system_playlist_runs (user_id, in_flight)
|
|
VALUES ($1, true)
|
|
ON CONFLICT (user_id) DO UPDATE
|
|
SET in_flight = true
|
|
WHERE system_playlist_runs.in_flight = false
|
|
RETURNING user_id, last_run_at, last_run_date, in_flight, last_error;
|
|
|
|
-- name: FinishSystemPlaylistRun :exec
|
|
-- Mark a successful build complete.
|
|
UPDATE system_playlist_runs
|
|
SET last_run_at = $2,
|
|
last_run_date = $3,
|
|
in_flight = false,
|
|
last_error = NULL
|
|
WHERE user_id = $1;
|
|
|
|
-- name: FailSystemPlaylistRun :exec
|
|
-- Mark a failed build complete (in_flight=false, error captured).
|
|
UPDATE system_playlist_runs
|
|
SET in_flight = false,
|
|
last_error = $2
|
|
WHERE user_id = $1;
|
|
|
|
-- name: ClearStaleSystemPlaylistInFlight :exec
|
|
-- Cron startup recovery: clear any in_flight=true rows from a previously
|
|
-- crashed process. Safe because at startup, by definition, nothing is
|
|
-- still building.
|
|
UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true;
|
|
|
|
-- name: PickSeedArtists :many
|
|
-- Top-5 most-engaged distinct artist candidates in the user's last 7
|
|
-- days. The Go-side picker (pickSeedArtistsForDay) shuffles these
|
|
-- daily-deterministically and takes the first 3 so the set of
|
|
-- "Songs like X" mixes rotates day-to-day.
|
|
-- Score = unskipped-play count + 5 if user has liked the artist.
|
|
WITH plays AS (
|
|
SELECT t.artist_id,
|
|
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count
|
|
FROM play_events pe
|
|
JOIN tracks t ON t.id = pe.track_id
|
|
WHERE pe.user_id = $1
|
|
AND pe.started_at > now() - INTERVAL '7 days'
|
|
AND t.artist_id IS NOT NULL
|
|
GROUP BY t.artist_id
|
|
),
|
|
liked AS (
|
|
SELECT artist_id FROM general_likes_artists WHERE user_id = $1
|
|
)
|
|
SELECT p.artist_id,
|
|
(p.play_count + CASE WHEN l.artist_id IS NOT NULL THEN 5 ELSE 0 END)::bigint AS score
|
|
FROM plays p
|
|
LEFT JOIN liked l ON l.artist_id = p.artist_id
|
|
ORDER BY score DESC, p.artist_id
|
|
LIMIT 5;
|
|
|
|
-- name: PickTopPlayedTracksForUser :many
|
|
-- For-You candidate seeds. Returns the user's top-5 most-played
|
|
-- non-skipped tracks in the last 7 days; tie-break by track_id for
|
|
-- determinism. The Go-side picker (pickForYouSeedForDay) chooses one
|
|
-- of the returned rows as today's seed via userIDHash so the
|
|
-- candidate pool rotates day-to-day while staying stable within a
|
|
-- day.
|
|
SELECT t.id
|
|
FROM play_events pe
|
|
JOIN tracks t ON t.id = pe.track_id
|
|
WHERE pe.user_id = $1
|
|
AND pe.started_at > now() - INTERVAL '7 days'
|
|
AND pe.was_skipped = false
|
|
GROUP BY t.id
|
|
ORDER BY COUNT(*) DESC, t.id
|
|
LIMIT 5;
|
|
|
|
-- name: PickTopPlayedTrackForArtistByUser :one
|
|
-- "Songs like X" seed selection. Returns the user's most-played non-skipped
|
|
-- track by artist X in the last 7 days. Falls back to the artist's most-
|
|
-- recent album's first track if no plays exist.
|
|
-- Cast to uuid so sqlc infers pgtype.UUID rather than interface{}.
|
|
SELECT COALESCE(
|
|
(SELECT t.id
|
|
FROM play_events pe
|
|
JOIN tracks t ON t.id = pe.track_id
|
|
WHERE pe.user_id = $1
|
|
AND t.artist_id = $2
|
|
AND pe.started_at > now() - INTERVAL '7 days'
|
|
AND pe.was_skipped = false
|
|
GROUP BY t.id
|
|
ORDER BY COUNT(*) DESC, t.id
|
|
LIMIT 1),
|
|
(SELECT t.id
|
|
FROM tracks t
|
|
JOIN albums a ON a.id = t.album_id
|
|
WHERE t.artist_id = $2
|
|
ORDER BY a.release_date DESC NULLS LAST,
|
|
t.disc_number NULLS LAST,
|
|
t.track_number NULLS LAST,
|
|
t.id
|
|
LIMIT 1)
|
|
)::uuid AS id;
|
|
|
|
-- name: PickTopAlbumCoverForArtistByUser :one
|
|
-- "Songs like X" cover. Most-played album by user; tie-break by most-recent
|
|
-- release. NULL if the artist has no albums or all covers are NULL.
|
|
-- Note: albums uses cover_art_path (not cover_path).
|
|
SELECT a.cover_art_path
|
|
FROM albums a
|
|
LEFT JOIN tracks t ON t.album_id = a.id
|
|
LEFT JOIN play_events pe
|
|
ON pe.track_id = t.id
|
|
AND pe.user_id = $1
|
|
AND pe.was_skipped = false
|
|
WHERE a.artist_id = $2
|
|
AND a.cover_art_path IS NOT NULL
|
|
GROUP BY a.id, a.release_date, a.cover_art_path
|
|
ORDER BY COUNT(pe.id) DESC, a.release_date DESC NULLS LAST, a.id
|
|
LIMIT 1;
|
|
|
|
-- name: DeleteSystemPlaylistsForUser :exec
|
|
-- Atomic-replace step. CASCADE on playlist_tracks.playlist_id handles
|
|
-- the join rows.
|
|
DELETE FROM playlists WHERE user_id = $1 AND kind = 'system';
|
|
|
|
-- name: ListPlaylistsByUserAndKind :many
|
|
-- Used in integration tests; production filter applied in Go
|
|
-- (api.handleListPlaylists fetches via Service.List and filters by Kind in
|
|
-- memory so it can also surface other users' public playlists alongside
|
|
-- the caller's own kind-filtered ones).
|
|
-- Sorted by updated_at DESC.
|
|
SELECT id, user_id, name, description, is_public, kind, system_variant,
|
|
seed_artist_id, cover_path, track_count, duration_sec,
|
|
created_at, updated_at
|
|
FROM playlists
|
|
WHERE user_id = $1
|
|
AND ($2::text = 'all' OR kind = $2)
|
|
ORDER BY updated_at DESC;
|
|
|
|
-- name: CreateSystemPlaylist :one
|
|
-- Inserts a system-generated playlist. Used by BuildSystemPlaylists.
|
|
-- For 'for_you' variant, pass seed_artist_id as NULL (zero-value pgtype.UUID
|
|
-- with Valid=false).
|
|
INSERT INTO playlists (
|
|
user_id, name, description, is_public,
|
|
kind, system_variant, seed_artist_id, cover_path
|
|
) VALUES ($1, $2, '', false, 'system', $3, $4, $5)
|
|
RETURNING *;
|
|
|
|
-- name: GetSystemPlaylistByVariantForUser :one
|
|
-- Returns the user's system playlist for the given variant
|
|
-- ('for_you' / 'songs_like_artist' / 'discover'), or pgx.ErrNoRows
|
|
-- when the user hasn't built that variant yet. For
|
|
-- 'songs_like_artist' (which can have multiple rows per user, one
|
|
-- per seed), this returns whichever LIMIT 1 picks — callers that
|
|
-- need all instances should use a different query.
|
|
SELECT p.id, p.user_id, p.name, p.kind, p.system_variant,
|
|
p.seed_artist_id, p.cover_path, p.track_count
|
|
FROM playlists p
|
|
WHERE p.user_id = $1
|
|
AND p.kind = 'system'
|
|
AND p.system_variant = $2
|
|
ORDER BY p.created_at DESC
|
|
LIMIT 1;
|