From 6babcc7f172a57971f608e70d3beb436cf26e50a Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 28 Apr 2026 17:00:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(stammbaum):=20V55=20adds=20unique=5Fspouse?= =?UTF-8?q?=5Fpair=20index=20=E2=80=94=20symmetric=20SPOUSE=5FOF=20enforce?= =?UTF-8?q?d=20at=20DB=20level?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../migration/V55__add_spouse_symmetric_index.sql | 6 ++++++ .../RelationshipServiceIntegrationTest.java | 14 ++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V55__add_spouse_symmetric_index.sql diff --git a/backend/src/main/resources/db/migration/V55__add_spouse_symmetric_index.sql b/backend/src/main/resources/db/migration/V55__add_spouse_symmetric_index.sql new file mode 100644 index 00000000..cee18767 --- /dev/null +++ b/backend/src/main/resources/db/migration/V55__add_spouse_symmetric_index.sql @@ -0,0 +1,6 @@ +-- Symmetric SPOUSE_OF: enforce only one row per unordered pair, mirroring the +-- SIBLING_OF index added in V54. +CREATE UNIQUE INDEX unique_spouse_pair ON person_relationships ( + LEAST(person_id, related_person_id), + GREATEST(person_id, related_person_id) +) WHERE relation_type = 'SPOUSE_OF'; diff --git a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceIntegrationTest.java index 8654fa1d..003922ab 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceIntegrationTest.java @@ -117,6 +117,20 @@ class RelationshipServiceIntegrationTest { assertThat(relationshipRepository.findById(created.id())).isPresent(); } + @Test + void addRelationship_throws_409_when_reverse_SPOUSE_OF_pair_already_exists() { + // V55 enforces symmetric uniqueness for SPOUSE_OF. Inserting (alice, bob, SPOUSE_OF) + // and then (bob, alice, SPOUSE_OF) must be rejected, just like reverse SIBLING_OF. + relationshipService.addRelationship(alice.getId(), + new CreateRelationshipRequest(bob.getId(), "SPOUSE_OF", null, null, null)); + + var reverse = new CreateRelationshipRequest(alice.getId(), "SPOUSE_OF", null, null, null); + assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse)) + .isInstanceOf(DomainException.class) + .extracting("code") + .isEqualTo(ErrorCode.DUPLICATE_RELATIONSHIP); + } + @Test void deleteRelationship_succeeds_for_symmetric_type_from_either_side() { // alice SPOUSE_OF bob. Bob deletes from his side.