docs(person): ADR-039, DB diagrams, and V76 deploy runbook note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-12 18:22:38 +02:00
parent 9664a83dae
commit ac61987904
4 changed files with 119 additions and 4 deletions

View File

@@ -518,6 +518,26 @@ docker exec -i archive-db psql -U ${POSTGRES_USER} ${POSTGRES_DB} < backup-YYYYM
Automated backup (nightly `pg_dump` + MinIO `mc mirror` over Tailscale to `heim-nas`) is a follow-up issue. Until that ships: **manual backups are the only recovery option.** Automated backup (nightly `pg_dump` + MinIO `mc mirror` over Tailscale to `heim-nas`) is a follow-up issue. Until that ships: **manual backups are the only recovery option.**
### Deploy note — V76 (persons birth/death → date + precision, #773)
V76 drops `persons.birth_year`/`death_year` after backfilling the new
`birth_date`/`death_date` + precision columns — a **one-way migration** (Flyway cannot
roll it back). Before deploying:
1. Take a manual `pg_dump` (see above) — there is no automated nightly backup yet, so
confirm the dump completed before starting the deploy.
2. No maintenance window is required: the pre-check + DDL run in one atomic Flyway
transaction, and this single-writer archive has no concurrent importers during deploy.
If post-deploy data issues are found, restore **only the persons table** from the
pre-migration dump (targeted restore, not a full-database restore):
```bash
pg_restore -t persons -d ${POSTGRES_DB} backup-YYYYMMDD.dump
```
(For a plain-SQL dump, extract the persons COPY block instead of replaying the full file.)
### Rollback ### Rollback
Each release tag corresponds to a docker image tag on the host daemon (built via DooD; no registry). Rolling back to a previous tag is one command: Each release tag corresponds to a docker image tag on the host daemon (built via DooD; no registry). Rolling back to a previous tag is one command:

View File

@@ -0,0 +1,91 @@
# ADR-039 — Person life dates become LocalDate + DatePrecision
**Status:** Accepted
**Date:** 2026-06-12
**Issue:** #773 (Zeitstrahl milestone, foundational)
## Context
`Person` stored `birthYear`/`deathYear` as `Integer`. A known exact birthday
(`1901-03-14`) had nowhere to live, and every display was stuck at year precision.
The Zeitstrahl's derived life-events need real dates with precision metadata, and the
document domain already solved exactly this problem with
`Document.documentDate` + `metaDatePrecision`.
V76 replaces the two integer columns with `birth_date`/`death_date` (`DATE`, nullable)
plus `birth_date_precision`/`death_date_precision` (`VARCHAR(16) NOT NULL DEFAULT
'UNKNOWN'`), backfilling existing years as `YYYY-01-01` at `YEAR` precision.
## Decisions
### 1. `DatePrecision` stays in `document/` and is imported cross-domain
The `person` package imports `org.raddatz.familienarchiv.document.DatePrecision`
directly. The layering rule (controllers → services → own repository) governs
service-to-repository coupling, **not** value-type sharing — an enum has no behaviour
and no persistence side effects. Creating a `common/` package for one enum would be
premature structure; if a third domain needs it, revisit then. The enum remains a
verbatim mirror of the import normalizer's `Precision` values (ADR-025) — changes must
stay in sync with `tools/import-normalizer/dates.py`.
### 2. Precision columns are NOT NULL with default `UNKNOWN`
Mirrors `Document.metaDatePrecision`. The illegal state "date present, precision null"
cannot exist: the named CHECK constraints
(`chk_person_birth_date_precision_coherence`, `…_values`,
`chk_person_birth_before_death`, and the death-side twins) enforce
`(date IS NULL) = (precision = 'UNKNOWN')` and the temporal order at the DB level;
`PersonService.validateLifeDates` enforces the same rules first so users get a
structured 400 (`INVALID_DATE_PRECISION` / `BIRTH_AFTER_DEATH`) instead of a
constraint-violation 500.
Storage accepts all seven `DatePrecision` values (enum-to-string mapping consistency),
but the person new/edit form offers only **DAY / MONTH / YEAR**`RANGE` and `SEASON`
are semantically nonsensical for a birth or death, and `APPROX` is excluded from the
form to reduce cognitive load for the senior author audience. Legacy `APPROX` rows
still render correctly (display delegates to `formatDocumentDate`).
### 3. Derived-year pattern for backward-compatible DTOs
`PersonNodeDTO` (Stammbaum) and `RelationshipDTO` keep `Integer birthYear/deathYear`,
derived null-safely in the relationship services (`birthDate != null ?
birthDate.getYear() : null` — never 0, never empty string; REQ-PERSON-DATE-01). The
native queries behind `PersonSummaryDTO` project
`CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear`.
### 4. `PersonSummaryDTO` intentionally exposes years only
The person list/search views show year precision only; full precision lives on the
person detail page. Do **not** add `LocalDate getBirthDate()` to the interface without
updating all four native queries (`findAllWithDocumentCount`,
`searchWithDocumentCount`, `findTopByDocumentCount`, `findByFilter`) — the interface
projection is satisfied purely by the SQL SELECT aliases.
### 5. `preferHumanDate` extends ADR-025's human-edit-preserve rule
The importer stays year-shaped: `PersonUpsertCommand` keeps `Integer birthYear/
deathYear` because the spreadsheet only knows a year — pushing `LocalDate` into the
importer would fabricate precision. On upsert, `PersonService.preferHumanDate` returns
a `DatePrecisionPair` record (date and precision travel as one value so they cannot go
out of sync):
- existing precision DAY/MONTH/SEASON/RANGE/APPROX → hand-entered, preserved verbatim;
- existing precision YEAR/UNKNOWN → refreshed from the canonical year as
`YYYY-01-01` + `YEAR` (or cleared to null/`UNKNOWN` when the sheet has no year).
The original integer/string `preferHuman` overloads remain for non-date fields.
### 6. Known limitation — mixed-precision comparison
`validateLifeDates` compares stored `LocalDate` values. A DAY-precision birth late in
the same year as a YEAR-precision death (stored as Jan 1st) is rejected with
`BIRTH_AFTER_DEATH`. This is intentional; the `error_birth_after_death` i18n message
carries the workaround hint (enter the following year as the death year).
## Consequences
- V76 is one-way (columns dropped). Rollback = targeted `pg_restore -t persons` from
the pre-deploy dump — see `docs/DEPLOYMENT.md` §5.
- Exact dates now render on person cards, hover cards, and the mention dropdown; the
Stammbaum and person list are visually unchanged.
- The Zeitstrahl can derive birth/death life-events at full precision.

