This commit captures work from multiple sessions building the statistics system and frontend component library. Backend - Phase 3.5: Statistics System - Box score statistics with materialized views - Play stat calculator for real-time updates - Stat view refresher service - Alembic migration for materialized views - Test coverage: 41 new tests (all passing) Frontend - Phase F1: Foundation - Composables: useGameState, useGameActions, useWebSocket - Type definitions and interfaces - Store setup with Pinia Frontend - Phase F2: Game Display - ScoreBoard, GameBoard, CurrentSituation, PlayByPlay components - Demo page at /demo Frontend - Phase F3: Decision Inputs - DefensiveSetup, OffensiveApproach, StolenBaseInputs components - DecisionPanel orchestration - Demo page at /demo-decisions - Test coverage: 213 tests passing Frontend - Phase F4: Dice & Manual Outcome - DiceRoller component - ManualOutcomeEntry with validation - PlayResult display - GameplayPanel orchestration - Demo page at /demo-gameplay - Test coverage: 119 tests passing Frontend - Phase F5: Substitutions - PinchHitterSelector, DefensiveReplacementSelector, PitchingChangeSelector - SubstitutionPanel with tab navigation - Demo page at /demo-substitutions - Test coverage: 114 tests passing Documentation: - PHASE_3_5_HANDOFF.md - Statistics system handoff - PHASE_F2_COMPLETE.md - Game display completion - Frontend phase planning docs - NEXT_SESSION.md updated for Phase F6 Configuration: - Package updates (Nuxt 4 fixes) - Tailwind config enhancements - Game store updates Test Status: - Backend: 731/731 passing (100%) - Frontend: 446/446 passing (100%) - Total: 1,177 tests passing Next Phase: F6 - Integration (wire all components into game page) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
"""
|
|
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()
|