Build the allpicks grid from one query #450
No reviewers
Labels
No labels
Kestra
bug
enhancement
someday
subtask
☁️ api
🎛️ infrastructure
🐞 sentry
📆 2025 Season
📝 pages
allpicks
📝 pages
picks
📝 pages
standings
🚀 performance
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
johnsturgeon/tgfp-web!450
Loading…
Reference in a new issue
No description provided.
Delete branch "n-1-query-allpicks-page-319"
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?
The cost
/allpicksis a grid: a row per player, a column per game. Everything itshowed per cell was fetched per cell.
Measured with a seeded 34-player, 16-game week:
~21 per player: one for
picks_for_week(row styling), four forwins_for_week/losses_for_week/bonus_for_week(the last called twice),and one per game for
pick_for_game_id.pick_for_game_id's comment claims to"search cached picks first" -- that cache went with
WeekInfo, so it is a freshSELECT every time.
Games and teams were already fine at one query each.
The change
PlayerPicks(app/models/player_picks.py) is what the page reads: a player,their picks for the week keyed by game, and the wins/losses/bonus those picks
earned. Not a table -- the picks themselves are still rows in
playergamepick.PlayerPicks.for_weekassembles one per player from a single grouped query.The template's structure is unchanged -- outer loop per player, inner loop per
game -- it just asks
player_picks.pick_for(game)instead of the database.Player._record_from_picksbecomes public; it was always a pure fold over alist of picks.
Deliberately untouched:
pick_for_game_idand the per-player record methods.The standings page still uses them, and one player at a time never had this
problem.
Tests
First coverage of the route. Four say what the page shows -- a row per
player, correct records per player, every pick named,
--no pick--for aplayer with none. Those are what make the query change safe to trust.
The fifth is the guard, and it asserts a constant rather than a number:
render 2 players x 2 games, then 12 x 8, and assert the pick-query count is
identical. A threshold would pin today's cost and break on an unrelated change.
Reverting the template to
pick_for_game_id/wins_for_weekfails it.Two mutations checked: swapping
for_weekback to a per-player query fails thecost test, and making
pick_forreturnNonefails the rendering test.Note
The count lands at 2, not 1. The second is
allpicksbase.j2:41asking whetherthe viewing player has picks before showing the page at all -- constant, not
per-cell.
standings.j2still callswins_for_week/losses_for_week/bonus_for_weekper player, which is 3 queries each. Smaller, different page,left alone.
resolves #319
🤖 Generated with Claude Code
This is a clean, well-scoped PR. I've verified the rename is consistent, all callers updated, the new query path, and the template rewrite. One genuine (small) performance/docstring accuracy point stood out.
The change collapses the
/allpicksgrid from ~700 per-cell queries to a single grouped fetch, and it's done cleanly:PlayerPicks.for_weekassembles one row per player fromPlayerGamePick.picks_by_player_for_week, the template loops over the pre-built objects, and the_record_from_picks→record_from_picksrename is applied everywhere it's called. The new rendering tests pin what the page shows and the cost test guards the query count as an invariant, which is the right shape for making this refactor trustworthy.Worth fixing
app/main.py:485/app/models/player_picks.py:35— ordering makes the docstring's central claim false and leaves some queries on the table.PlayerPicks.for_weekscores each pick by walkingpick.game.winning_team→game.home_team/away_team, and its docstring says "the page has already loaded the week's games and every team, so those resolve from the session's identity map." ButTeam.all_teams(session=...)is called on the next line (app/main.py:488), so at the momentfor_weekruns the teams are not in the identity map yet. Those relationship accesses lazy-load, issuing roughly oneSELECT ... FROM teamper final game. It's bounded by games (the result is cached on the sharedGameinstance), not an N+1 across the grid, so it won't scale with players — but it's real, and simply moving theTeam.all_teams(...)call above thePlayerPicks.for_week(...)call eliminates it and makes the docstring accurate. Note the cost test wouldn't catch this either way, since it filters toFROM playergamepickonly.Nits
tests/test_player_model.py:4— the module docstring still refers toPlayer._record_from_picks; the method is now public (record_from_picks). Stale reference only, no functional impact.app/templates/allpicks.j2:55—row_with_stylenow receivesplayer_picks.picks(a dict) instead of the old list, but the macro only uses the arg in anis not definedcheck that can never be true for a passed parameter. Pre-existing dead logic, not introduced here; worth a cleanup pass someday but not part of this change.Nothing blocking — the correctness of the output is well covered and the perf win is real.