Skip to content

Commit 9b6acf6

Browse files
committed
fix(backend): lightweight eager-load for /roms/{id}/simple
The v2 gallery per-card endpoint reused get_rom, which runs the full detail eager-load (saves, states, screenshots, collections) that SimpleRomSchema never serializes, making cards slow to hydrate. Add get_rom_simple backed by a with_simple_details decorator that loads only what SimpleRomSchema needs: platform, metadatum, rom_users, files, sibling_roms (+ their rom_users), notes, and the deferred file-count columns. Fixes #3771 Generated-By: PostHog Code Task-Id: 9233f519-079f-4990-8b48-9d12c81440b4
1 parent cb7a67a commit 9b6acf6

3 files changed

Lines changed: 83 additions & 1 deletion

File tree

backend/endpoints/roms/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ def get_rom_simple(
10831083
fetched on user-driven detail interactions (game details page, quick-
10841084
note dialog open, achievements panel)."""
10851085

1086-
rom = db_rom_handler.get_rom(id)
1086+
rom = db_rom_handler.get_rom_simple(id)
10871087

10881088
if not rom:
10891089
raise RomNotFoundInDatabaseException(id)

backend/handler/database/roms_handler.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,50 @@ def wrapper(*args, **kwargs):
243243
return wrapper
244244

245245

246+
def with_simple_details(func):
247+
"""Lightweight eager-load for the `SimpleRomSchema` (v2 gallery card).
248+
249+
Loads only the relationships `SimpleRomSchema` serializes (rom_users,
250+
files, sibling_roms + their rom_users, notes) and the deferred file-count
251+
columns, skipping the `saves` / `states` / `screenshots` / `collections`
252+
arrays that `with_details` pulls for the detail view. Keeps per-card
253+
hydration cheap on low-power hosts.
254+
"""
255+
256+
@functools.wraps(func)
257+
def wrapper(*args, **kwargs):
258+
kwargs["query"] = select(Rom).options(
259+
selectinload(Rom.platform),
260+
selectinload(Rom.rom_users).options(noload(RomUser.rom)),
261+
selectinload(Rom.metadatum).options(noload(RomMetadata.rom)),
262+
selectinload(Rom.files).options(
263+
joinedload(RomFile.rom).load_only(Rom.fs_path, Rom.fs_name),
264+
selectinload(RomFile.track_meta),
265+
),
266+
selectinload(Rom.sibling_roms).options(
267+
noload(Rom.platform),
268+
noload(Rom.metadatum),
269+
# Per-sibling is_main_sibling resolution needs each sibling's
270+
# RomUser (relationship is `lazy="raise"`).
271+
selectinload(Rom.rom_users).options(noload(RomUser.rom)),
272+
load_only(
273+
Rom.id,
274+
Rom.name,
275+
Rom.platform_id,
276+
Rom.fs_name_no_tags,
277+
Rom.fs_name_no_ext,
278+
),
279+
),
280+
selectinload(Rom.notes),
281+
undefer(Rom.multi_file),
282+
undefer(Rom.top_level_file_count),
283+
undefer(Rom.has_soundtrack),
284+
)
285+
return func(*args, **kwargs)
286+
287+
return wrapper
288+
289+
246290
class DBRomsHandler(DBBaseHandler):
247291
@begin_session
248292
@with_details
@@ -268,6 +312,18 @@ def get_rom(
268312
) -> Rom | None:
269313
return session.scalar(query.filter_by(id=id).limit(1))
270314

315+
@begin_session
316+
@with_simple_details
317+
def get_rom_simple(
318+
self,
319+
id: int,
320+
*,
321+
query: Query = None, # type: ignore
322+
session: Session = None, # type: ignore
323+
) -> Rom | None:
324+
"""Get a rom by ID with only the loads `SimpleRomSchema` needs."""
325+
return session.scalar(query.filter_by(id=id).limit(1))
326+
271327
@begin_session
272328
@with_details
273329
def get_roms_by_ids(

backend/tests/endpoints/roms/test_rom.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ def test_get_rom(client: TestClient, access_token: str, rom: Rom):
4444
assert body["id"] == rom.id
4545

4646

47+
def test_get_rom_simple(client: TestClient, access_token: str, rom: Rom):
48+
response = client.get(
49+
f"/api/roms/{rom.id}/simple",
50+
headers={"Authorization": f"Bearer {access_token}"},
51+
)
52+
assert response.status_code == status.HTTP_200_OK
53+
54+
body = response.json()
55+
assert body["id"] == rom.id
56+
# SimpleRomSchema stays lightweight: none of the detail-only arrays are
57+
# present, so the endpoint must not eager-load them.
58+
assert "user_saves" not in body
59+
assert "user_states" not in body
60+
assert "user_screenshots" not in body
61+
assert "user_collections" not in body
62+
assert "all_user_notes" not in body
63+
64+
65+
def test_get_rom_simple_missing_returns_404(client: TestClient, access_token: str):
66+
response = client.get(
67+
"/api/roms/999999/simple",
68+
headers={"Authorization": f"Bearer {access_token}"},
69+
)
70+
assert response.status_code == status.HTTP_404_NOT_FOUND
71+
72+
4773
def test_download_multi_file_rom_content(
4874
client: TestClient, access_token: str, multi_file_rom: Rom
4975
):

0 commit comments

Comments
 (0)