Files
familienarchiv/docs/adr/039-person-life-dates-localdate-precision.md
2026-06-12 18:22:38 +02:00

4.7 KiB

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 / YEARRANGE 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.