diff --git a/internal/adapters/store/uploaded_transcripts.go b/internal/adapters/store/uploaded_transcripts.go index c62fb5a..4d30e7a 100644 --- a/internal/adapters/store/uploaded_transcripts.go +++ b/internal/adapters/store/uploaded_transcripts.go @@ -17,6 +17,10 @@ import ( // nothing about whether someone else's upload exists (ADR-033). var ErrUploadNotFound = errors.New("store: uploaded transcript not found") +// ErrUploadInProgress is returned by DeleteUpload for an upload that is still queued or +// transcribing: the worker would write its result to a row that no longer exists. +var ErrUploadInProgress = errors.New("store: uploaded transcript is still being processed") + // Uploaded transcripts are PRIVATE per-user content, stored in their own // RLS-scoped table (migration 017) — never in the shared `transcripts` (ADR-021). // Every method goes through withUser, so row-level security confines it to the @@ -103,6 +107,33 @@ func (s *Store) FailUpload(ctx context.Context, userID, id string, kind domain.U string(kind)) } +// DeleteUpload removes one of the caller's finished (done or failed) uploads. An upload +// still queued or transcribing is refused with ErrUploadInProgress and left untouched; one +// that is not the caller's is ErrUploadNotFound, as for every other write (#37). +func (s *Store) DeleteUpload(ctx context.Context, userID, id string) error { + return s.withUser(ctx, userID, func(tx pgx.Tx) error { + ct, err := tx.Exec(ctx, + `DELETE FROM uploaded_transcripts + WHERE id = $1 AND user_id = $2 AND status IN ('done', 'failed')`, id, userID) + if err != nil { + return fmt.Errorf("store: delete upload: %w", err) + } + if ct.RowsAffected() > 0 { + return nil + } + var exists bool + if err := tx.QueryRow(ctx, + `SELECT EXISTS (SELECT 1 FROM uploaded_transcripts WHERE id = $1 AND user_id = $2)`, + id, userID).Scan(&exists); err != nil { + return fmt.Errorf("store: delete upload: %w", err) + } + if exists { + return ErrUploadInProgress + } + return ErrUploadNotFound + }) +} + // updateUpload runs one UPDATE keyed on ($1 = id, $2 = userID) plus extra args, // and maps "no row touched" to ErrUploadNotFound — which is what a write to another // user's upload looks like under RLS (the row is invisible, not forbidden).