fix(data): reject stale portfolio trigger snapshots
This commit is contained in:
@@ -215,6 +215,9 @@ public async Task ConcurrentRequiredLinkDeletes_ShouldSerializeAndRejectInvalidP
|
||||
[InlineData("portfolio_game_masters", "player_id")]
|
||||
public async Task RepeatableReadConcurrentRequiredLinkDeletes_ShouldBeRejectedWithoutInvalidPublicCard(string linkTable, string linkColumn)
|
||||
|
||||
[Fact]
|
||||
public async Task RepeatableReadDraftLinkDeleteRacingPublish_ShouldBeRejectedWithoutInvalidPublicCard()
|
||||
|
||||
[Theory]
|
||||
[InlineData("portfolio_game_sessions")]
|
||||
[InlineData("portfolio_game_masters")]
|
||||
@@ -229,7 +232,7 @@ public async Task RequiredParentCascadeDelete_ShouldFailCommitForPublishedCard(s
|
||||
public async Task ParentCardAndGroupCascadeDeletes_ShouldCommit()
|
||||
```
|
||||
|
||||
The direct-delete, moved-link, and direct parent-cascade theories must expect PostgreSQL `23514` at commit. The explicit-unpublish scenario must delete the session successfully while preserving the first `published_at`. The `READ COMMITTED` concurrency scenarios must bound commits with timeouts, prove there is no deadlock or write-skew, and prove that an invalid public card cannot commit. The equivalent `REPEATABLE READ` scenario must reject both published-card transactions with `0A000`, because a stale snapshot after lock acquisition cannot safely validate the invariant. The parent-card and owning-group cascade scenarios must commit successfully.
|
||||
The direct-delete, moved-link, and direct parent-cascade theories must expect PostgreSQL `23514` at commit. The explicit-unpublish scenario must delete the session successfully while preserving the first `published_at`. The `READ COMMITTED` concurrency scenarios must bound commits with timeouts, prove there is no deadlock or write-skew, and prove that an invalid public card cannot commit. The `REPEATABLE READ` scenarios must reject triggered portfolio writes with `0A000`, including draft-link deletion racing with publication, because a stale snapshot after lock acquisition cannot safely validate the invariant. The parent-card and owning-group cascade scenarios must commit successfully.
|
||||
|
||||
- [ ] **Step 3: Run the Task 1 tests to verify RED**
|
||||
|
||||
@@ -310,13 +313,7 @@ BEGIN
|
||||
target_portfolio_game_id := OLD.portfolio_game_id;
|
||||
END IF;
|
||||
|
||||
IF current_setting('transaction_isolation') <> 'read committed'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM portfolio_games pg
|
||||
WHERE pg.id = target_portfolio_game_id
|
||||
AND pg.is_public = true
|
||||
) THEN
|
||||
IF current_setting('transaction_isolation') <> 'read committed' THEN
|
||||
RAISE EXCEPTION
|
||||
'portfolio publication validation requires read committed isolation'
|
||||
USING ERRCODE = '0A000';
|
||||
@@ -400,7 +397,7 @@ CREATE INDEX ix_portfolio_game_reviews_pending
|
||||
WHERE moderation_status = 'Pending';
|
||||
```
|
||||
|
||||
The deferred constraint triggers retain the link-table `ON DELETE CASCADE` behavior. At transaction commit they acquire the same transaction-level advisory lock and reject a surviving published card when either required link set is empty. The intentionally global lock is appropriate for low-volume portfolio publication writes: under the application default `READ COMMITTED` isolation level it serializes validation, prevents write-skew across distinct child links, and gives multi-card transactions one lock order. PostgreSQL retains stale snapshots under `REPEATABLE READ` and `SERIALIZABLE`, so the guard rejects publication-related writes at those isolation levels with `0A000`. Child delete triggers do not lock or update the parent card. Draft edits, explicit unpublishing, and card or club cascade deletion remain valid. Normal session-deletion handlers explicitly unpublish linked cards before deleting sessions.
|
||||
The deferred constraint triggers retain the link-table `ON DELETE CASCADE` behavior. At transaction commit they acquire the same transaction-level advisory lock and reject a surviving published card when either required link set is empty. The intentionally global lock is appropriate for low-volume portfolio publication writes: under the application default `READ COMMITTED` isolation level it serializes validation, prevents write-skew across distinct child links, and gives multi-card transactions one lock order. PostgreSQL retains stale snapshots under `REPEATABLE READ` and `SERIALIZABLE`, so the guard rejects every triggered portfolio write at those isolation levels with `0A000`. Child delete triggers do not lock or update the parent card. At `READ COMMITTED`, draft edits, explicit unpublishing, and card or club cascade deletion remain valid. Normal session-deletion handlers explicitly unpublish linked cards before deleting sessions.
|
||||
|
||||
- [ ] **Step 5: Explicitly unpublish linked cards in both session-deletion handlers**
|
||||
|
||||
@@ -446,7 +443,7 @@ Run:
|
||||
dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --filter "FullyQualifiedName~PortfolioMigrationTests|FullyQualifiedName~PortfolioMigrationPostgresTests|FullyQualifiedName~PortfolioSessionDeletionSourceTests|FullyQualifiedName~PortfolioSchemaGateSourceTests"
|
||||
```
|
||||
|
||||
Expected: PASS, including PostgreSQL 17 migration application, rejected direct required-link deletes, rejected moved links and session/player cascades, successful explicit unpublish plus session delete with preserved `published_at`, bounded `READ COMMITTED` concurrent publish/delete and distinct-link deletion without deadlock, write-skew, or invalid public commit, rejected `REPEATABLE READ` published-card writes, successful parent-card and owning-group cascades, Discord identity scoping, and Compose schema gating.
|
||||
Expected: PASS, including PostgreSQL 17 migration application, rejected direct required-link deletes, rejected moved links and session/player cascades, successful explicit unpublish plus session delete with preserved `published_at`, bounded `READ COMMITTED` concurrent publish/delete and distinct-link deletion without deadlock, write-skew, or invalid public commit, rejected `REPEATABLE READ` triggered writes including draft-delete versus publish races, successful parent-card and owning-group cascades, Discord identity scoping, and Compose schema gating.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user