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.