Add annual ranking snapshot storage
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
id: TASK-167
|
||||||
|
title: Add annual ranking snapshot storage
|
||||||
|
status: done
|
||||||
|
type: backend
|
||||||
|
team: Arquitecto de Base de Datos
|
||||||
|
supporting_teams:
|
||||||
|
- Arquitecto Python
|
||||||
|
- Backend Senior
|
||||||
|
roadmap_item: foundation
|
||||||
|
---
|
||||||
|
|
||||||
|
# TASK-167 - Add annual ranking snapshot storage
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement storage primitives to persist annual top 20 player ranking snapshots, following the schema plan in `docs/annual-ranking-snapshot-schema-plan.md`, without adding generator logic or annual ranking APIs yet.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`TASK-166-design-annual-ranking-snapshot-schema.md` defined the intended schema and constraints for:
|
||||||
|
|
||||||
|
- `rcon_annual_ranking_snapshots`
|
||||||
|
- `rcon_annual_ranking_snapshot_items`
|
||||||
|
|
||||||
|
This task creates the storage layer so future yearly ranking generators can run on top of materialized RCON data in SQLite and PostgreSQL-compatible paths.
|
||||||
|
|
||||||
|
The implementation must stay backend-only and must not touch frontend assets.
|
||||||
|
|
||||||
|
## Validation Scope
|
||||||
|
|
||||||
|
The work intentionally stayed limited to backend storage schema initialization and migration compatibility.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
### Archivos modificados
|
||||||
|
|
||||||
|
- backend/app/rcon_admin_log_materialization.py
|
||||||
|
- backend/app/postgres_rcon_storage.py
|
||||||
|
- backend/app/sqlite_to_postgres_migration.py
|
||||||
|
- ai/tasks/done/TASK-167-add-annual-ranking-snapshot-storage.md
|
||||||
|
|
||||||
|
### Tablas nuevas añadidas
|
||||||
|
|
||||||
|
- rcon_annual_ranking_snapshots
|
||||||
|
- rcon_annual_ranking_snapshot_items
|
||||||
|
|
||||||
|
### Compatibilidad SQLite/Postgres
|
||||||
|
|
||||||
|
- SQLite: tablas y restricciones añadidas en `initialize_rcon_materialized_storage()` (backend/app/rcon_admin_log_materialization.py).
|
||||||
|
- PostgreSQL: tablas, índices y restricciones añadidas a `RCON_SCHEMA_SQL` (backend/app/postgres_rcon_storage.py).
|
||||||
|
- Migración: tablas añadidas a `RCON_TABLES` y a `_sync_sequences()` en `backend/app/sqlite_to_postgres_migration.py`.
|
||||||
|
|
||||||
|
### Validaciones realizadas
|
||||||
|
|
||||||
|
- `python -m compileall backend/app`
|
||||||
|
- `scripts/run-integration-tests.ps1`
|
||||||
|
- Validación de inicialización de storage SQLite con base temporal y consulta en `sqlite_master` para ambas tablas.
|
||||||
|
|
||||||
|
### Limitaciones conocidas
|
||||||
|
|
||||||
|
- No se implementó generador anual ni endpoint de ranking anual.
|
||||||
|
- No se tocó frontend.
|
||||||
|
- No hay validación de runtime PostgreSQL porque no había entorno Postgres requerido por el alcance de esta task.
|
||||||
|
|
||||||
|
### Próximo task recomendado
|
||||||
|
|
||||||
|
- TASK-168 (Generador de snapshot anual top 20).
|
||||||
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-167
|
|
||||||
title: Add annual ranking snapshot storage
|
|
||||||
status: pending
|
|
||||||
type: backend
|
|
||||||
team: Arquitecto de Base de Datos
|
|
||||||
supporting_teams:
|
|
||||||
- Arquitecto Python
|
|
||||||
- Backend Senior
|
|
||||||
roadmap_item: foundation
|
|
||||||
---
|
|
||||||
|
|
||||||
# TASK-167 - Add annual ranking snapshot storage
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Implement storage primitives to persist annual top 20 player ranking snapshots, following the schema plan in `docs/annual-ranking-snapshot-schema-plan.md`, without adding generator logic or annual ranking APIs yet.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
`TASK-166-design-annual-ranking-snapshot-schema.md` defined the intended schema and constraints for:
|
|
||||||
- `rcon_annual_ranking_snapshots`
|
|
||||||
- `rcon_annual_ranking_snapshot_items`
|
|
||||||
|
|
||||||
This task creates the storage layer so future yearly ranking generators can run on top of materialized RCON data in SQLite and PostgreSQL-compatible paths.
|
|
||||||
|
|
||||||
The implementation must stay backend-only and must not touch frontend assets.
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
1. Read all files listed below before any edit.
|
|
||||||
2. Add snapshot table creation for both SQLite and PostgreSQL-compatible schema paths if present.
|
|
||||||
3. Include SQLite/Postgres-safe DDL in migration-like storage initializer surfaces used by the project.
|
|
||||||
4. Keep changes minimal and avoid endpoint or generator creation.
|
|
||||||
5. Validate syntax/initialization and keep changed files within expected scope.
|
|
||||||
6. Update task outcome once completed and move to `ai/tasks/done` only after scope checks pass.
|
|
||||||
|
|
||||||
## Files to Read First
|
|
||||||
|
|
||||||
- `AGENTS.md`
|
|
||||||
- `ai/repo-context.md`
|
|
||||||
- `ai/architecture-index.md`
|
|
||||||
- `docs/annual-ranking-snapshot-schema-plan.md`
|
|
||||||
- `backend/app/rcon_admin_log_materialization.py`
|
|
||||||
- `backend/app/postgres_rcon_storage.py`
|
|
||||||
- `backend/app/sqlite_to_postgres_migration.py`
|
|
||||||
- `backend/app/rcon_historical_leaderboards.py`
|
|
||||||
- `backend/app/rcon_historical_player_stats.py`
|
|
||||||
|
|
||||||
## Expected Files to Modify
|
|
||||||
|
|
||||||
- `backend/app/rcon_admin_log_materialization.py` or equivalent materialized-RCON storage init module
|
|
||||||
- `backend/app/postgres_rcon_storage.py` or equivalent PostgreSQL schema initializer
|
|
||||||
- `backend/app/sqlite_to_postgres_migration.py` if migration table lists and sequence sync require alignment
|
|
||||||
- `ai/tasks/done/TASK-167-add-annual-ranking-snapshot-storage.md`
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
- No generador anual de ranking en este scope.
|
|
||||||
- No endpoint anual agregado aún.
|
|
||||||
- No modificaciones de frontend.
|
|
||||||
- No tocar `frontend/assets/js/partida-actual.js`.
|
|
||||||
- No tocar `frontend/assets/img/clans/bxb.png`.
|
|
||||||
- Mantener compatibilidad SQLite/Postgres si la capa existente ya la usa.
|
|
||||||
- No reactivar Elo/MMR.
|
|
||||||
- No reintroducir Comunidad Hispana #03.
|
|
||||||
- No ampliar `historical workers` salvo cambios estrictamente necesarios en inicialización de storage.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
- `python -m compileall backend/app`
|
|
||||||
- `scripts/run-integration-tests.ps1` (si aplica).
|
|
||||||
- Validación de inicialización de storage para SQLite.
|
|
||||||
- Validación de compatibilidad SQL/DDL con Postgres en los módulos de compatibilidad existentes.
|
|
||||||
- `git diff --name-only` debe mostrar sólo el alcance esperado.
|
|
||||||
|
|
||||||
## Outcome
|
|
||||||
|
|
||||||
Documentar en el cierre de task:
|
|
||||||
- Archivos modificados.
|
|
||||||
- Tablas nuevas añadidas (`rcon_annual_ranking_snapshots`, `rcon_annual_ranking_snapshot_items`).
|
|
||||||
- Compatibilidad SQLite/Postgres.
|
|
||||||
- Validaciones ejecutadas.
|
|
||||||
- Limitaciones conocidas.
|
|
||||||
- Siguiente task recomendada: generador de snapshot anual top 20.
|
|
||||||
|
|
||||||
@@ -188,6 +188,39 @@ CREATE TABLE IF NOT EXISTS rcon_match_player_stats (
|
|||||||
UNIQUE(target_key, match_key, player_id)
|
UNIQUE(target_key, match_key, player_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshots (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
year INTEGER NOT NULL,
|
||||||
|
server_key TEXT NOT NULL,
|
||||||
|
metric TEXT NOT NULL,
|
||||||
|
limit_size INTEGER NOT NULL DEFAULT 20,
|
||||||
|
source_basis TEXT NOT NULL DEFAULT 'rcon-admin-log',
|
||||||
|
window_start TEXT,
|
||||||
|
window_end TEXT,
|
||||||
|
generated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
status TEXT NOT NULL DEFAULT 'ready',
|
||||||
|
source_matches_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
CHECK (limit_size > 0),
|
||||||
|
CHECK (metric IN ('kills', 'deaths', 'matches_over_100_kills', 'support')),
|
||||||
|
UNIQUE (year, server_key, metric)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshot_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
snapshot_id BIGINT NOT NULL REFERENCES rcon_annual_ranking_snapshots(id) ON DELETE CASCADE,
|
||||||
|
ranking_position INTEGER NOT NULL,
|
||||||
|
player_id TEXT NOT NULL,
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
metric_value BIGINT NOT NULL DEFAULT 0,
|
||||||
|
matches_considered INTEGER NOT NULL DEFAULT 0,
|
||||||
|
kills BIGINT NOT NULL DEFAULT 0,
|
||||||
|
deaths BIGINT NOT NULL DEFAULT 0,
|
||||||
|
teamkills BIGINT NOT NULL DEFAULT 0,
|
||||||
|
kd_ratio DOUBLE PRECISION NOT NULL DEFAULT 0.0,
|
||||||
|
UNIQUE(snapshot_id, ranking_position),
|
||||||
|
UNIQUE(snapshot_id, player_id)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS rcon_scoreboard_match_candidates (
|
CREATE TABLE IF NOT EXISTS rcon_scoreboard_match_candidates (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
server_slug TEXT NOT NULL,
|
server_slug TEXT NOT NULL,
|
||||||
@@ -219,6 +252,14 @@ CREATE INDEX IF NOT EXISTS idx_rcon_materialized_matches_recent
|
|||||||
ON rcon_materialized_matches(target_key, ended_at DESC, ended_server_time DESC);
|
ON rcon_materialized_matches(target_key, ended_at DESC, ended_server_time DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match
|
CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match
|
||||||
ON rcon_match_player_stats(target_key, match_key);
|
ON rcon_match_player_stats(target_key, match_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rcon_annual_ranking_snapshots_year
|
||||||
|
ON rcon_annual_ranking_snapshots(year, server_key, metric);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rcon_annual_ranking_snapshots_status
|
||||||
|
ON rcon_annual_ranking_snapshots(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rcon_annual_snapshot_items_snapshot
|
||||||
|
ON rcon_annual_ranking_snapshot_items(snapshot_id, ranking_position);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rcon_annual_snapshot_items_player
|
||||||
|
ON rcon_annual_ranking_snapshot_items(snapshot_id, player_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_rcon_scoreboard_candidates_server_end
|
CREATE INDEX IF NOT EXISTS idx_rcon_scoreboard_candidates_server_end
|
||||||
ON rcon_scoreboard_match_candidates(server_slug, ended_at DESC, started_at DESC);
|
ON rcon_scoreboard_match_candidates(server_slug, ended_at DESC, started_at DESC);
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -83,6 +83,51 @@ def initialize_rcon_materialized_storage(*, db_path: Path | None = None) -> Path
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match
|
CREATE INDEX IF NOT EXISTS idx_rcon_match_player_stats_match
|
||||||
ON rcon_match_player_stats(target_key, match_key);
|
ON rcon_match_player_stats(target_key, match_key);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
year INTEGER NOT NULL,
|
||||||
|
server_key TEXT NOT NULL,
|
||||||
|
metric TEXT NOT NULL,
|
||||||
|
limit_size INTEGER NOT NULL DEFAULT 20,
|
||||||
|
source_basis TEXT NOT NULL DEFAULT 'rcon-admin-log',
|
||||||
|
window_start TEXT,
|
||||||
|
window_end TEXT,
|
||||||
|
generated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
status TEXT NOT NULL DEFAULT 'ready',
|
||||||
|
source_matches_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
CHECK (limit_size > 0),
|
||||||
|
CHECK (metric IN ('kills', 'deaths', 'matches_over_100_kills', 'support')),
|
||||||
|
UNIQUE (year, server_key, metric)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rcon_annual_ranking_snapshots_year
|
||||||
|
ON rcon_annual_ranking_snapshots(year, server_key, metric);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rcon_annual_ranking_snapshots_status
|
||||||
|
ON rcon_annual_ranking_snapshots(status);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rcon_annual_ranking_snapshot_items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
snapshot_id INTEGER NOT NULL REFERENCES rcon_annual_ranking_snapshots(id) ON DELETE CASCADE,
|
||||||
|
ranking_position INTEGER NOT NULL,
|
||||||
|
player_id TEXT NOT NULL,
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
metric_value INTEGER NOT NULL DEFAULT 0,
|
||||||
|
matches_considered INTEGER NOT NULL DEFAULT 0,
|
||||||
|
kills INTEGER NOT NULL DEFAULT 0,
|
||||||
|
deaths INTEGER NOT NULL DEFAULT 0,
|
||||||
|
teamkills INTEGER NOT NULL DEFAULT 0,
|
||||||
|
kd_ratio REAL NOT NULL DEFAULT 0.0,
|
||||||
|
UNIQUE(snapshot_id, ranking_position),
|
||||||
|
UNIQUE(snapshot_id, player_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rcon_annual_snapshot_items_snapshot
|
||||||
|
ON rcon_annual_ranking_snapshot_items(snapshot_id, ranking_position);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_rcon_annual_snapshot_items_player
|
||||||
|
ON rcon_annual_ranking_snapshot_items(snapshot_id, player_id);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
return resolved_path
|
return resolved_path
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ RCON_TABLES = (
|
|||||||
"rcon_player_profile_snapshots",
|
"rcon_player_profile_snapshots",
|
||||||
"rcon_materialized_matches",
|
"rcon_materialized_matches",
|
||||||
"rcon_match_player_stats",
|
"rcon_match_player_stats",
|
||||||
|
"rcon_annual_ranking_snapshots",
|
||||||
|
"rcon_annual_ranking_snapshot_items",
|
||||||
"rcon_scoreboard_match_candidates",
|
"rcon_scoreboard_match_candidates",
|
||||||
)
|
)
|
||||||
DISPLAY_TABLES = (
|
DISPLAY_TABLES = (
|
||||||
@@ -315,6 +317,8 @@ def _sync_sequences() -> None:
|
|||||||
"rcon_player_profile_snapshots",
|
"rcon_player_profile_snapshots",
|
||||||
"rcon_materialized_matches",
|
"rcon_materialized_matches",
|
||||||
"rcon_match_player_stats",
|
"rcon_match_player_stats",
|
||||||
|
"rcon_annual_ranking_snapshots",
|
||||||
|
"rcon_annual_ranking_snapshot_items",
|
||||||
"rcon_scoreboard_match_candidates",
|
"rcon_scoreboard_match_candidates",
|
||||||
)
|
)
|
||||||
with connect_display_postgres() as connection:
|
with connect_display_postgres() as connection:
|
||||||
|
|||||||
Reference in New Issue
Block a user