"""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