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.**
|
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:
|
||||||
|
|||||||
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
|
@startuml db-orm
|
||||||
' Schema source: Flyway V1–V72 (excl. V37, V43 — intentionally removed)
|
' Schema source: Flyway V1–V76 (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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user