fix(stats): rebuild player_stats as a projection and persist AI seats - #28
Merged
Merged
Conversation
player_stats was incremented by both aggregate_game_stats and the aggregate-game-stats Edge Function, and a queue retry reran both. Migration 20260914000001 makes it a projection: rebuild_player_stats(user) recomputes absolute values from completed games and TurnScored events under a per-user advisory lock, refresh_player_stats_for_game rebuilds a game's human seats, aggregate_game_stats keeps its signature and calls the refresh, and complete_game_atomic refreshes in its own transaction. A one-time rebuild replaces existing rows. Execution is service_role only. game_players gains is_ai and ai_profile with a nullable user_id and primary key (game_id, seat_number), so games with AI seats persist; completion matches AI rankings by seat number. The Worker now sends JSON arrays: its hand-built array literals were rejected by PostgREST (22P02), so no game was persisted. The Edge Function, its caller and flags, the unused GamePersistenceService and the unused web stats writers are removed. pgTAP covers repeats, retries, solo, multiplayer, AI seats, incomplete games, missing events, the win rule and the rebuild. The migration applies before or after 20260913000002 with an identical resulting schema. Status and roadmap record the release steps: a stats-count readback before applying, then deleting the deployed Edge Function and rebuilding once after the Worker release.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Status action 7: fixes the stats double count and the persistence failures behind it.
22P02 malformed record literal.create_game_atomictherefore always failed, so the deployed Worker has likely never persisted a game. It now sends JSON arrays of objects. The double counting was real in code (the SQL aggregation plus the Edge Function, and again on each retry) but probably never ran in production.supabase/migrations/20260914000001_player_stats_projection.sql:rebuild_player_stats(user_id)recomputes absolute values from completed games andTurnScoredevents, under a per-user advisory lock, and skips no-op writes.refresh_player_stats_for_game(game_id)rebuilds every human seat of a game.aggregate_game_stats(uuid)keeps its signature, so the grants in20260913000002still apply, and now calls the refresh.complete_game_atomicrefreshes stats inside its own transaction.update_category_statsis dropped.game_players.is_ai,ai_profile, a nullableuser_idwith a CHECK tying them together, primary key(game_id, seat_number), and a unique(game_id, user_id).service_roleonly.20260913000002, with an identical schema snapshot either way.user_id: nullplus their profile, and completion addresses them by seat number.winner_id, and only human seats' domain events persist.GamePersistenceServiceandAggregationRequestSchemaare removed.supabase/functions/aggregate-game-stats/and the unused webcreatePlayerStats/updatePlayerStatswriters.database.ts: the minimal typed change, hand-applied from local generation; the only other difference in the generated file is an unrelatedgraphql_publicblock.game_playersSELECT policy found here, and section 2 gains a Decision Quality follow-up.No migration applied, nothing deployed.
Release notes for the operator (status action 3)
gamescount,player_statsrows and summedgames_played. The one-time rebuild resets stats that no completed game backs, so decide before applying.20260914000001alone. It fails and rolls back if existinggame_playersrows collide on(game_id, seat_number).dicee-web, move the domain, then deploydicee.aggregate-game-statsEdge Function, thenselect public.rebuild_player_stats(user_id) from public.player_stats;once.Verification
supabase test db: 177 passed, including 30 new projection tests.test:agent: 528 passed. Webtest:agent: 1594 passed.pnpm validate:ci, CI and CodeQL.TurnScoredcarrieswas_optimal/ev_difference; thegame_playersread policy recursion is pre-existing (roadmap step 2).