""" Common database queries for SBA Scout. Provides reusable query functions for accessing player, team, and card data. """ from datetime import datetime from typing import Optional, Sequence from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from .models import ( BatterCard, Lineup, MatchupCache, PitcherCard, Player, SyncStatus, Team, Transaction, ) # ============================================================================= # Team Queries # ============================================================================= async def get_all_teams( session: AsyncSession, season: int, active_only: bool = True, ) -> Sequence[Team]: """ Get all teams for a season. Args: session: Database session season: Season number active_only: If True, exclude IL and MiL teams Returns: List of Team objects """ query = select(Team).where(Team.season == season) if active_only: # Exclude teams ending in IL or MiL query = query.where( ~Team.abbrev.endswith("IL"), ~Team.abbrev.endswith("MiL"), ) query = query.order_by(Team.abbrev) result = await session.execute(query) return result.scalars().all() async def get_team_by_abbrev( session: AsyncSession, abbrev: str, season: int, ) -> Optional[Team]: """Get a team by abbreviation and season.""" query = select(Team).where(Team.abbrev == abbrev, Team.season == season) result = await session.execute(query) return result.scalar_one_or_none() async def get_team_by_id(session: AsyncSession, team_id: int) -> Optional[Team]: """Get a team by ID.""" query = select(Team).where(Team.id == team_id) result = await session.execute(query) return result.scalar_one_or_none() # ============================================================================= # Player Queries # ============================================================================= async def get_players_by_team( session: AsyncSession, team_id: int, include_cards: bool = True, ) -> Sequence[Player]: """ Get all players on a team. Args: session: Database session team_id: Team ID include_cards: If True, eagerly load card data Returns: List of Player objects """ query = select(Player).where(Player.team_id == team_id) if include_cards: query = query.options( selectinload(Player.batter_card), selectinload(Player.pitcher_card), ) query = query.order_by(Player.name) result = await session.execute(query) return result.scalars().all() async def get_player_by_id( session: AsyncSession, player_id: int, include_cards: bool = True, ) -> Optional[Player]: """Get a player by ID.""" query = select(Player).where(Player.id == player_id) if include_cards: query = query.options( selectinload(Player.batter_card), selectinload(Player.pitcher_card), ) result = await session.execute(query) return result.scalar_one_or_none() async def get_player_by_name( session: AsyncSession, name: str, season: int, include_cards: bool = True, ) -> Optional[Player]: """Get a player by name and season (case-insensitive).""" query = select(Player).where( Player.name.ilike(name), Player.season == season, ) if include_cards: query = query.options( selectinload(Player.batter_card), selectinload(Player.pitcher_card), ) result = await session.execute(query) return result.scalar_one_or_none() async def search_players( session: AsyncSession, query_str: str, season: int, limit: int = 20, ) -> Sequence[Player]: """ Search players by name (partial match). Args: session: Database session query_str: Search string season: Season number limit: Maximum results to return Returns: List of matching Player objects """ query = ( select(Player) .where( Player.name.ilike(f"%{query_str}%"), Player.season == season, ) .options( selectinload(Player.batter_card), selectinload(Player.pitcher_card), ) .order_by(Player.name) .limit(limit) ) result = await session.execute(query) return result.scalars().all() async def get_pitchers( session: AsyncSession, team_id: Optional[int] = None, season: Optional[int] = None, ) -> Sequence[Player]: """Get all pitchers, optionally filtered by team.""" query = select(Player).where( (Player.pos_1 == "SP") | (Player.pos_1 == "RP") | (Player.pos_1 == "CP") | (Player.pos_2 == "SP") | (Player.pos_2 == "RP") | (Player.pos_2 == "CP") ) if team_id: query = query.where(Player.team_id == team_id) if season: query = query.where(Player.season == season) query = query.options(selectinload(Player.pitcher_card)).order_by(Player.name) result = await session.execute(query) return result.scalars().all() async def get_batters( session: AsyncSession, team_id: Optional[int] = None, season: Optional[int] = None, ) -> Sequence[Player]: """Get all batters (non-pitcher position players).""" batter_positions = ["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "DH"] query = select(Player).where(Player.pos_1.in_(batter_positions)) if team_id: query = query.where(Player.team_id == team_id) if season: query = query.where(Player.season == season) query = query.options(selectinload(Player.batter_card)).order_by(Player.name) result = await session.execute(query) return result.scalars().all() # ============================================================================= # Card Data Queries # ============================================================================= async def get_batter_card( session: AsyncSession, player_id: int, ) -> Optional[BatterCard]: """Get batter card data for a player.""" query = select(BatterCard).where(BatterCard.player_id == player_id) result = await session.execute(query) return result.scalar_one_or_none() async def get_pitcher_card( session: AsyncSession, player_id: int, ) -> Optional[PitcherCard]: """Get pitcher card data for a player.""" query = select(PitcherCard).where(PitcherCard.player_id == player_id) result = await session.execute(query) return result.scalar_one_or_none() async def get_players_missing_cards( session: AsyncSession, season: int, card_type: str = "batter", ) -> Sequence[Player]: """ Get players who don't have card data imported yet. Args: session: Database session season: Season number card_type: "batter" or "pitcher" Returns: List of Player objects without card data """ if card_type == "batter": # Get batters without batter cards subquery = select(BatterCard.player_id) query = ( select(Player) .where( Player.season == season, Player.pos_1.in_(["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "DH"]), ~Player.id.in_(subquery), ) .order_by(Player.name) ) else: # Get pitchers without pitcher cards subquery = select(PitcherCard.player_id) query = ( select(Player) .where( Player.season == season, Player.pos_1.in_(["SP", "RP", "CP"]), ~Player.id.in_(subquery), ) .order_by(Player.name) ) result = await session.execute(query) return result.scalars().all() # ============================================================================= # Roster Queries (for your team) # ============================================================================= async def get_my_roster( session: AsyncSession, team_abbrev: str, season: int, ) -> dict[str, Sequence[Player]]: """ Get full roster for your team including IL and MiL. Returns: Dict with keys 'majors', 'minors', 'il' containing player lists """ # Main roster majors_team = await get_team_by_abbrev(session, team_abbrev, season) majors = await get_players_by_team(session, majors_team.id) if majors_team else [] # IL roster il_team = await get_team_by_abbrev(session, f"{team_abbrev}IL", season) il = await get_players_by_team(session, il_team.id) if il_team else [] # Minor league roster mil_team = await get_team_by_abbrev(session, f"{team_abbrev}MiL", season) minors = await get_players_by_team(session, mil_team.id) if mil_team else [] return { "majors": majors, "minors": minors, "il": il, } # ============================================================================= # Sync Status Queries # ============================================================================= async def get_sync_status( session: AsyncSession, entity_type: str, ) -> Optional[SyncStatus]: """Get sync status for an entity type.""" query = select(SyncStatus).where(SyncStatus.entity_type == entity_type) result = await session.execute(query) return result.scalar_one_or_none() async def update_sync_status( session: AsyncSession, entity_type: str, count: int, error: Optional[str] = None, ) -> None: """Update sync status after a sync operation.""" status = await get_sync_status(session, entity_type) if status: status.last_sync = datetime.utcnow() status.last_sync_count = count status.last_error = error else: status = SyncStatus( entity_type=entity_type, last_sync=datetime.utcnow(), last_sync_count=count, last_error=error, ) session.add(status) # ============================================================================= # Matchup Cache Queries # ============================================================================= async def get_cached_matchup( session: AsyncSession, batter_id: int, pitcher_id: int, weights_hash: str, ) -> Optional[MatchupCache]: """Get cached matchup if it exists and weights haven't changed.""" query = select(MatchupCache).where( MatchupCache.batter_id == batter_id, MatchupCache.pitcher_id == pitcher_id, MatchupCache.weights_hash == weights_hash, ) result = await session.execute(query) return result.scalar_one_or_none() async def invalidate_matchup_cache(session: AsyncSession) -> int: """ Invalidate all matchup cache entries. Called when weights change. Returns: Number of entries deleted """ result = await session.execute( MatchupCache.__table__.delete() # type: ignore ) return result.rowcount or 0 # ============================================================================= # Lineup Queries # ============================================================================= async def get_lineups(session: AsyncSession) -> Sequence[Lineup]: """Get all saved lineups.""" query = select(Lineup).order_by(Lineup.name) result = await session.execute(query) return result.scalars().all() async def get_lineup_by_name( session: AsyncSession, name: str, ) -> Optional[Lineup]: """Get a lineup by name.""" query = select(Lineup).where(Lineup.name == name) result = await session.execute(query) return result.scalar_one_or_none() async def save_lineup( session: AsyncSession, name: str, batting_order: list[int], positions: dict[str, int], lineup_type: str = "standard", description: Optional[str] = None, starting_pitcher_id: Optional[int] = None, ) -> Lineup: """Save or update a lineup.""" existing = await get_lineup_by_name(session, name) if existing: existing.batting_order = batting_order existing.positions = positions existing.lineup_type = lineup_type existing.description = description existing.starting_pitcher_id = starting_pitcher_id existing.updated_at = datetime.utcnow() return existing else: lineup = Lineup( name=name, batting_order=batting_order, positions=positions, lineup_type=lineup_type, description=description, starting_pitcher_id=starting_pitcher_id, ) session.add(lineup) return lineup async def delete_lineup( session: AsyncSession, name: str, ) -> bool: """Delete a lineup by name. Returns: True if lineup was deleted, False if not found """ lineup = await get_lineup_by_name(session, name) if lineup: await session.delete(lineup) return True return False