v_supply_layers_latest (м.125) схлопывает все L3 dev_group'ы района в 1 строку — DISTINCT ON без dev_group_name заново вносит #970 на read-слое future_supply #1650

Closed
opened 2026-06-16 18:46:57 +00:00 by bot-backend · 0 comments
Collaborator

Severity: high · Категория: data-loss / double-count-inverse (under-count) / stale-schema · Файл: data/sql/125_supply_layers.sql (view) consumed by backend/app/services/site_finder/future_supply.py:125_supply_layers.sql:146-170; future_supply.py:296-309

Файлы: data/sql/125_supply_layers.sql:146-170 (определение view), data/sql/128_supply_layers_dev_group_key.sql (фикс таблицы, но НЕ view), backend/app/services/site_finder/future_supply.py:296-309 (consumer), :377-378 (агрегация L3).

Суть: Миграция 128 (#970, CRITICAL) добавила dev_group_name в supply_layers и в uq_supply_layers_logical, чтобы 15 dev_group'ов района (Академический: 13808 юнитов / 54 объекта / 15 групп) перестали схлопываться в 1 строку. Воркер supply_layers_refresh.py теперь корректно пишет одну L3-строку на (district_name, dev_group_name) (INSERT стр.94, ON CONFLICT 7-колоночный ключ стр.103-104). НО read-view v_supply_layers_latest определён только в м.125 и НИКОГДА не пересоздавался: его ключ DISTINCT ON (layer, district_name, complex_id, obj_class, source) НЕ содержит dev_group_name, и dev_group_name отсутствует в SELECT-листе. Для каждой L3-строки complex_id=NULL, obj_class=NULL, source='domrf_multiphase', поэтому все dev_group'ы района делят ОДИН кортеж DISTINCT ON → view возвращает ровно ОДНУ L3-строку на район, остальные молча отбрасываются.

Импакт: future_supply.py читает исключительно FROM v_supply_layers_latest (296-309) и в compute_future_supply_pressure суммирует future_units_by_horizon (377-378) только по выжившей строке → hidden+future занижены ~на 95.6% → months_of_pressure и saturating index смещены вниз → future_supply_index сообщает ложно-низкое будущее конкурирующее предложение. Это толкает gate/BUY-логику к ложно-оптимистичному вердикту — ровно отказ 'projected_supply=0 → ложный BUY', о котором предупреждает docstring future_supply.py:294. Фикс на уровне таблицы (#128) невидим для consumer'а, потому что никто не читает таблицу напрямую.

Фикс: отдельной миграцией CREATE OR REPLACE VIEW v_supply_layers_latest, добавив dev_group_name И в список DISTINCT ON, И в ведущие ORDER BY-выражения (перед snapshot_date DESC), И в SELECT-лист — зеркально 7-колоночному ключу uq_supply_layers_logical из м.128: DISTINCT ON (layer, district_name, complex_id, obj_class, dev_group_name, source). После этого все dev_group'ы района сосуществуют в view и future_supply агрегирует полный объём L3.

Почему баг: The whole point of #970/#128 was that dev_group_name must be part of the dedup key so the 15 dev_groups of a district don't collapse to 1 row. The table now stores them correctly, but v_supply_layers_latest (the only path future_supply.py uses, per future_supply.py:15/277/303) does not carry dev_group_name in either the SELECT or the DISTINCT ON, so PostgreSQL's DISTINCT ON returns just the first row per (layer,district,NULL,NULL,source) tuple and discards the other dev_groups. compute_future_supply_pressure then sums future_units_by_horizon over only the single surviving L3 row, so hidden + future is far too small → months_of_pressure and the saturating index are biased low → future_supply_index (schemas/location.py:51, also Excel export) reports falsely low future competing supply. This pushes gate/BUY logic toward a falsely optimistic verdict — the exact 'projected_supply=0 → ложный BUY' failure the module comment (future_supply.py:294) warns about. The table-level fix is invisible to every consumer because nothing reads the table directly.

Как чинить: Add a new migration (next NN) that does CREATE OR REPLACE VIEW v_supply_layers_latest adding dev_group_name BOTH to the DISTINCT ON key (after obj_class, before source) and to the SELECT list, and matching the leading ORDER BY expressions to the new DISTINCT ON key — exactly mirroring the 7-column logical key from #128 (layer, district_name, complex_id, obj_class, dev_group_name, source). After redeploy the view will surface one row per dev_group again.


deep-audit backend v2 (2-й проход, после PR #1543) · verify+harden, confidence 0.92 · unit B38

**Severity:** high · **Категория:** data-loss / double-count-inverse (under-count) / stale-schema · **Файл:** `data/sql/125_supply_layers.sql (view) consumed by backend/app/services/site_finder/future_supply.py:125_supply_layers.sql:146-170; future_supply.py:296-309` Файлы: data/sql/125_supply_layers.sql:146-170 (определение view), data/sql/128_supply_layers_dev_group_key.sql (фикс таблицы, но НЕ view), backend/app/services/site_finder/future_supply.py:296-309 (consumer), :377-378 (агрегация L3). Суть: Миграция 128 (#970, CRITICAL) добавила dev_group_name в supply_layers и в uq_supply_layers_logical, чтобы 15 dev_group'ов района (Академический: 13808 юнитов / 54 объекта / 15 групп) перестали схлопываться в 1 строку. Воркер supply_layers_refresh.py теперь корректно пишет одну L3-строку на (district_name, dev_group_name) (INSERT стр.94, ON CONFLICT 7-колоночный ключ стр.103-104). НО read-view v_supply_layers_latest определён только в м.125 и НИКОГДА не пересоздавался: его ключ DISTINCT ON (layer, district_name, complex_id, obj_class, source) НЕ содержит dev_group_name, и dev_group_name отсутствует в SELECT-листе. Для каждой L3-строки complex_id=NULL, obj_class=NULL, source='domrf_multiphase', поэтому все dev_group'ы района делят ОДИН кортеж DISTINCT ON → view возвращает ровно ОДНУ L3-строку на район, остальные молча отбрасываются. Импакт: future_supply.py читает исключительно FROM v_supply_layers_latest (296-309) и в compute_future_supply_pressure суммирует future_units_by_horizon (377-378) только по выжившей строке → hidden+future занижены ~на 95.6% → months_of_pressure и saturating index смещены вниз → future_supply_index сообщает ложно-низкое будущее конкурирующее предложение. Это толкает gate/BUY-логику к ложно-оптимистичному вердикту — ровно отказ 'projected_supply=0 → ложный BUY', о котором предупреждает docstring future_supply.py:294. Фикс на уровне таблицы (#128) невидим для consumer'а, потому что никто не читает таблицу напрямую. Фикс: отдельной миграцией CREATE OR REPLACE VIEW v_supply_layers_latest, добавив dev_group_name И в список DISTINCT ON, И в ведущие ORDER BY-выражения (перед snapshot_date DESC), И в SELECT-лист — зеркально 7-колоночному ключу uq_supply_layers_logical из м.128: DISTINCT ON (layer, district_name, complex_id, obj_class, dev_group_name, source). После этого все dev_group'ы района сосуществуют в view и future_supply агрегирует полный объём L3. **Почему баг:** The whole point of #970/#128 was that dev_group_name must be part of the dedup key so the 15 dev_groups of a district don't collapse to 1 row. The table now stores them correctly, but `v_supply_layers_latest` (the only path future_supply.py uses, per future_supply.py:15/277/303) does not carry dev_group_name in either the SELECT or the DISTINCT ON, so PostgreSQL's DISTINCT ON returns just the first row per (layer,district,NULL,NULL,source) tuple and discards the other dev_groups. compute_future_supply_pressure then sums `future_units_by_horizon` over only the single surviving L3 row, so `hidden + future` is far too small → months_of_pressure and the saturating index are biased low → future_supply_index (schemas/location.py:51, also Excel export) reports falsely low future competing supply. This pushes gate/BUY logic toward a falsely optimistic verdict — the exact 'projected_supply=0 → ложный BUY' failure the module comment (future_supply.py:294) warns about. The table-level fix is invisible to every consumer because nothing reads the table directly. **Как чинить:** Add a new migration (next NN) that does `CREATE OR REPLACE VIEW v_supply_layers_latest` adding `dev_group_name` BOTH to the DISTINCT ON key (after obj_class, before source) and to the SELECT list, and matching the leading ORDER BY expressions to the new DISTINCT ON key — exactly mirroring the 7-column logical key from #128 (layer, district_name, complex_id, obj_class, dev_group_name, source). After redeploy the view will surface one row per dev_group again. --- <sub>deep-audit backend v2 (2-й проход, после PR #1543) · verify+harden, confidence 0.92 · unit B38</sub>
bot-backend added the
week ревью 1
label 2026-06-16 18:46:57 +00:00
bot-backend added the
needs-human
label 2026-06-17 06:26:29 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1650
No description provided.