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) <noreply@anthropic.com>
This commit is contained in:
Georges-Antoine Assi
2026-06-25 09:10:47 -04:00
parent bd22a46eda
commit 6e3ef32815
2 changed files with 28 additions and 9 deletions

View File

@@ -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

View File

@@ -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(
{