"""Бинды 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}"