diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 52d08e38..4493c2f9 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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.** +### 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 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: diff --git a/docs/adr/039-person-life-dates-localdate-precision.md b/docs/adr/039-person-life-dates-localdate-precision.md new file mode 100644 index 00000000..e21ebd6e --- /dev/null +++ b/docs/adr/039-person-life-dates-localdate-precision.md @@ -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. diff --git a/docs/architecture/db/db-orm.puml b/docs/architecture/db/db-orm.puml index c4bfa9a4..8b54f2cc 100644 --- a/docs/architecture/db/db-orm.puml +++ b/docs/architecture/db/db-orm.puml @@ -1,6 +1,6 @@ @startuml db-orm -' Schema source: Flyway V1–V72 (excl. V37, V43 — intentionally removed) -' Schema as of: V72 (2026-06-08) +' Schema source: Flyway V1–V76 (excl. V37, V43 — intentionally removed) +' Schema as of: V76 (2026-06-12) ' ⚠ This is a versioned snapshot. Update when the schema changes significantly. hide circle @@ -184,8 +184,10 @@ package "Persons" { title : VARCHAR(50) person_type : VARCHAR(20) NOT NULL notes : TEXT - birth_year : INTEGER - death_year : INTEGER + birth_date : DATE + birth_date_precision : VARCHAR(16) NOT NULL + death_date : DATE + death_date_precision : VARCHAR(16) NOT NULL generation : SMALLINT family_member : BOOLEAN NOT NULL source_ref : VARCHAR(255) UNIQUE diff --git a/docs/architecture/db/db-relationships.puml b/docs/architecture/db/db-relationships.puml index 2b70124f..3695a9d6 100644 --- a/docs/architecture/db/db-relationships.puml +++ b/docs/architecture/db/db-relationships.puml @@ -4,6 +4,8 @@ ' ⚠ This is a versioned snapshot. Update when the schema changes significantly. ' Note: V69 adds columns only (persons.source_ref, tag.source_ref, document ' 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 skinparam linetype ortho