97 lines
5.1 KiB
PL/PgSQL
97 lines
5.1 KiB
PL/PgSQL
-- 0009: the import run ledger — one durable row per audio source import.
|
|
--
|
|
-- Status: NOT YET APPLIED. Proposed alongside the PostgreSQL audio-import store
|
|
-- (`umbratrans_web/audio_import_postgres.py`). Apply with 0008 already present;
|
|
-- this file depends only on `public.organizations` and the append-only guard
|
|
-- function 0008 defines, and it is additive and idempotent.
|
|
--
|
|
-- Why a fifth table. 0008 stores *what was imported* — the source reference, the
|
|
-- speakers, chapters and utterances. It has no row for *the act of importing*, so
|
|
-- the importer had no durable identity to return for an import and would have
|
|
-- had to mint one from nothing. `project_source_refs.project_id` is one row per
|
|
-- project and identifies the project, not the run; two imports of the same book
|
|
-- at different content hashes are two runs and one project.
|
|
--
|
|
-- The audio side named this failure directly: an import that returns a
|
|
-- `project_id` and an `import_id` that correspond to no row is worse than an
|
|
-- error, because it looks like success. `translation_imports.import_id` is the
|
|
-- row that makes `import_id` name something, and `counts` records what that run
|
|
-- actually wrote so a later "the numbers do not add up" question has an answer.
|
|
--
|
|
-- The source hash is the idempotency key and stays on `project_source_refs`
|
|
-- (UNIQUE (system, external_id)). This table is an append-only history: it is
|
|
-- never updated, and a re-import appends a new run rather than editing the last.
|
|
|
|
BEGIN;
|
|
|
|
DO $preflight$
|
|
BEGIN
|
|
IF to_regclass('public.project_source_refs') IS NULL THEN
|
|
RAISE EXCEPTION 'public.project_source_refs is missing; apply 0008 before 0009';
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
WHERE n.nspname = 'app' AND p.proname = 'prevent_translation_source_mutation') THEN
|
|
RAISE EXCEPTION 'app.prevent_translation_source_mutation() is missing; 0008 must be applied first';
|
|
END IF;
|
|
END
|
|
$preflight$;
|
|
|
|
CREATE TABLE IF NOT EXISTS public.translation_imports (
|
|
import_id uuid PRIMARY KEY,
|
|
project_id uuid NOT NULL,
|
|
organization_id uuid NOT NULL REFERENCES public.organizations(id),
|
|
-- The exporting system and node, copied from the source reference at the
|
|
-- moment of import so the run stays legible even if a node row is removed.
|
|
system text NOT NULL CHECK (length(btrim(system)) BETWEEN 1 AND 64),
|
|
node_id text NOT NULL,
|
|
external_id text NOT NULL,
|
|
-- sha256 of the manifest bytes for this run. A run with a hash already
|
|
-- present for the project is a replay; the importer refuses it before here.
|
|
content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
|
|
-- What this run actually wrote, not what it planned. Identical bytes dedupe
|
|
-- against rows already present, so the written counts can be lower than the
|
|
-- plan's; this is the written number and is the one an audit should read.
|
|
counts jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
imported_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS translation_imports_org_idx
|
|
ON public.translation_imports (organization_id);
|
|
CREATE INDEX IF NOT EXISTS translation_imports_project_idx
|
|
ON public.translation_imports (project_id, imported_at DESC);
|
|
|
|
-- Append-only on UPDATE, reusing the guard 0008 already defines. A run record is
|
|
-- a statement about a moment; editing it in place would break the link between
|
|
-- the row and the counts it reports. DELETE is permitted for the same reason as
|
|
-- the other four tables: retention and re-import need it.
|
|
DO $guard$
|
|
BEGIN
|
|
EXECUTE 'DROP TRIGGER IF EXISTS translation_imports_append_only ON public.translation_imports';
|
|
EXECUTE 'CREATE TRIGGER translation_imports_append_only BEFORE UPDATE ON public.translation_imports FOR EACH ROW EXECUTE FUNCTION app.prevent_translation_source_mutation()';
|
|
END
|
|
$guard$;
|
|
|
|
DO $rls$
|
|
BEGIN
|
|
EXECUTE 'ALTER TABLE public.translation_imports ENABLE ROW LEVEL SECURITY';
|
|
EXECUTE 'DROP POLICY IF EXISTS translation_imports_tenant ON public.translation_imports';
|
|
EXECUTE 'CREATE POLICY translation_imports_tenant ON public.translation_imports USING (app.is_platform_admin() OR app.is_organization_member(organization_id))';
|
|
END
|
|
$rls$;
|
|
|
|
COMMENT ON TABLE public.translation_imports IS
|
|
'One row per audio source import run: the durable identity an import returns, and what it actually wrote.';
|
|
COMMENT ON COLUMN public.translation_imports.counts IS
|
|
'Rows actually written by this run (after deduplication), keyed by chapters/speakers/utterances/artifacts/warnings.';
|
|
COMMENT ON COLUMN public.translation_imports.content_hash IS
|
|
'sha256 of the manifest bytes for this run. The idempotency key lives on project_source_refs; this is the run history.';
|
|
|
|
COMMIT;
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Rollback, in reverse dependency order. Not applied automatically.
|
|
--
|
|
-- DROP POLICY IF EXISTS translation_imports_tenant ON public.translation_imports;
|
|
-- DROP TRIGGER IF EXISTS translation_imports_append_only ON public.translation_imports;
|
|
-- DROP TABLE IF EXISTS public.translation_imports;
|