Files
familienarchiv/docs/adr/032-person-delete-db-level-integrity.md
Marcel 73dd6c80fa docs(adr): record DB-level person-delete integrity decision (ADR-032) (#684)
Capture the reversal of V56's no-FK decision, the DB-layer-integrity
principle, and the cascade-boundary invariant (the cascade never reaches
documents rows). Numbered 032 — 028-031 are already taken on main; the
issue's '028 is next' was written before main moved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:34:46 +02:00

3.2 KiB

ADR-032 — Person-delete referential integrity lives in the database, and the cascade never reaches documents

Date: 2026-06-06 Status: Accepted Issue: #684 (move person-delete FK detach to database-level ON DELETE) Milestone:


Context

Deleting a Person had to detach the two FKs into persons that lacked any ON DELETE behaviour: documents.sender_id and document_receivers.person_id (both from V1). PersonService.deletePerson and mergePersons did this in Java — nulling the sender and deleting receiver join rows before deleteById — so the integrity guarantee lived in application code. Any other delete path (a future endpoint, a manual psql, a batch job) could still orphan rows or fail with an FK-violation 500.

A related soft reference made it worse: transcription_block_mentioned_persons.person_id was a UUID column with no FK (V56, a deliberate "no FK" choice), so a person delete left dangling @-mention rows. The literal @DisplayName lives in transcription_blocks.text, so only the link was ever at stake — not the visible name.

Decision

Move person-delete integrity into the database (migration V71) and thin the service to a plain deleteById:

  • documents.sender_idON DELETE SET NULL (documents.senderText preserves the raw textual attribution, so nulling the link loses no historical record).
  • document_receivers.person_idON DELETE CASCADE (the symmetric completion of V14, which gave the document_id side the same).
  • transcription_block_mentioned_persons.person_id → a real FK with ON DELETE CASCADE, reversing V56's "no FK" decision. The read renderer already degrades a @DisplayName with no sidecar row to plain escaped text, so removing the link is invisible to the reader.

Cascade-boundary invariant: the cascade stays strictly at the join/reference layer and never reaches documents rows — a cascade into documents would destroy historical letters. This is pinned by a non-negotiable document-survival assertion in PersonRepositoryTest.

Consequences

  • A person delete is safe from every path, not just PersonService. The service and merge stay thin (deleteById + the cascade); reassignSenderToNull and deleteReceiverReferences are deleted.
  • This fixes the pre-existing dead-link-on-deleted-person case — it is not a purely invisible refactor.
  • DB cascades run below AuditService, so the row-level cleanup is intentionally not audit-logged; the person-delete action itself is still logged at the service layer.
  • The V71 FK validation requires cleaning pre-existing orphan mention rows first; the migration does this in a DO block that logs the purge count via RAISE NOTICE.

Alternatives considered

  • Keep integrity in Java — rejected; it only protects the one code path and re-breaks the moment a second delete path appears.
  • Cascade documents.sender_id — rejected; it would delete historical letters when a sender is removed. SET NULL keeps the letter and its senderText.
  • Leave the mention sidecar FK-less (honour V56) — rejected; the "no FK" rationale was stale, the name survives in the block text regardless, and the FK removes the orphan-row class of bug.