83 lines
3.9 KiB
Python
83 lines
3.9 KiB
Python
"""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
|