gendesign/backend/tests/sql/test_2464_act_date_backfill.py
bot-backend cdf493f345
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864) (#3022)
2026-08-21 12:01:52 +00:00

204 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Миграция 191 чинит ровно 11 строк с датой Генплана и ничего кроме (#2464).
До #2981 `_extract_act_date` брал первое «от DD.MM.YYYY» во всём OCR-тексте, и в
`act_date` уезжала дата из «Решение Екатеринбургской городской Думы от 06.07.2004
№ 60/1 «Об утверждении Генерального плана города»».
#2981 чинит извлечение **только вперёд**: UPSERT загрузчика заканчивается
`ON CONFLICT DO NOTHING`, а не DO UPDATE, поэтому пятничный прогон существующие
строки не перезапишет. Отсюда разовая миграция.
Замер прода 20.08.2026: 11 строк с `act_date='2004-07-06'` — 9 по документу
89adb28a… (развязка) и 2 по 9b9d9a99… (Энергостроителей). Верные даты взяты из
самих PDF, распознанных тем же трактом, что использует загрузчик.
Тест герметичный: временная копия таблицы, боевые данные не читаются и не меняются.
Проверяется тело миграции целиком, а не переписанный от руки UPDATE.
"""
from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
import re
from datetime import date
from pathlib import Path
import pytest
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
_MIGRATION = (
Path(__file__).resolve().parents[3]
/ "data"
/ "sql"
/ "191_land_reservation_act_date_backfill.sql"
)
def _dsn() -> str:
raw = os.environ.get("TEST_DATABASE_URL") or os.environ.get(
"DATABASE_URL", "postgresql+psycopg://gendesign@localhost:15432/gendesign"
)
return (
raw
if raw.startswith("postgresql+")
else raw.replace("postgresql://", "postgresql+psycopg://")
)
def _db_reachable() -> tuple[bool, str]:
try:
eng = create_engine(_dsn(), connect_args={"connect_timeout": 3})
with eng.connect() as c:
c.execute(text("SELECT 1"))
return True, ""
except Exception as exc:
return False, str(exc)
_DB_OK, _DB_ERR = _db_reachable()
pytestmark = pytest.mark.skipif(not _DB_OK, reason=f"Postgres недоступен: {_DB_ERR}")
_URL_РАЗВЯЗКА = "https://xn--80acgfbsl1azdqr.xn--p1ai/file/89adb28a3677e7df933e2d9ce0f205c8"
_URL_ЭНЕРГО = "https://xn--80acgfbsl1azdqr.xn--p1ai/file/9b9d9a998f578db56315bb816fc2ebf5"
_URL_ЧУЖОЙ = "https://xn--80acgfbsl1azdqr.xn--p1ai/file/ffffffffffffffffffffffffffffffff"
_TEMP = """
CREATE TEMP TABLE land_reservation (
id bigserial PRIMARY KEY,
cad_num text NOT NULL,
act_date date,
doc_url text
) ON COMMIT DROP
"""
# Прод-форма: 9 строк развязки + 2 Энергостроителей + контрольные посторонние.
_SEED = (
[(f"66:41:0603006:{i}", date(2004, 7, 6), _URL_РАЗВЯЗКА) for i in range(9)]
+ [(f"66:41:0303004:{i}", date(2004, 7, 6), _URL_ЭНЕРГО) for i in range(2)]
+ [
# Чужой документ с той же датой — трогать НЕЛЬЗЯ: 06.07.2004 может быть
# и настоящей датой акта, сужение по doc_url именно для этого.
("66:41:0000001:1", date(2004, 7, 6), _URL_ЧУЖОЙ),
# Верные строки других документов — не должны сдвинуться.
("66:41:0502080:22", date(2015, 9, 29), _URL_ЧУЖОЙ),
]
)
def _body() -> list[str]:
"""Тело миграции без комментариев, разбитое на выполнимые куски."""
raw = _MIGRATION.read_text(encoding="utf-8")
без_комментов = "\n".join(ln for ln in raw.splitlines() if not ln.lstrip().startswith("--"))
return [
chunk.strip()
for chunk in re.split(r";\s*(?:\n|$)", без_комментов)
if chunk.strip() and chunk.strip().upper() not in {"BEGIN", "COMMIT"}
]
def _run() -> dict[tuple[str, date], int]:
session = sessionmaker(bind=create_engine(_dsn()))()
try:
session.execute(text(_TEMP))
for cad, d, url in _SEED:
session.execute(
text("INSERT INTO land_reservation (cad_num, act_date, doc_url) VALUES (:c,:d,:u)"),
{"c": cad, "d": d, "u": url},
)
for chunk in _body():
session.execute(text(chunk))
rows = session.execute(
text(
"SELECT right(doc_url,32) AS h, act_date, count(*)"
" FROM land_reservation GROUP BY 1,2"
)
).all()
return {(str(r[0]), r[1]): int(r[2]) for r in rows}
finally:
session.rollback()
session.close()
def test_genplan_dates_are_replaced_with_real_ones() -> None:
"""Головной: 9 + 2 строки получают дату своего постановления."""
res = _run()
assert res.get(("89adb28a3677e7df933e2d9ce0f205c8", date(2022, 5, 27))) == 9, res
assert res.get(("9b9d9a998f578db56315bb816fc2ebf5", date(2020, 2, 12))) == 2, res
def test_no_2004_rows_left_in_target_documents() -> None:
"""Контроль полноты: в этих двух документах даты 2004 года не остаётся."""
res = _run()
for h in ("89adb28a3677e7df933e2d9ce0f205c8", "9b9d9a998f578db56315bb816fc2ebf5"):
assert res.get((h, date(2004, 7, 6))) is None, f"остались строки 2004 у {h}: {res}"
def test_other_documents_with_same_date_are_untouched() -> None:
"""Контроль сужения: та же дата в ЧУЖОМ документе не трогается.
Без условия по doc_url миграция переписала бы любую строку с 06.07.2004,
включая те, где эта дата настоящая.
"""
res = _run()
assert res.get(("ffffffffffffffffffffffffffffffff", date(2004, 7, 6))) == 1, res
def test_correct_rows_do_not_move() -> None:
"""Контроль от переусердствования: верные даты остаются на месте."""
res = _run()
assert res.get(("ffffffffffffffffffffffffffffffff", date(2015, 9, 29))) == 1, res
def test_migration_is_idempotent() -> None:
"""Повторный запуск ничего не меняет — условие act_date='2004-07-06' не выполнится."""
session = sessionmaker(bind=create_engine(_dsn()))()
try:
session.execute(text(_TEMP))
for cad, d, url in _SEED:
session.execute(
text("INSERT INTO land_reservation (cad_num, act_date, doc_url) VALUES (:c,:d,:u)"),
{"c": cad, "d": d, "u": url},
)
for _ in range(2):
for chunk in _body():
session.execute(text(chunk))
n = session.execute(
text("SELECT count(*) FROM land_reservation WHERE act_date = DATE '2022-05-27'")
).scalar()
assert n == 9, f"после двух прогонов строк с 2022-05-27: {n}"
finally:
session.rollback()
session.close()
def test_without_the_migration_rows_stay_wrong() -> None:
"""Контроль-двойник: БЕЗ прогона миграции строки остаются с датой Генплана.
Обязателен: без него тест неотличим от «оно и так было правильно».
"""
session = sessionmaker(bind=create_engine(_dsn()))()
try:
session.execute(text(_TEMP))
for cad, d, url in _SEED:
session.execute(
text("INSERT INTO land_reservation (cad_num, act_date, doc_url) VALUES (:c,:d,:u)"),
{"c": cad, "d": d, "u": url},
)
# Миграцию НЕ прогоняем.
n = session.execute(
text("SELECT count(*) FROM land_reservation WHERE act_date = DATE '2004-07-06'")
).scalar()
assert n == 12, f"засеяно не то состояние — строк с датой Генплана: {n}"
assert (
session.execute(
text("SELECT count(*) FROM land_reservation WHERE act_date = DATE '2022-05-27'")
).scalar()
== 0
), "верная дата появилась без миграции — тест смотрел бы не туда"
finally:
session.rollback()
session.close()