// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.31.1
// source: tracks.sql

package dbq

import (
	"context"

	"github.com/jackc/pgx/v5/pgtype"
)

const adoptTrackPath = `-- name: AdoptTrackPath :execrows
UPDATE tracks
   SET file_path = $1,
       missing_since = NULL
 WHERE id = $2
   AND missing_since IS NOT NULL
`

type AdoptTrackPathParams struct {
	FilePath string
	ID       pgtype.UUID
}

// Re-points a missing row at the path its file turned up on, and clears the
// mark. The caller's normal UpsertTrack then conflicts on file_path and updates
// THIS row in place, so the track id survives and its likes, play history and
// playlist memberships come with it.
//
// `missing_since IS NOT NULL` again, this time as a race guard: two files can't
// both adopt the same row, and :execrows reports 0 to whichever loses.
func (q *Queries) AdoptTrackPath(ctx context.Context, arg AdoptTrackPathParams) (int64, error) {
	result, err := q.db.Exec(ctx, adoptTrackPath, arg.FilePath, arg.ID)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected(), nil
}

const clearTracksMissing = `-- name: ClearTracksMissing :execrows
UPDATE tracks
   SET missing_since = NULL
 WHERE id = ANY($1::uuid[])
   AND missing_since IS NOT NULL
`

// Clears the mark on rows whose file is back. Runs independently of the mtime
// skip check, so a file that reappears unchanged is un-marked even though the
// scanner skips re-reading its tags.
func (q *Queries) ClearTracksMissing(ctx context.Context, ids []pgtype.UUID) (int64, error) {
	result, err := q.db.Exec(ctx, clearTracksMissing, ids)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected(), nil
}

const countTracksByAlbum = `-- name: CountTracksByAlbum :one
SELECT count(*) FROM tracks WHERE album_id = $1
`

