"""Refresh helper for mv_quarter_price_per_m2 (Issue #33 D1). Not scheduled automatically in this PR — intended for manual invocation or a Celery beat task in a follow-up issue (Issue #33 PR C). Usage example (manual, via psql-connected shell or admin endpoint): from sqlalchemy.orm import Session from app.services.site_finder.quarter_price_refresh import refresh_quarter_price count = refresh_quarter_price(db) # logs: "mv_quarter_price_per_m2 refreshed: 52492 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_quarter_price(db: Session, *, concurrently: bool = True) -> int: """REFRESH MATERIALIZED VIEW mv_quarter_price_per_m2. Args: db: SQLAlchemy Session (sync). concurrently: When True, uses REFRESH CONCURRENTLY — non-blocking but requires the unique index mv_quarter_price_pk to exist (created by 95_mv_quarter_price.sql) and the MV to be already populated. Pass False only for the very first populate or after MV recreation. Returns: Row count in the MV after refresh (for observability / alerting). Raises: OperationalError: Re-raised if the error is not the known "cannot refresh materialized view concurrently" case. """ try: if concurrently: db.execute(text("REFRESH MATERIALIZED VIEW CONCURRENTLY mv_quarter_price_per_m2")) else: db.execute(text("REFRESH MATERIALIZED VIEW mv_quarter_price_per_m2")) 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 refresh" ) db.rollback() db.execute(text("REFRESH MATERIALIZED VIEW mv_quarter_price_per_m2")) db.commit() else: raise row = db.execute(text("SELECT COUNT(*) FROM mv_quarter_price_per_m2")).first() count = int(row[0]) if row else 0 logger.info("mv_quarter_price_per_m2 refreshed: %d rows", count) return count