textmachine/backend/internal/store/jobs.go

98 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package store
import (
"context"
"database/sql"
"errors"
)
// jobs.go: snapshots + durable jobs. A job = chapter×stage; durability is per-chunk
// (checkpoints, see ledger.go). A snapshot materializes the job context so the
// request-hash is deterministic for its entire lifetime (§3.13.2).
// UpsertSnapshot stores a materialized job-context snapshot (idempotent: the
// snapshot_id is itself a content hash, so re-inserting the same id is a
// no-op).
func (s *Store) UpsertSnapshot(snapshotID, briefHash, payloadJSON string) error {
ctx, cancel := opContext()
defer cancel()
_, err := s.w.ExecContext(ctx, `
INSERT INTO snapshots (snapshot_id, brief_hash, payload) VALUES (?, ?, ?)
ON CONFLICT (snapshot_id) DO NOTHING`,
snapshotID, briefHash, payloadJSON)
return err
}
// Job is one chapter×stage unit of work.
type Job struct {
ID int64
BookID string
Chapter int
Stage string
Status string
SnapshotID string
}
// EnsureJob returns the existing job for (book, chapter, stage) or creates it
// bound to snapshotID. An existing job KEEPS its original snapshot — Р6:
// volatile parts are excluded from the resume key, confirming a term between
// runs must not invalidate already-translated chunks.
func (s *Store) EnsureJob(bookID string, chapter int, stage, snapshotID string) (*Job, error) {
ctx, cancel := opContext()
defer cancel()
if _, err := s.w.ExecContext(ctx, `
INSERT INTO jobs (book_id, chapter, stage, snapshot_id) VALUES (?, ?, ?, ?)
ON CONFLICT (book_id, chapter, stage) DO NOTHING`,
bookID, chapter, stage, snapshotID); err != nil {
return nil, err
}
return s.getJob(ctx, bookID, chapter, stage)
}
func (s *Store) getJob(ctx context.Context, bookID string, chapter int, stage string) (*Job, error) {
j := Job{BookID: bookID, Chapter: chapter, Stage: stage}
err := s.r.QueryRowContext(ctx,
`SELECT id, status, snapshot_id FROM jobs WHERE book_id = ? AND chapter = ? AND stage = ?`,
bookID, chapter, stage).Scan(&j.ID, &j.Status, &j.SnapshotID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &j, nil
}
// SetJobStatus transitions a job.
func (s *Store) SetJobStatus(jobID int64, status string) error {
ctx, cancel := opContext()
defer cancel()
_, err := s.w.ExecContext(ctx,
`UPDATE jobs SET status = ?, updated_at = datetime('now') WHERE id = ?`, status, jobID)
return err
}
// SnapshotPayload returns the stored payload JSON of a snapshot, or "" when the id is unknown (an id
// written by an older schema, or a row whose snapshot row was never upserted). Read-only; the caller
// treats an unknown id as "cannot reason about this move" and falls back to the conservative answer.
func (s *Store) SnapshotPayload(snapshotID string) (string, error) {
ctx, cancel := opContext()
defer cancel()
var payload string
err := s.r.QueryRowContext(ctx, `SELECT payload FROM snapshots WHERE snapshot_id = ?`, snapshotID).Scan(&payload)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
}
return payload, err
}
// UpdateJobSnapshot re-pins a job to a new snapshot — an EXPLICIT operator
// command (tmctl --resnapshot): re-translating already-paid-for chunks happens
// only deliberately, never as a silent side effect of a config edit (Р6).
func (s *Store) UpdateJobSnapshot(jobID int64, snapshotID string) error {
ctx, cancel := opContext()
defer cancel()
_, err := s.w.ExecContext(ctx,
`UPDATE jobs SET snapshot_id = ?, updated_at = datetime('now') WHERE id = ?`, snapshotID, jobID)
return err
}