fix(data): harden portfolio publication concurrency

This commit is contained in:
2026-06-01 09:46:18 +03:00
parent d591e5ed5a
commit 3c1a98bcc4
11 changed files with 648 additions and 79 deletions
@@ -77,9 +77,9 @@ CHECK (NOT is_public OR (
- Index on `(group_id, completed_at DESC)`.
- Partial public index on `(completed_at DESC)` where `is_public = true`.
Application validation additionally requires at least one linked completed session and at least one linked GM before publishing because those requirements span child tables.
Application validation additionally requires at least one linked completed session and at least one linked GM before publishing because those requirements span child tables. Publishing locks the parent card, validates both required link sets, then sets `is_public = true` and `published_at = COALESCE(published_at, now())` so `published_at` remains the first-publication timestamp. Link replacement locks the parent card and unpublishes it before replacing required links.
Database triggers on `portfolio_game_sessions` and `portfolio_game_masters` protect the same invariant after link removal. After a link is deleted, the trigger locks the parent card, checks both required link sets, and sets `is_public = false` with a fresh `updated_at` when either set is empty. It deliberately preserves `published_at` as the timestamp of the first publication. The link foreign keys retain `ON DELETE CASCADE`; when the card itself or its owning club is deleted, the trigger update is a harmless no-op for the disappearing parent row.
Deferred database constraint triggers validate the same invariant at transaction commit after a card transitions to public or a required session/master link is deleted or moved. They raise a check-violation error if a published card would commit without both required link sets. The deferred guard is a database backstop and deliberately does not lock or update a parent card from a child delete trigger, avoiding reverse lock order. Normal session-deletion handlers explicitly unpublish linked cards in the same transaction before deleting the session. The link foreign keys retain `ON DELETE CASCADE`; when the card itself or its owning club is deleted, deferred validation sees no surviving published card and remains harmless.
### `portfolio_game_sessions`
@@ -341,7 +341,8 @@ Follow TDD for production changes.
### Schema And Contracts
- Migration source-contract tests assert the four new tables, format constraint, publication guard, case-insensitive slug uniqueness, group and GM-profile indexes, card-oriented pending-review index, and the link-removal trigger protection.
- Migration source-contract tests assert the four new tables, format constraint, publication guard, case-insensitive slug uniqueness, group and GM-profile indexes, card-oriented pending-review index, and deferred constraint-trigger backstop.
- PostgreSQL integration tests apply migrations V001 through V029 to `postgres:17-alpine` and cover direct invalid link removal, explicit unpublish before session deletion, concurrent publish/delete ordering without deadlock, and parent/card cascade deletion.
- Public DTO reflection/source tests assert that private identifiers and physical storage paths are absent.
- Existing showcase tests continue to assert the future-session catalog boundary.