diff --git a/app/routers_v2/refractor.py b/app/routers_v2/refractor.py index cb5d06a..d28aba2 100644 --- a/app/routers_v2/refractor.py +++ b/app/routers_v2/refractor.py @@ -23,8 +23,12 @@ _NEXT_THRESHOLD_ATTR = { 4: None, } +# Sentinel used by _build_card_state_response to distinguish "caller did not +# pass image_url" (do the DB lookup) from "caller passed None" (use None). +_UNSET = object() -def _build_card_state_response(state, player_name=None) -> dict: + +def _build_card_state_response(state, player_name=None, image_url=_UNSET) -> dict: """Serialise a RefractorCardState into the standard API response shape. Produces a flat dict with player_id and team_id as plain integers, @@ -67,22 +71,27 @@ def _build_card_state_response(state, player_name=None) -> dict: if player_name is not None: result["player_name"] = player_name - # Resolve image_url from the variant card row - image_url = None - if state.variant and state.variant > 0: - card_type = ( - state.track.card_type if hasattr(state, "track") and state.track else None - ) - if card_type: - CardModel = BattingCard if card_type == "batter" else PitchingCard - try: - variant_card = CardModel.get( - (CardModel.player_id == state.player_id) - & (CardModel.variant == state.variant) - ) - image_url = variant_card.image_url - except CardModel.DoesNotExist: - pass + # Resolve image_url from the variant card row. + # When image_url is pre-fetched by the caller (batch list path), it is + # passed directly and the per-row DB query is skipped entirely. + if image_url is _UNSET: + image_url = None + if state.variant and state.variant > 0: + card_type = ( + state.track.card_type + if hasattr(state, "track") and state.track + else None + ) + if card_type: + CardModel = BattingCard if card_type == "batter" else PitchingCard + try: + variant_card = CardModel.get( + (CardModel.player_id == state.player_id) + & (CardModel.variant == state.variant) + ) + image_url = variant_card.image_url + except CardModel.DoesNotExist: + pass result["image_url"] = image_url return result @@ -230,14 +239,43 @@ async def list_card_states( query = query.where(RefractorCardState.last_evaluated_at.is_null(False)) total = query.count() or 0 + states_page = list(query.offset(offset).limit(limit)) + + # Pre-fetch image_urls in at most 2 bulk queries (one per card table) so + # that _build_card_state_response never issues a per-row CardModel.get(). + batter_pids: set[int] = set() + pitcher_pids: set[int] = set() + for state in states_page: + if state.variant and state.variant > 0: + card_type = state.track.card_type if state.track else None + if card_type == "batter": + batter_pids.add(state.player_id) + elif card_type in ("sp", "rp"): + pitcher_pids.add(state.player_id) + + image_url_map: dict[tuple[int, int], str | None] = {} + if batter_pids: + for card in BattingCard.select().where(BattingCard.player_id.in_(batter_pids)): + image_url_map[(card.player_id, card.variant)] = card.image_url + if pitcher_pids: + for card in PitchingCard.select().where( + PitchingCard.player_id.in_(pitcher_pids) + ): + image_url_map[(card.player_id, card.variant)] = card.image_url + items = [] - for state in query.offset(offset).limit(limit): + for state in states_page: player_name = None try: player_name = state.player.p_name except Exception: pass - items.append(_build_card_state_response(state, player_name=player_name)) + img_url = image_url_map.get((state.player_id, state.variant)) + items.append( + _build_card_state_response( + state, player_name=player_name, image_url=img_url + ) + ) return {"count": total, "items": items}