feat(store): persist and read back the reconciliation
CompleteUpload writes it (SQL NULL when there was none, never the JSON text 'null': the page claims a second opinion only if the column is non-NULL); GetUpload reads it, List does not. Mutation-checked. domain.go and the store are one commit: the store tests need the new field. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,12 +52,20 @@ func (s *Store) CompleteUpload(ctx context.Context, userID, id string, t domain.
|
||||
if err != nil {
|
||||
return fmt.Errorf("store: complete upload: marshal segments: %w", err)
|
||||
}
|
||||
// A nil Reconciliation must be SQL NULL, not the JSON text "null": the page decides
|
||||
// whether to claim a second opinion by whether this column is NULL.
|
||||
var rec []byte
|
||||
if t.Reconciliation != nil {
|
||||
if rec, err = json.Marshal(t.Reconciliation); err != nil {
|
||||
return fmt.Errorf("store: complete upload: marshal reconciliation: %w", err)
|
||||
}
|
||||
}
|
||||
return s.updateUpload(ctx, userID, id, "complete upload",
|
||||
`UPDATE uploaded_transcripts
|
||||
SET status = 'done', content = $3, segments = $4, endpoint = $5,
|
||||
audio_seconds = $6, error_kind = NULL, finished_at = NOW()
|
||||
audio_seconds = $6, reconciliation = $7, error_kind = NULL, finished_at = NOW()
|
||||
WHERE id = $1 AND user_id = $2`,
|
||||
t.Text, segs, t.Endpoint, t.AudioSeconds)
|
||||
t.Text, segs, t.Endpoint, t.AudioSeconds, rec)
|
||||
}
|
||||
|
||||
// FailUpload moves an upload to failed with a typed reason.
|
||||
@@ -157,19 +165,19 @@ func (s *Store) FailInterruptedUploads(ctx context.Context) (int, error) {
|
||||
}
|
||||
|
||||
const uploadSelect = `SELECT id, user_id, title, status, content, segments, endpoint,
|
||||
audio_seconds, error_kind, had_video, created_at, finished_at FROM uploaded_transcripts`
|
||||
audio_seconds, error_kind, had_video, reconciliation, created_at, finished_at FROM uploaded_transcripts`
|
||||
|
||||
// scanUpload scans one uploadSelect row. withText controls whether the transcript
|
||||
// body and segments are kept (GetUpload) or dropped (ListUploads).
|
||||
func scanUpload(row pgx.Row, u *domain.UploadedTranscript, withText bool) error {
|
||||
var (
|
||||
status, errKind, endpoint, content *string
|
||||
segments []byte
|
||||
segments, reconciliation []byte
|
||||
seconds *float64
|
||||
finished *time.Time
|
||||
)
|
||||
if err := row.Scan(&u.ID, &u.UserID, &u.Title, &status, &content, &segments, &endpoint,
|
||||
&seconds, &errKind, &u.HadVideo, &u.CreatedAt, &finished); err != nil {
|
||||
&seconds, &errKind, &u.HadVideo, &reconciliation, &u.CreatedAt, &finished); err != nil {
|
||||
return err
|
||||
}
|
||||
u.Status = domain.UploadStatus(deref(status))
|
||||
@@ -184,6 +192,13 @@ func scanUpload(row pgx.Row, u *domain.UploadedTranscript, withText bool) error
|
||||
if !withText {
|
||||
return nil
|
||||
}
|
||||
if len(reconciliation) > 0 {
|
||||
var r domain.Reconciliation
|
||||
if err := json.Unmarshal(reconciliation, &r); err != nil {
|
||||
return fmt.Errorf("unmarshal reconciliation: %w", err)
|
||||
}
|
||||
u.Reconciliation = &r
|
||||
}
|
||||
u.Content = deref(content)
|
||||
if len(segments) > 0 {
|
||||
if err := json.Unmarshal(segments, &u.Segments); err != nil {
|
||||
|
||||
@@ -214,7 +214,10 @@ type UploadedTranscript struct {
|
||||
// HadVideo records that the uploaded file carried a video track. Only its audio
|
||||
// was used and the video was discarded; the page says so. It is set at upload
|
||||
// and never cleared (ADR-033).
|
||||
HadVideo bool
|
||||
CreatedAt time.Time
|
||||
FinishedAt time.Time // zero until the job reaches done or failed
|
||||
HadVideo bool
|
||||
// Reconciliation is how a merged transcript came about, or nil for a single
|
||||
// engine (ADR-034). Set by Get, not by List.
|
||||
Reconciliation *Reconciliation
|
||||
CreatedAt time.Time
|
||||
FinishedAt time.Time // zero until the job reaches done or failed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user