Fetch match-history participants in one query per page
CI / Build and push docker image (push) Successful in 53s

This commit is contained in:
2026-09-28 05:21:34 +08:00
parent 834f1f26a9
commit 08f158ef28
2 changed files with 43 additions and 3 deletions
@@ -8,7 +8,9 @@ verbatim through the ``result`` JSON column.
"""
from __future__ import annotations
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
from uuid import UUID
from kaya.core import HttpContext, KayaApp
from kaya.openapi import operation
@@ -48,8 +50,9 @@ def _parse_game_type(
return game_type, None
async def _serialize_match(match: Match, viewer: str) -> Dict[str, Any]:
participants = await MatchPlayer.filter(match_id=match.id).order_by("seat")
def _serialize_match(
match: Match, viewer: str, participants: List[MatchPlayer]
) -> Dict[str, Any]:
return {
"id": str(match.id),
"game_type": match.game_type,
@@ -109,7 +112,21 @@ def register(app: KayaApp, platform: "Platform") -> None:
matches, next_cursor = await paginate(
queryset, [("finished_at", "DESC"), ("id", "ASC")], cursor
)
results = [await _serialize_match(m, user.sub) for m in matches]
# One participants query for the whole page instead of one per
# match; the grouping keeps each match's players seat-ordered.
by_match: Dict[UUID, List[MatchPlayer]] = defaultdict(list)
if matches:
participants = (
await MatchPlayer.filter(match_id__in=[m.id for m in matches])
.order_by("match_id", "seat")
)
for player in participants:
# Tortoise sets the FK's raw id on instances but does not
# declare it on the model class, hence the getattr.
by_match[getattr(player, "match_id")].append(player)
results = [
_serialize_match(m, user.sub, by_match.get(m.id, [])) for m in matches
]
await send_json(ctx, 200, {"results": results, "next_cursor": next_cursor})
@app.GET("/api/leaderboard")
@@ -4,6 +4,7 @@ from __future__ import annotations
import unittest
import uuid
from datetime import datetime, timezone
from unittest import mock
from httpx import ASGITransport, AsyncClient
@@ -197,6 +198,28 @@ class StatsRouteTest(unittest.TestCase):
second.json()["results"][0]["id"],
)
@async_test
async def test_my_matches_fetches_participants_in_one_query(self) -> None:
app, platform, tortoise_mixin = make_platform()
await _seed_two_matches(tortoise_mixin)
transport = ASGITransport(app=app)
with mock.patch.object(
MatchPlayer, "filter", wraps=MatchPlayer.filter
) as spy:
async with AsyncClient(
transport=transport, base_url="http://127.0.0.1"
) as client:
with oidc_user(platform.oidc, "alice"):
response = await client.get("/api/me/matches")
self.assertEqual(200, response.status_code)
# One query for all matches, not one per match row.
self.assertEqual(1, spy.call_count)
results = response.json()["results"]
self.assertEqual(2, len(results))
for result in results:
seats = [player["seat"] for player in result["players"]]
self.assertEqual([0, 1], seats)
@async_test
async def test_my_matches_requires_auth(self) -> None:
app, _, _ = make_platform()