diff --git a/backend/src/main/resources/db/migration/V39__add_tag_hierarchy.sql b/backend/src/main/resources/db/migration/V39__add_tag_hierarchy.sql new file mode 100644 index 00000000..330d10df --- /dev/null +++ b/backend/src/main/resources/db/migration/V39__add_tag_hierarchy.sql @@ -0,0 +1,9 @@ +-- Add self-referencing parent FK for tag hierarchy (adjacency list model). +-- ON DELETE SET NULL: deleting a parent promotes its children to root level. +ALTER TABLE tag ADD COLUMN parent_id UUID REFERENCES tag(id) ON DELETE SET NULL; +ALTER TABLE tag ADD CONSTRAINT chk_tag_no_self_reference CHECK (parent_id != id); +CREATE INDEX idx_tag_parent_id ON tag(parent_id); + +-- Optional color token (e.g. "sage", "teal") for root-level tags. +-- Validated against the allowed palette in TagService before save. +ALTER TABLE tag ADD COLUMN color VARCHAR(20); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java index db5b98a3..78ff9861 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/MigrationIntegrationTest.java @@ -168,8 +168,63 @@ class MigrationIntegrationTest { assertThat(rows).isEqualTo(1); } + // ─── V39: tag hierarchy — parent_id FK + self-reference check + color ────── + + @Test + void v39_parentId_allowsNull() { + UUID tagId = createTag("TagWithoutParent"); + + Integer count = jdbc.queryForObject( + "SELECT COUNT(*) FROM tag WHERE id = ? AND parent_id IS NULL", Integer.class, tagId); + assertThat(count).isEqualTo(1); + } + + @Test + void v39_selfReferenceCheck_rejectsSelfAsParent() { + UUID tagId = createTag("SelfRef"); + + assertThatThrownBy(() -> + jdbc.update("UPDATE tag SET parent_id = id WHERE id = ?", tagId) + ).isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + void v39_parentId_acceptsValidParent() { + UUID parent = createTag("Parent"); + UUID child = createTag("Child"); + + int rows = jdbc.update("UPDATE tag SET parent_id = ? WHERE id = ?", parent, child); + assertThat(rows).isEqualTo(1); + } + + @Test + void v39_color_allowsNull() { + UUID tagId = createTag("ColorlessTag"); + + Integer count = jdbc.queryForObject( + "SELECT COUNT(*) FROM tag WHERE id = ? AND color IS NULL", Integer.class, tagId); + assertThat(count).isEqualTo(1); + } + + @Test + void v39_color_storesTokenName() { + UUID tagId = createTag("ColoredTag"); + + int rows = jdbc.update("UPDATE tag SET color = 'sage' WHERE id = ?", tagId); + String stored = jdbc.queryForObject("SELECT color FROM tag WHERE id = ?", String.class, tagId); + + assertThat(rows).isEqualTo(1); + assertThat(stored).isEqualTo("sage"); + } + // ─── helpers ───────────────────────────────────────────────────────────── + private UUID createTag(String name) { + UUID id = UUID.randomUUID(); + jdbc.update("INSERT INTO tag (id, name) VALUES (?, ?)", id, name); + return id; + } + private UUID createDocument() { Document doc = documentRepository.save(Document.builder() .title("Testdokument")