gendesign/backend/app/services/site_finder/ird_overlay_lookup.py
bot-backend 7cde3b8eaa
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
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 2m31s
CI / backend-tests (pull_request) Successful in 17m27s
fix(ptica): шесть lookup'ов /analyze глотали ошибку БД без SAVEPOINT (#2464)
Шесть функций в site_finder работают на ОБЩЕЙ с analyze_parcel сессии и
глотают (OperationalError, ProgrammingError) вокруг db.execute. Без SAVEPOINT
проглоченная ошибка оставляет транзакцию в aborted-состоянии: сам lookup
возвращает пустой результат «штатно», а падает следующий за ним запрос —
в другом месте отчёта и с другой причиной в логе.

Соседний ppt_tep_lookup — та же семья, та же таблица planning_projects —
SAVEPOINT имел. Правка сводит шесть отставших к нему:

  planning_lookup, granddoc_lookup, ird_overlay_lookup,
  functional_zone_lookup, reservation_lookup  — по одному db.execute
  zone_regulation                             — upsert был защищён, SELECT нет

Тест параметризован по всем шести именам, плюс два контроля: «без SAVEPOINT
сессия травится» и «здоровый путь по-прежнему читает строки». На origin/main
шесть падают с AbortedTransactionError, оба контроля зелёные.

Стабы _FakeDB/_DB в пяти существующих тестах получили no-op begin_nested —
без него 28 тестов падали на AttributeError. В шапке каждого стаба указано,
где SAVEPOINT проверяется по-настоящему, чтобы no-op не читался как покрытие.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:18:59 +05:00

96 lines
5.2 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.

"""ИРД-overlay lookup для analyze_parcel (#1067 D9).
Читает ``ird_overlays`` (м.132, наполняется harvest-task #1067 B5) и отдаёт ИРД-ограничения,
пересекающие участок: ЗОУИТ / ПЗЗ-границы / ОКН / ООПТ-леса-вода — с reg_numb_border и (когда
резолвлено, #1067 C8/D9-content) читаемым типом/индексом зоны.
Отличие от ``quarter_dump_lookup`` (nspd_quarter_dumps blob-кеш): тот отдаёт per-quarter дамп
ЗОУИТ/zoning из NSPD QuarterDump; ``ird_overlays`` — выделенный per-feature ИРД-store с
reg_numb_border + content-колонками (для геопортал-резолва ВРИ/типа). Комплементарно: канонический
ИРД-слой «волшебной таблицы».
Graceful: если таблица ``ird_overlays`` ещё не задеплоена (pre-migration) или БД-ошибка — возвращает
пустой результат (analyze не падает). Wiring в analyze — flag-gated (D9b).
"""
from __future__ import annotations
import logging
from typing import Any
from sqlalchemy import text
from sqlalchemy.exc import DataError, OperationalError, ProgrammingError
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# Пустой результат — sentinel для analyze try/except (НЕ мутировать).
IRD_EMPTY_RESULT: dict[str, Any] = {"ird_overlaps": [], "ird_by_kind": {}}
# ИРД-overlay по участку: ST_Intersects через GIST idx_ird_overlays_geom. geom уже EPSG:4326
# (harvest стор ST_Transform 3857→4326). Участок передаётся как WKT в 4326. CAST psycopg v3.
# content добавлен для геопортальных ЗОУИТ-слоёв (gknspecial_harvest #1063/#1064/#1065/#1066).
_IRD_OVERLAP_SQL = text(
"""
SELECT layer_kind, reg_numb_border, category, subcategory,
zone_index, zone_type_name, content
FROM ird_overlays
WHERE ST_Intersects(geom, ST_GeomFromText(CAST(:parcel_wkt AS text), 4326))
ORDER BY layer_kind, reg_numb_border
"""
)
def parcel_ird_overlaps(db: Session, parcel_wkt: str | None) -> dict[str, Any]:
"""ИРД-ограничения, пересекающие участок.
Args:
db: сессия.
parcel_wkt: геометрия участка в WKT (EPSG:4326). None → пустой результат.
Returns:
{'ird_overlaps': [{layer_kind, reg_numb_border, category, subcategory,
zone_index, zone_type_name, content}], 'ird_by_kind': {layer_kind: count}}.
content — JSONB-dict с ограничениями/реквизитами зоны (геопортальные слои:
gknspecial_harvest #1063/#1064/#1065/#1066); None для НСПД-слоёв до резолва D9.
Пусто при отсутствии участка/таблицы/пересечений (analyze не падает).
"""
if not parcel_wkt:
return {"ird_overlaps": [], "ird_by_kind": {}}
try:
# #2464: SAVEPOINT перед проглатыванием ошибки. Сессия ОБЩАЯ с analyze_parcel
# (build_ird_analyze_block зовёт шесть таких lookup'ов подряд в одном словаре),
# и на Postgres упавший запрос оставляет транзакцию в aborted-состоянии —
# падают все следующие, включая запись прогона. Образец рядом:
# ppt_tep_lookup.py делает ровно так же.
with db.begin_nested():
rows = db.execute(_IRD_OVERLAP_SQL, {"parcel_wkt": parcel_wkt}).mappings().all()
except (OperationalError, ProgrammingError, DataError) as exc:
# OperationalError — БД недоступна / connection drop.
# ProgrammingError — ird_overlays ещё не задеплоена (UndefinedTable, pre-migration).
# DataError — malformed WKT в ST_GeomFromText (PostGIS ERROR пробрасывает SQLAlchemy
# как DataError/InternalError). parcel_wkt системно-генерируемый, но при D9b-wiring
# в analyze graceful-degrade — единственная корректная стратегия (analyze не падает).
logger.warning("parcel_ird_overlaps: ird_overlays недоступна, skip: %s", exc)
return {"ird_overlaps": [], "ird_by_kind": {}}
overlaps: list[dict[str, Any]] = []
by_kind: dict[str, int] = {}
for r in rows:
kind = r["layer_kind"]
overlaps.append(
{
"layer_kind": kind,
"reg_numb_border": r["reg_numb_border"],
"category": r["category"],
"subcategory": r["subcategory"],
"zone_index": r["zone_index"],
"zone_type_name": r["zone_type_name"],
"content": r["content"],
}
)
by_kind[kind] = by_kind.get(kind, 0) + 1
return {"ird_overlaps": overlaps, "ird_by_kind": by_kind}
__all__ = ["IRD_EMPTY_RESULT", "parcel_ird_overlaps"]