feat(server): scanner + DeleteTrackFile write library_changes
Wires sync.LogChange into the library mutation sites so /api/library/sync reflects upserts and deletes. Architectural pivot: LogChange's signature is now (ctx, dbq.DBTX, ...) so it works with both *pgxpool.Pool and pgx.Tx. The scanner doesn't run mutations in explicit transactions, so it pool-binds; delete.go matches. Tx-bound callers (likes/playlists in subsequent commits) keep atomicity. Also: sync.FormatUUID centralizes the pgtype.UUID → canonical string conversion that both the scanner and the sync handler need; library_sync.go now uses it instead of a local copy. Best-effort logging on scanner failures (Warn, don't fail the scan): a LogChange error after a successful upsert is rare and self-healing — the next scan that touches the entity re-emits the change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,20 +4,28 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
)
|
||||
|
||||
// LogChange writes a row to library_changes inside the supplied transaction.
|
||||
// Callers MUST pass the same tx that performed the underlying mutation —
|
||||
// the change row and the mutation must commit or roll back together.
|
||||
// LogChange writes a row to library_changes via the supplied DBTX. dbq.DBTX
|
||||
// is satisfied by both *pgxpool.Pool and pgx.Tx, so callers can opt into
|
||||
// transactional safety where they already hold a tx, or pool-bind for
|
||||
// best-effort logging when no tx is available.
|
||||
//
|
||||
// Callers SHOULD pass a tx that owns the underlying mutation — the change
|
||||
// row and the mutation then commit or roll back together. When pool-bound,
|
||||
// the scanner-style "log after success" pattern keeps the log consistent
|
||||
// with reality at the cost of a tiny race window: if the LogChange call
|
||||
// itself fails after the mutation succeeded, that mutation won't appear in
|
||||
// the change log until the entity is touched again.
|
||||
//
|
||||
// entityID is the string form of the row's primary key. For composite-key
|
||||
// entities (likes, playlist_tracks), use EncodeLikeID / EncodePlaylistTrackID
|
||||
// to produce stable strings.
|
||||
func LogChange(ctx context.Context, tx pgx.Tx, entityType EntityType, entityID string, op Op) error {
|
||||
q := dbq.New(tx)
|
||||
// to produce stable strings. For pgtype.UUID values, use FormatUUID.
|
||||
func LogChange(ctx context.Context, dbtx dbq.DBTX, entityType EntityType, entityID string, op Op) error {
|
||||
q := dbq.New(dbtx)
|
||||
if err := q.InsertLibraryChange(ctx, dbq.InsertLibraryChangeParams{
|
||||
EntityType: string(entityType),
|
||||
EntityID: entityID,
|
||||
@@ -28,6 +36,31 @@ func LogChange(ctx context.Context, tx pgx.Tx, entityType EntityType, entityID s
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormatUUID renders a pgtype.UUID as the canonical 8-4-4-4-12 hex form.
|
||||
// Returns "" if the UUID is not valid.
|
||||
func FormatUUID(u pgtype.UUID) string {
|
||||
if !u.Valid {
|
||||
return ""
|
||||
}
|
||||
return formatUUIDBytes(u.Bytes)
|
||||
}
|
||||
|
||||
func formatUUIDBytes(b [16]byte) string {
|
||||
const hex = "0123456789abcdef"
|
||||
out := make([]byte, 36)
|
||||
pos := 0
|
||||
for i := 0; i < 16; i++ {
|
||||
if i == 4 || i == 6 || i == 8 || i == 10 {
|
||||
out[pos] = '-'
|
||||
pos++
|
||||
}
|
||||
out[pos] = hex[b[i]>>4]
|
||||
out[pos+1] = hex[b[i]&0x0f]
|
||||
pos += 2
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// EncodeLikeID joins a user UUID and an entity UUID into the stable
|
||||
// composite identifier used for like_* rows in library_changes. Both
|
||||
// sides are UUID strings so no escaping is needed.
|
||||
|
||||
Reference in New Issue
Block a user