""" StatViewRefresher - Refresh materialized views for statistics. Provides methods to refresh PostgreSQL materialized views created in migration 004. Uses REFRESH MATERIALIZED VIEW CONCURRENTLY to allow reads during refresh. Author: Claude Date: 2025-11-07 """ import logging from sqlalchemy import text from app.database.session import AsyncSessionLocal logger = logging.getLogger(f"{__name__}.StatViewRefresher") class StatViewRefresher: """ Service for refreshing materialized views containing game statistics. Refreshes three materialized views: - batting_game_stats: Player batting statistics aggregated from plays - pitching_game_stats: Player pitching statistics aggregated from plays - game_stats: Team totals and linescore aggregated from plays Uses CONCURRENTLY option to allow reads during refresh (requires unique indexes). """ # View names in order (dependencies matter for concurrent refresh) VIEWS = ["batting_game_stats", "pitching_game_stats", "game_stats"] async def refresh_all(self) -> None: """ Refresh all stat materialized views concurrently. Refreshes views in order to handle any dependencies. Uses REFRESH MATERIALIZED VIEW CONCURRENTLY to avoid locking. Raises: Exception: If any refresh fails """ async with AsyncSessionLocal() as session: try: for view_name in self.VIEWS: logger.debug(f"Refreshing materialized view: {view_name}") await self._refresh_view(session, view_name) await session.commit() logger.info( f"Successfully refreshed all {len(self.VIEWS)} materialized views" ) except Exception as e: await session.rollback() logger.error( f"Failed to refresh materialized views: {e}", exc_info=True ) raise async def refresh_view(self, view_name: str) -> None: """ Refresh a single materialized view. Args: view_name: Name of the materialized view to refresh Raises: ValueError: If view_name is not recognized Exception: If refresh fails """ if view_name not in self.VIEWS: raise ValueError(f"Unknown view: {view_name}. Must be one of {self.VIEWS}") async with AsyncSessionLocal() as session: try: await self._refresh_view(session, view_name) await session.commit() logger.info(f"Successfully refreshed materialized view: {view_name}") except Exception as e: await session.rollback() logger.error(f"Failed to refresh view {view_name}: {e}", exc_info=True) raise async def _refresh_view(self, session, view_name: str) -> None: """ Execute REFRESH MATERIALIZED VIEW CONCURRENTLY statement. Args: session: Active database session view_name: Name of the view to refresh """ # CONCURRENTLY requires unique index (created in migration) # Allows reads during refresh but is slightly slower query = text(f"REFRESH MATERIALIZED VIEW CONCURRENTLY {view_name}") await session.execute(query) logger.debug(f"Executed: REFRESH MATERIALIZED VIEW CONCURRENTLY {view_name}") # Singleton instance stat_view_refresher = StatViewRefresher()