fix(roms): sort grouped galleries by the best sibling's key - #4465
Conversation
With group_by_meta_id, the sibling-dedup window picks each group's representative by main-sibling/prerelease/region rank, and the outer ORDER BY only ever saw that rom's own sort key, so a group whose rank-preferred copy was never touched sorted into the NULL tail even when another sibling was played yesterday. RomUser-column sorts now aggregate the key across the group inside the existing window pass (MAX descending, MIN ascending, so a group lands in the NULL bucket only when every sibling's key is NULL), and the outer ordering reads that group value through a null-safe outer join that keeps the covering-index plan (the inner-join and CTE shapes measured 3-7x worse; this one is ~1.55x master on the one family it fixes and compiles byte-identically everywhere else). The displayed representative stays the rank-preferred one. Metadata and Rom-side grouped sorts keep representative order deliberately, since materializing a roms-side key into the window drops it off idx_roms_sibling_cover, the regression migration 0107 prevents. Fixes #4447 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ldi4YaXJWDDmFdbKfQay8L
…ate order_by Code-review pass fixes: - A hidden sibling's key is masked to NULL inside the group aggregate, so a rom the user hid can no longer drive its group's position. - The aggregate shares row_number's window spec with a whole-partition frame, so MariaDB sorts the derived table once instead of twice. - _mapped_sort_column resolves order_by only to mapped columns; a relationship or dunder name now falls back to the name sort instead of raising while the ORDER BY is built. - filter_roms takes everything after the query keyword-only. - Tests: hidden-sibling behavior test, grouped id-index test, the covering-index check compiles the window-level SELECT, alias-free ORDER BY assertions, MariaDB-dialect null-safe join assertion, and _make_rom/_ordered_names replace the copied fixture helpers. Known and deliberate, per review: the aggregate is direction-dependent (MAX on desc, MIN on asc), rating's 0 sentinel stays a real key, and non-aggregated (enum, lexical, roms-side) keys keep representative semantics. The stale-sidecar gap it flagged is fixed separately by the per-user cache versioning PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ldi4YaXJWDDmFdbKfQay8L
Simplify pass: - get_roms_query returns the resolved _GallerySortKey and filter_roms accepts it, so one request resolves the sort once and the grouped aggregate cannot disagree with the ordering the query was built with. - _GallerySortKey carries a single source tag instead of two mutually exclusive booleans. - _rom_user_not_hidden() replaces the three copies of the null-safe visibility predicate. - _GROUP_SORT_AGGREGATE_TYPES trimmed to the types RomUser has (DateTime, Integer); Date/Numeric were unreachable. - order_dir normalizes once per entry point. Skipped, deliberately: folding dedup_subquery into the join condition (plan-pinned SQL) and a single-owner ORDER BY refactor (touches every get_roms_query caller). Compiled SQL is unchanged, per the shape tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ldi4YaXJWDDmFdbKfQay8L
…egate-4447 # Conflicts: # backend/handler/database/roms_handler.py # backend/tests/handler/database/test_roms_hltb_length.py # backend/tests/handler/database/test_roms_metadata_sort.py # backend/tests/handler/database/test_roms_rom_user_sort.py
After merging master, the dialect-aware ordering drops the leading IS NULL term on non-PostgreSQL descending sorts and adds it to nullable metadata keys; the shape tests now assert that form and pin the MariaDB driver where the emulation branch is what they check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ldi4YaXJWDDmFdbKfQay8L
Polish pass: the A-D sweep found the merged diff already in shape; one three-line parameter comment ran long. 165 tests across the affected suites pass on the merged tree and trunk fmt/check are clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ldi4YaXJWDDmFdbKfQay8L
Greptile SummaryThis PR centralizes gallery sort-key resolution and changes grouped galleries sorted by numeric or date-valued
Confidence Score: 4/5The PR is not safe to merge until permission-hidden siblings are prevented from determining visible groups’ sort positions. The new aggregate evaluates all siblings before Files Needing Attention: backend/handler/database/roms_handler.py Important Files Changed
Prompt To Fix All With AI### Issue 1
backend/handler/database/roms_handler.py:1708-1710
**Hidden siblings affect sorting**
When a non-representative sibling is hidden through `hidden_rom_ids`, this aggregate still includes it because the CASE masks only `RomUser.hidden`. The permission filter runs later against the representative row, so an inaccessible sibling with the newest play or highest rating can move a visible group to the wrong position. Exclude permission-hidden siblings from the aggregate or apply those visibility constraints before building the deduplication window.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "review(pr-ready step 4): tighten the sor..." | Re-trigger Greptile |
Review follow-up from Greptile on the PR: the aggregate masked only the personal RomUser.hidden flag, so a sibling hidden through admin hidden_rom_ids/hidden_platform_ids could still drive its group's position. The admin visibility filters now apply before the dedup window builds, so a permission-hidden sibling can neither represent a group nor feed its sort key, and a permission-hidden representative no longer hides its whole group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ldi4YaXJWDDmFdbKfQay8L
There was a problem hiding this comment.
🟡 Changes recommended
Hidden ROM/platform rows can still affect representative selection and grouped ordering, and invalid order_by fallback coverage is still requested.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes grouped gallery ordering by sorting on the best eligible sibling key for supported per-user date and integer fields.
Changes:
- Adds direction-aware sibling aggregation.
- Centralizes gallery sort-key resolution.
- Updates endpoint wiring and expands grouped-query and index tests.
File summaries
| File | Summary |
|---|---|
backend/tests/handler/database/test_roms_rom_user_sort.py |
Tests grouped per-user sorting behavior. |
backend/tests/handler/database/test_roms_metadata_sort.py |
Tests representative metadata sorting. |
backend/tests/handler/database/test_roms_group_by_index.py |
Verifies covering-index query coverage. |
backend/handler/database/roms_handler.py |
Implements aggregation and shared sort resolution. |
backend/endpoints/roms/__init__.py |
Passes resolved sort keys through gallery queries. |
Review details
Suppressed comments (1)
backend/handler/database/roms_handler.py:1710
- This CASE masks only
RomUser.hidden, buthidden_platform_idsandhidden_rom_idsare applied later at lines 1781-1787. A ROM hidden from the requesting user can therefore still supply the newestlast_playedor highest rating and move the group's position, even though it is excluded from the result. Apply those admin-visibility predicates to the aggregate input as well, while preserving the current dedup input if representative selection must remain unchanged.
]
window_columns: list[ColumnElement] = [
func.row_number()
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review follow-up from Copilot on the PR: query-shape coverage for relationship and dunder order_by values, with and without a user, asserting they resolve to the name sort without raising. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ldi4YaXJWDDmFdbKfQay8L
|
❌ Preview build failed. |
…egate-4447 Resolves the overlap with #4463's per-user sidecar cache versions: the endpoint keeps the resolved sort key while adopting master's normalise-once ordering params and single sidecar cache key, and `sorts_by_rom_user_column` now answers from `_resolve_gallery_sort_key` so the cache key and the ORDER BY agree on what a RomUser sort is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
Fixes #4447
In grouped galleries, a group sorted by the representative rom's key, so a group whose representative carried no value (never played, unrated) landed in the NULL bucket even when a sibling had one. The gallery now sorts each group by the best visible sibling's key.
MAX(key) OVER (partition)aggregate on descending sorts andMINon ascending ones, for RomUser-column sorts whose type (DateTime, Integer) keeps MIN/MAX consistent with ORDER BY semantics on every dialect. The aggregate sharesrow_number's window spec with a whole-partition frame, so the derived table is sorted once.IS NOT DISTINCT FROM), which pins MariaDB's covering-index plan; measured with EXPLAIN at ~1.55x the ungrouped cost on the fixed query family, with the rejected shapes at 3-7x. Non-aggregated sorts (enum, lexical, roms-side metadata keys) keep byte-identical SQL and representative semantics: an aggregated roms-side key would push the window offidx_roms_sibling_cover, and MIN/MAX diverges from ORDER BY for enums.get_roms_queryand the grouped reorder now share one_resolve_gallery_sort_key/_gallery_order_clausespair; the endpoint resolves the sort once andfilter_romsconsumes the resolved key. Bogusorder_byvalues (relationship or dunder names) now fall back to the name sort instead of raising while the ORDER BY is built.Ran the full pr-ready gauntlet (security-audit: clean; code-review xhigh; simplify; review-polish), one commit per pass, plus a merge of current master resolving the overlap with #4454's NULLS LAST rework. 165 tests across the affected suites pass on MariaDB, trunk is clean. Backend-only, no migrations, no API shape change.
Deliberate scope, noted for reviewers: the aggregate is direction-dependent (a group spanning old and new plays takes the extreme end of its range in both directions, as the issue requests); enum/boolean/roms-side keys keep representative semantics; the PostgreSQL plan for the null-safe join shape was not benchmarked (MariaDB was) and deserves an EXPLAIN before release. The stale-sidecar-cache gap that review surfaced is fixed separately by #4463.
Checklist
AI disclosure: This PR was implemented and reviewed end to end with Claude Code (implementation, the four-pass review gauntlet, EXPLAIN-based plan validation, and tests), directed and supervised by the maintainer.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ldi4YaXJWDDmFdbKfQay8L