diff --git a/backend/src/main/resources/db/migration/V77__add_timeline_events.sql b/backend/src/main/resources/db/migration/V77__add_timeline_events.sql new file mode 100644 index 00000000..aba99c10 --- /dev/null +++ b/backend/src/main/resources/db/migration/V77__add_timeline_events.sql @@ -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);