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:
@@ -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:
|
||||
|
||||
91
docs/adr/039-person-life-dates-localdate-precision.md
Normal file
91
docs/adr/039-person-life-dates-localdate-precision.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user