feat(timeline): add V77 migration for timeline_events table

Creates timeline_events plus the timeline_event_persons and
timeline_event_documents join tables, all FK columns ON DELETE CASCADE
(a person/document delete drops the join row, the event survives —
V71-class hardening). Two CHECK constraints push integrity to Postgres:
chk_timeline_event_range enforces event_date_end non-null IFF RANGE (a
strict biconditional, intentionally tighter than Document's open-ended
ranges), and chk_timeline_event_precision forbids exactly UNKNOWN while
keeping SEASON/APPROX legal. FK and query-column indexes added up-front
to avoid the V62 retrofit debt. Forward-only, additive DDL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-12 23:04:02 +02:00
committed by marcel
parent 595007213c
commit 513cdb7a4d

View File

@@ -0,0 +1,65 @@
-- V77: timeline domain foundation (Zeitstrahl) — curated timeline events.
-- Forward-only, additive DDL. No rollback script: rollback requires manual DROP TABLE
-- (timeline_event_documents, timeline_event_persons, then timeline_events). See ADR-040.
--
-- The date block (event_date / date_precision / event_date_end) mirrors documents' so events
-- and letters share one rendering path. Two divergences from documents are INTENTIONAL and
-- enforced here in Postgres (ADR-040):
-- 1. The RANGE rule is a strict biconditional (event_date_end non-null IFF RANGE), unlike
-- documents' open-ended ranges — a curated event always has a known, closed end.
-- 2. date_precision <> 'UNKNOWN' — only OCR-inferred letters are ever undated; a curated
-- event always has at least a year. SEASON and APPROX stay legal (Sommer/ca. 1914).
CREATE TABLE timeline_events (
id UUID NOT NULL DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
type VARCHAR(16) NOT NULL,
event_date DATE NOT NULL,
date_precision VARCHAR(16) NOT NULL DEFAULT 'YEAR',
event_date_end DATE,
description TEXT,
created_by UUID NOT NULL,
created_at TIMESTAMP,
updated_by UUID NOT NULL,
updated_at TIMESTAMP,
version BIGINT,
CONSTRAINT pk_timeline_events PRIMARY KEY (id),
-- event_date_end is non-null IFF precision is RANGE (both directions).
CONSTRAINT chk_timeline_event_range
CHECK ((date_precision = 'RANGE') = (event_date_end IS NOT NULL)),
-- Curated events are never undated. Forbids exactly UNKNOWN — every other
-- DatePrecision value (DAY, MONTH, SEASON, YEAR, RANGE, APPROX) stays legal.
CONSTRAINT chk_timeline_event_precision
CHECK (date_precision <> 'UNKNOWN')
);
-- Join table: events ↔ persons involved.
CREATE TABLE timeline_event_persons (
timeline_event_id UUID NOT NULL,
person_id UUID NOT NULL,
CONSTRAINT pk_timeline_event_persons PRIMARY KEY (timeline_event_id, person_id),
CONSTRAINT fk_tep_event
FOREIGN KEY (timeline_event_id) REFERENCES timeline_events(id) ON DELETE CASCADE,
CONSTRAINT fk_tep_person
FOREIGN KEY (person_id) REFERENCES persons(id) ON DELETE CASCADE
);
-- Join table: events ↔ supporting letters.
CREATE TABLE timeline_event_documents (
timeline_event_id UUID NOT NULL,
document_id UUID NOT NULL,
CONSTRAINT pk_timeline_event_documents PRIMARY KEY (timeline_event_id, document_id),
CONSTRAINT fk_ted_event
FOREIGN KEY (timeline_event_id) REFERENCES timeline_events(id) ON DELETE CASCADE,
CONSTRAINT fk_ted_document
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
);
-- Indexes added up-front (avoid the V62 FK-index retrofit debt): the two query columns plus
-- explicit indexes on all four FK columns.
CREATE INDEX idx_timeline_events_event_date ON timeline_events (event_date);
CREATE INDEX idx_timeline_events_type ON timeline_events (type);
CREATE INDEX idx_timeline_event_persons_person_id ON timeline_event_persons (person_id);
CREATE INDEX idx_timeline_event_persons_event_id ON timeline_event_persons (timeline_event_id);
CREATE INDEX idx_timeline_event_documents_document_id ON timeline_event_documents (document_id);
CREATE INDEX idx_timeline_event_documents_event_id ON timeline_event_documents (timeline_event_id);