gendesign/backend/app/services/site_finder/pat_lookup.py
bot-backend c52c2c4063
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m5s
CI / backend-tests (pull_request) Successful in 15m6s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
fix(site_finder): SAVEPOINT-isolate DB errors across cluster A (#2464)
developer_attribution, connection_capacity_lookup (gas/heat helpers),
competitors, saturation, supply_layers, and pat_lookup swallowed DB errors
without begin_nested()/rollback(), leaving the shared analyze-request
Session aborted for every subsequent db.execute (silently breaking
persist_analysis_run and the forecast enqueue). zone_regulation's
backfill_ekb_zone_regulations gets a plain rollback() since it only ever
runs on its own owned Celery-task session.

Refs #2464 (cluster A session-poisoning).
2026-07-07 17:32:32 +05:00

70 lines
2.8 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.

"""ПАТ-lookup для site-finder analyze (#1065 / #1067).
Читает pat_subzones и возвращает подзоны ПАТ, пересекающие переданный участок.
Graceful: если таблица ещё не задеплоена или БД недоступна — возвращает пустой список.
Wiring в analyze_parcel — follow-up (не в этом PR).
"""
from __future__ import annotations
import logging
from typing import Any
from sqlalchemy import text
from sqlalchemy.exc import OperationalError, ProgrammingError
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# ST_Intersects к pat_subzones, GIST idx_pat_subzones_geom.
# Исключаем subzone_no=0 (aggregate_outline) — клиент получает конкретные подзоны 1..6.
_PAT_INTERSECTS_SQL = text(
"""
SELECT subzone_no, name, restriction, aerodrome
FROM pat_subzones
WHERE subzone_no > 0
AND ST_Intersects(geom, ST_GeomFromText(CAST(:parcel_wkt AS text), 4326))
ORDER BY subzone_no
"""
)
def parcel_pat_subzones(db: Session, parcel_wkt: str | None) -> list[dict[str, Any]]:
"""Подзоны ПАТ, пересекающие участок.
Args:
db: SQLAlchemy-сессия.
parcel_wkt: геометрия участка WKT (EPSG:4326). None → пустой результат.
Returns:
Список dict [{subzone_no, name, restriction, aerodrome}] отсортированный
по subzone_no. Пустой список при отсутствии пересечений, ошибке БД
или недеплоенной таблице.
"""
if not parcel_wkt:
return []
try:
# SAVEPOINT — db shared с caller (analyze_parcel, shared request-scoped Session).
# Голый db.rollback() orphan'ит outer SessionTransaction (см. velocity.py:170) —
# begin_nested откатывает только эту savepoint, не мешая persist_analysis_run
# и прочим db.execute дальше по /analyze (#2464 cluster A finding 7).
with db.begin_nested():
rows = db.execute(_PAT_INTERSECTS_SQL, {"parcel_wkt": parcel_wkt}).mappings().all()
except (OperationalError, ProgrammingError) as exc:
# Таблица ещё не задеплоена / структура изменилась — graceful degrade.
logger.warning("parcel_pat_subzones: pat_subzones недоступна, skip: %s", exc)
return []
return [
{
"subzone_no": r["subzone_no"],
"name": r["name"],
"restriction": r["restriction"],
"aerodrome": r["aerodrome"],
}
for r in rows
]
__all__ = ["parcel_pat_subzones"]