View File

@@ -1,6 +1,6 @@
@startuml db-orm @startuml db-orm
' Schema source: Flyway V1V72 (excl. V37, V43 — intentionally removed) ' Schema source: Flyway V1V76 (excl. V37, V43 — intentionally removed)
' Schema as of: V72 (2026-06-08) ' Schema as of: V76 (2026-06-12)
' ⚠ This is a versioned snapshot. Update when the schema changes significantly. ' ⚠ This is a versioned snapshot. Update when the schema changes significantly.
hide circle hide circle
@@ -184,8 +184,10 @@ package "Persons" {
title : VARCHAR(50) title : VARCHAR(50)
person_type : VARCHAR(20) NOT NULL person_type : VARCHAR(20) NOT NULL
notes : TEXT notes : TEXT
birth_year : INTEGER birth_date : DATE
death_year : INTEGER birth_date_precision : VARCHAR(16) NOT NULL
death_date : DATE
death_date_precision : VARCHAR(16) NOT NULL
generation : SMALLINT generation : SMALLINT
family_member : BOOLEAN NOT NULL family_member : BOOLEAN NOT NULL
source_ref : VARCHAR(255) UNIQUE source_ref : VARCHAR(255) UNIQUE

View File

@@ -4,6 +4,8 @@
' ⚠ This is a versioned snapshot. Update when the schema changes significantly. ' ⚠ This is a versioned snapshot. Update when the schema changes significantly.
' Note: V69 adds columns only (persons.source_ref, tag.source_ref, document ' Note: V69 adds columns only (persons.source_ref, tag.source_ref, document
' precision/attribution fields); no new FK relationships, so this diagram is unchanged. ' precision/attribution fields); no new FK relationships, so this diagram is unchanged.
' Note: V76 swaps persons.birth_year/death_year for birth_date/death_date +
' precision columns; columns only, no new FK relationships, diagram unchanged.
hide circle hide circle
skinparam linetype ortho skinparam linetype ortho