5faa57634b
The #1249 mechanism (stamp WHY a track is in the snapshot at build time, freeze it onto the play at ingestion, break it down in metrics) generalizes from a For You one-off to the standard for every system mix (#1270): - Migration 0039 widens both pick_kind CHECKs (drop + re-add in the same change) to taste/fresh + Discover's dormant/cross_user/random + tier1-3 for the rule-#131 eligibility ladders. - GetForYouPickKindForTrack becomes GetSystemPickKindForTrack (user, variant, track); ingestion stamps any systemPlaylistSources play from its own variant's live snapshot, live + offline paths. - Discover stamps its candidate bucket on discoverTrack before the interleave, making the 40/30/30 allocation measurable; dedup keeps the taking bucket's stamp. - Metrics replace the for_you special-case with one pick-kind vocabulary — any family with attributed plays gets a breakdown, future stamping mixes need no metrics change. - Web: breakdown sub-rows are now toggled per surface (collapsed by default) so eight stamping mixes don't swamp the card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
393 lines
11 KiB
Go
393 lines
11 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 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 getSystemPickKindForTrack = `-- name: GetSystemPickKindForTrack :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 = $2
|
|
AND pt.track_id = $3
|
|
LIMIT 1
|
|
`
|
|
|
|
type GetSystemPickKindForTrackParams struct {
|
|
UserID pgtype.UUID
|
|
SystemVariant *string
|
|
TrackID pgtype.UUID
|
|
}
|
|
|
|
// Looks a track up in the user's CURRENT snapshot of the given system
|
|
// variant and returns its pick_kind. Used at play-ingestion time to
|
|
// freeze provenance onto the play_event — snapshots rebuild daily, so
|
|
// attribution can't be reconstructed at read time (#1249/#1270). No
|
|
// row = track not in today's snapshot (caller stores NULL); a row with
|
|
// NULL pick_kind = the variant doesn't stamp (yet). songs_like_artist
|
|
// is non-singleton (up to 3 mixes/user); a track in two of them takes
|
|
// whichever LIMIT 1 hits — acceptable, tier stamps there describe the
|
|
// same eligibility ladder.
|
|
func (q *Queries) GetSystemPickKindForTrack(ctx context.Context, arg GetSystemPickKindForTrackParams) (*string, error) {
|
|
row := q.db.QueryRow(ctx, getSystemPickKindForTrack, arg.UserID, arg.SystemVariant, arg.TrackID)
|
|
var pick_kind *string
|
|
err := row.Scan(&pick_kind)
|
|
return pick_kind, 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 system-playlist plays whose track was
|
|
// found (with a stamped kind) in the user's live snapshot for that
|
|
// variant at ingestion time (#1249, generalized in #1270).
|
|
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
|
|
}
|