Move person-delete FK detach to database-level ON DELETE (#684) #736

Merged
marcel merged 10 commits from feat/issue-684-person-delete-db-fk into main 2026-06-06 12:34:47 +02:00
4 changed files with 20 additions and 5 deletions
Showing only changes of commit 4c2d7ba128 - Show all commits

View File

@@ -207,4 +207,4 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
)
""", nativeQuery = true)
void insertMissingReceiverReference(@Param("source") UUID source, @Param("target") UUID target);
}
}

View File

@@ -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");
}
}

View File

@@ -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();

View File

@@ -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