Files
Oxicloud/migrations/20260930000001_jobs_run_findings.sql
T
2026-07-29 07:57:06 +02:00

63 lines
3.3 KiB
SQL

-- ============================================================================
-- Slice 7 of Part 2 — persistent finding storage.
--
-- Findings from consistency jobs (and eventually storage_migration failure
-- rows, reextract failures) currently flow into `tracing::warn!` targeted
-- at `oxicloud::consistency`. That works for live tailing but rotates
-- away — an operator opening the admin UI a day after a run has nothing
-- to drill into. This table makes findings first-class + queryable.
--
-- `run_findings` is INTENTIONALLY generic. The consistency-check plan and
-- the plan doc's tenant table both list `kind` + `severity` + `resource_id`
-- + `detail` as the union of what every current tenant needs. Adding a
-- tenant with a novel per-finding field means widening `detail`
-- (JSONB, per-tenant shape), not adding a column.
--
-- Cascade rule: findings live and die with their parent run. Deleting a
-- terminal run row (retention pruning, planned) also drops its findings.
-- ============================================================================
CREATE TABLE jobs.run_findings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
run_id UUID NOT NULL
REFERENCES jobs.recoverable_runs(id)
ON DELETE CASCADE,
-- Machine-readable kind (e.g. 'stale_used_bytes', 'missing_blob').
-- Stable across releases per the audit-log convention — see
-- feedback memory `enum_over_string_literals_in_logs`. New failure
-- mode = new value, never repurpose an existing one.
kind TEXT NOT NULL,
-- Severity spectrum:
-- 'data_loss' — bytes / rows unreachable or gone.
-- 'inconsistent' — counters / materialised values wrong,
-- content intact.
-- 'anomaly' — surprising state worth surfacing, no known impact.
-- TEXT (not ENUM) so tenants can grow the vocabulary without a schema
-- migration; the app-layer types.rs is where the canonical set lives.
severity TEXT NOT NULL,
-- Nullable — some findings pertain to the run as a whole
-- (e.g. "backend enumeration truncated after 1M keys") rather than
-- one specific resource.
resource_id UUID,
-- Per-tenant per-finding structured detail (cached/actual/delta,
-- blob_hash, expected/stored path, ...). Consumers key off `kind`
-- to know the shape.
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Time-ordered listing per run: `GET /api/admin/jobs/{name}/runs/{id}/findings`
-- pages by (run_id, created_at) — index-only scan.
CREATE INDEX ON jobs.run_findings (run_id, created_at);
-- Aggregation queries — "how many `missing_blob` findings across all
-- runs of `files_consistency`" or "how many `data_loss`-severity
-- findings today". Both need `(kind)` and `(severity)` predicates; a
-- composite works for either since the leading column is selective.
CREATE INDEX ON jobs.run_findings (kind, severity, created_at);
COMMENT ON TABLE jobs.run_findings IS
'Structured per-finding records emitted by recoverable jobs. Replaces '
'the transitional `tracing::warn!(event=consistency_finding)` calls '
'in the consistency tenants — see docs/plan/job-registry.md Part 2.';