From f9ac963b9fbcd6ac44bf91755900255a0c486e41 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 16 Apr 2026 15:15:17 +0200 Subject: [PATCH] feat(#221): add V39 migration for tag hierarchy and colors Adds parent_id FK (ON DELETE SET NULL), self-reference check constraint, parent_id index, and nullable color column to the tag table. Co-Authored-By: Claude Sonnet 4.6 --- .../db/migration/V39__add_tag_hierarchy.sql | 9 +++ .../repository/MigrationIntegrationTest.java | 55 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V39__add_tag_hierarchy.sql 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")