diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java index ecd77d54..0ca82751 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java @@ -207,4 +207,4 @@ public interface PersonRepository extends JpaRepository { ) """, nativeQuery = true) void insertMissingReceiverReference(@Param("source") UUID source, @Param("target") UUID target); -} \ No newline at end of file +} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonRepositoryTest.java index 874f8b3c..21c24847 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonRepositoryTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonRepositoryTest.java @@ -776,6 +776,7 @@ class PersonRepositoryTest { // AC-3: the @-mention sidecar is a CASCADE soft reference, but the literal "@Name" lives // in transcription_blocks.text and must stay visible as plain text after the person goes. Person mentioned = personRepository.save(Person.builder().firstName("Auguste").lastName("Raddatz").build()); + Person survivor = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build()); Document doc = documentRepository.save(Document.builder() .title("Brief").originalFilename("brief.pdf") .status(DocumentStatus.UPLOADED).build()); @@ -790,12 +791,18 @@ class PersonRepositoryTest { entityManager.createNativeQuery( "INSERT INTO transcription_blocks (id, annotation_id, document_id, text) VALUES (?1, ?2, ?3, ?4)") .setParameter(1, blockId).setParameter(2, annotationId).setParameter(3, doc.getId()) - .setParameter(4, "Brief an @Auguste Raddatz").executeUpdate(); + .setParameter(4, "Brief an @Auguste Raddatz und @Clara Cram").executeUpdate(); + // Two mention rows on the same block: the deleted person and an innocent bystander. entityManager.createNativeQuery( "INSERT INTO transcription_block_mentioned_persons (block_id, person_id, display_name) " + "VALUES (?1, ?2, ?3)") .setParameter(1, blockId).setParameter(2, mentioned.getId()) .setParameter(3, "Auguste Raddatz").executeUpdate(); + entityManager.createNativeQuery( + "INSERT INTO transcription_block_mentioned_persons (block_id, person_id, display_name) " + + "VALUES (?1, ?2, ?3)") + .setParameter(1, blockId).setParameter(2, survivor.getId()) + .setParameter(3, "Clara Cram").executeUpdate(); entityManager.flush(); entityManager.clear(); @@ -808,9 +815,15 @@ class PersonRepositoryTest { .setParameter(1, mentioned.getId()).getSingleResult(); assertThat(mentionRows.longValue()).isZero(); + // The cascade is scoped to the deleted person — the bystander's mention row is untouched. + Number survivorRows = (Number) entityManager.createNativeQuery( + "SELECT count(*) FROM transcription_block_mentioned_persons WHERE person_id = ?1") + .setParameter(1, survivor.getId()).getSingleResult(); + assertThat(survivorRows.longValue()).isEqualTo(1); + String text = (String) entityManager.createNativeQuery( "SELECT text FROM transcription_blocks WHERE id = ?1") .setParameter(1, blockId).getSingleResult(); - assertThat(text).isEqualTo("Brief an @Auguste Raddatz"); + assertThat(text).isEqualTo("Brief an @Auguste Raddatz und @Clara Cram"); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceIntegrationTest.java index d26883cd..66e0a93b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceIntegrationTest.java @@ -205,7 +205,7 @@ class PersonServiceIntegrationTest { // The ON DELETE cascade fires beneath Hibernate — flush the delete and clear the L1 // cache so the asserting reads observe the post-delete database state, not stale - // managed entities (the EAGER @ElementCollection on receivers makes this load-bearing). + // managed entities still holding the dropped sender/receiver associations. entityManager.flush(); entityManager.clear(); diff --git a/docs/adr/032-person-delete-db-level-integrity.md b/docs/adr/032-person-delete-db-level-integrity.md index de44a50c..76902841 100644 --- a/docs/adr/032-person-delete-db-level-integrity.md +++ b/docs/adr/032-person-delete-db-level-integrity.md @@ -45,7 +45,9 @@ letters. This is pinned by a non-negotiable document-survival assertion in 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. + 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