Files
minstrel/internal/db/dbq/events.sql.go
T
bvandeusen f6d1cf24f0
test-go / test (push) Successful in 1m0s
test-go / integration (push) Successful in 5m10s
feat(library): detect missing files and stop offering them — #2523
Nothing in Minstrel ever noticed a deleted file. The walk only visits
paths that exist, so a row whose file was gone was never scanned, never
errored, never counted — permanently invisible. classifyEvent ignores
fsnotify removals by design, and the safety-net scan is the same walk, so
it covers additions only. Rows accumulated forever.

Found on the operator's library: a completed scan reported
skipped=24185 errored=0 while the MBID backfill (which opens files by DB
path rather than walking) logged ~40 "no such file or directory" across
three reorganised albums. Those rows also kept their pre-#2499 welded
genre, which is how this surfaced — the version-stamped tag re-read can
only reach files the walk visits.

The harm is not cosmetic. tracks is the candidate universe for
recommendation.sql / discover.sql / system_mixes.sql and nothing filtered
on file existence, so a mix could spend a slot on a track that cannot
stream.

Marks rather than deletes. A missing file is a claim about the filesystem
and the filesystem lies transiently — an unmounted volume, a network
blip, a container that started before its media mount attached. Every
sweep in internal/gc resolves a truth INSIDE the database and is safe to
run blind; this one is not, so no deletion happens here. Three guards
refuse to act on ambiguous evidence: every scan root must resolve to a
non-empty directory, the walk must have seen at least one file, and one
reconcile may newly mark at most 25% of the library. Clearing a mark is
never the dangerous direction, so it runs unconditionally — otherwise a
library that tripped the cap could never recover once the mount returned.

Only a full Scan reconciles. The walk's set of seen paths is the
evidence, and ScanFiles has no basis for concluding anything about files
it did not look at.

Excludes marked tracks from all 13 track-emitting queries (radio x2,
system mixes x5, discover x4, most-played x2), the 6 play-history seed
picks, and the genre browse axis. Deliberately NOT filtered: the shared
ListPlaylistTracks read path, because it also serves user-curated
playlists where hiding a track the user added would be wrong — system
playlists shed orphans on their next daily rebuild instead. History and
the taste profile also keep them: those record the past, and a track you
played 200 times still says something about your taste.

Reconcile tallies land in scan_runs so a disappearance is visible rather
than discovered when a mix comes up short.
2026-08-06 14:34:53 -04:00

405 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, device_class 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,
&i.DeviceClass,
)
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, device_class 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,
&i.DeviceClass,
)
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,
device_class
) VALUES ($1, $2, $3, $4, $5, $6, $7::text,
$8::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, device_class
`
type InsertPlayEventParams struct {
UserID pgtype.UUID
TrackID pgtype.UUID
SessionID pgtype.UUID
StartedAt pgtype.Timestamptz
ClientID *string
Source *string
PickKind *string
DeviceClass *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,
arg.DeviceClass,
)
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,
&i.DeviceClass,
)
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, t.tag_source, t.tag_sources_version, t.tag_read_version, t.missing_since 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,
&i.TagSource,
&i.TagSourcesVersion,
&i.TagReadVersion,
&i.MissingSince,
); 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, device_class
`
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,
&i.DeviceClass,
)
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
}