diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index fe480e4d4..f408b081a 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -296,7 +296,6 @@ class RomSchema(BaseModel): missing_from_fs: bool has_notes: bool - siblings: list[SiblingRomSchema] rom_user: RomUserSchema merged_screenshots: list[str] merged_ra_metadata: RomRAMetadata | None @@ -304,15 +303,6 @@ class RomSchema(BaseModel): @classmethod def populate_properties(cls, db_rom: Rom, request: Request) -> Rom: db_rom.rom_user = RomUserSchema.for_user(request.user.id, db_rom) # type: ignore - db_rom.siblings = [ # type: ignore - SiblingRomSchema( - id=s.id, - name=s.name, - fs_name_no_tags=s.fs_name_no_tags, - fs_name_no_ext=s.fs_name_no_ext, - ) - for s in db_rom.sibling_roms - ] db_rom.has_notes = any( # type: ignore note.is_public or note.user_id == request.user.id for note in db_rom.notes ) @@ -330,10 +320,6 @@ class RomSchema(BaseModel): def sort_files(cls, v: list[RomFileSchema]) -> list[RomFileSchema]: return sorted(v, key=lambda x: x.file_name) - @field_validator("siblings") - def sort_siblings(cls, v: list[SiblingRomSchema]) -> list[SiblingRomSchema]: - return sorted(v, key=lambda x: x.sort_comparator) - class SiblingRomSchema(BaseModel): id: int @@ -355,9 +341,13 @@ class SiblingRomSchema(BaseModel): class SimpleRomSchema(RomSchema): + siblings: list[int] = [] + @classmethod def from_orm_with_request(cls, db_rom: Rom, request: Request) -> SimpleRomSchema: db_rom = cls.populate_properties(db_rom, request) + # `sibling_ids` is set on each Rom by the list endpoint before serialization. + db_rom.siblings = getattr(db_rom, "sibling_ids", []) # type: ignore return cls.model_validate(db_rom) @classmethod @@ -387,16 +377,30 @@ class UserCollectionSchema(BaseModel): class DetailedRomSchema(RomSchema): + siblings: list[SiblingRomSchema] = [] user_saves: list[SaveSchema] user_states: list[StateSchema] user_screenshots: list[ScreenshotSchema] user_collections: list[UserCollectionSchema] all_user_notes: list[UserNoteSchema] + @field_validator("siblings") + def sort_siblings(cls, v: list[SiblingRomSchema]) -> list[SiblingRomSchema]: + return sorted(v, key=lambda x: x.sort_comparator) + @classmethod def from_orm_with_request(cls, db_rom: Rom, request: Request) -> DetailedRomSchema: user_id = request.user.id db_rom = cls.populate_properties(db_rom, request) + db_rom.siblings = [ # type: ignore + SiblingRomSchema( + id=s.id, + name=s.name, + fs_name_no_tags=s.fs_name_no_tags, + fs_name_no_ext=s.fs_name_no_ext, + ) + for s in db_rom.sibling_roms + ] db_rom.user_saves = [ # type: ignore SaveSchema.model_validate(s) for s in db_rom.saves if s.user_id == user_id diff --git a/backend/endpoints/roms/__init__.py b/backend/endpoints/roms/__init__.py index c6b78decc..da81e5081 100644 --- a/backend/endpoints/roms/__init__.py +++ b/backend/endpoints/roms/__init__.py @@ -569,12 +569,20 @@ def get_roms( with sync_session.begin() as session: rom_id_index = session.scalars(query.with_only_columns(Rom.id)).all() # type: ignore + def _transform(items): + sibling_ids_by_rom = db_rom_handler.get_sibling_ids_for_roms( + [i.id for i in items] + ) + for rom in items: + rom.sibling_ids = sibling_ids_by_rom.get(rom.id, []) + return [ + SimpleRomSchema.from_orm_with_request(i, request) for i in items + ] + return paginate( session, query, - transformer=lambda items: [ - SimpleRomSchema.from_orm_with_request(i, request) for i in items - ], + transformer=_transform, additional_data={ "char_index": char_index_dict, "rom_id_index": rom_id_index, diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 302ed9b20..3e5bf25d9 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -38,7 +38,7 @@ from decorators.database import begin_session from handler.metadata.base_handler import UniversalPlatformSlug as UPS from models.assets import Save, Screenshot, State from models.platform import Platform -from models.rom import Rom, RomFile, RomMetadata, RomNote, RomUser +from models.rom import Rom, RomFile, RomMetadata, RomNote, RomUser, SiblingRom from utils.database import ( json_array_contains_all, json_array_contains_any, @@ -146,7 +146,14 @@ def with_details(func): joinedload(RomFile.rom).load_only(Rom.fs_path, Rom.fs_name) ), selectinload(Rom.sibling_roms).options( - noload(Rom.platform), noload(Rom.metadatum) + load_only( + Rom.id, + Rom.name, + Rom.fs_name_no_tags, + Rom.fs_name_no_ext, + ), + noload(Rom.platform), + noload(Rom.metadatum), ), selectinload(Rom.collections), selectinload(Rom.notes), @@ -195,6 +202,33 @@ class DBRomsHandler(DBBaseHandler): return [] return session.scalars(query.filter(Rom.id.in_(ids))).all() + @begin_session + def get_sibling_ids_for_roms( + self, + rom_ids: Iterable[int], + *, + session: Session = None, # type: ignore + ) -> dict[int, list[int]]: + """Return {rom_id: [sibling_rom_id, ...]} for the given rom IDs. + + Single query against the sibling_roms view, projecting only the two + id columns — no Rom row hydration. + """ + ids = list(rom_ids) + if not ids: + return {} + + rows = session.execute( + select(SiblingRom.rom_id, SiblingRom.sibling_rom_id).where( + SiblingRom.rom_id.in_(ids) + ) + ).all() + + result: dict[int, list[int]] = {rom_id: [] for rom_id in ids} + for rom_id, sibling_rom_id in rows: + result[rom_id].append(sibling_rom_id) + return result + def filter_by_platform_id(self, query: Query, platform_id: int): return query.filter(Rom.platform_id == platform_id) @@ -564,10 +598,8 @@ class DBRomsHandler(DBBaseHandler): selectinload(Rom.files).options( joinedload(RomFile.rom).load_only(Rom.fs_path, Rom.fs_name) ), - # Show sibling rom badges on cards - selectinload(Rom.sibling_roms).options( - noload(Rom.platform), noload(Rom.metadatum) - ), + # Sibling badges are populated separately via get_sibling_ids_for_roms + # to avoid hydrating full Rom rows (including JSON metadata) per sibling. # Show notes indicator on cards selectinload(Rom.notes), ) diff --git a/frontend/src/__generated__/models/SimpleRomSchema.ts b/frontend/src/__generated__/models/SimpleRomSchema.ts index 68b97669f..9962f0e4f 100644 --- a/frontend/src/__generated__/models/SimpleRomSchema.ts +++ b/frontend/src/__generated__/models/SimpleRomSchema.ts @@ -15,7 +15,6 @@ import type { RomMobyMetadata } from './RomMobyMetadata'; import type { RomRAMetadata } from './RomRAMetadata'; import type { RomSSMetadata } from './RomSSMetadata'; import type { RomUserSchema } from './RomUserSchema'; -import type { SiblingRomSchema } from './SiblingRomSchema'; export type SimpleRomSchema = { id: number; igdb_id: (number | null); @@ -83,7 +82,7 @@ export type SimpleRomSchema = { updated_at: string; missing_from_fs: boolean; has_notes: boolean; - siblings: Array; + siblings: Array; rom_user: RomUserSchema; merged_screenshots: Array; merged_ra_metadata: (RomRAMetadata | null);