From 6e3ef328153e2763c1bc5bade6932213bf581528 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 25 Jun 2026 09:10:47 -0400 Subject: [PATCH] fix(roms): gate sidecar caching on all active filters The char index and rom id index sidecars are cached under a key that encodes only user/order/grouping. is_unscoped previously excluded only scope and search, so metadata/tag/status filters and the bool flags applied to the query bypassed the gate: a filtered all-games request stored a narrowed id list under the shared "all" key and later unfiltered (or differently-filtered) requests read it back, showing the wrong set and count of games. Treat any narrowing parameter as scoped so those sets compute live. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/endpoints/roms/__init__.py | 26 +++++++++++++++++++++++++- backend/tools/generate_test_data.py | 11 +++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/backend/endpoints/roms/__init__.py b/backend/endpoints/roms/__init__.py index 72b241b71..d4940dea7 100644 --- a/backend/endpoints/roms/__init__.py +++ b/backend/endpoints/roms/__init__.py @@ -573,13 +573,37 @@ def get_roms( include_file_stats=True, ) - # Cache only the unscoped library scan; scoped/searched sets are narrower and computed live. + # Cache only the fully unscoped library scan; any narrowing parameter makes + # the result set narrower, so it is computed live. The sidecar cache key + # encodes only user/order/grouping, not the filters, so every filter applied + # to `query` below must gate caching here or a narrowed list leaks under the + # shared "all" key. Bool flags use `is not None` since False is an active + # filter. Logic operators are omitted: they only matter when their list + # filter is set, which is already covered. is_unscoped = not ( search_term or platform_ids or collection_id or virtual_collection_id or smart_collection_id + or genres + or franchises + or collections + or companies + or age_ratings + or statuses + or regions + or languages + or player_counts + or updated_after + or matched is not None + or favorite is not None + or duplicate is not None + or last_played is not None + or playable is not None + or has_ra is not None + or missing is not None + or verified is not None ) # Get the char index for the roms diff --git a/backend/tools/generate_test_data.py b/backend/tools/generate_test_data.py index 72c376390..0c26319a9 100644 --- a/backend/tools/generate_test_data.py +++ b/backend/tools/generate_test_data.py @@ -714,11 +714,6 @@ def main() -> int: parser.add_argument( "--password", default="password", help="Plain password for every seeded user" ) - parser.add_argument( - "--user-prefix", - default="loadtest", - help="Prefix for seeded usernames (kept unique by id)", - ) parser.add_argument( "--wipe", action="store_true", @@ -852,11 +847,11 @@ def main() -> int: # Suffix with the assigned id so usernames/emails never collide with # rows already in the target database. if i == 0: - username, role = f"{args.user_prefix}_admin_{uid}", Role.ADMIN + username, role = f"admin_{uid}", Role.ADMIN elif i <= 2: - username, role = f"{args.user_prefix}_editor_{uid}", Role.EDITOR + username, role = f"editor_{uid}", Role.EDITOR else: - username, role = f"{args.user_prefix}_viewer_{uid}", Role.VIEWER + username, role = f"viewer_{uid}", Role.VIEWER created = rand_past(rng, now) user_rows.append( {