Compare commits
No commits in common. "455766dcc4802b7acc03f856e53a05a76e985930" and "60bb7cf54e505506aff44f0e02b452f72b741781" have entirely different histories.
455766dcc4
...
60bb7cf54e
2 changed files with 16 additions and 81 deletions
|
|
@ -246,19 +246,22 @@ def load_ps_35_220(db: Session, xlsx_bytes: bytes, reserve_asof: date | None) ->
|
||||||
reserve_unit = 'МВт',
|
reserve_unit = 'МВт',
|
||||||
installed_capacity_mva = :installed,
|
installed_capacity_mva = :installed,
|
||||||
district = :district,
|
district = :district,
|
||||||
-- #2464-B: сюда БОЛЬШЕ НЕ пишем степень загрузки:
|
-- #2464-B: сюда БОЛЬШЕ НЕ пишем степень загрузки.
|
||||||
-- load_index — категориальная колонка, её заполняет
|
-- load_index — категориальная колонка
|
||||||
-- rosseti_wfs_loader._map_load_index. Историю см. в
|
-- ('open'|'limited'|'closed'|NULL, см.
|
||||||
-- git log этого файла.
|
-- data/sql/180_connection_capacity.sql:35), её
|
||||||
--
|
-- заполняет rosseti_wfs_loader._map_load_index.
|
||||||
-- ВАЖНО: в этом комментарии НЕЛЬЗЯ упоминать
|
-- Раньше тут стоял COALESCE(load_index,
|
||||||
-- бинд-параметры в синтаксисе «двоеточие + имя».
|
-- CAST(:load_pct AS text)) — при пустой ячейке
|
||||||
-- SQLAlchemy text() парсит бинды и внутри
|
-- в колонку легло бы число строкой ("72.5"),
|
||||||
-- SQL-комментариев: упоминание снятого параметра
|
-- а фронтовый classifyLoadIndex такое значение
|
||||||
-- «(двоеточие)load_pct» в тексте комментария
|
-- отбрасывает в null («неизвестно»), и в
|
||||||
-- сделало его ОБЯЗАТЕЛЬНЫМ, все 71 UPDATE падали
|
-- power_summary.by_load_index появился бы
|
||||||
-- с 18.08 по 02.09, а per-row except глотал это
|
-- бакет с именем "72.5".
|
||||||
-- как «битую строку» — задача оставалась зелёной.
|
-- Сегодня не стреляло только потому, что у всех
|
||||||
|
-- 3416 строк load_index уже заполнен
|
||||||
|
-- (open 2741 / limited 346 / closed 329, NULL 0)
|
||||||
|
-- и COALESCE не проваливался.
|
||||||
capacity_source = 'eesk_35_220',
|
capacity_source = 'eesk_35_220',
|
||||||
reserve_asof = :asof
|
reserve_asof = :asof
|
||||||
WHERE sc_name_norm = :name_norm
|
WHERE sc_name_norm = :name_norm
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
"""Бинды 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}"
|
|
||||||
Loading…
Add table
Reference in a new issue