- 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>
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_id→ON DELETE SET NULL(documents.senderTextpreserves the raw textual attribution, so nulling the link loses no historical record).document_receivers.person_id→ON DELETE CASCADE(the symmetric completion of V14, which gave thedocument_idside the same).transcription_block_mentioned_persons.person_id→ a real FK withON DELETE CASCADE, reversing V56's "no FK" decision. The read renderer already degrades a@DisplayNamewith 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);reassignSenderToNullanddeleteReceiverReferencesare 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@Namein the block text as-is — the reader sees@Auguste Raddatzas 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
DOblock that logs the purge count viaRAISE 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 NULLkeeps the letter and itssenderText. - 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.