diff --git a/backend/clean_test_data.py b/backend/clean_test_data.py new file mode 100644 index 0000000..77cf9a1 --- /dev/null +++ b/backend/clean_test_data.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +""" +Database Cleanup Script + +Clear out test data from database tables. +Use with caution - this will DELETE data! + +Usage: + python clean_test_data.py # Interactive mode + python clean_test_data.py --all # Delete all data + python clean_test_data.py --game # Delete specific game +""" + +import asyncio +import logging +import sys +from uuid import UUID +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database.session import AsyncSessionLocal +from app.models.db_models import ( + Game, + Play, + Lineup, + GameSession, + RosterLink, + GameCardsetLink +) + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class DatabaseCleaner: + """Clean test data from database.""" + + async def count_records(self, session: AsyncSession) -> dict: + """Count records in each table.""" + counts = {} + + # Count games + result = await session.execute(select(Game)) + counts['games'] = len(list(result.scalars().all())) + + # Count plays + result = await session.execute(select(Play)) + counts['plays'] = len(list(result.scalars().all())) + + # Count lineups + result = await session.execute(select(Lineup)) + counts['lineups'] = len(list(result.scalars().all())) + + # Count roster links + result = await session.execute(select(RosterLink)) + counts['roster_links'] = len(list(result.scalars().all())) + + # Count game sessions + result = await session.execute(select(GameSession)) + counts['game_sessions'] = len(list(result.scalars().all())) + + # Count cardset links + result = await session.execute(select(GameCardsetLink)) + counts['game_cardset_links'] = len(list(result.scalars().all())) + + return counts + + async def delete_game(self, game_id: UUID) -> None: + """ + Delete a specific game and all related data. + + Cascade deletes will automatically remove: + - Plays + - Lineups + - RosterLinks + - GameSession + - GameCardsetLinks + """ + async with AsyncSessionLocal() as session: + try: + # Check if game exists + result = await session.execute( + select(Game).where(Game.id == game_id) + ) + game = result.scalar_one_or_none() + + if not game: + logger.warning(f"Game {game_id} not found") + return + + logger.info(f"Deleting game {game_id} ({game.league_id})...") + + # Delete game (cascade will handle related records) + await session.delete(game) + await session.commit() + + logger.info(f"āœ… Successfully deleted game {game_id}") + + except Exception as e: + await session.rollback() + logger.error(f"āŒ Error deleting game: {e}", exc_info=True) + raise + + async def delete_all(self, confirm: bool = False) -> None: + """ + Delete ALL data from all tables. + + WARNING: This is destructive and cannot be undone! + """ + if not confirm: + logger.warning("āš ļø delete_all() requires confirm=True parameter") + return + + async with AsyncSessionLocal() as session: + try: + logger.info("šŸ—‘ļø Deleting ALL data from database...") + + # Get counts before deletion + before_counts = await self.count_records(session) + logger.info(f"Current record counts: {before_counts}") + + # Delete in correct order (children first) + logger.info("Deleting plays...") + await session.execute(delete(Play)) + + logger.info("Deleting lineups...") + await session.execute(delete(Lineup)) + + logger.info("Deleting roster links...") + await session.execute(delete(RosterLink)) + + logger.info("Deleting game sessions...") + await session.execute(delete(GameSession)) + + logger.info("Deleting game cardset links...") + await session.execute(delete(GameCardsetLink)) + + logger.info("Deleting games...") + await session.execute(delete(Game)) + + await session.commit() + + # Verify deletion + after_counts = await self.count_records(session) + logger.info(f"After deletion: {after_counts}") + + logger.info("āœ… Successfully deleted all data") + + except Exception as e: + await session.rollback() + logger.error(f"āŒ Error deleting data: {e}", exc_info=True) + raise + + async def list_games(self) -> None: + """List all games in the database.""" + async with AsyncSessionLocal() as session: + result = await session.execute( + select(Game).order_by(Game.created_at.desc()) + ) + games = result.scalars().all() + + if not games: + logger.info("No games found in database") + return + + logger.info(f"\n{'='*80}") + logger.info(f"Found {len(games)} game(s) in database:") + logger.info(f"{'='*80}") + + for game in games: + logger.info(f"\nGame ID: {game.id}") + logger.info(f" League: {game.league_id}") + logger.info(f" Status: {game.status}") + logger.info(f" Mode: {game.game_mode}") + logger.info(f" Teams: {game.away_team_id} @ {game.home_team_id}") + logger.info(f" Score: {game.away_score}-{game.home_score}") + if game.current_inning: + logger.info(f" State: {game.current_half} {game.current_inning}") + logger.info(f" AI: Home={game.home_team_is_ai}, Away={game.away_team_is_ai}") + logger.info(f" Created: {game.created_at}") + + async def show_stats(self) -> None: + """Show database statistics.""" + async with AsyncSessionLocal() as session: + counts = await self.count_records(session) + + logger.info(f"\n{'='*80}") + logger.info("DATABASE STATISTICS") + logger.info(f"{'='*80}") + logger.info(f"Games: {counts['games']:>6}") + logger.info(f"Plays: {counts['plays']:>6}") + logger.info(f"Lineups: {counts['lineups']:>6}") + logger.info(f"Roster Links: {counts['roster_links']:>6}") + logger.info(f"Game Sessions: {counts['game_sessions']:>6}") + logger.info(f"Cardset Links: {counts['game_cardset_links']:>6}") + logger.info(f"{'='*80}\n") + + +async def interactive_menu(): + """Interactive menu for cleanup operations.""" + cleaner = DatabaseCleaner() + + while True: + print("\n" + "="*60) + print("DATABASE CLEANUP MENU") + print("="*60) + print("1. Show database statistics") + print("2. List all games") + print("3. Delete specific game") + print("4. Delete ALL data (āš ļø DANGER)") + print("5. Exit") + print("="*60) + + choice = input("\nEnter your choice (1-5): ").strip() + + if choice == "1": + await cleaner.show_stats() + + elif choice == "2": + await cleaner.list_games() + + elif choice == "3": + game_id_str = input("Enter game UUID: ").strip() + try: + game_id = UUID(game_id_str) + confirm = input(f"Delete game {game_id}? (yes/no): ").strip().lower() + if confirm == "yes": + await cleaner.delete_game(game_id) + else: + logger.info("Cancelled") + except ValueError: + logger.error("Invalid UUID format") + + elif choice == "4": + await cleaner.show_stats() + print("\nāš ļø WARNING: This will DELETE ALL DATA from the database!") + confirm1 = input("Are you absolutely sure? (type 'DELETE ALL'): ").strip() + if confirm1 == "DELETE ALL": + confirm2 = input("Type 'yes' to confirm: ").strip().lower() + if confirm2 == "yes": + await cleaner.delete_all(confirm=True) + else: + logger.info("Cancelled") + else: + logger.info("Cancelled") + + elif choice == "5": + logger.info("Exiting...") + break + + else: + logger.warning("Invalid choice") + + +async def main(): + """Main entry point.""" + cleaner = DatabaseCleaner() + + # Parse command line arguments + if len(sys.argv) == 1: + # No arguments - run interactive mode + await interactive_menu() + + elif "--help" in sys.argv or "-h" in sys.argv: + print(__doc__) + + elif "--stats" in sys.argv: + await cleaner.show_stats() + + elif "--list" in sys.argv: + await cleaner.list_games() + + elif "--all" in sys.argv: + await cleaner.show_stats() + print("\nāš ļø WARNING: This will DELETE ALL DATA!") + confirm = input("Type 'yes' to confirm: ").strip().lower() + if confirm == "yes": + await cleaner.delete_all(confirm=True) + else: + logger.info("Cancelled") + + elif "--game" in sys.argv: + idx = sys.argv.index("--game") + if idx + 1 < len(sys.argv): + try: + game_id = UUID(sys.argv[idx + 1]) + await cleaner.delete_game(game_id) + except ValueError: + logger.error("Invalid UUID format") + else: + logger.error("--game requires a UUID argument") + + else: + print("Unknown arguments. Use --help for usage information.") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("\nInterrupted by user") + except Exception as e: + logger.error(f"Error: {e}", exc_info=True) + sys.exit(1) diff --git a/backend/test_db_playground.py b/backend/test_db_playground.py new file mode 100644 index 0000000..50a18a1 --- /dev/null +++ b/backend/test_db_playground.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +""" +Interactive Database Playground + +Test and experiment with database models and operations. +Run this script to create test games and explore the database operations. + +Usage: + python test_db_playground.py +""" + +import asyncio +import logging +from uuid import uuid4, UUID +from typing import Optional + +from app.database.operations import DatabaseOperations +from app.models.roster_models import PdRosterLinkData, SbaRosterLinkData + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class DatabasePlayground: + """Interactive playground for testing database operations.""" + + def __init__(self): + self.db_ops = DatabaseOperations() + self.test_game_id: Optional[UUID] = None + + async def demo_pd_game(self): + """Create and populate a PD league game with roster and lineup.""" + logger.info("=" * 60) + logger.info("DEMO: Creating PD League Game") + logger.info("=" * 60) + + # Create game + self.test_game_id = uuid4() + game = await self.db_ops.create_game( + game_id=self.test_game_id, + league_id="pd", + home_team_id=1, + away_team_id=2, + game_mode="friendly", + visibility="public", + home_team_is_ai=False, + away_team_is_ai=True, + ai_difficulty="balanced" + ) + logger.info(f"āœ… Created PD game: {game.id}") + logger.info(f" Home Team: {game.home_team_id} (Human)") + logger.info(f" Away Team: {game.away_team_id} (AI - {game.ai_difficulty})") + + # Add cards to roster + logger.info("\nšŸ“‹ Adding cards to roster...") + home_cards = [101, 102, 103, 104, 105, 106, 107, 108, 109] + away_cards = [201, 202, 203, 204, 205, 206, 207, 208, 209] + + for card_id in home_cards: + roster_link = await self.db_ops.add_pd_roster_card( + game_id=self.test_game_id, + card_id=card_id, + team_id=1 + ) + logger.info(f" Added card {card_id} to home roster (link_id={roster_link.id})") + + for card_id in away_cards: + roster_link = await self.db_ops.add_pd_roster_card( + game_id=self.test_game_id, + card_id=card_id, + team_id=2 + ) + logger.info(f" Added card {card_id} to away roster (link_id={roster_link.id})") + + # Get roster + home_roster = await self.db_ops.get_pd_roster(self.test_game_id, team_id=1) + away_roster = await self.db_ops.get_pd_roster(self.test_game_id, team_id=2) + logger.info(f"\nāœ… Home roster: {len(home_roster)} cards") + logger.info(f"āœ… Away roster: {len(away_roster)} cards") + + # Set lineup + logger.info("\n⚾ Setting starting lineup...") + positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'] + + for i, (card_id, position) in enumerate(zip(home_cards, positions), start=1): + lineup = await self.db_ops.add_pd_lineup_card( + game_id=self.test_game_id, + team_id=1, + card_id=card_id, + position=position, + batting_order=i if position != 'P' else None, + is_starter=True + ) + logger.info(f" {position}: Card {card_id} (batting {lineup.batting_order or 'N/A'})") + + # Get active lineup + home_lineup = await self.db_ops.get_active_lineup(self.test_game_id, team_id=1) + logger.info(f"\nāœ… Active lineup: {len(home_lineup)} players") + + # Update game state + logger.info("\nšŸŽ® Simulating game progress...") + await self.db_ops.update_game_state( + game_id=self.test_game_id, + inning=3, + half="bottom", + home_score=2, + away_score=1, + status="active" + ) + logger.info(" Updated to: Bottom 3rd, Home 2 - Away 1") + + # Save a play + play_data = { + 'game_id': self.test_game_id, + 'play_number': 1, + 'inning': 3, + 'half': 'bottom', + 'outs_before': 1, + 'batting_order': 3, + 'away_score': 1, + 'home_score': 2, + 'batter_id': home_lineup[2].id, + 'pitcher_id': home_lineup[0].id, + 'catcher_id': home_lineup[1].id, + 'dice_roll': '14+6', + 'hit_type': 'FB', + 'result_description': 'Deep fly ball to center field - Home Run!', + 'outs_recorded': 0, + 'runs_scored': 1, + 'pa': 1, + 'ab': 1, + 'hit': 1, + 'homerun': 1, + 'rbi': 1, + 'is_go_ahead': False, + 'complete': True, + 'locked': False + } + + play = await self.db_ops.save_play(play_data) + logger.info(f"\n⚾ Saved play #{play.play_number}: {play.result_description}") + + # Load game state + logger.info("\nšŸ”„ Loading complete game state...") + game_state = await self.db_ops.load_game_state(self.test_game_id) + if game_state: + logger.info(f"āœ… Loaded game state:") + logger.info(f" Status: {game_state['game']['status']}") + logger.info(f" Score: {game_state['game']['away_score']}-{game_state['game']['home_score']}") + logger.info(f" Lineups: {len(game_state['lineups'])} players") + logger.info(f" Plays: {len(game_state['plays'])} plays recorded") + + return self.test_game_id + + async def demo_sba_game(self): + """Create and populate an SBA league game with roster and lineup.""" + logger.info("\n" + "=" * 60) + logger.info("DEMO: Creating SBA League Game") + logger.info("=" * 60) + + # Create game + game_id = uuid4() + game = await self.db_ops.create_game( + game_id=game_id, + league_id="sba", + home_team_id=10, + away_team_id=11, + game_mode="ranked", + visibility="public", + home_team_is_ai=False, + away_team_is_ai=False + ) + logger.info(f"āœ… Created SBA game: {game.id}") + logger.info(f" Home Team: {game.home_team_id}") + logger.info(f" Away Team: {game.away_team_id}") + logger.info(f" Mode: {game.game_mode}") + + # Add players to roster + logger.info("\nšŸ“‹ Adding players to roster...") + home_players = [501, 502, 503, 504, 505, 506, 507, 508, 509] + away_players = [601, 602, 603, 604, 605, 606, 607, 608, 609] + + for player_id in home_players: + roster_link = await self.db_ops.add_sba_roster_player( + game_id=game_id, + player_id=player_id, + team_id=10 + ) + logger.info(f" Added player {player_id} to home roster (link_id={roster_link.id})") + + for player_id in away_players: + roster_link = await self.db_ops.add_sba_roster_player( + game_id=game_id, + player_id=player_id, + team_id=11 + ) + logger.info(f" Added player {player_id} to away roster (link_id={roster_link.id})") + + # Get roster + home_roster = await self.db_ops.get_sba_roster(game_id, team_id=10) + away_roster = await self.db_ops.get_sba_roster(game_id, team_id=11) + logger.info(f"\nāœ… Home roster: {len(home_roster)} players") + logger.info(f"āœ… Away roster: {len(away_roster)} players") + + # Set lineup + logger.info("\n⚾ Setting starting lineup...") + positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'] + + for i, (player_id, position) in enumerate(zip(home_players, positions), start=1): + lineup = await self.db_ops.add_sba_lineup_player( + game_id=game_id, + team_id=10, + player_id=player_id, + position=position, + batting_order=i if position != 'P' else None, + is_starter=True + ) + logger.info(f" {position}: Player {player_id} (batting {lineup.batting_order or 'N/A'})") + + # Create game session + session = await self.db_ops.create_game_session(game_id) + logger.info(f"\nāœ… Created game session for game: {session.game_id}") + + # Update session snapshot + await self.db_ops.update_session_snapshot( + game_id=game_id, + state_snapshot={'test': 'data', 'connected_users': ['user1', 'user2']} + ) + logger.info("āœ… Updated session snapshot") + + return game_id + + async def demo_cleanup(self): + """Demonstrate querying capabilities.""" + if not self.test_game_id: + logger.warning("No test game created yet") + return + + logger.info("\n" + "=" * 60) + logger.info("DEMO: Query Capabilities") + logger.info("=" * 60) + + # Get game + game = await self.db_ops.get_game(self.test_game_id) + if game: + logger.info(f"\nšŸ“Š Game Details:") + logger.info(f" ID: {game.id}") + logger.info(f" League: {game.league_id}") + logger.info(f" Status: {game.status}") + logger.info(f" Inning: {game.current_half} {game.current_inning}") + logger.info(f" Score: {game.away_score}-{game.home_score}") + + # Get plays + plays = await self.db_ops.get_plays(self.test_game_id) + logger.info(f"\n⚾ Plays: {len(plays)} total") + for play in plays: + logger.info(f" Play #{play.play_number}: {play.result_description}") + + # Get active lineup + home_lineup = await self.db_ops.get_active_lineup(self.test_game_id, team_id=1) + logger.info(f"\nšŸ‘„ Active Lineup: {len(home_lineup)} players") + for player in sorted(home_lineup, key=lambda x: x.batting_order or 99): + logger.info(f" {player.position}: Card {player.card_id} (batting {player.batting_order or 'N/A'})") + + +async def main(): + """Run interactive playground demos.""" + playground = DatabasePlayground() + + try: + # Run PD game demo + pd_game_id = await playground.demo_pd_game() + + # Run SBA game demo + sba_game_id = await playground.demo_sba_game() + + # Show query capabilities + await playground.demo_cleanup() + + logger.info("\n" + "=" * 60) + logger.info("āœ… ALL DEMOS COMPLETED SUCCESSFULLY!") + logger.info("=" * 60) + logger.info(f"\nCreated games:") + logger.info(f" PD Game: {pd_game_id}") + logger.info(f" SBA Game: {sba_game_id}") + logger.info("\nYou can query these games in the database to explore the data!") + + except Exception as e: + logger.error(f"\nāŒ Error during demo: {e}", exc_info=True) + raise + + +if __name__ == "__main__": + asyncio.run(main())