5.2.2: Логирование пути и версии миграций
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m21s

This commit is contained in:
poignatov
2026-02-23 12:02:18 +03:00
parent bff62c0b8f
commit cea2c341a2
6 changed files with 206 additions and 2 deletions

View File

@@ -0,0 +1,37 @@
---
name: normalized_total_score fix migration
overview: "Новая миграция 000023: пересоздать weekly_report_mv с max_goal_score и удалить max_score из weekly_goals."
todos:
- id: migration-up
content: "Добавить 000023 up: DROP MV, CREATE MV с max_goal_score (из 000020), DROP COLUMN IF EXISTS max_score"
status: pending
- id: migration-down
content: "Добавить 000023 down: восстановить MV со старой формулой (max_score) и колонку max_score"
status: pending
- id: verify-local
content: Применить миграцию локально и проверить Релокация 2026-08 (normalized 32.74 и 21.55)
status: pending
isProject: false
---
# План: Исправить normalized_total_score через новую миграцию
## Проблема
На проде (и в локальной копии продовой БД) `normalized_total_score` не учитывает `max_goal_score`: в определении материализованного представления `weekly_report_mv` до сих пор используется колонка `wg.max_score`, которая не заполняется (всегда NULL) → формула всегда даёт `normalized_total_score = total_score`.
## Решение
Новая миграция (не менять 000020/000022):
1. **Пересоздать `weekly_report_mv**` с определением из 000020: в формуле использовать `max_goal_score`, тот же подзапрос по `n.created_date` и фильтр «только прошлые недели».
2. **Удалить колонку `max_score` из `weekly_goals**`, если есть: `ALTER TABLE weekly_goals DROP COLUMN IF EXISTS max_score;`
После применения и `REFRESH` (или при следующем кроне) для прошлых недель normalized будет ограничиваться целями (например, Релокация 2026-08: 39.14 → 32.74).
## Todos
- **migration-up** — Добавить миграцию `000023_fix_weekly_report_mv_use_max_goal_score.up.sql`: DROP MV, CREATE MV с max_goal_score (копия определения из 000020), DROP COLUMN IF EXISTS max_score в weekly_goals
- **migration-down** — Добавить `000023_fix_weekly_report_mv_use_max_goal_score.down.sql`: восстановить MV со старой формулой (max_score) и колонку max_score в weekly_goals
- **verify-local** — Применить миграцию локально и проверить по Релокации за 2026-08: normalized_total_score = 32.74 (project_id 27) и 21.55 (project_id 592)

View File

@@ -1 +1 @@
5.2.1 5.2.2

View File

@@ -2963,6 +2963,8 @@ func (a *App) runMigrations() error {
databaseURL := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", databaseURL := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable",
userInfo.String(), dbHost, dbPort, dbName) userInfo.String(), dbHost, dbPort, dbName)
log.Printf("Migrations path: %s", migrationsPath)
// Create migrate instance // Create migrate instance
m, err := migrate.New( m, err := migrate.New(
fmt.Sprintf("file://%s", migrationsPath), fmt.Sprintf("file://%s", migrationsPath),
@@ -3007,6 +3009,8 @@ func (a *App) runMigrations() error {
} }
log.Printf("Fixed dirty migration state for version %d", currentVersion) log.Printf("Fixed dirty migration state for version %d", currentVersion)
// Continue to apply migrations normally // Continue to apply migrations normally
} else {
log.Printf("Current DB migration version: %d", currentVersion)
} }
} }

View File

@@ -0,0 +1,81 @@
-- Migration: Rollback to MV using max_score and restore max_score column.
DROP MATERIALIZED VIEW IF EXISTS projects_median_mv;
ALTER TABLE weekly_goals ADD COLUMN IF NOT EXISTS max_score NUMERIC(10,4);
UPDATE weekly_goals SET max_score = max_goal_score WHERE max_score IS NULL;
DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv;
CREATE MATERIALIZED VIEW weekly_report_mv AS
SELECT
p.id AS project_id,
agg.report_year,
agg.report_week,
COALESCE(agg.total_score, 0.0000) AS total_score,
CASE
WHEN wg.max_score IS NULL THEN COALESCE(agg.total_score, 0.0000)
ELSE LEAST(COALESCE(agg.total_score, 0.0000), wg.max_score)
END AS normalized_total_score
FROM
projects p
LEFT JOIN
(
SELECT
n.project_id,
EXTRACT(ISOYEAR FROM n.created_date)::INTEGER AS report_year,
EXTRACT(WEEK FROM n.created_date)::INTEGER AS report_week,
SUM(n.score) AS total_score
FROM
nodes n
WHERE
(EXTRACT(ISOYEAR FROM n.created_date)::INTEGER < EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER)
OR (EXTRACT(ISOYEAR FROM n.created_date)::INTEGER = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND EXTRACT(WEEK FROM n.created_date)::INTEGER < EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER)
GROUP BY
1, 2, 3
) agg
ON p.id = agg.project_id
LEFT JOIN
weekly_goals wg
ON wg.project_id = p.id
AND wg.goal_year = agg.report_year
AND wg.goal_week = agg.report_week
WHERE
p.deleted = FALSE
ORDER BY
p.id, agg.report_year, agg.report_week
WITH DATA;
CREATE INDEX idx_weekly_report_mv_project_year_week
ON weekly_report_mv(project_id, report_year, report_week);
COMMENT ON MATERIALIZED VIEW weekly_report_mv IS 'Materialized view aggregating weekly scores by project using ISOYEAR for correct week calculations at year boundaries. Includes all projects via LEFT JOIN. Adds normalized_total_score using weekly_goals.max_score snapshot. Contains only historical data (excludes current week). Uses nodes.created_date (denormalized) instead of entries.created_date.';
-- Recreate projects_median_mv (last 4 weeks per 000008)
CREATE MATERIALIZED VIEW projects_median_mv AS
SELECT
p.id AS project_id,
p.user_id,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY normalized_total_score) AS median_score
FROM (
SELECT
project_id,
normalized_total_score,
report_year,
report_week,
ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY report_year DESC, report_week DESC) as rn
FROM weekly_report_mv
WHERE
(report_year < EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER)
OR (report_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND report_week < EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER)
) sub
JOIN projects p ON p.id = sub.project_id
WHERE rn <= 4 AND p.deleted = FALSE
GROUP BY p.id, p.user_id
WITH DATA;
CREATE INDEX idx_projects_median_mv_project_id ON projects_median_mv(project_id);
CREATE INDEX idx_projects_median_mv_user_id ON projects_median_mv(user_id);
COMMENT ON MATERIALIZED VIEW projects_median_mv IS 'Materialized view calculating median score for each project based on last 4 weeks of historical data. Includes user_id for multi-tenant support.';

