Merge pull request 'ЕЭСК: бинд-параметр в SQL-комментарии молча ронял все 71 UPDATE две недели' (#3317) from fix/eesk-bind-in-comment into main
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 1m53s
Deploy / build-worker (push) Successful in 2m57s
Deploy / deploy (push) Successful in 1m18s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s

This commit is contained in:
lekss361 2026-09-09 22:05:38 +00:00
commit 455766dcc4
2 changed files with 81 additions and 16 deletions

View file

@ -246,22 +246,19 @@ def load_ps_35_220(db: Session, xlsx_bytes: bytes, reserve_asof: date | None) ->
reserve_unit = 'МВт',
installed_capacity_mva = :installed,
district = :district,
-- #2464-B: сюда БОЛЬШЕ НЕ пишем степень загрузки.
-- load_index категориальная колонка
-- ('open'|'limited'|'closed'|NULL, см.
-- data/sql/180_connection_capacity.sql:35), её
-- заполняет rosseti_wfs_loader._map_load_index.
-- Раньше тут стоял COALESCE(load_index,
-- CAST(:load_pct AS text)) при пустой ячейке
-- в колонку легло бы число строкой ("72.5"),
-- а фронтовый classifyLoadIndex такое значение
-- отбрасывает в null («неизвестно»), и в
-- power_summary.by_load_index появился бы
-- бакет с именем "72.5".
-- Сегодня не стреляло только потому, что у всех
-- 3416 строк load_index уже заполнен
-- (open 2741 / limited 346 / closed 329, NULL 0)
-- и COALESCE не проваливался.
-- #2464-B: сюда БОЛЬШЕ НЕ пишем степень загрузки:
-- load_index категориальная колонка, её заполняет
-- rosseti_wfs_loader._map_load_index. Историю см. в
-- git log этого файла.
--
-- ВАЖНО: в этом комментарии НЕЛЬЗЯ упоминать
-- бинд-параметры в синтаксисе «двоеточие + имя».
-- SQLAlchemy text() парсит бинды и внутри
-- SQL-комментариев: упоминание снятого параметра
-- «(двоеточие)load_pct» в тексте комментария
-- сделало его ОБЯЗАТЕЛЬНЫМ, все 71 UPDATE падали
-- с 18.08 по 02.09, а per-row except глотал это
-- как «битую строку» задача оставалась зелёной.
capacity_source = 'eesk_35_220',
reserve_asof = :asof
WHERE sc_name_norm = :name_norm

View file

@ -0,0 +1,68 @@
"""Бинды SQL-стейтментов ЕЭСК-лоадера обязаны совпадать с передаваемыми params.
ПОЧЕМУ ЭТОТ ТЕСТ СУЩЕСТВУЕТ. 18.08-02.09.2026 все 71 UPDATE резервов ПС
падали, а задача оставалась зелёной: упоминание СНЯТОГО параметра load_pct
в SQL-КОММЕНТАРИИ (в синтаксисе «двоеточие+имя») SQLAlchemy text() распарсил
как обязательный бинд, params его не содержал, каждая строка падала на
compile, а per-row except глотал это как «битую строку». ЕЭСК-резервы не
обновлялись две недели, freshness этого не видел (задача done).
Тест ловит МЕХАНИЗМ: собирает все text()-стейтменты модуля и сверяет их
бинд-имена с ключами словаря params из того же контекста. Никакой БД не
нужно дефект живёт на этапе компиляции стейтмента.
"""
from __future__ import annotations
import re
from pathlib import Path
from sqlalchemy import text
LOADER = (
Path(__file__).resolve().parents[1]
/ "app"
/ "services"
/ "site_finder"
/ "eesk_reserve_loader.py"
)
def _extract_update_sql() -> str:
"""Достаёт текст UPDATE power_supply_centers из исходника лоадера."""
src = LOADER.read_text(encoding="utf-8")
m = re.search(r'text\("""\s*(UPDATE power_supply_centers.*?)"""\)', src, re.S)
assert m, "UPDATE power_supply_centers не найден в лоадере"
return m.group(1)
def _extract_params_keys() -> set[str]:
"""Ключи словаря params, который передаётся в этот execute."""
src = LOADER.read_text(encoding="utf-8")
m = re.search(r"params = \{(.*?)\}", src, re.S)
assert m, "словарь params не найден"
return set(re.findall(r'"([a-z_]+)":', m.group(1)))
def test_update_binds_are_subset_of_params() -> None:
"""Каждый бинд стейтмента обязан приходить из params — иначе UPDATE падает
на КАЖДОЙ строке, и per-row SAVEPOINT-except прячет это под «битую строку»."""
stmt = text(_extract_update_sql())
binds = {p.key for p in stmt._bindparams.values()}
params = _extract_params_keys()
missing = binds - params
assert not missing, (
f"стейтмент требует биндов {sorted(missing)}, которых нет в params — "
"все строки батча упадут молча. Частая причина: упоминание "
"«двоеточие+имя» в SQL-комментарии (text() парсит бинды и там)."
)
def test_comment_does_not_reintroduce_phantom_bind() -> None:
"""Регресс 18.08: снятый параметр упомянули в комментарии в живом
синтаксисе бинда. Проверяем ЗНАЧЕНИЕМ: компиляция с ровно теми params,
что собирает лоадер, не требует ничего лишнего."""
stmt = text(_extract_update_sql())
have = _extract_params_keys()
for bp in stmt._bindparams.values():
assert bp.key in have, f"фантомный бинд из комментария: {bp.key!r}"