"""Сессия-двойник с настоящей семантикой aborted-транзакции Postgres. Зачем отдельный помощник, а не MagicMock: у мока нет состояния «транзакция оборвана», поэтому второй `execute` в тесте проходит независимо от того, обёрнут первый в SAVEPOINT или нет. Проверка на отравление сессии на таком моке зелена по построению — она может поймать только проглоченное исключение. Замер 19.08.2026 (мутационный прогон по кластеру A эпика #2464): у трёх мест из одиннадцати снятие SAVEPOINT не роняет НИ ОДНОГО теста — saturation (465 passed), zone_regulation (195), pat_lookup (21). Код там правильный; беззащитны регрессии. Семантика, которую двойник воспроизводит: * упавший `execute` переводит транзакцию в aborted; * любой следующий `execute` в aborted-состоянии падает с тем же текстом, что отдаёт Postgres, — до конца транзакции; * выход из `begin_nested()` с исключением = ROLLBACK TO SAVEPOINT: снимает aborted, внешняя транзакция остаётся рабочей. """ from __future__ import annotations from contextlib import contextmanager from typing import Any _ABORTED_MESSAGE = "current transaction is aborted, commands ignored until end of transaction block" class AbortedTransactionError(RuntimeError): """Аналог psycopg InFailedSqlTransaction.""" class PostgresLikeSession: """Минимальная сессия: aborted-состояние + рабочий SAVEPOINT. fail_on: номера вызовов `execute` (с единицы), которые должны упасть. error: исключение, которым падать (важно — обработчики ловят конкретные типы SQLAlchemy, а не голый Exception). rows / row: что отдавать при успешном вызове. """ def __init__( self, *, fail_on: tuple[int, ...] = (1,), error: BaseException | None = None, rows: list[Any] | None = None, row: Any = None, ) -> None: self.calls = 0 self.aborted = False self.savepoints_entered = 0 self._fail_on = fail_on self._error = error or RuntimeError("simulated DB failure") self._rows = rows if rows is not None else [] self._row = row @contextmanager def begin_nested(self): # type: ignore[no-untyped-def] self.savepoints_entered += 1 try: yield except BaseException: # ROLLBACK TO SAVEPOINT — внешняя транзакция снова пригодна. self.aborted = False raise def execute(self, *_args: Any, **_kwargs: Any) -> _Result: if self.aborted: raise AbortedTransactionError(_ABORTED_MESSAGE) self.calls += 1 if self.calls in self._fail_on: self.aborted = True raise self._error return _Result(self._rows, self._row) # ── прочее, что могут дёрнуть обработчики ──────────────────────────────── def commit(self) -> None: if self.aborted: raise AbortedTransactionError(_ABORTED_MESSAGE) def rollback(self) -> None: self.aborted = False class _Result: def __init__(self, rows: list[Any], row: Any) -> None: self._rows, self._row = rows, row def mappings(self) -> _Result: return self def all(self) -> list[Any]: return self._rows def first(self) -> Any: return self._row def scalar(self) -> Any: return self._row