gendesign/backend/tests/sql/test_2464_act_date_backfill.py
bot-backend 14818f0303
All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 15s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m56s
CI Trade-In / backend-tests (pull_request) Successful in 5m25s
CI / backend-tests (pull_request) Successful in 18m18s
chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864)
Почему: файлы в main были отформатированы старым ruff (хук 0.7.4), а после
#3021 хук, CI и `uv run ruff format` — на 0.15.20. Без нормализации каждый
коммит, касающийся одного из этих 161 файлов, тащил бы посторонние хунки
на нетронутых строках (длинные assert-сообщения и т.п.).

Что: `ruff format` 0.15.20 по backend/ и tradein-mvp/backend/; ничего,
кроме форматирования (958+/1034−). `ruff check` 0.15.20 зелёный на обоих
проектах, повторный `format --check` — 1001 файл без изменений
(идемпотентно), compileall чистый.

Refs #2864
2026-08-21 16:39:28 +05:00

204 lines
8.7 KiB
Python
Raw 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()