5.2 KiB
Player Period Stats Read Model
Objective
Define the PostgreSQL read model that backs personal player stats by period so /api/stats/players/{player_id} does not aggregate large RCON historical tables on every public request when a regenerated read model is available.
Table
Primary table:
player_period_stats
Operational scope:
- PostgreSQL is the default operational store
- SQLite remains available only as explicit local compatibility for validation and isolated maintenance runs
Read-model nature:
- this table is regenerable
- it is not the canonical source of truth
- canonical historical data remains in
rcon_materialized_matchesandrcon_match_player_stats
Stored Fields
Each row stores one player projection for one public scope and one active period window:
period_typewindow_kindperiod_startperiod_endserver_idplayer_idplayer_namematches_consideredkillsdeathsteamkillsranking_positionkd_ratiokills_per_matchfirst_seen_atlast_seen_atupdated_at
Current period types:
weeklymonthlyyearly
Current scope rows:
all-serverscomunidad-hispana-01comunidad-hispana-02
Why server_id exists:
- the public profile endpoint already supports
server_id - keeping one row per scope preserves the current frontend contract without recalculating large runtime aggregates for server-filtered profiles
Why window_kind exists:
- the profile payload already exposes weekly and monthly ranking context
- persisting
current-week,previous-week,current-monthorprevious-monthkeeps the public contract stable without recomputing that selection on each request
Period Windows
Window selection follows the same policy already used by ranking/stats reads where applicable:
weekly- uses
select_leaderboard_window(... timeframe="weekly") - current week is used only when the closed-match threshold is sufficient
- otherwise the read model stores the previous week window
- uses
monthly- uses
select_leaderboard_window(... timeframe="monthly") - from day 1 to day 7 it stores the previous month
- from day 8 onward it stores the current month
- uses
yearly- stores the current UTC year from January 1 until refresh time
- it is prepared internally even if the current public profile contract still requests weekly/monthly only
Refresh Command
Manual command:
python -m app.rcon_historical_player_stats refresh-player-period-stats
SQLite-only local override:
python -m app.rcon_historical_player_stats refresh-player-period-stats --sqlite-path backend/data/hll_vietnam_dev.sqlite3
Refresh policy:
- rebuild from materialized RCON/AdminLog tables
- replace rows scope by scope and period by period
- keep the latest player name seen inside the selected period window
- persist ranking position by kills inside each generated period window
Public Read Path
Priority for /api/stats/players/{player_id}:
- use
player_period_statswhen the requested scope has generated rows for the required periods - serve the requested weekly/monthly totals and ranking context directly from the read model
- fall back to runtime aggregation only when:
- the read model table is unavailable
- the requested scope has no generated rows yet
- the player has no generated row for one required period
- a controlled read error occurs
Returned compatibility:
- the payload still returns
player_id - the payload still returns
player_name - the payload still returns
matches_considered - the payload still returns
kills - the payload still returns
deaths - the payload still returns
teamkills - the payload still returns
kd_ratio - the payload still returns
kills_per_match - the payload still returns weekly/monthly ranking blocks
Source metadata:
- read-model path reports
source.read_model = "player-period-stats" - read-model path reports
source.fallback_used = false - runtime fallback keeps the same public contract and reports
source.fallback_used = true
PostgreSQL Notes
No extra PostgreSQL extensions are required.
Indexes kept for the public profile flow:
(player_id, period_type, server_id)(server_id, period_type)last_seen_atupdated_at
Production Validation
Recommended checks after refresh:
- run
python -m app.rcon_historical_player_stats refresh-player-period-stats - confirm the command reports rows for:
all-serverscomunidad-hispana-01comunidad-hispana-02weeklymonthlyyearly
- call
/api/stats/players/<known-player>?timeframe=weekly - verify response metadata reports
read_model=player-period-stats - verify response metadata reports
fallback_used=falsewhen the read model is populated - verify fallback metadata appears only when the read model is empty, incomplete or unavailable
Current Limitations
- periodic refresh is still manual; there is no scheduled operational refresh yet
- the public route still exposes weekly/monthly only; yearly is prepared internally for future use
- runtime fallback remains necessary until production refresh automation is in place
- canonical historical truth remains in materialized RCON tables, not in the read model