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 __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()
|
||||||
|
|||||||
Reference in New Issue
Block a user