Rewrites the internal/db/queries comments to rule 32 and regenerates dbq, whose doc comments and embedded SQL carry them. The bugs behind the tiered mixes, the md5 order seed, the score bands and the round-robin arms are restated as the constraint each one enforces; counts from one library and stale cross-references are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
316 lines
10 KiB
Go
316 lines
10 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: discover.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const listCrossUserLikedTracksForDiscover = `-- name: ListCrossUserLikedTracksForDiscover :many
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM general_likes gl
|
|
JOIN tracks t ON t.id = gl.track_id
|
|
WHERE t.missing_since IS NULL -- never offer a file that is gone
|
|
AND gl.user_id != $1
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM play_events pe
|
|
WHERE pe.user_id = $1
|
|
AND pe.track_id = t.id
|
|
AND pe.was_skipped = false
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM general_likes ml
|
|
WHERE ml.user_id = $1 AND ml.track_id = t.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM 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
|
|
`
|
|
|
|
type ListCrossUserLikedTracksForDiscoverParams struct {
|
|
UserID pgtype.UUID
|
|
Column2 string
|
|
}
|
|
|
|
type ListCrossUserLikedTracksForDiscoverRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// Tracks any OTHER user has liked, that this user hasn't played,
|
|
// liked, or had quarantined. On single-user servers this returns 0
|
|
// 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 would fail the
|
|
// entire Discover build.
|
|
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 {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListCrossUserLikedTracksForDiscoverRow
|
|
for rows.Next() {
|
|
var i ListCrossUserLikedTracksForDiscoverRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listDormantArtistTracksForDiscover = `-- name: ListDormantArtistTracksForDiscover :many
|
|
|
|
WITH user_artist_counts AS (
|
|
SELECT t.artist_id, COUNT(*) AS play_count
|
|
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.artist_id
|
|
),
|
|
dormant_artists AS (
|
|
SELECT a.id
|
|
FROM artists a
|
|
LEFT JOIN user_artist_counts uac ON uac.artist_id = a.id
|
|
WHERE COALESCE(uac.play_count, 0) < 10
|
|
)
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
JOIN dormant_artists da ON da.id = t.artist_id
|
|
WHERE t.missing_since IS NULL -- never offer a file that is gone
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM play_events pe
|
|
WHERE pe.user_id = $1
|
|
AND pe.track_id = t.id
|
|
AND pe.was_skipped = false
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM general_likes gl
|
|
WHERE gl.user_id = $1 AND gl.track_id = t.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY md5(t.id::text || $2::text)
|
|
LIMIT 80
|
|
`
|
|
|
|
type ListDormantArtistTracksForDiscoverParams struct {
|
|
UserID pgtype.UUID
|
|
Column2 string
|
|
}
|
|
|
|
type ListDormantArtistTracksForDiscoverRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// Discover playlist bucket queries. Each returns up to its LIMIT
|
|
// count of (track_id, album_id, artist_id) triples ordered
|
|
// daily-deterministically via md5(track_id || dateStr). The Go-side
|
|
// bucket allocator (internal/playlists/discover.go) applies
|
|
// per-album (<=2) and per-artist (<=3) caps then redistributes any
|
|
// bucket deficit equally across the others. LIMIT values are
|
|
// generous so the caps + redistribution have headroom.
|
|
// Tracks whose artist this user has played fewer than 10 times total.
|
|
// The artist threshold defines "dormant" — we want to surface the
|
|
// long tail of artists in the user's library that they rarely listen
|
|
// to. Excludes tracks the user has played, liked, or has quarantined.
|
|
// $1 = user_id, $2 = date string for md5 ordering.
|
|
func (q *Queries) ListDormantArtistTracksForDiscover(ctx context.Context, arg ListDormantArtistTracksForDiscoverParams) ([]ListDormantArtistTracksForDiscoverRow, error) {
|
|
rows, err := q.db.Query(ctx, listDormantArtistTracksForDiscover, arg.UserID, arg.Column2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListDormantArtistTracksForDiscoverRow
|
|
for rows.Next() {
|
|
var i ListDormantArtistTracksForDiscoverRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listRandomUnheardTracksForDiscover = `-- name: ListRandomUnheardTracksForDiscover :many
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
WHERE t.missing_since IS NULL -- never offer a file that is gone
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM play_events pe
|
|
WHERE pe.user_id = $1
|
|
AND pe.track_id = t.id
|
|
AND pe.was_skipped = false
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM general_likes gl
|
|
WHERE gl.user_id = $1 AND gl.track_id = t.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY md5(t.id::text || $2::text)
|
|
LIMIT 200
|
|
`
|
|
|
|
type ListRandomUnheardTracksForDiscoverParams struct {
|
|
UserID pgtype.UUID
|
|
Column2 string
|
|
}
|
|
|
|
type ListRandomUnheardTracksForDiscoverRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// Random sample from the entire library. Same exclusion filters as
|
|
// the other two buckets. The generous LIMIT (200) gives the caller
|
|
// headroom for the per-album/artist caps and the slot redistribution.
|
|
// $1 = user_id, $2 = date string for md5 ordering.
|
|
func (q *Queries) ListRandomUnheardTracksForDiscover(ctx context.Context, arg ListRandomUnheardTracksForDiscoverParams) ([]ListRandomUnheardTracksForDiscoverRow, error) {
|
|
rows, err := q.db.Query(ctx, listRandomUnheardTracksForDiscover, arg.UserID, arg.Column2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListRandomUnheardTracksForDiscoverRow
|
|
for rows.Next() {
|
|
var i ListRandomUnheardTracksForDiscoverRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listTasteUnheardTracksForDiscover = `-- name: ListTasteUnheardTracksForDiscover :many
|
|
WITH scored AS (
|
|
SELECT t.id, t.album_id, t.artist_id,
|
|
SUM(nt.weight) AS weight,
|
|
md5(t.id::text || $2::text) AS tiebreak
|
|
FROM tracks t
|
|
JOIN LATERAL regexp_split_to_table(coalesce(t.genre, ''), '[;,]') AS g_split(g) ON true
|
|
JOIN taste_profile_tags nt ON nt.user_id = $3 AND trim(g_split.g) = nt.tag
|
|
WHERE t.missing_since IS NULL -- never offer a file that is gone
|
|
AND nt.weight > 0
|
|
AND trim(g_split.g) <> ''
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM play_events pe
|
|
WHERE pe.user_id = $3
|
|
AND pe.track_id = t.id
|
|
AND pe.was_skipped = false
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM general_likes gl
|
|
WHERE gl.user_id = $3 AND gl.track_id = t.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM quarantine q
|
|
WHERE q.user_id = $3 AND q.track_id = t.id
|
|
)
|
|
GROUP BY t.id, t.album_id, t.artist_id
|
|
),
|
|
album_capped AS (
|
|
SELECT s.id, s.album_id, s.artist_id, s.weight, s.tiebreak,
|
|
row_number() OVER (PARTITION BY s.album_id ORDER BY s.weight DESC, s.tiebreak) AS album_rank
|
|
FROM scored s
|
|
),
|
|
artist_capped AS (
|
|
SELECT a.id, a.album_id, a.artist_id, a.weight, a.tiebreak, a.album_rank,
|
|
row_number() OVER (PARTITION BY a.artist_id ORDER BY a.weight DESC, a.tiebreak) AS artist_rank
|
|
FROM album_capped a
|
|
WHERE a.album_rank <= $4::int
|
|
)
|
|
SELECT c.id, c.album_id, c.artist_id
|
|
FROM artist_capped c
|
|
WHERE c.artist_rank <= $1::int
|
|
ORDER BY c.weight DESC, c.tiebreak
|
|
LIMIT 120
|
|
`
|
|
|
|
type ListTasteUnheardTracksForDiscoverParams struct {
|
|
MaxPerArtist int32
|
|
DateSeed string
|
|
UserID pgtype.UUID
|
|
MaxPerAlbum int32
|
|
}
|
|
|
|
type ListTasteUnheardTracksForDiscoverRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// Taste-targeted novelty: unheard tracks whose genres overlap the user's
|
|
// taste-profile tags (taste_profile_tags), ranked by summed tag
|
|
// weight — "new to you, but your vibe" rather than the crude random arm.
|
|
// Genres live inline on tracks.genre as a delimited string, split the
|
|
// same way the radio tag_overlap arm does (regexp_split_to_table on
|
|
// [;,]). Same exclusion filters as the other buckets. Returns nothing
|
|
// when the user has no taste tags yet (cold start), so the caller
|
|
// redistributes its slots to the other buckets. Stamped 'taste_unheard'.
|
|
//
|
|
// The per-album and per-artist caps apply BEFORE the LIMIT. Summed weight
|
|
// rewards a track for carrying many of the user's tags, so a few artists
|
|
// whose every track is tagged with the whole profile would take every row of
|
|
// a plain LIMIT, the caller's caps would then leave a handful, and the arm
|
|
// would hand its slots to the others. Ranking within album, then within
|
|
// artist over what the album cap kept, is the same walk capByAlbumAndArtist
|
|
// makes, so the caller's caps keep everything here.
|
|
func (q *Queries) ListTasteUnheardTracksForDiscover(ctx context.Context, arg ListTasteUnheardTracksForDiscoverParams) ([]ListTasteUnheardTracksForDiscoverRow, error) {
|
|
rows, err := q.db.Query(ctx, listTasteUnheardTracksForDiscover,
|
|
arg.MaxPerArtist,
|
|
arg.DateSeed,
|
|
arg.UserID,
|
|
arg.MaxPerAlbum,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListTasteUnheardTracksForDiscoverRow
|
|
for rows.Next() {
|
|
var i ListTasteUnheardTracksForDiscoverRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|