View File

@@ -0,0 +1,82 @@
-- Migration: Fix weekly_report_mv to use max_goal_score for normalized_total_score.
-- Safe to run on DBs where 000020 was not applied (MV still uses max_score, column exists but is NULL).
-- projects_median_mv depends on weekly_report_mv, so we drop and recreate it.
DROP MATERIALIZED VIEW IF EXISTS projects_median_mv;
DROP MATERIALIZED VIEW IF EXISTS weekly_report_mv;
CREATE MATERIALIZED VIEW weekly_report_mv AS
SELECT
p.id AS project_id,
agg.report_year,
agg.report_week,
COALESCE(agg.total_score, 0.0000) AS total_score,
CASE
WHEN wg.max_goal_score IS NULL THEN COALESCE(agg.total_score, 0.0000)
ELSE LEAST(COALESCE(agg.total_score, 0.0000), wg.max_goal_score)
END AS normalized_total_score
FROM
projects p
LEFT JOIN
(
SELECT
n.project_id,
EXTRACT(ISOYEAR FROM n.created_date)::INTEGER AS report_year,
EXTRACT(WEEK FROM n.created_date)::INTEGER AS report_week,
SUM(n.score) AS total_score
FROM
nodes n
WHERE
(EXTRACT(ISOYEAR FROM n.created_date)::INTEGER < EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER)
OR (EXTRACT(ISOYEAR FROM n.created_date)::INTEGER = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND EXTRACT(WEEK FROM n.created_date)::INTEGER < EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER)
GROUP BY
1, 2, 3
) agg
ON p.id = agg.project_id
LEFT JOIN
weekly_goals wg
ON wg.project_id = p.id
AND wg.goal_year = agg.report_year
AND wg.goal_week = agg.report_week
WHERE
p.deleted = FALSE
ORDER BY
p.id, agg.report_year, agg.report_week
WITH DATA;
CREATE INDEX idx_weekly_report_mv_project_year_week
ON weekly_report_mv(project_id, report_year, report_week);
COMMENT ON MATERIALIZED VIEW weekly_report_mv IS 'Materialized view aggregating weekly scores by project using ISOYEAR for correct week calculations at year boundaries. Includes all projects via LEFT JOIN. Adds normalized_total_score using weekly_goals.max_goal_score. Contains only historical data (excludes current week). Uses nodes.created_date (denormalized) instead of entries.created_date.';
-- Recreate projects_median_mv (depends on weekly_report_mv, last 4 weeks per 000008)
CREATE MATERIALIZED VIEW projects_median_mv AS
SELECT
p.id AS project_id,
p.user_id,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY normalized_total_score) AS median_score
FROM (
SELECT
project_id,
normalized_total_score,
report_year,
report_week,
ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY report_year DESC, report_week DESC) as rn
FROM weekly_report_mv
WHERE
(report_year < EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER)
OR (report_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND report_week < EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER)
) sub
JOIN projects p ON p.id = sub.project_id
WHERE rn <= 4 AND p.deleted = FALSE
GROUP BY p.id, p.user_id
WITH DATA;
CREATE INDEX idx_projects_median_mv_project_id ON projects_median_mv(project_id);
CREATE INDEX idx_projects_median_mv_user_id ON projects_median_mv(user_id);
COMMENT ON MATERIALIZED VIEW projects_median_mv IS 'Materialized view calculating median score for each project based on last 4 weeks of historical data. Includes user_id for multi-tenant support.';
ALTER TABLE weekly_goals DROP COLUMN IF EXISTS max_score;

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "5.2.1", "version": "5.2.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",