Files
familienarchiv/docs/adr/032-person-delete-db-level-integrity.md
Marcel 6603bc5333 test(person): address PR #736 review nits
- AC-3 cascade test: assert an innocent bystander's mention row survives the
  delete, proving the cascade is scoped to the deleted person (Nora).
- Fix integration-test comment: receivers is @ManyToMany(LAZY), not an EAGER
  @ElementCollection (Sara).
- ADR-032: note the @ prefix is kept in the degraded path, stripped in live
  mentions (Leonie).
- Add trailing newline to PersonRepository.java (Felix).

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

3.5 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. Note the read renderer strips the @ prefix when it emits a live mention link, but the degraded (deleted-person) path leaves the literal @Name in the block text as-is — the reader sees @Auguste Raddatz as plain text, never a dead link.
  • 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.