// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 // source: system_playlists.sql package dbq import ( "context" "github.com/jackc/pgx/v5/pgtype" ) const clearStaleSystemPlaylistInFlight = `-- name: ClearStaleSystemPlaylistInFlight :exec UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true ` // Cron startup recovery: clear any in_flight=true rows from a previously // crashed process. Safe because at startup, by definition, nothing is // still building. func (q *Queries) ClearStaleSystemPlaylistInFlight(ctx context.Context) error { _, err := q.db.Exec(ctx, clearStaleSystemPlaylistInFlight) return err } const createSystemPlaylist = `-- name: CreateSystemPlaylist :one 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 id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at, kind, system_variant, seed_artist_id ` type CreateSystemPlaylistParams struct { UserID pgtype.UUID Name string SystemVariant *string SeedArtistID pgtype.UUID CoverPath *string } // 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). func (q *Queries) CreateSystemPlaylist(ctx context.Context, arg CreateSystemPlaylistParams) (Playlist, error) { row := q.db.QueryRow(ctx, createSystemPlaylist, arg.UserID, arg.Name, arg.SystemVariant, arg.SeedArtistID, arg.CoverPath, ) var i Playlist err := row.Scan( &i.ID, &i.UserID, &i.Name, &i.Description, &i.IsPublic, &i.CoverPath, &i.TrackCount, &i.DurationSec, &i.CreatedAt, &i.UpdatedAt, &i.Kind, &i.SystemVariant, &i.SeedArtistID, ) return i, err } const deleteSystemPlaylistsForUser = `-- name: DeleteSystemPlaylistsForUser :exec DELETE FROM playlists WHERE user_id = $1 AND kind = 'system' ` // Atomic-replace step. CASCADE on playlist_tracks.playlist_id handles // the join rows. func (q *Queries) DeleteSystemPlaylistsForUser(ctx context.Context, userID pgtype.UUID) error { _, err := q.db.Exec(ctx, deleteSystemPlaylistsForUser, userID) return err } const failSystemPlaylistRun = `-- name: FailSystemPlaylistRun :exec UPDATE system_playlist_runs SET in_flight = false, last_error = $2 WHERE user_id = $1 ` type FailSystemPlaylistRunParams struct { UserID pgtype.UUID LastError *string } // Mark a failed build complete (in_flight=false, error captured). func (q *Queries) FailSystemPlaylistRun(ctx context.Context, arg FailSystemPlaylistRunParams) error { _, err := q.db.Exec(ctx, failSystemPlaylistRun, arg.UserID, arg.LastError) return err } const finishSystemPlaylistRun = `-- name: FinishSystemPlaylistRun :exec UPDATE system_playlist_runs SET last_run_at = $2, last_run_date = $3, in_flight = false, last_error = NULL WHERE user_id = $1 ` type FinishSystemPlaylistRunParams struct { UserID pgtype.UUID LastRunAt pgtype.Timestamptz LastRunDate pgtype.Date } // Mark a successful build complete. func (q *Queries) FinishSystemPlaylistRun(ctx context.Context, arg FinishSystemPlaylistRunParams) error { _, err := q.db.Exec(ctx, finishSystemPlaylistRun, arg.UserID, arg.LastRunAt, arg.LastRunDate) return err } const getSystemPlaylistByVariantForUser = `-- name: GetSystemPlaylistByVariantForUser :one 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 ` type GetSystemPlaylistByVariantForUserParams struct { UserID pgtype.UUID SystemVariant *string } type GetSystemPlaylistByVariantForUserRow struct { ID pgtype.UUID UserID pgtype.UUID Name string Kind string SystemVariant *string SeedArtistID pgtype.UUID CoverPath *string TrackCount int32 } // 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. func (q *Queries) GetSystemPlaylistByVariantForUser(ctx context.Context, arg GetSystemPlaylistByVariantForUserParams) (GetSystemPlaylistByVariantForUserRow, error) { row := q.db.QueryRow(ctx, getSystemPlaylistByVariantForUser, arg.UserID, arg.SystemVariant) var i GetSystemPlaylistByVariantForUserRow err := row.Scan( &i.ID, &i.UserID, &i.Name, &i.Kind, &i.SystemVariant, &i.SeedArtistID, &i.CoverPath, &i.TrackCount, ) return i, err } const getSystemPlaylistRun = `-- name: GetSystemPlaylistRun :one SELECT user_id, last_run_at, last_run_date, in_flight, last_error FROM system_playlist_runs WHERE user_id = $1 ` func (q *Queries) GetSystemPlaylistRun(ctx context.Context, userID pgtype.UUID) (SystemPlaylistRun, error) { row := q.db.QueryRow(ctx, getSystemPlaylistRun, userID) var i SystemPlaylistRun err := row.Scan( &i.UserID, &i.LastRunAt, &i.LastRunDate, &i.InFlight, &i.LastError, ) return i, err } const listActiveUsersForSystemPlaylists = `-- name: ListActiveUsersForSystemPlaylists :many 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' ) ` // M7 #352 slice 2: system-generated playlist queries. // Active = had a play in the last 7 days. The cron iterates this list. func (q *Queries) ListActiveUsersForSystemPlaylists(ctx context.Context) ([]pgtype.UUID, error) { rows, err := q.db.Query(ctx, listActiveUsersForSystemPlaylists) if err != nil { return nil, err } defer rows.Close() var items []pgtype.UUID for rows.Next() { var id pgtype.UUID if err := rows.Scan(&id); err != nil { return nil, err } items = append(items, id) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listPlaylistsByUserAndKind = `-- name: ListPlaylistsByUserAndKind :many 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 ` type ListPlaylistsByUserAndKindParams struct { UserID pgtype.UUID Column2 string } type ListPlaylistsByUserAndKindRow struct { ID pgtype.UUID UserID pgtype.UUID Name string Description string IsPublic bool Kind string SystemVariant *string SeedArtistID pgtype.UUID CoverPath *string TrackCount int32 DurationSec int32 CreatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz } // 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. func (q *Queries) ListPlaylistsByUserAndKind(ctx context.Context, arg ListPlaylistsByUserAndKindParams) ([]ListPlaylistsByUserAndKindRow, error) { rows, err := q.db.Query(ctx, listPlaylistsByUserAndKind, arg.UserID, arg.Column2) if err != nil { return nil, err } defer rows.Close() var items []ListPlaylistsByUserAndKindRow for rows.Next() { var i ListPlaylistsByUserAndKindRow if err := rows.Scan( &i.ID, &i.UserID, &i.Name, &i.Description, &i.IsPublic, &i.Kind, &i.SystemVariant, &i.SeedArtistID, &i.CoverPath, &i.TrackCount, &i.DurationSec, &i.CreatedAt, &i.UpdatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const pickSeedArtists = `-- name: PickSeedArtists :many WITH liked AS ( SELECT gla.artist_id FROM general_likes_artists gla WHERE gla.user_id = $1 ), recent7 AS ( SELECT t.artist_id, COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count, 0 AS tier 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 ), recent30 AS ( SELECT t.artist_id, COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count, 1 AS tier FROM play_events pe JOIN tracks t ON t.id = pe.track_id WHERE pe.user_id = $1 AND pe.started_at > now() - INTERVAL '30 days' AND t.artist_id IS NOT NULL GROUP BY t.artist_id ), alltime AS ( SELECT t.artist_id, COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count, 2 AS tier FROM play_events pe JOIN tracks t ON t.id = pe.track_id WHERE pe.user_id = $1 AND t.artist_id IS NOT NULL GROUP BY t.artist_id ), likedonly AS ( SELECT l.artist_id, 0::bigint AS play_count, 3 AS tier FROM liked l ), chosen AS ( SELECT artist_id, play_count, tier FROM recent7 UNION ALL SELECT artist_id, play_count, tier FROM recent30 WHERE NOT EXISTS (SELECT 1 FROM recent7) UNION ALL SELECT artist_id, play_count, tier FROM alltime WHERE NOT EXISTS (SELECT 1 FROM recent7) AND NOT EXISTS (SELECT 1 FROM recent30) UNION ALL SELECT artist_id, play_count, tier FROM likedonly WHERE NOT EXISTS (SELECT 1 FROM recent7) AND NOT EXISTS (SELECT 1 FROM recent30) AND NOT EXISTS (SELECT 1 FROM alltime) ) SELECT c.artist_id, (c.play_count + CASE WHEN l.artist_id IS NOT NULL THEN 5 ELSE 0 END)::bigint AS score, c.tier::int AS tier FROM chosen c LEFT JOIN liked l ON l.artist_id = c.artist_id ORDER BY score DESC, c.artist_id LIMIT 5 ` type PickSeedArtistsRow struct { ArtistID pgtype.UUID Score int64 Tier int32 } // Top-5 most-engaged distinct artist candidates, tiered so the // "Songs like X" mixes never silently vanish (#1255): the old hard // 7-day window emptied the seed pool after a quiet week, and the // daily atomic-replace build then deleted every existing mix until // the user played something again. Same fallback shape as // PickTopPlayedTracksForUser (For You's seeds): // // tier 0 engagement in the last 7 days // tier 1 (only if tier 0 empty) last 30 days // tier 2 (only if tiers 0+1 empty) all-time // tier 3 (only if 0-2 empty) liked artists with no play history // // All returned rows share one tier; produceSeedMixes maps it onto the // rule-#131 pick-kind ladder and stamps the built tracks, so metrics // can compare mixes seeded from fresh vs stale engagement. // Score = unskipped-play count + 5 if user has liked the artist. The // Go-side picker (pickSeedArtistsForDay) shuffles the 5 // daily-deterministically and takes 3 so the mix set rotates. func (q *Queries) PickSeedArtists(ctx context.Context, userID pgtype.UUID) ([]PickSeedArtistsRow, error) { rows, err := q.db.Query(ctx, pickSeedArtists, userID) if err != nil { return nil, err } defer rows.Close() var items []PickSeedArtistsRow for rows.Next() { var i PickSeedArtistsRow if err := rows.Scan(&i.ArtistID, &i.Score, &i.Tier); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const pickTopAlbumCoverForArtistByUser = `-- name: PickTopAlbumCoverForArtistByUser :one 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 ` type PickTopAlbumCoverForArtistByUserParams struct { UserID pgtype.UUID ArtistID pgtype.UUID } // "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). func (q *Queries) PickTopAlbumCoverForArtistByUser(ctx context.Context, arg PickTopAlbumCoverForArtistByUserParams) (*string, error) { row := q.db.QueryRow(ctx, pickTopAlbumCoverForArtistByUser, arg.UserID, arg.ArtistID) var cover_art_path *string err := row.Scan(&cover_art_path) return cover_art_path, err } const pickTopPlayedTrackForArtistByUser = `-- name: PickTopPlayedTrackForArtistByUser :one 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 ` type PickTopPlayedTrackForArtistByUserParams struct { UserID pgtype.UUID ArtistID pgtype.UUID } // "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{}. func (q *Queries) PickTopPlayedTrackForArtistByUser(ctx context.Context, arg PickTopPlayedTrackForArtistByUserParams) (pgtype.UUID, error) { row := q.db.QueryRow(ctx, pickTopPlayedTrackForArtistByUser, arg.UserID, arg.ArtistID) var id pgtype.UUID err := row.Scan(&id) return id, err } const pickTopPlayedTracksForUser = `-- name: PickTopPlayedTracksForUser :many WITH recent AS ( SELECT t.id, COUNT(*) AS c, 0 AS tier FROM play_events pe JOIN tracks t ON t.id = pe.track_id WHERE pe.user_id = $1 AND pe.started_at > now() - INTERVAL '30 days' AND pe.was_skipped = false GROUP BY t.id ), alltime AS ( SELECT t.id, COUNT(*) AS c, 1 AS tier FROM play_events pe JOIN tracks t ON t.id = pe.track_id WHERE pe.user_id = $1 AND pe.was_skipped = false GROUP BY t.id ), liked AS ( SELECT gl.track_id AS id, 0::bigint AS c, 2 AS tier FROM general_likes gl WHERE gl.user_id = $1 ), chosen AS ( SELECT id, c, tier FROM recent UNION ALL SELECT id, c, tier FROM alltime WHERE NOT EXISTS (SELECT 1 FROM recent) UNION ALL SELECT id, c, tier FROM liked WHERE NOT EXISTS (SELECT 1 FROM recent) AND NOT EXISTS (SELECT 1 FROM alltime) ) SELECT id FROM chosen ORDER BY tier, c DESC, id LIMIT 5 ` // For-You candidate seeds, tiered so For-You never silently vanishes: // // tier 0 top non-skip plays in the last 30 days // tier 1 (only if tier 0 empty) all-time top non-skip plays // tier 2 (only if tiers 0+1 empty) liked tracks // // Returns up to 5 ids; tie-break by track_id for determinism. The // Go-side picker (pickDailySeeds) draws the day's seeds from these. // Widened from a hard 7-day window, which made For-You disappear // after a week of not listening and never recover on a self-hosted // library with sparse history. func (q *Queries) PickTopPlayedTracksForUser(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) { rows, err := q.db.Query(ctx, pickTopPlayedTracksForUser, userID) if err != nil { return nil, err } defer rows.Close() var items []pgtype.UUID for rows.Next() { var id pgtype.UUID if err := rows.Scan(&id); err != nil { return nil, err } items = append(items, id) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const tryClaimSystemPlaylistRun = `-- name: TryClaimSystemPlaylistRun :one 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 ` // Atomic test-and-set on in_flight. Returns the row if we won the claim, // or pgx.ErrNoRows if someone else is already building. func (q *Queries) TryClaimSystemPlaylistRun(ctx context.Context, userID pgtype.UUID) (SystemPlaylistRun, error) { row := q.db.QueryRow(ctx, tryClaimSystemPlaylistRun, userID) var i SystemPlaylistRun err := row.Scan( &i.UserID, &i.LastRunAt, &i.LastRunDate, &i.InFlight, &i.LastError, ) return i, err }