gendesign/tradein-mvp/backend/app/core/fdw.py
lekss361 698ef4f003 feat(tradein): postgres_fdw live read of gendesign.cad_buildings (replaces snapshot)
Replaces tradein.cad_buildings manual snapshot (36k rows, frozen 2026-05-22) with
live postgres_fdw foreign table reading gendesign.v_tradein_cad_buildings directly.
Also reverts PR #492 HTTP-based cadastral.py — superseded by FDW.

Architecture:
- gendesign DB: new role tradein_fdw_reader + flat view v_tradein_cad_buildings
  (EKB-only slice of cad_buildings with lat/lon flattened from geom)
- networks: gendesign-postgres added to gendesign_shared (alias);
  tradein-postgres added to gendesign_shared for FDW connect
- tradein DB: postgres_fdw extension + FOREIGN TABLE gendesign_cad_buildings
- tradein backend: startup hook creates/refreshes USER MAPPING with password
  from env GENDESIGN_FDW_PASSWORD (password rotation handled via restart)
- geocoder.py: cadastral primary for forward + reverse + suggest;
  Yandex/Nominatim fallback. reverse_geocode wraps Nominatim in try/except —
  no more HTTPStatusError → 500.
- house_metadata.py and trade_in.py admin stats switched to foreign table
- DROP TABLE cad_buildings in tradein (legacy snapshot removed entirely)
- import-cadastre.sh deleted (no manual sync needed)

Fixes:
- /trade-in/api/v1/geocode/reverse 500 (Nominatim ban → no fallback)
- estimate confidence_explanation=address_not_geocoded for addresses in our
  cadastre (e.g. Хохрякова 81) that Nominatim doesn't return

Deploy ordering: main migration 100_tradein_fdw_role.sql adds role+view first
(strict deploy.yml). Tradein next deploy applies 060_postgres_fdw_extension.sql
and 061_drop_legacy_cad_buildings.sql (idempotent, errors ignored). Tradein
backend startup creates USER MAPPING when env var present. Verify post-deploy
with: docker exec tradein-postgres psql -U tradein -d tradein -c \
  "SELECT count(*) FROM gendesign_cad_buildings"  # expect ~36k EKB buildings.
2026-05-24 11:13:44 +03:00

65 lines
2.1 KiB
Python

"""postgres_fdw USER MAPPING management for gendesign_remote server.
USER MAPPING password is in env GENDESIGN_FDW_PASSWORD (not in SQL migration —
would leak password to disk).
This helper applies idempotent CREATE / ALTER USER MAPPING. Reapplied on every
backend startup so password rotation through .env.runtime is picked up after restart.
"""
from __future__ import annotations
import logging
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.config import settings
logger = logging.getLogger(__name__)
def ensure_fdw_user_mapping(db: Session) -> None:
"""Idempotent CREATE OR ALTER USER MAPPING for tradein → gendesign_remote.
Skips silently if password not configured (dev / first deploy).
"""
password = settings.gendesign_fdw_password
if not password:
logger.warning(
"GENDESIGN_FDW_PASSWORD not set — skipping FDW user mapping "
"(gendesign_cad_buildings will fail)"
)
return
if not isinstance(password, str) or not password.strip():
logger.warning("GENDESIGN_FDW_PASSWORD empty — skipping FDW user mapping")
return
# Check existence
exists = db.execute(
text(
"SELECT 1 FROM pg_user_mappings "
"WHERE srvname = 'gendesign_remote' AND usename = current_user"
)
).first()
# Password cannot be a bind parameter in CREATE/ALTER USER MAPPING — must inline.
# Escape single quotes by doubling.
escaped_pw = password.replace("'", "''")
if exists is None:
db.execute(
text(
f"CREATE USER MAPPING FOR CURRENT_USER SERVER gendesign_remote "
f"OPTIONS (user 'tradein_fdw_reader', password '{escaped_pw}')"
)
)
logger.info("created FDW user mapping for gendesign_remote")
else:
db.execute(
text(
f"ALTER USER MAPPING FOR CURRENT_USER SERVER gendesign_remote "
f"OPTIONS (SET password '{escaped_pw}')"
)
)
logger.info("refreshed FDW user mapping password for gendesign_remote")
db.commit()