#!/usr/bin/env python3 """ Combat Data Migration Script. This script migrates existing inline combat encounter data from game_sessions to the dedicated combat_encounters table. The migration is idempotent - it's safe to run multiple times. Sessions that have already been migrated (have active_combat_encounter_id) are skipped. Usage: python scripts/migrate_combat_data.py Note: - Run this after deploying the new combat database schema - The application handles automatic migration on-demand, so this is optional - This script is useful for proactively migrating all data at once """ import sys import os import json from pathlib import Path # Add project root to path project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) from dotenv import load_dotenv # Load environment variables before importing app modules load_dotenv() from app.services.database_service import get_database_service from app.services.combat_repository import get_combat_repository from app.models.session import GameSession from app.models.combat import CombatEncounter from app.utils.logging import get_logger logger = get_logger(__file__) def migrate_inline_combat_encounters() -> dict: """ Migrate all inline combat encounters to the dedicated table. Scans all game sessions for inline combat_encounter data and migrates them to the combat_encounters table. Updates sessions to use the new active_combat_encounter_id reference. Returns: Dict with migration statistics: - total_sessions: Number of sessions scanned - migrated: Number of sessions with combat data migrated - skipped: Number of sessions already migrated or without combat - errors: Number of sessions that failed to migrate """ db = get_database_service() repo = get_combat_repository() stats = { 'total_sessions': 0, 'migrated': 0, 'skipped': 0, 'errors': 0, 'error_details': [] } print("Scanning game_sessions for inline combat data...") # Query all sessions (paginated) offset = 0 limit = 100 while True: try: rows = db.list_rows( table_id='game_sessions', limit=limit, offset=offset ) except Exception as e: logger.error("Failed to query sessions", error=str(e)) print(f"Error querying sessions: {e}") break if not rows: break for row in rows: stats['total_sessions'] += 1 session_id = row.id try: # Parse session data session_json = row.data.get('sessionData', '{}') session_data = json.loads(session_json) # Check if already migrated (has reference, no inline data) if (session_data.get('active_combat_encounter_id') and not session_data.get('combat_encounter')): stats['skipped'] += 1 continue # Check if has inline combat data to migrate combat_data = session_data.get('combat_encounter') if not combat_data: stats['skipped'] += 1 continue # Parse combat encounter encounter = CombatEncounter.from_dict(combat_data) user_id = session_data.get('user_id', row.data.get('userId', '')) logger.info("Migrating inline combat encounter", session_id=session_id, encounter_id=encounter.encounter_id) # Check if encounter already exists in repository existing = repo.get_encounter(encounter.encounter_id) if existing: # Already migrated, just update session reference session_data['active_combat_encounter_id'] = encounter.encounter_id session_data['combat_encounter'] = None else: # Save to repository repo.create_encounter( encounter=encounter, session_id=session_id, user_id=user_id ) session_data['active_combat_encounter_id'] = encounter.encounter_id session_data['combat_encounter'] = None # Update session db.update_row( table_id='game_sessions', row_id=session_id, data={'sessionData': json.dumps(session_data)} ) stats['migrated'] += 1 print(f" Migrated: {session_id} -> {encounter.encounter_id}") except Exception as e: stats['errors'] += 1 error_msg = f"Session {session_id}: {str(e)}" stats['error_details'].append(error_msg) logger.error("Failed to migrate session", session_id=session_id, error=str(e)) print(f" Error: {session_id} - {e}") offset += limit # Safety check to prevent infinite loop if offset > 10000: print("Warning: Stopped after 10000 sessions (safety limit)") break return stats def main(): """Run the migration.""" print("=" * 60) print("Code of Conquest - Combat Data Migration") print("=" * 60) print() # Verify environment variables required_vars = [ 'APPWRITE_ENDPOINT', 'APPWRITE_PROJECT_ID', 'APPWRITE_API_KEY', 'APPWRITE_DATABASE_ID' ] missing_vars = [var for var in required_vars if not os.getenv(var)] if missing_vars: print("ERROR: Missing required environment variables:") for var in missing_vars: print(f" - {var}") print() print("Please ensure your .env file is configured correctly.") sys.exit(1) print("Environment configuration:") print(f" Endpoint: {os.getenv('APPWRITE_ENDPOINT')}") print(f" Project: {os.getenv('APPWRITE_PROJECT_ID')}") print(f" Database: {os.getenv('APPWRITE_DATABASE_ID')}") print() # Confirm before proceeding print("This script will migrate inline combat data to the dedicated") print("combat_encounters table. This operation is safe and idempotent.") print() response = input("Proceed with migration? (y/N): ").strip().lower() if response != 'y': print("Migration cancelled.") sys.exit(0) print() print("Starting migration...") print() try: stats = migrate_inline_combat_encounters() print() print("=" * 60) print("Migration Results") print("=" * 60) print() print(f"Total sessions scanned: {stats['total_sessions']}") print(f"Successfully migrated: {stats['migrated']}") print(f"Skipped (no combat): {stats['skipped']}") print(f"Errors: {stats['errors']}") print() if stats['error_details']: print("Error details:") for error in stats['error_details'][:10]: # Show first 10 print(f" - {error}") if len(stats['error_details']) > 10: print(f" ... and {len(stats['error_details']) - 10} more") print() if stats['errors'] > 0: print("Some sessions failed to migrate. Check logs for details.") sys.exit(1) else: print("Migration completed successfully!") except Exception as e: logger.error("Migration failed", error=str(e)) print() print(f"MIGRATION FAILED: {str(e)}") print() print("Check logs for details.") sys.exit(1) if __name__ == '__main__': main()