#!/usr/bin/env python3 """ Ground Zero Database Purge Script Deletes ALL test users (e.g., tester_pro@profibot.hu, noreply@...), and cascade deletes all their Organizations, Branches, and Assets. Outputs the exact number of deleted rows for each table. """ import asyncio import sys from sqlalchemy import text, delete, select from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker # Import settings sys.path.insert(0, '/app') from app.core.config import settings async def purge_test_data(): """Main purge function""" # Create async engine engine = create_async_engine( str(settings.SQLALCHEMY_DATABASE_URI), echo=False, pool_size=5, max_overflow=10, ) async_session = sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) deleted_counts = {} async with async_session() as session: try: # Start transaction await session.begin() print("šŸ” Starting Ground Zero database purge...") print("=" * 60) # 1. First, identify test users to delete test_email_patterns = [ '%test%', '%noreply%', '%profibot%', 'tester_pro@profibot.hu', 'test@example.com', '%example.com' ] # Build WHERE clause for test users where_conditions = [] for pattern in test_email_patterns: where_conditions.append(f"email ILIKE '{pattern}'") where_clause = " OR ".join(where_conditions) # 2. Get user IDs to delete for cascade operations query = text(f""" SELECT id, email FROM identity.users WHERE {where_clause} AND is_deleted = false """) result = await session.execute(query) test_users = result.fetchall() if not test_users: print("āœ… No test users found to delete.") await session.commit() return deleted_counts user_ids = [str(user[0]) for user in test_users] user_emails = [user[1] for user in test_users] print(f"šŸ“‹ Found {len(test_users)} test users to delete:") for email in user_emails[:10]: # Show first 10 print(f" - {email}") if len(test_users) > 10: print(f" ... and {len(test_users) - 10} more") user_ids_str = ",".join(user_ids) # 3. Delete in reverse cascade order to respect foreign keys # First, get person IDs for these users person_query = text(f""" SELECT DISTINCT person_id FROM identity.users WHERE id IN ({user_ids_str}) AND person_id IS NOT NULL """) person_result = await session.execute(person_query) person_ids = [str(row[0]) for row in person_result.fetchall()] person_ids_str = ",".join(person_ids) if person_ids else "NULL" # Delete from tables that reference users/persons # a) Delete from gamification tables gamification_tables = [ ("gamification.user_stats", "user_id"), ("gamification.user_badges", "user_id"), ("gamification.points_ledger", "user_id"), ("gamification.user_contributions", "user_id"), ] for table, column in gamification_tables: delete_query = text(f""" DELETE FROM {table} WHERE {column} IN ({user_ids_str}) RETURNING * """) result = await session.execute(delete_query) deleted = result.rowcount deleted_counts[table] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} rows from {table}") # b) Delete from social tables - with error handling for missing tables social_tables = [ ("identity.social_accounts", "user_id"), ("marketplace.service_requests", "user_id"), ] # Try to delete from service_reviews if it exists social_tables_to_try = social_tables + [("identity.service_reviews", "user_id")] for table, column in social_tables_to_try: try: delete_query = text(f""" DELETE FROM {table} WHERE {column} IN ({user_ids_str}) RETURNING * """) result = await session.execute(delete_query) deleted = result.rowcount deleted_counts[table] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} rows from {table}") except Exception as e: if "does not exist" in str(e) or "UndefinedTableError" in str(e): print(f" āš ļø Table {table} doesn't exist, skipping...") continue else: raise # c) Delete from organization-related tables # First get organization IDs owned by these users org_query = text(f""" SELECT id FROM marketplace.organizations WHERE owner_id IN ({user_ids_str}) """) org_result = await session.execute(org_query) org_ids = [str(row[0]) for row in org_result.fetchall()] org_ids_str = ",".join(org_ids) if org_ids else "NULL" if org_ids: # Delete branches for these organizations branch_query = text(f""" DELETE FROM marketplace.branches WHERE organization_id IN ({org_ids_str}) RETURNING * """) result = await session.execute(branch_query) deleted = result.rowcount deleted_counts["marketplace.branches"] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} branches") # Delete organization members org_member_query = text(f""" DELETE FROM marketplace.organization_members WHERE organization_id IN ({org_ids_str}) RETURNING * """) result = await session.execute(org_member_query) deleted = result.rowcount deleted_counts["marketplace.organization_members"] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} organization members") # Delete organizations org_delete_query = text(f""" DELETE FROM marketplace.organizations WHERE id IN ({org_ids_str}) RETURNING * """) result = await session.execute(org_delete_query) deleted = result.rowcount deleted_counts["marketplace.organizations"] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} organizations") # d) Delete from asset-related tables # Get asset IDs owned by these users asset_query = text(f""" SELECT id FROM data.assets WHERE owner_id IN ({user_ids_str}) """) asset_result = await session.execute(asset_query) asset_ids = [str(row[0]) for row in asset_result.fetchall()] asset_ids_str = ",".join(asset_ids) if asset_ids else "NULL" if asset_ids: # Delete asset costs asset_cost_query = text(f""" DELETE FROM data.asset_costs WHERE asset_id IN ({asset_ids_str}) RETURNING * """) result = await session.execute(asset_cost_query) deleted = result.rowcount deleted_counts["data.asset_costs"] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} asset costs") # Delete asset events asset_event_query = text(f""" DELETE FROM data.asset_events WHERE asset_id IN ({asset_ids_str}) RETURNING * """) result = await session.execute(asset_event_query) deleted = result.rowcount deleted_counts["data.asset_events"] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} asset events") # Delete assets asset_delete_query = text(f""" DELETE FROM data.assets WHERE id IN ({asset_ids_str}) RETURNING * """) result = await session.execute(asset_delete_query) deleted = result.rowcount deleted_counts["data.assets"] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} assets") # e) Delete wallets wallet_query = text(f""" DELETE FROM identity.wallets WHERE user_id IN ({user_ids_str}) RETURNING * """) result = await session.execute(wallet_query) deleted = result.rowcount deleted_counts["identity.wallets"] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} wallets") # f) Finally delete users user_delete_query = text(f""" DELETE FROM identity.users WHERE id IN ({user_ids_str}) RETURNING * """) result = await session.execute(user_delete_query) deleted = result.rowcount deleted_counts["identity.users"] = deleted print(f" šŸ—‘ļø Deleted {deleted} users") # g) Delete persons (if they have no other users) if person_ids: person_delete_query = text(f""" DELETE FROM identity.persons WHERE id IN ({person_ids_str}) AND NOT EXISTS ( SELECT 1 FROM identity.users WHERE person_id = persons.id ) RETURNING * """) result = await session.execute(person_delete_query) deleted = result.rowcount deleted_counts["identity.persons"] = deleted if deleted > 0: print(f" šŸ—‘ļø Deleted {deleted} persons") # Commit transaction await session.commit() print("=" * 60) print("āœ… Ground Zero purge completed successfully!") print("\nšŸ“Š Deletion Summary:") for table, count in deleted_counts.items(): print(f" {table}: {count} rows") total_deleted = sum(deleted_counts.values()) print(f"\nšŸ“ˆ Total rows deleted: {total_deleted}") return deleted_counts except Exception as e: await session.rollback() print(f"āŒ Error during purge: {e}") raise if __name__ == "__main__": asyncio.run(purge_test_data())