#!/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: # type: ignore 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)