perf: batch image_url prefetch in list_card_states to eliminate N+1 (#199) #206
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-reviewing
ai-working
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
tech-debt
todo
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#206
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "issue/199-perf-n-1-query-in-build-card-state-response-for-im"
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?
Closes #199
Problem
_build_card_state_responseissued a separateCardModel.get()for every card state withvariant > 0. On the/cardslist endpoint withlimit=100, this meant up to 100 extra DB round-trips — one per card — scaling linearly with page size.Fix
Pre-fetch all
image_urlvalues before the response-building loop using at most 2 bulk queries (one againstBattingCard, one againstPitchingCard):battervssp/rp).SELECT … WHERE player_id IN (…)against each card table (skipped entirely if the set is empty).(player_id, variant) → image_urldict._build_card_state_responsevia a newimage_urlparameter.The helper now skips its own DB lookup whenever the caller passes
image_url(using a sentinel_UNSETto distinguish "not provided" fromNone). The single-cardget_card_statepath is unchanged and still resolvesimage_urlinline — acceptable for a single-item response.Files changed
app/routers_v2/refractor.pyQuery reduction
AI Code Review
Files Reviewed
app/routers_v2/refractor.py(modified)Findings
Correctness
No issues found. The implementation is correct across all code paths:
_UNSETsentinel correctly distinguishesimage_url=None(explicit pre-fetch result) fromimage_url=_UNSET(no pre-fetch, do inline lookup). The sentinel is defined at module level and used viaisidentity check — clean pattern.state.variant and state.variant > 0before partitioning intobatter_pids/pitcher_pids. States withvariant=Noneorvariant=0fall through toimage_url_map.get(...)which returnsNone, matching original behavior.(player_id, variant)correctly disambiguates multiple variants per player. The bulk query fetches all cards for the partitioned player IDs (including variant=0), but the keyed lookup is precise.image_url=Noneexplicitly, which short-circuits the inline DB lookup (None is not _UNSET). Result isimage_url: null— same as original.state.trackguard in the pre-fetch loop (if state.track else None) is sufficient —list_card_statesJOINsRefractorTrackunconditionally, sostate.trackis always populated.if batter_pids:/if pitcher_pids:) correctly prevent emptyIN ()clauses that would produce a DB error.get_card_state) is unchanged — still calls_build_card_state_response(state)with noimage_urlargument, triggering the inline lookup. Correct.Security
No issues found. No new inputs, no injection vectors introduced.
Style & Conventions
No issues found. No lazy imports added. Type annotations on local variables (
set[int],dict[tuple[int, int], str | None]) are not evaluated at runtime and are consistent with the style elsewhere.Suggestions
BattingCard/PitchingCardrows for the partitioned player IDs, including non-variant rows (variant=0). Adding& (CardModel.variant > 0)to each bulk query would reduce the result set slightly and make intent clearer. Non-blocking — map key includes variant so no correctness impact.Verdict: APPROVED
Clean, well-scoped N+1 fix. The
_UNSETsentinel approach correctly preserves backward compatibility for the single-card endpoint while eliminating per-row queries on the list path. No correctness, security, or convention issues found. Ready to merge.Automated review by Claude PR Reviewer
Approved via pd-pr