212 lines
11 KiB
SQL
212 lines
11 KiB
SQL
-- +goose Up
|
|
|
|
-- The reporting database (research/23 §3): state materialized from the engine's event stream and
|
|
-- from `tmctl status --json`. It is NOT an event store — no history, no replay. Every table below
|
|
-- is a projection the API reads directly.
|
|
--
|
|
-- Vocabularies are checked in DDL because the PLATFORM owns them: these values are produced by our
|
|
-- own materializer, so an unknown one is a defect, not data. Engine vocabulary (stage names, flag
|
|
-- reasons, dispositions) is deliberately absent — it never crosses this seam.
|
|
|
|
create table books (
|
|
id text primary key,
|
|
owner_id text not null references users (id) on delete cascade,
|
|
title text not null,
|
|
-- Codes, never names: a language name in a shared layer is pair-specific data (canon §2).
|
|
source_lang text not null,
|
|
target_lang text not null,
|
|
genre text not null default '',
|
|
status text not null check (status in (
|
|
'uploading', 'parsing', 'not_started', 'translating', 'awaiting_bank', 'finalizing',
|
|
'ready', 'stopped', 'rejected', 'failed', 'paused')),
|
|
chapter_count integer not null default 0,
|
|
character_count bigint not null default 0,
|
|
note_count integer not null default 0,
|
|
added_at timestamptz not null default now(),
|
|
-- ONE monotonic counter per book. Every book-scoped read and every SSE frame id of that book's
|
|
-- run carry it, which is what makes "drop a read older than an event already applied"
|
|
-- decidable at all (K-4 proposal). Bumped once per materializing transaction; the rows that
|
|
-- transaction touched are stamped with the new value, so a delta read is `revision > $1`.
|
|
revision bigint not null default 0,
|
|
-- The engine project directory: book.yaml, the source file and the engine's private SQLite
|
|
-- live here. The platform never opens that SQLite (D39.85 §4) — it only spawns tmctl with this
|
|
-- as the working directory.
|
|
workdir text not null,
|
|
engine_book_id text not null,
|
|
source_sha256 bytea,
|
|
-- The chunker version the persisted manifest was produced under. A change re-numbers chapters
|
|
-- (engine backlog row 100), and chapter ids must survive that: they are ours, not the engine's.
|
|
chunker_version text not null default ''
|
|
);
|
|
|
|
create index books_owner_idx on books (owner_id, added_at desc, id);
|
|
|
|
create table runs (
|
|
id text primary key,
|
|
book_id text not null references books (id) on delete cascade,
|
|
-- The book vocabulary minus the values that describe a book without a run.
|
|
status text not null check (status in (
|
|
'translating', 'awaiting_bank', 'finalizing', 'ready', 'stopped', 'failed', 'paused')),
|
|
verify_bank boolean not null,
|
|
started_at timestamptz not null default now(),
|
|
finished_at timestamptz,
|
|
-- Progress is per phase because a unit is done only once its edit resolved: one end-to-end
|
|
-- counter reads zero for the whole draft wave (contract §2.5; engine backlog row 99).
|
|
draft_done integer not null default 0,
|
|
draft_total integer not null default 0,
|
|
edit_done integer not null default 0,
|
|
edit_total integer not null default 0,
|
|
eta_seconds integer,
|
|
revision bigint not null default 0
|
|
);
|
|
|
|
-- One live run per book. The queue serializes by book_id (P-3) and the engine holds an EXCLUSIVE
|
|
-- lock on the project file anyway, so a second live run is a defect the database should refuse
|
|
-- rather than a state the API has to explain.
|
|
create unique index runs_one_live_per_book on runs (book_id) where finished_at is null;
|
|
create index runs_book_idx on runs (book_id, started_at desc);
|
|
|
|
-- One engine PROCESS per attempt. The ratified idempotency key is (run_id, seq) — where run_id is
|
|
-- the ENGINE's per-invocation id, not the platform run id: resume spawns a new process whose seq
|
|
-- restarts at 1, so keying on the platform run would silently drop every event of attempt 2.
|
|
create table run_attempts (
|
|
id bigint generated always as identity primary key,
|
|
run_id text not null references runs (id) on delete cascade,
|
|
attempt_no integer not null,
|
|
-- Echoed by the stream's `hello` frame (engine backlog rows 102/103). Null until the handshake
|
|
-- arrives: the row is created when the process is spawned, before it says anything.
|
|
engine_run_id text,
|
|
started_at timestamptz not null default now(),
|
|
ended_at timestamptz,
|
|
exit_code integer,
|
|
-- A high-water mark instead of an event log: the reporting database materializes state, it
|
|
-- does not source events (research/23 §3). A duplicate line inside one stream is dropped by
|
|
-- seq <= last_seq, and the mark moves in the same transaction as the effect it guards.
|
|
last_seq bigint not null default 0,
|
|
unique (run_id, attempt_no)
|
|
);
|
|
|
|
create unique index run_attempts_engine_run_idx on run_attempts (engine_run_id);
|
|
|
|
create table chapters (
|
|
id text primary key,
|
|
book_id text not null references books (id) on delete cascade,
|
|
-- Displayed ordinal, NOT a key: numbering is dense, so editing the source shifts every later
|
|
-- chapter (chunk/chunker.go:99-105). The key is the opaque id, which the platform keeps
|
|
-- stable across re-chunks.
|
|
number integer not null,
|
|
-- Empty when the book's data carries no heading: there is no hardwired "Chapter N" form
|
|
-- (owner, 04.08 — K-3), and a book without chapter numbers is legal.
|
|
heading text not null default '',
|
|
units_total integer not null default 0,
|
|
-- K-10 is open. Both phase counters exist so that answering it is a projection change rather
|
|
-- than a migration; units_done stays the aggregate the resync path can restore today.
|
|
units_draft_done integer not null default 0,
|
|
units_edit_done integer not null default 0,
|
|
units_done integer not null default 0,
|
|
note_count integer not null default 0,
|
|
revision bigint not null default 0,
|
|
unique (book_id, number)
|
|
);
|
|
|
|
create index chapters_book_order_idx on chapters (book_id, number);
|
|
-- Delta reads on reconnect: "everything in this book newer than the revision the client holds".
|
|
create index chapters_book_revision_idx on chapters (book_id, revision);
|
|
|
|
create table units (
|
|
id text primary key,
|
|
chapter_id text not null references chapters (id) on delete cascade,
|
|
ordinal integer not null,
|
|
source text not null,
|
|
target text not null default '',
|
|
-- Derived from the PAIR, not from the verdict: a flagged unit legally ships WITH text
|
|
-- (contract §2.7). The two checks below ARE that derivation, so a materializer bug that
|
|
-- writes "translated with no text" fails here instead of reaching a reader.
|
|
state text not null check (state in ('translated', 'withheld', 'pending')),
|
|
revision bigint not null default 0,
|
|
unique (chapter_id, ordinal),
|
|
constraint units_translated_has_text check (state <> 'translated' or target <> ''),
|
|
constraint units_unshipped_is_empty check (state = 'translated' or target = '')
|
|
);
|
|
|
|
create index units_chapter_revision_idx on units (chapter_id, revision);
|
|
|
|
create table notes (
|
|
id text primary key,
|
|
book_id text not null references books (id) on delete cascade,
|
|
chapter_id text references chapters (id) on delete cascade,
|
|
unit_id text references units (id) on delete cascade,
|
|
-- The ENGINE's flag reason, stored and never projected. The product phrase and the severity
|
|
-- step are applied at read time from the contract's map (companion appendix A, K-6), so the
|
|
-- owner's wording lands as a data change with no backfill — and an unknown reason still gets
|
|
-- a neutral phrase instead of a hole.
|
|
reason text not null,
|
|
created_at timestamptz not null default now(),
|
|
revision bigint not null default 0
|
|
);
|
|
|
|
create index notes_book_revision_idx on notes (book_id, revision);
|
|
create index notes_unit_idx on notes (unit_id);
|
|
|
|
create table bank_terms (
|
|
id text primary key,
|
|
book_id text not null references books (id) on delete cascade,
|
|
src text not null,
|
|
dst text not null default '',
|
|
-- Null is legal and means "the engine did not decide": a ruby candidate that is neither a name
|
|
-- nor a place carries no type (membank/memseed.go:323-326). Such a row still needs signing.
|
|
kind text check (kind in ('name', 'place', 'title', 'term', 'nickname')),
|
|
status text not null check (status in ('auto', 'draft', 'approved')),
|
|
origin text not null check (origin in ('seed', 'ruby', 'mined')),
|
|
sense text not null default '',
|
|
since_chapter integer not null default 0,
|
|
until_chapter integer not null default 0,
|
|
revision bigint not null default 0,
|
|
-- The engine's own uniqueness key (store/migrate.go:202). Without the window the same src
|
|
-- arrives as several legal rows that look like duplicates.
|
|
unique (book_id, src, sense, since_chapter, until_chapter)
|
|
);
|
|
|
|
create index bank_terms_book_revision_idx on bank_terms (book_id, revision);
|
|
|
|
-- Signing is NOT a row edit: the pipeline replaces a book's whole glossary from its deterministic
|
|
-- inputs each run (seeding.go:18/110), so a decision is an accumulating instruction that the
|
|
-- worker writes into the mined-delta / mined-rejects files before resuming.
|
|
create table bank_decisions (
|
|
book_id text not null references books (id) on delete cascade,
|
|
term_id text not null references bank_terms (id) on delete cascade,
|
|
action text not null check (action in ('promote', 'decline')),
|
|
dst text not null default '',
|
|
decided_at timestamptz not null default now(),
|
|
decided_by text not null references users (id),
|
|
primary key (book_id, term_id),
|
|
-- The contract's conditional requirement, enforced where it cannot be generated away: an
|
|
-- approved term with an empty rendering matches nothing yet reads as an intended one.
|
|
constraint bank_decisions_promote_has_dst check (action <> 'promote' or dst <> '')
|
|
);
|
|
|
|
create table exports (
|
|
id text primary key,
|
|
book_id text not null references books (id) on delete cascade,
|
|
format text not null,
|
|
ready boolean not null default false,
|
|
-- Storage key of the artifact. The download URL is minted per request for the owner and is
|
|
-- never indexable (PT-34), so it is not a column.
|
|
artifact text not null default '',
|
|
created_at timestamptz not null default now(),
|
|
ready_at timestamptz,
|
|
failed_reason text not null default ''
|
|
);
|
|
|
|
create index exports_book_idx on exports (book_id, created_at desc);
|
|
|
|
-- +goose Down
|
|
drop table exports;
|
|
drop table bank_decisions;
|
|
drop table bank_terms;
|
|
drop table notes;
|
|
drop table units;
|
|
drop table chapters;
|
|
drop table run_attempts;
|
|
drop table runs;
|
|
drop table books;
|