Files
minstrel/internal/db/dbq/events.sql.go
T
bvandeusen fb4431207d
test-go / test (push) Successful in 31s
test-web / test (push) Successful in 37s
test-go / integration (push) Successful in 4m37s
feat(recommendation): For You exploration attribution — taste vs fresh picks
Milestone #127 step 2 (#1249). For You deliberately blends two
populations — a head of top-scored taste picks and a tail sampled from
deeper ranking (the freshness injection) — but the metrics judged it as
one blob, so its skip rate couldn't distinguish "the taste engine is
missing" from "the freshness tax is too high". That number decides the
exploration share before we tune it.

- Migration 0038: nullable pick_kind ('taste'|'fresh') on both
  playlist_tracks (stamped at snapshot build) and play_events (frozen at
  play-ingestion — the snapshot rebuilds daily, so attribution cannot be
  reconstructed at read time).
- Builder: pickHeadAndTail marks head=taste / tail=fresh; the small-pool
  fallback is all taste (top-N-by-score IS the taste mechanism). Other
  variants persist NULL.
- Ingestion: for_you plays (live + offline replay) look the track up in
  the user's current snapshot; not found → unattributed, never guessed.
- Metrics: For You's row gains a breakdown (taste / fresh / earlier
  unattributed plays), parent row stays the sum; web card renders the
  sub-rows indented with the same baseline deltas + low-data dimming.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
2026-07-02 20:39:41 -04:00

