gendesign/backend/app/services/site_finder/layout_velocity_refresh.py
lekss361 46c2a84ea4 fix(analytics): address review-bot PR B fixes (#113)
SQL (data/sql/94_mv_layout_velocity.sql):
- 🟠 idempotency WARN comment перед DROP CASCADE
- 🟡 WITH NO DATA + REFRESH after CREATE INDEX (меньше lock duration)
- 🟡 WHERE cm.objective_group = 'Екатеринбург' (cross-region Cartesian защита)
- 🟡 COMMENT ON MATERIALIZED VIEW с источниками + refresh механизмом

Python (services/site_finder/layout_velocity_refresh.py):
- 🟡 db.commit() после REFRESH (visibility через connection pool)
- 🟡 убрать f-string в SQL — две явных text() ветки
- 🟡 try/except OperationalError fallback на non-concurrent
2026-05-16 11:17:37 +03:00

57 lines
2 KiB
Python

"""Refresh helper for mv_layout_velocity (Issue #113 PR B).
Not scheduled automatically in this PR — intended for manual invocation or
a Celery beat task in a follow-up issue.
Usage example (manual, via psql-connected shell or admin endpoint):
from sqlalchemy.orm import Session
from app.services.site_finder.layout_velocity_refresh import refresh_layout_velocity
count = refresh_layout_velocity(db)
# logs: "mv_layout_velocity refreshed: 459 rows"
"""
from __future__ import annotations
import logging
from sqlalchemy import text
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
def refresh_layout_velocity(db: Session, *, concurrently: bool = True) -> int:
"""REFRESH MATERIALIZED VIEW mv_layout_velocity.
Args:
db: SQLAlchemy Session (sync).
concurrently: When True, uses REFRESH CONCURRENTLY — non-blocking but
requires the unique index mv_layout_velocity_pk to exist (created
by 94_mv_layout_velocity.sql). Pass False only for the very first
populate or when the MV was just recreated.
Returns:
Row count in the MV after refresh (for observability / alerting).
"""
try:
if concurrently:
db.execute(text("REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity"))
else:
db.execute(text("REFRESH MATERIALIZED VIEW mv_layout_velocity"))
db.commit()
except OperationalError as e:
if concurrently and "cannot refresh materialized view" in str(e).lower():
logger.warning(
"CONCURRENTLY failed (MV likely not populated), falling back to non-concurrent"
)
db.rollback()
db.execute(text("REFRESH MATERIALIZED VIEW mv_layout_velocity"))
db.commit()
else:
raise
row = db.execute(text("SELECT COUNT(*) FROM mv_layout_velocity")).first()
count = int(row[0]) if row else 0
logger.info("mv_layout_velocity refreshed: %d rows", count)
return count