fix(db): add PRIMARY KEY to group_permissions and promote tbmp UNIQUE to PK (#469) #492
Reference in New Issue
Block a user
Delete Branch "fix/issue-469-group-permissions-pk"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #469.
(group_id, permission)rows accumulated since the initial schema (usesctidcomparison, safe on fresh DBs as a no-op)NOT NULLonpermissioncolumn and addspk_group_permissions PRIMARY KEY (group_id, permission)transcription_block_mentioned_persons— dropsuq_tbmp_block_person, addspk_tbmp PRIMARY KEY (block_id, person_id)— naming-convention consistencyUserService.createGroup: Switched to builder and added null-guard fordto.permissions— prevents null propagating into the@ElementCollectionafter V64 adds NOT NULLPre-flight query (run against production before deploying)
Expected output: zero rows. V63 handles any duplicates found, but documenting the pre-state is good practice.
Rollback
If V64 needs to be reverted after production deploy:
Test plan
MigrationIntegrationTest#v64_pk_group_permissions_exists— RED before V64, GREEN afterMigrationIntegrationTest#v64_permission_column_isNotNullable— RED before V64, GREEN afterMigrationIntegrationTest#v64_rejectsDuplicateGroupPermission— RED before V64, GREEN after (DataIntegrityViolationException on duplicate insert)MigrationIntegrationTest#v65_pk_tbmp_exists— RED before V65, GREEN afterUserServiceTest#createGroup_withNullPermissions_savesGroupWithEmptyPermissionSet— RED before null-guard fix, GREEN after👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
What I checked
TDD evidence · Naming · Function size · Builder pattern · Guard clauses · Java idioms
Findings
Positive
createGroupcorrectly switches from setter-chain to builder — this is the right call and matches the project code style. The null-guarddto.getPermissions() != null ? dto.getPermissions() : new HashSet<>()mirrors the@Builder.Defaultintent inUserGroup.ctidnote and V64's "future seed can use ON CONFLICT DO NOTHING" note are exactly the right level of documentation.v64_rejectsDuplicateGroupPermissionuses@Transactional(propagation = NOT_SUPPORTED)correctly — the duplicate PK violation must hit a real commit boundary to fire.Suggestions (non-blocking)
v64_rejectsDuplicateGroupPermissiondoes manual cleanup viaDELETEstatements after the assertion. IfassertThatThrownByitself throws unexpectedly (e.g., the exception is not of the expected type), the cleanup is skipped and the test leaves dirty data in the container. Atry-finallyblock or a dedicated@AfterEachcleanup would be safer:UserServiceTestuses a fully-qualifiedorg.raddatz.familienarchiv.user.GroupDTOinline — typically a sign of an import collision. Minor style smell; if there's a collision it should show in the import block, not inline.No blockers. Clean implementation.
🏛️ Markus Keller — Application Architect
Verdict: ✅ Approved
What I checked
Constraint placement · Migration sequencing · Module boundaries · Documentation updates
Findings
Strong
This PR does exactly what the architecture principles demand: push integrity to PostgreSQL, not to application code. A PK on
(group_id, permission)is atomic and race-condition-free; an application-layer duplicate check is not. The three-step migration sequence (deduplicate → add NOT NULL + PK → promote tbmp UNIQUE to PK) is the correct decomposition — each step is independently safe to run and reviewable.The V63 dedup migration using
ctidis a correct PostgreSQL idiom for removing duplicates while preserving one row when no better discriminant exists. It is a no-op on clean databases, which means the migration is safe regardless of production state.The
UserService.createGroupfix is correctly scoped to the user domain. No boundary leak.Documentation check
db-orm.pumlanddb-relationships.puml— no update neededClean.
Operational notes
The PR body includes a pre-flight query and rollback SQL for V64 — this is exactly the right operational discipline. V65 (UNIQUE → PK promotion) involves a
DROP CONSTRAINT+ADD CONSTRAINTwhich in PostgreSQL replaces the backing B-tree. For a family archive table, this is negligible. Iftranscription_block_mentioned_personsever reaches significant size, this would need an online approach, but that is not a current concern.No blockers.
🔐 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
What I checked
SQL injection · Input handling · Auth / permission surface · Data exposure · Logging
Findings
No security concerns.
This PR is administrative DDL (primary key and NOT NULL constraints) with a matching service null-guard. The security surface is minimal:
ALTER TABLE,DELETE ... USING). No user-controlled input.ctidis a PostgreSQL system column — not user-controlled, not injectable.UserService.createGroup: The null-guard prevents aNullPointerExceptionfrom propagating into the@ElementCollection. No new input validation surface is introduced; the method was already guarded by@RequirePermissionat the controller level.JdbcTemplatecalls in the test class use parameterized?placeholders — injection-safe.The PK constraint on
group_permissionsactually improves the security posture marginally: it eliminates a class of silent data duplication that could cause inconsistent permission evaluation if the application ever iterated over permissions without deduplicating.Nothing to flag.
🧪 Sara Holt — Senior QA Engineer
Verdict: ⚠️ Approved with concerns
What I checked
Test pyramid placement · Naming · Coverage gaps · Assertion quality · Test isolation · Flakiness risk
Positive
MigrationIntegrationTest(Testcontainers + real Postgres — not H2). The unit test forcreateGroupbelongs inUserServiceTestwith Mockito. Right layer for each concern.@Transactional(propagation = NOT_SUPPORTED)onv64_rejectsDuplicateGroupPermissionis the correct annotation — without it, the test's own transaction would absorb the duplicate insert and no constraint violation would surface.assertThatThrownByassertion checks the correct type (DataIntegrityViolationException). Good specificity.createGroup_withNullPermissions_savesGroupWithEmptyPermissionSetusesargThatto verify the saved entity's state rather than only checkingverify(groupRepository).save(any()). This is meaningful behavior verification.Concerns
Blocker-level: Test isolation risk in
v64_rejectsDuplicateGroupPermissionThe cleanup at the end of the test:
…runs after the assertion. If
assertThatThrownByitself throws an unexpected exception (e.g., aDataIntegrityViolationExceptionthat doesn't match, or the firstjdbc.updatethrows), cleanup is skipped. Since this test isNOT_SUPPORTED, there is no transaction to roll back — the data is stranded in the container.Subsequent tests that run in the same Testcontainers instance may be unaffected (because the group ID is random), but this is a fragile pattern. Wrap in
try-finally:Suggestion: Missing explicit V63 coverage
V63 is the dedup migration. The current tests verify that V64 ran (PK exists, NOT NULL exists, duplicates rejected), but there is no test that V63 actually removed duplicates from a pre-existing dirty state. On a fresh test database there are no duplicates, so V63 is a no-op and is never exercised meaningfully. This is acceptable for a CI-only test environment (duplicates would only exist in production), but it is worth noting that the dedup logic is untested at the migration layer. A comment in the test class documenting this gap would be valuable:
Non-blocking: test naming consistency
v64_pk_group_permissions_existsandv65_pk_tbmp_existsuse a version-prefixed naming style that is standard for migration tests in this project. Acceptable.Overall: solid coverage. The
try-finallychange is the one I'd want before merge.⚙️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
What I checked
Migration deployment safety · Rollback procedure · CI impact · Infrastructure scope
Findings
Strong
The PR description has exactly what I want before a schema-altering migration hits production:
Migration deployment analysis
DELETE ... USING ctid— no-op on clean DBs; handles dirty state atomicallytranscription_block_mentioned_persons; fast on current dataset sizeAll three run inside Flyway's managed transaction envelope — if any step fails, Flyway marks the migration as failed and the app does not start. This is safe behavior.
No CI changes — this PR has no Gitea Actions workflow modifications, no Compose file changes, no Dockerfile changes.
One note for future migrations
V64's comment documents the ON CONFLICT DO NOTHING pattern for future seed migrations. This is useful institutional knowledge. Consider adding a brief note to
docs/infrastructure/or aCONTRIBUTING.mdmigration section so the guidance is discoverable outside the migration file itself. Not a blocker — just a suggestion for discoverability.Ready to ship.
📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
What I checked
Requirements traceability · Acceptance criteria coverage · Scope completeness · Issue-to-PR alignment
Findings
Traceability
Issue #469 targets
group_permissionsdata integrity (no PK or UNIQUE constraint since the initial schema). The PR closes #469 and delivers:group_permissionscreateGroupnull-guardScope alignment
V65 (tbmp UNIQUE → PK promotion) is not explicitly in the original issue title, but it is a naming-convention cleanup that is logically related (data integrity / constraint naming). It is a low-risk, well-motivated addition that does not expand scope meaningfully.
Acceptance criteria coverage
The PR body's test plan is a de-facto acceptance criteria checklist:
v64_pk_group_permissions_exists→ confirms the PK was created ✅v64_permission_column_isNotNullable→ confirms NOT NULL was added ✅v64_rejectsDuplicateGroupPermission→ confirms duplicates are rejected ✅v65_pk_tbmp_exists→ confirms tbmp naming cleanup ✅createGroup_withNullPermissions_savesGroupWithEmptyPermissionSet→ confirms app-level safety ✅One gap to note
The issue presumably existed because earlier migrations lacked proper constraints. It would strengthen the issue backlog if the root-cause note (no PK since V1) were captured as a brief comment on issue #469 so that pattern is documented for future contributors. Not a merge blocker — more a backlog hygiene note.
Requirements fully satisfied.
🎨 Leonie Voss — UI/UX Designer & Accessibility Strategist
Verdict: ✅ Approved — No UI changes in scope
What I checked
Frontend changes · Svelte components · Tailwind classes · Accessibility impact · Brand compliance
Findings
This PR is entirely backend: three Flyway SQL migrations and a Java service fix. There are no
.sveltefiles changed, no TypeScript changes, no Tailwind classes, no frontend routes, no API response shape changes.Nothing in this PR touches the user interface, accessibility, or brand compliance.
One indirect note: V64 adds NOT NULL to
group_permissions.permission. If any future UI allows creating groups without selecting permissions, the frontend form should communicate clearly that at least one permission must be chosen (or that an empty permission set is valid — which thecreateGroupnull-guard confirms it is). This is not a concern for this PR, but worth keeping in mind when building the group management UI.LGTM from my seat.
Addressed Sara's concern: wrapped the cleanup in
v64_rejectsDuplicateGroupPermissionin atry-finallyblock to guarantee data is cleaned up even if the assertion throws unexpectedly. Commitbfc3a176— all 28 migration integration tests still green.