387 lines
10 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: events.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const getCurrentSessionVectorForUser = `-- name: GetCurrentSessionVectorForUser :one
SELECT pe.session_vector_at_play
FROM play_events pe
JOIN play_sessions s ON s.id = pe.session_id
WHERE pe.user_id = $1
AND s.ended_at IS NULL
ORDER BY pe.started_at DESC
LIMIT 1
`
// Returns the session_vector_at_play of the user's most recent play_event
// in a still-active (un-timed-out) session. NoRows means no current vector.
// Joined with play_sessions so closed sessions don't leak stale vectors.
func (q *Queries) GetCurrentSessionVectorForUser(ctx context.Context, userID pgtype.UUID) ([]byte, error) {
row := q.db.QueryRow(ctx, getCurrentSessionVectorForUser, userID)
var session_vector_at_play []byte
err := row.Scan(&session_vector_at_play)
return session_vector_at_play, err
}
const getForYouPickKindForTrack = `-- name: GetForYouPickKindForTrack :one
SELECT pt.pick_kind
FROM playlist_tracks pt
JOIN playlists p ON p.id = pt.playlist_id
WHERE p.user_id = $1
AND p.system_variant = 'for_you'
AND pt.track_id = $2
LIMIT 1
`
type GetForYouPickKindForTrackParams struct {
UserID pgtype.UUID
TrackID pgtype.UUID
}
// Looks a track up in the user's CURRENT For You snapshot and returns
// its pick_kind ('taste'/'fresh'). Used at play-ingestion time to
// freeze exploration attribution onto the play_event — the snapshot is
// rebuilt daily, so attribution can't be reconstructed at read time
// (#1249). No row = track not in today's snapshot (caller stores NULL).
func (q *Queries) GetForYouPickKindForTrack(ctx context.Context, arg GetForYouPickKindForTrackParams) (*string, error) {
row := q.db.QueryRow(ctx, getForYouPickKindForTrack, arg.UserID, arg.TrackID)
var pick_kind *string
err := row.Scan(&pick_kind)
return pick_kind, err
}
const getMostRecentPlaySessionForUser = `-- name: GetMostRecentPlaySessionForUser :one
SELECT id, user_id, started_at, ended_at, last_event_at, track_count, client_id FROM play_sessions
WHERE user_id = $1
ORDER BY last_event_at DESC
LIMIT 1
`
func (q *Queries) GetMostRecentPlaySessionForUser(ctx context.Context, userID pgtype.UUID) (PlaySession, error) {
row := q.db.QueryRow(ctx, getMostRecentPlaySessionForUser, userID)
var i PlaySession
err := row.Scan(
&i.ID,
&i.UserID,
&i.StartedAt,
&i.EndedAt,
&i.LastEventAt,
&i.TrackCount,
&i.ClientID,
)
return i, err
}
const getOpenPlayEventForUser = `-- name: GetOpenPlayEventForUser :one
SELECT id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind FROM play_events
WHERE user_id = $1 AND ended_at IS NULL
ORDER BY started_at DESC
LIMIT 1
`
// Returns the most recent play_event for a user where ended_at IS NULL.
// Used by the auto-close-prior step in playevents.RecordPlayStarted.
func (q *Queries) GetOpenPlayEventForUser(ctx context.Context, userID pgtype.UUID) (PlayEvent, error) {
row := q.db.QueryRow(ctx, getOpenPlayEventForUser, userID)
var i PlayEvent
err := row.Scan(
&i.ID,
&i.UserID,
&i.TrackID,
&i.SessionID,
&i.StartedAt,
&i.EndedAt,
&i.DurationPlayedMs,
&i.CompletionRatio,
&i.WasSkipped,
&i.ClientID,
&i.SessionVectorAtPlay,
&i.ScrobbledAt,
&i.Source,
&i.PickKind,
)
return i, err
}
const getPlayEventByID = `-- name: GetPlayEventByID :one
SELECT id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind FROM play_events WHERE id = $1
`
func (q *Queries) GetPlayEventByID(ctx context.Context, id pgtype.UUID) (PlayEvent, error) {
row := q.db.QueryRow(ctx, getPlayEventByID, id)
var i PlayEvent
err := row.Scan(
&i.ID,
&i.UserID,
&i.TrackID,
&i.SessionID,
&i.StartedAt,
&i.EndedAt,
&i.DurationPlayedMs,
&i.CompletionRatio,
&i.WasSkipped,
&i.ClientID,
&i.SessionVectorAtPlay,
&i.ScrobbledAt,
&i.Source,
&i.PickKind,
)
return i, err
}
const insertPlayEvent = `-- name: InsertPlayEvent :one
INSERT INTO play_events (
user_id, track_id, session_id, started_at, client_id, source, pick_kind
) VALUES ($1, $2, $3, $4, $5, $6, $7::text)
RETURNING id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind
`
type InsertPlayEventParams struct {
UserID pgtype.UUID
TrackID pgtype.UUID
SessionID pgtype.UUID
StartedAt pgtype.Timestamptz
ClientID *string
Source *string
PickKind *string
}
// pick_kind is non-NULL only for source='for_you' plays whose track was
// found in the user's live For You snapshot at ingestion time (#1249).
func (q *Queries) InsertPlayEvent(ctx context.Context, arg InsertPlayEventParams) (PlayEvent, error) {
row := q.db.QueryRow(ctx, insertPlayEvent,
arg.UserID,
arg.TrackID,
arg.SessionID,
arg.StartedAt,
arg.ClientID,
arg.Source,
arg.PickKind,
)
var i PlayEvent
err := row.Scan(
&i.ID,
&i.UserID,
&i.TrackID,
&i.SessionID,
&i.StartedAt,
&i.EndedAt,
&i.DurationPlayedMs,
&i.CompletionRatio,
&i.WasSkipped,
&i.ClientID,
&i.SessionVectorAtPlay,
&i.ScrobbledAt,
&i.Source,
&i.PickKind,
)
return i, err
}
const insertPlaySession = `-- name: InsertPlaySession :one
INSERT INTO play_sessions (user_id, started_at, last_event_at, client_id)
VALUES ($1, $2, $2, $3)
RETURNING id, user_id, started_at, ended_at, last_event_at, track_count, client_id
`
type InsertPlaySessionParams struct {
UserID pgtype.UUID
StartedAt pgtype.Timestamptz
ClientID *string
}
func (q *Queries) InsertPlaySession(ctx context.Context, arg InsertPlaySessionParams) (PlaySession, error) {
row := q.db.QueryRow(ctx, insertPlaySession, arg.UserID, arg.StartedAt, arg.ClientID)
var i PlaySession
err := row.Scan(
&i.ID,
&i.UserID,
&i.StartedAt,
&i.EndedAt,
&i.LastEventAt,
&i.TrackCount,
&i.ClientID,
)
return i, err
}
const insertSkipEvent = `-- name: InsertSkipEvent :one
INSERT INTO skip_events (user_id, track_id, session_id, skipped_at, position_ms)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, user_id, track_id, session_id, skipped_at, position_ms
`
type InsertSkipEventParams struct {
UserID pgtype.UUID
TrackID pgtype.UUID
SessionID pgtype.UUID
SkippedAt pgtype.Timestamptz
PositionMs int32
}
func (q *Queries) InsertSkipEvent(ctx context.Context, arg InsertSkipEventParams) (SkipEvent, error) {
row := q.db.QueryRow(ctx, insertSkipEvent,
arg.UserID,
arg.TrackID,
arg.SessionID,
arg.SkippedAt,
arg.PositionMs,
)
var i SkipEvent
err := row.Scan(
&i.ID,
&i.UserID,
&i.TrackID,
&i.SessionID,
&i.SkippedAt,
&i.PositionMs,
)
return i, err
}
const listRecentSessionTracks = `-- name: ListRecentSessionTracks :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 FROM tracks t
JOIN play_events pe ON pe.track_id = t.id
WHERE pe.session_id = $1
AND pe.started_at < $2
ORDER BY pe.started_at DESC
LIMIT $3
`
type ListRecentSessionTracksParams struct {
SessionID pgtype.UUID
StartedAt pgtype.Timestamptz
Limit int32
}
// Returns up to $3 tracks in session $1 whose play_event started before
// $2, ordered newest-first. Used by playevents.RecordPlayStarted to
// build the session_vector for the just-inserted play_event.
func (q *Queries) ListRecentSessionTracks(ctx context.Context, arg ListRecentSessionTracksParams) ([]Track, error) {
rows, err := q.db.Query(ctx, listRecentSessionTracks, arg.SessionID, arg.StartedAt, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Track
for rows.Next() {
var i Track
if err := rows.Scan(
&i.ID,
&i.Title,
&i.AlbumID,
&i.ArtistID,
&i.TrackNumber,
&i.DiscNumber,
&i.DurationMs,
&i.FilePath,
&i.FileSize,
&i.FileFormat,
&i.Bitrate,
&i.Mbid,
&i.Genre,
&i.AddedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const touchPlaySessionLastEvent = `-- name: TouchPlaySessionLastEvent :exec
UPDATE play_sessions
SET last_event_at = $2,
track_count = track_count + 1
WHERE id = $1
`
type TouchPlaySessionLastEventParams struct {
ID pgtype.UUID
LastEventAt pgtype.Timestamptz
}
func (q *Queries) TouchPlaySessionLastEvent(ctx context.Context, arg TouchPlaySessionLastEventParams) error {
_, err := q.db.Exec(ctx, touchPlaySessionLastEvent, arg.ID, arg.LastEventAt)
return err
}
const updatePlayEventEnded = `-- name: UpdatePlayEventEnded :one
UPDATE play_events
SET ended_at = $2,
duration_played_ms = $3,
completion_ratio = $4,
was_skipped = $5
WHERE id = $1
RETURNING id, user_id, track_id, session_id, started_at, ended_at, duration_played_ms, completion_ratio, was_skipped, client_id, session_vector_at_play, scrobbled_at, source, pick_kind
`
type UpdatePlayEventEndedParams struct {
ID pgtype.UUID
EndedAt pgtype.Timestamptz
DurationPlayedMs *int32
CompletionRatio *float64
WasSkipped bool
}
// Closes a play_event by id with the given ended_at, duration, and skip flag.
// completion_ratio is computed from duration_played_ms and the track duration
// (looked up by the caller — sqlc doesn't do joins on UPDATE).
func (q *Queries) UpdatePlayEventEnded(ctx context.Context, arg UpdatePlayEventEndedParams) (PlayEvent, error) {
row := q.db.QueryRow(ctx, updatePlayEventEnded,
arg.ID,
arg.EndedAt,
arg.DurationPlayedMs,
arg.CompletionRatio,
arg.WasSkipped,
)
var i PlayEvent
err := row.Scan(
&i.ID,
&i.UserID,
&i.TrackID,
&i.SessionID,
&i.StartedAt,
&i.EndedAt,
&i.DurationPlayedMs,
&i.CompletionRatio,
&i.WasSkipped,
&i.ClientID,
&i.SessionVectorAtPlay,
&i.ScrobbledAt,
&i.Source,
&i.PickKind,
)
return i, err
}
const updatePlayEventVector = `-- name: UpdatePlayEventVector :exec
UPDATE play_events
SET session_vector_at_play = $2
WHERE id = $1
`
type UpdatePlayEventVectorParams struct {
ID pgtype.UUID
SessionVectorAtPlay []byte
}
// Used right after InsertPlayEvent to populate session_vector_at_play
// once the vector has been computed.
func (q *Queries) UpdatePlayEventVector(ctx context.Context, arg UpdatePlayEventVectorParams) error {
_, err := q.db.Exec(ctx, updatePlayEventVector, arg.ID, arg.SessionVectorAtPlay)
return err
}