8.7 KiB
id, title, status, type, team, supporting_teams, roadmap_item, priority
| id | title | status | type | team | supporting_teams | roadmap_item | priority | ||
|---|---|---|---|---|---|---|---|---|---|
| TASK-200-fix-player-profile-runtime-postgres-grouping | Fix player profile runtime PostgreSQL grouping | done | backend | Backend Senior |
|
foundation | high |
TASK-200 - Fix player profile runtime PostgreSQL grouping
Goal
Corregir el 500 de /api/stats/players/{player_id}?server_id={server} cuando el jugador no tiene fila en player_period_stats para el timeframe solicitado y la ruta cae al fallback runtime sobre PostgreSQL.
Context
TASK-199 introdujo player_period_stats y el endpoint público ya usa correctamente el read model cuando existe una fila para el jugador/scope/timeframe solicitado. El problema aparece cuando el read model no tiene fila para ese jugador en el timeframe pedido y la lectura cae a _get_rcon_materialized_player_stats_runtime(...).
La causa raíz validada en producción es una query de _fetch_player_stats(...) en backend/app/rcon_historical_player_stats.py que usa una expresión equivalente a COALESCE(MAX(stats.player_name), stats.player_id) sin agregar ni agrupar stats.player_id. SQLite lo tolera, pero PostgreSQL lanza psycopg.errors.GroupingError, provocando un 500 para casos como server_id=comunidad-hispana-01 cuando no hay fila weekly o monthly en player_period_stats.
Preserve the current product identity: Spanish-speaking HLL Vietnam community, military/Vietnam/tactical/sober visual direction and controlled repository evolution.
Steps
- Inspect the listed files first.
- Fix the runtime fallback query used by
_fetch_player_stats(...)so it is valid in PostgreSQL. - Preserve SQLite/local compatibility when the explicit local path is used.
- Keep
player_period_statsas the preferred source whenever a row exists. - Keep runtime fallback when the read model is missing, incomplete or unavailable.
- Ensure the no-read-model-row case returns the existing controlled contract instead of a
500. - Validate the affected player-profile, player-search and ranking snapshot paths.
- Document the root cause, affected SQL, new behavior and production validation path.
Files to Read First
AGENTS.mdai/repo-context.mdai/architecture-index.mdbackend/app/rcon_historical_player_stats.pybackend/app/payloads.pybackend/app/routes.pybackend/app/postgres_rcon_storage.pyscripts/run-stats-validation.ps1docs/player-period-stats-read-model-plan.mdai/tasks/done/TASK-199-add-player-period-stats-read-model.md
Expected Files to Modify
backend/app/rcon_historical_player_stats.pyscripts/run-stats-validation.ps1ai/tasks/in-progress/TASK-200-fix-player-profile-runtime-postgres-grouping.md
Constraints
- Keep the change minimal and backend-only.
- Do not execute
ai-platform run. - Do not modify frontend.
- Do not change design.
- Do not touch images or assets.
- Do not reactivate Elo/MMR.
- Do not reintroduce Comunidad Hispana
#03. - Do not mix automation changes into this task.
- Do not change public contracts except to avoid the runtime
500. - Keep
player_period_statsas the preferred source when data exists. - Keep runtime fallback active when the read model has no row for the requested scope/window.
- If runtime also has no rows for that scope/window, return the existing controlled empty/zero contract instead of throwing.
- Do not break:
/api/stats/players/search/api/stats/players/{player_id}- ranking snapshots
refresh-player-search-indexrefresh-player-period-stats
Validation
Before completing the task ensure:
python -m py_compile backend/app/rcon_historical_player_stats.pypowershell -ExecutionPolicy Bypass -File scripts/run-stats-validation.ps1powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1- local validation proves
_fetch_player_stats(...)no longer emits PostgreSQL-invalid aggregate SQL - local validation proves the runtime fallback path does not throw when a server-specific read-model row is missing
- local validation proves
player_period_statsis still used when a row exists - local validation proves
player_search_indexstill works - local validation proves ranking snapshots are not broken
- validate these profile cases:
/api/stats/players/76561197978442431/api/stats/players/76561197978442431?server_id=all/api/stats/players/76561197978442431?server_id=all-servers/api/stats/players/76561197978442431?server_id=comunidad-hispana-02/api/stats/players/76561197978442431?server_id=comunidad-hispana-01
- the last case must not return
500 git diff --name-onlymatches the expected scope
Outcome
Implemented:
backend/app/rcon_historical_player_stats.py- fixed
_fetch_player_stats(...)runtime aggregation SQL - removed the PostgreSQL-invalid expression
COALESCE(MAX(stats.player_name), stats.player_id) - kept the player-name fallback in Python with
row["player_name"] or player_id
- fixed
scripts/run-stats-validation.ps1- added a SQL-shape regression check for
_fetch_player_stats(...) - added a fixture with a
yearly-only-playeroncomunidad-hispana-01 - validated the server-specific fallback path when
player_period_statshas no weekly/monthly row and runtime also has no rows in the requested window
- added a SQL-shape regression check for
Root cause of the 500:
- when
/api/stats/players/{player_id}could not serveplayer_period_stats, it fell back to_get_rcon_materialized_player_stats_runtime(...) _fetch_player_stats(...)executed a grouped aggregate query that selected:COALESCE(MAX(stats.player_name), stats.player_id) AS player_name
- PostgreSQL rejects
stats.player_idin that expression because it is neither aggregated nor part ofGROUP BY - SQLite/local compatibility masked the issue because SQLite tolerates that aggregate shape
Concrete affected SQL shape:
- previous:
SELECT COALESCE(MAX(stats.player_name), stats.player_id) AS player_name, ...
- new:
SELECT MAX(stats.player_name) AS player_name, ...
- public fallback behavior stays intact because Python already normalizes the result with
str(row["player_name"] or player_id)
Previous behavior:
- read-model hits returned
read_model=player-period-statsandfallback_used=false - if the requested scope/timeframe had no player row and runtime fallback executed on PostgreSQL, the endpoint could raise
psycopg.errors.GroupingError - the request then returned HTTP
500
New behavior:
player_period_statsremains the preferred source when a row exists- runtime fallback remains active when the read model is empty, incomplete or unavailable
- PostgreSQL fallback SQL is now valid
- if the requested scope/window has no runtime rows, the endpoint returns the existing controlled zero-value profile contract instead of throwing
- no frontend, design, asset, Elo/MMR or Comunidad Hispana
#03changes were made
Validations executed:
python -m py_compile backend/app/rcon_historical_player_stats.pypowershell -ExecutionPolicy Bypass -File scripts/run-stats-validation.ps1powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1
Validation notes:
- local import-based validation confirmed
_fetch_player_stats(...)no longer emits the PostgreSQL-invalid aggregate shape - local fixture validation confirmed the server-specific missing-read-model fallback does not throw and returns a controlled response
player_period_statsstill serves the profile path when rows existplayer_search_indexand ranking snapshot validations still pass throughscripts/run-stats-validation.ps1- live HTTP validation at
http://127.0.0.1:8000could not run in this environment because the backend was not up; the script reported that explicitly and route-contract checks still passed via local Python imports git diff --name-onlyincluded unrelated pre-existing changes inai/system-metrics.mdandfrontend/assets/img/weapons/black/*; those files were not modified by this task
How to validate in production:
- call:
/api/stats/players/76561197978442431/api/stats/players/76561197978442431?server_id=all/api/stats/players/76561197978442431?server_id=all-servers/api/stats/players/76561197978442431?server_id=comunidad-hispana-02/api/stats/players/76561197978442431?server_id=comunidad-hispana-01
- verify the first four still report:
source.read_model = "player-period-stats"source.fallback_used = false
- verify the last case no longer returns HTTP
500 - verify the last case returns a controlled stats payload with fallback metadata instead of a PostgreSQL grouping exception
Change Budget
- Prefer fewer than 5 modified files.
- Prefer changes under 200 lines when feasible.
- Split the work into follow-up tasks if limits are exceeded.