Fetch match-history participants in one query per page
CI / Build and push docker image (push) Successful in 53s
CI / Build and push docker image (push) Successful in 53s
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user