func (q *Queries) CountTracksByAlbum(ctx context.Context, albumID pgtype.UUID) (int64, error) {
	row := q.db.QueryRow(ctx, countTracksByAlbum, albumID)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const countTracksByArtist = `-- name: CountTracksByArtist :one
SELECT COUNT(*) FROM tracks WHERE artist_id = $1
`

// Used by request-progress reporting to count tracks ingested under a
// matched artist (sum across all the artist's albums) while a request
// is still in flight.
func (q *Queries) CountTracksByArtist(ctx context.Context, artistID pgtype.UUID) (int64, error) {
	row := q.db.QueryRow(ctx, countTracksByArtist, artistID)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const countTracksMatching = `-- name: CountTracksMatching :one
SELECT COUNT(*) FROM tracks
WHERE title ILIKE '%' || $1::text || '%'
  AND NOT EXISTS (
    SELECT 1 FROM lidarr_quarantine q
    WHERE q.user_id = $2 AND q.track_id = tracks.id
  )
`

type CountTracksMatchingParams struct {
	Column1 string
	UserID  pgtype.UUID
}

// $1 = title query, $2 = user_id (NULL to skip quarantine filter).
func (q *Queries) CountTracksMatching(ctx context.Context, arg CountTracksMatchingParams) (int64, error) {
	row := q.db.QueryRow(ctx, countTracksMatching, arg.Column1, arg.UserID)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const deleteTrack = `-- name: DeleteTrack :one
DELETE FROM tracks WHERE id = $1
RETURNING id, album_id, artist_id, file_path, mbid
`

type DeleteTrackRow struct {
	ID       pgtype.UUID
	AlbumID  pgtype.UUID
	ArtistID pgtype.UUID
	FilePath string
	Mbid     *string
}

// M7 #372: hard delete with FK cascade. The CASCADE on track_id from
// play_events / general_likes_tracks / lidarr_quarantine /
// lidarr_quarantine_actions handles their cleanup. RETURNING gives us
// album_id + artist_id for the album-empty / artist-empty cascade
// checks the service does next.
func (q *Queries) DeleteTrack(ctx context.Context, id pgtype.UUID) (DeleteTrackRow, error) {
	row := q.db.QueryRow(ctx, deleteTrack, id)
	var i DeleteTrackRow
	err := row.Scan(
		&i.ID,
		&i.AlbumID,
		&i.ArtistID,
		&i.FilePath,
		&i.Mbid,
	)
	return i, err
}

const findMissingTrackByFingerprint = `-- name: FindMissingTrackByFingerprint :many
SELECT id, file_path FROM tracks
 WHERE missing_since IS NOT NULL
   AND file_size = $1
   AND duration_ms = $2
 LIMIT 2
`

type FindMissingTrackByFingerprintParams struct {
	FileSize   int64
	DurationMs int32
}

type FindMissingTrackByFingerprintRow struct {
	ID       pgtype.UUID
	FilePath string
}

// Move detection fallback for files with no MBID (#2528). Exact byte size AND
// exact decoded duration is a strong pair: a plain move or rename preserves
// both, while a re-encode changes at least one — and a re-encode genuinely is a
// different file, so failing to match there is correct rather than a gap.
//
// Same missing-only constraint and same LIMIT 2 rationale as the MBID variant.
func (q *Queries) FindMissingTrackByFingerprint(ctx context.Context, arg FindMissingTrackByFingerprintParams) ([]FindMissingTrackByFingerprintRow, error) {
	rows, err := q.db.Query(ctx, findMissingTrackByFingerprint, arg.FileSize, arg.DurationMs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []FindMissingTrackByFingerprintRow
	for rows.Next() {
		var i FindMissingTrackByFingerprintRow
		if err := rows.Scan(&i.ID, &i.FilePath); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const findMissingTrackByMbid = `-- name: FindMissingTrackByMbid :many
SELECT id, file_path FROM tracks
 WHERE missing_since IS NOT NULL
   AND mbid IS NOT NULL
   AND mbid = $1::text
 LIMIT 2
`

type FindMissingTrackByMbidRow struct {
	ID       pgtype.UUID
	FilePath string
}

// Move detection, strongest signal (#2528). A file that turned up at a new path
// carrying a recording MBID we already have on a MISSING row is that recording,
// moved — not a new track.
//
// `missing_since IS NOT NULL` is the safety constraint, not an optimisation: a
// row whose file is present elsewhere on disk is a DUPLICATE, and re-pointing
// its file_path would corrupt the copy that still exists.
//
// LIMIT 2 because the caller only needs to know "exactly one" vs "more than
// one" — an ambiguous match must not be adopted arbitrarily.
func (q *Queries) FindMissingTrackByMbid(ctx context.Context, mbid string) ([]FindMissingTrackByMbidRow, error) {
	rows, err := q.db.Query(ctx, findMissingTrackByMbid, mbid)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []FindMissingTrackByMbidRow
	for rows.Next() {
		var i FindMissingTrackByMbidRow
		if err := rows.Scan(&i.ID, &i.FilePath); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTrackByID = `-- name: GetTrackByID :one
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at, tag_source, tag_sources_version, tag_read_version, missing_since FROM tracks WHERE id = $1
`

func (q *Queries) GetTrackByID(ctx context.Context, id pgtype.UUID) (Track, error) {
	row := q.db.QueryRow(ctx, getTrackByID, id)
	var i Track
	err := row.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,
	)
	return i, err
}

const getTrackByPath = `-- name: GetTrackByPath :one
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at, tag_source, tag_sources_version, tag_read_version, missing_since FROM tracks WHERE file_path = $1
`

func (q *Queries) GetTrackByPath(ctx context.Context, filePath string) (Track, error) {
	row := q.db.QueryRow(ctx, getTrackByPath, filePath)
	var i Track
	err := row.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,
	)
	return i, err
}

const getTracksByIDs = `-- name: GetTracksByIDs :many
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at, tag_source, tag_sources_version, tag_read_version, missing_since FROM tracks WHERE id = ANY($1::uuid[])
`

// Batched lookup used by /api/library/sync to hydrate upsert payloads
// (#357). Mirror of GetArtistsByIDs.
func (q *Queries) GetTracksByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Track, error) {
	rows, err := q.db.Query(ctx, getTracksByIDs, dollar_1)
	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 listArtistTracksForUser = `-- name: ListArtistTracksForUser :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,
       albums.title AS album_title,
       artists.name AS artist_name
FROM tracks t
JOIN albums  ON albums.id  = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.artist_id = $1
  AND NOT EXISTS (
      SELECT 1 FROM lidarr_quarantine q
      WHERE q.user_id = $2 AND q.track_id = t.id
  )
ORDER BY albums.release_date NULLS LAST, albums.sort_title,
         t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.id
`

type ListArtistTracksForUserParams struct {
	ArtistID pgtype.UUID
	UserID   pgtype.UUID
}

type ListArtistTracksForUserRow struct {
	Track      Track
	AlbumTitle string
	ArtistName string
}

// M6a: every track for the artist across their albums, with album_title
// and artist_name joined. Honors per-user lidarr_quarantine. Used by
// /api/artists/{id}/tracks for the artist-card play affordance, which
// shuffles client-side. Ordering matches album/track natural order so
// the shuffle has a deterministic input.
func (q *Queries) ListArtistTracksForUser(ctx context.Context, arg ListArtistTracksForUserParams) ([]ListArtistTracksForUserRow, error) {
	rows, err := q.db.Query(ctx, listArtistTracksForUser, arg.ArtistID, arg.UserID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListArtistTracksForUserRow
	for rows.Next() {
		var i ListArtistTracksForUserRow
		if err := rows.Scan(
			&i.Track.ID,
			&i.Track.Title,
			&i.Track.AlbumID,
			&i.Track.ArtistID,
			&i.Track.TrackNumber,
			&i.Track.DiscNumber,
			&i.Track.DurationMs,
			&i.Track.FilePath,
			&i.Track.FileSize,
			&i.Track.FileFormat,
			&i.Track.Bitrate,
			&i.Track.Mbid,
			&i.Track.Genre,
			&i.Track.AddedAt,
			&i.Track.UpdatedAt,
			&i.Track.TagSource,
			&i.Track.TagSourcesVersion,
			&i.Track.TagReadVersion,
			&i.Track.MissingSince,
			&i.AlbumTitle,
			&i.ArtistName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listRandomTracksForUser = `-- name: ListRandomTracksForUser :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,
       albums.title AS album_title,
       artists.name AS artist_name
FROM tracks t
JOIN albums  ON albums.id  = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE NOT EXISTS (
      SELECT 1 FROM lidarr_quarantine q
      WHERE q.user_id = $1 AND q.track_id = t.id
  )
ORDER BY random()
LIMIT $2
`

type ListRandomTracksForUserParams struct {
	UserID pgtype.UUID
	Limit  int32
}

type ListRandomTracksForUserRow struct {
	Track      Track
	AlbumTitle string
	ArtistName string
}

// #427 S4: backing query for GET /api/library/shuffle — the online
// source for "Shuffle all". N random tracks across the whole
// library, per-user-quarantine filtered. $1 user_id, $2 limit.
func (q *Queries) ListRandomTracksForUser(ctx context.Context, arg ListRandomTracksForUserParams) ([]ListRandomTracksForUserRow, error) {
	rows, err := q.db.Query(ctx, listRandomTracksForUser, arg.UserID, arg.Limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListRandomTracksForUserRow
	for rows.Next() {
		var i ListRandomTracksForUserRow
		if err := rows.Scan(
			&i.Track.ID,
			&i.Track.Title,
			&i.Track.AlbumID,
			&i.Track.ArtistID,
			&i.Track.TrackNumber,
			&i.Track.DiscNumber,
			&i.Track.DurationMs,
			&i.Track.FilePath,
			&i.Track.FileSize,
			&i.Track.FileFormat,
			&i.Track.Bitrate,
			&i.Track.Mbid,
			&i.Track.Genre,
			&i.Track.AddedAt,
			&i.Track.UpdatedAt,
			&i.Track.TagSource,
			&i.Track.TagSourcesVersion,
			&i.Track.TagReadVersion,
			&i.Track.MissingSince,
			&i.AlbumTitle,
			&i.ArtistName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listTrackPathsForReconcile = `-- name: ListTrackPathsForReconcile :many
SELECT id, file_path, missing_since FROM tracks
`

type ListTrackPathsForReconcileRow struct {
	ID           pgtype.UUID
	FilePath     string
	MissingSince pgtype.Timestamptz
}

// Every row's path + current missing mark, for the scanner's reconcile pass
// (#2523). Deliberately unfiltered and unpaged: reconcile has to compare the
// WHOLE table against what the walk saw, and a filtered subset would let rows
// outside it drift forever. Three narrow columns keep it cheap even on a
// library of a few hundred thousand tracks.
func (q *Queries) ListTrackPathsForReconcile(ctx context.Context) ([]ListTrackPathsForReconcileRow, error) {
	rows, err := q.db.Query(ctx, listTrackPathsForReconcile)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListTrackPathsForReconcileRow
	for rows.Next() {
		var i ListTrackPathsForReconcileRow
		if err := rows.Scan(&i.ID, &i.FilePath, &i.MissingSince); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listTracksByAlbum = `-- name: ListTracksByAlbum :many
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at, tag_source, tag_sources_version, tag_read_version, missing_since FROM tracks
WHERE album_id = $1
  AND NOT EXISTS (
    SELECT 1 FROM lidarr_quarantine q
    WHERE q.user_id = $2 AND q.track_id = tracks.id
  )
ORDER BY disc_number NULLS LAST, track_number NULLS LAST
`

type ListTracksByAlbumParams struct {
	AlbumID pgtype.UUID
	UserID  pgtype.UUID
}

// $1 = album_id, $2 = user_id. Pass pgtype.UUID{Valid: false} (NULL)
// to skip the per-user quarantine filter; the NOT EXISTS clause on
// a NULL user_id never matches a row, so every track passes through.
func (q *Queries) ListTracksByAlbum(ctx context.Context, arg ListTracksByAlbumParams) ([]Track, error) {
	rows, err := q.db.Query(ctx, listTracksByAlbum, arg.AlbumID, arg.UserID)
	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 listTracksMissingMbidWithPath = `-- name: ListTracksMissingMbidWithPath :many
SELECT id, file_path
  FROM tracks
 WHERE mbid IS NULL
 ORDER BY id
 LIMIT $1
`

type ListTracksMissingMbidWithPathRow struct {
	ID       pgtype.UUID
	FilePath string
}

// Track recording-MBID backfill: tracks with NULL mbid that still have
// a file to re-read. $1 caps the batch (mirrors the album backfill).
func (q *Queries) ListTracksMissingMbidWithPath(ctx context.Context, limit int32) ([]ListTracksMissingMbidWithPathRow, error) {
	rows, err := q.db.Query(ctx, listTracksMissingMbidWithPath, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListTracksMissingMbidWithPathRow
	for rows.Next() {
		var i ListTracksMissingMbidWithPathRow
		if err := rows.Scan(&i.ID, &i.FilePath); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const markTracksMissing = `-- name: MarkTracksMissing :execrows
UPDATE tracks
   SET missing_since = now()
 WHERE id = ANY($1::uuid[])
   AND missing_since IS NULL
`

// Marks rows whose file the walk did not see. `missing_since IS NULL` in the
// predicate makes this idempotent: a row already marked keeps its ORIGINAL
// timestamp, so "how long has it been gone" survives repeated scans. Losing
// that would make any age-based cleanup policy meaningless.
//
// updated_at is deliberately NOT touched. It tracks content changes and gates
// the scanner's mtime skip; moving it here would make a returning file look
// newer than its own mtime and stop its tags being re-read.
func (q *Queries) MarkTracksMissing(ctx context.Context, ids []pgtype.UUID) (int64, error) {
	result, err := q.db.Exec(ctx, markTracksMissing, ids)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected(), nil
}

const searchTracks = `-- name: SearchTracks :many
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at, tag_source, tag_sources_version, tag_read_version, missing_since FROM tracks
WHERE title ILIKE '%' || $1::text || '%'
  AND NOT EXISTS (
    SELECT 1 FROM lidarr_quarantine q
    WHERE q.user_id = $2 AND q.track_id = tracks.id
  )
ORDER BY title
LIMIT $3 OFFSET $4
`

type SearchTracksParams struct {
	Column1 string
	UserID  pgtype.UUID
	Limit   int32
	Offset  int32
}

// $1 = title query, $2 = user_id (NULL to skip quarantine filter),
// $3 = limit, $4 = offset.
func (q *Queries) SearchTracks(ctx context.Context, arg SearchTracksParams) ([]Track, error) {
	rows, err := q.db.Query(ctx, searchTracks,
		arg.Column1,
		arg.UserID,
		arg.Limit,
		arg.Offset,
	)
	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 setTrackMbidIfNull = `-- name: SetTrackMbidIfNull :exec
UPDATE tracks
   SET mbid = $2, updated_at = now()
 WHERE id = $1 AND mbid IS NULL
`

type SetTrackMbidIfNullParams struct {
	ID   pgtype.UUID
	Mbid *string
}

// Heal a track's recording MBID only while still NULL — idempotent, so
// re-running the backfill is a no-op for already-healed rows.
func (q *Queries) SetTrackMbidIfNull(ctx context.Context, arg SetTrackMbidIfNullParams) error {
	_, err := q.db.Exec(ctx, setTrackMbidIfNull, arg.ID, arg.Mbid)
	return err
}

const upsertTrack = `-- name: UpsertTrack :one
INSERT INTO tracks (
    title, album_id, artist_id, track_number, disc_number,
    duration_ms, file_path, file_size, file_format, bitrate, mbid, genre,
    tag_read_version
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (file_path) DO UPDATE SET
    title = EXCLUDED.title,
    album_id = EXCLUDED.album_id,
    artist_id = EXCLUDED.artist_id,
    track_number = EXCLUDED.track_number,
    disc_number = EXCLUDED.disc_number,
    duration_ms = EXCLUDED.duration_ms,
    file_size = EXCLUDED.file_size,
    file_format = EXCLUDED.file_format,
    bitrate = EXCLUDED.bitrate,
    mbid = EXCLUDED.mbid,
    genre = EXCLUDED.genre,
    -- Stamped on update too, so a tag-repair pass marks rows as done and the
    -- next scan can short-circuit them again (#2499).
    tag_read_version = EXCLUDED.tag_read_version,
    updated_at = now()
RETURNING id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at, tag_source, tag_sources_version, tag_read_version, missing_since
`

type UpsertTrackParams struct {
	Title          string
	AlbumID        pgtype.UUID
	ArtistID       pgtype.UUID
	TrackNumber    *int32
	DiscNumber     *int32
	DurationMs     int32
	FilePath       string
	FileSize       int64
	FileFormat     string
	Bitrate        *int32
	Mbid           *string
	Genre          *string
	TagReadVersion int16
}

// file_path is the canonical identity for library scan; mbid is secondary.
func (q *Queries) UpsertTrack(ctx context.Context, arg UpsertTrackParams) (Track, error) {
	row := q.db.QueryRow(ctx, upsertTrack,
		arg.Title,
		arg.AlbumID,
		arg.ArtistID,
		arg.TrackNumber,
		arg.DiscNumber,
		arg.DurationMs,
		arg.FilePath,
		arg.FileSize,
		arg.FileFormat,
		arg.Bitrate,
		arg.Mbid,
		arg.Genre,
		arg.TagReadVersion,
	)
	var i Track
	err := row.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,
	)
	return i, err
}
