"""Static guards for migration 164 (issue #2235). Прод применяет data/sql построчно строго (ON_ERROR_STOP). Полный DB-прогон идемпотентности требует живой БД; здесь мы фиксируем структурные инварианты, которые ГАРАНТИРУЮТ идемпотентность и НЕдеструктивность по построению: 1. Транзакционность (BEGIN/COMMIT) — атомарный апдейт. 2. Идемпотентность: апдейт ограничен строками, чей source_url ещё НЕ канонический (`source_url <> '…offer/'||source_id||'/'`) И входит в группу с >1 строкой (CTE `shared`, HAVING count(*)>1). После первого прогона таких строк нет → повторный прогон = no-op. 3. НЕдеструктивность: миграция канонизирует source_url, а НЕ гасит строки — `is_active = false` в файле быть НЕ должно (защита от регресса к деструктивному «схлопыванию», отвергнутому по данным: дубли — это РАЗНЫЕ офферы, не дубли-строки). 4. Скоуп — только yandex. """ from __future__ import annotations import re from pathlib import Path _SQL_DIR = Path(__file__).resolve().parents[1] / "data" / "sql" _MIGRATION_164 = _SQL_DIR / "164_yandex_url_canonicalize_active_dups.sql" def _sql() -> str: return _MIGRATION_164.read_text(encoding="utf-8") def _executable_sql() -> str: """SQL без комментариев — только исполняемые строки (`--`-комменты вырезаны). Заголовок миграции в прозе объясняет, ПОЧЕМУ отвергнуто «схлопывание» (is_active=false), поэтому destructive-проверки должны смотреть на код, а не на комментарии. В этой миграции `--` не встречается внутри строковых литералов. """ lines = [] for raw in _MIGRATION_164.read_text(encoding="utf-8").splitlines(): code = raw.split("--", 1)[0] if code.strip(): lines.append(code) return "\n".join(lines) def _flat(text: str) -> str: return re.sub(r"\s+", " ", text).strip() def test_migration_164_exists() -> None: assert _MIGRATION_164.exists(), f"missing migration: {_MIGRATION_164}" def test_migration_164_is_transactional() -> None: sql = _sql() assert "BEGIN;" in sql assert "COMMIT;" in sql def test_migration_164_is_idempotent_by_construction() -> None: """Апдейт исключает уже-канонические строки → повторный прогон no-op.""" flat = _flat(_executable_sql()) # Группировка дублей (HAVING count(*) > 1) + пропуск уже-канонических строк. assert "having count(*) > 1" in flat.lower() assert "source_url <> 'https://realty.yandex.ru/offer/'" in flat def test_migration_164_is_non_destructive() -> None: """Канонизация source_url, НЕ гашение строк: никакого is_active=false в коде.""" flat = _flat(_executable_sql()).lower() assert "is_active = false" not in flat assert "is_active=false" not in flat # Мутируется именно source_url. assert "set source_url = 'https://realty.yandex.ru/offer/'" in flat def test_migration_164_scoped_to_yandex_active() -> None: flat = _flat(_executable_sql()) assert "l.source = 'yandex'" in flat assert "l.is_active" in flat # Только числовой yandex offerId идёт в канонический URL. assert "l.source_id ~ '^[0-9]+$'" in flat