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 __future__ import annotations
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
from uuid import UUID
from kaya.core import HttpContext, KayaApp from kaya.core import HttpContext, KayaApp
from kaya.openapi import operation from kaya.openapi import operation
@@ -48,8 +50,9 @@ def _parse_game_type(
return game_type, None return game_type, None
async def _serialize_match(match: Match, viewer: str) -> Dict[str, Any]: def _serialize_match(
participants = await MatchPlayer.filter(match_id=match.id).order_by("seat") match: Match, viewer: str, participants: List[MatchPlayer]
) -> Dict[str, Any]:
return { return {
"id": str(match.id), "id": str(match.id),
"game_type": match.game_type, "game_type": match.game_type,
@@ -109,7 +112,21 @@ def register(app: KayaApp, platform: "Platform") -> None:
matches, next_cursor = await paginate( matches, next_cursor = await paginate(
queryset, [("finished_at", "DESC"), ("id", "ASC")], cursor 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}) await send_json(ctx, 200, {"results": results, "next_cursor": next_cursor})
@app.GET("/api/leaderboard") @app.GET("/api/leaderboard")
@@ -4,6 +4,7 @@ from __future__ import annotations
import unittest import unittest
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from unittest import mock
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
@@ -197,6 +198,28 @@ class StatsRouteTest(unittest.TestCase):
second.json()["results"][0]["id"], 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_test
async def test_my_matches_requires_auth(self) -> None: async def test_my_matches_requires_auth(self) -> None:
app, _, _ = make_platform() app, _, _ = make_platform()