1313from redis .exceptions import TimeoutError as RedisTimeoutError
1414
1515from ..migrations .data_migrator import DataMigrationError , DataMigrator
16+ from ..migrations .datetime_migration import ConversionFailureMode
1617
1718
1819def run_async (coro ):
@@ -68,8 +69,10 @@ def migrate_data():
6869 help = "Directory containing migration files (default: <root>/data-migrations)" ,
6970)
7071@click .option ("--module" , help = "Python module containing migrations" )
72+ @click .option ("--detailed" , is_flag = True , help = "Show detailed migration information" )
73+ @click .option ("--verbose" , "-v" , is_flag = True , help = "Enable verbose output" )
7174@handle_redis_errors
72- def status (migrations_dir : str , module : str ):
75+ def status (migrations_dir : str , module : str , detailed : bool , verbose : bool ):
7376 """Show current migration status."""
7477 # Default directory to <root>/data-migrations when not provided
7578 from ...settings import get_root_migrations_dir
@@ -90,14 +93,56 @@ def status(migrations_dir: str, module: str):
9093 click .echo (f" Pending: { status_info ['pending_count' ]} " )
9194
9295 if status_info ["pending_migrations" ]:
93- click .echo ("\n Pending migrations:" )
96+ click .echo ("\n ⚠️ Pending migrations:" )
9497 for migration_id in status_info ["pending_migrations" ]:
95- click .echo (f"- { migration_id } " )
98+ click .echo (f" - { migration_id } " )
9699
97100 if status_info ["applied_migrations" ]:
98- click .echo ("\n Applied migrations:" )
101+ click .echo ("\n ✅ Applied migrations:" )
99102 for migration_id in status_info ["applied_migrations" ]:
100- click .echo (f"- { migration_id } " )
103+ click .echo (f" ✓ { migration_id } " )
104+
105+ # Show detailed information if requested
106+ if detailed :
107+ click .echo ("\n Detailed Migration Information:" )
108+
109+ # Get all discovered migrations for detailed info
110+ all_migrations = run_async (migrator .discover_migrations ())
111+
112+ for migration_id , migration in all_migrations .items ():
113+ is_applied = migration_id in status_info ["applied_migrations" ]
114+ status_icon = "✓" if is_applied else "○"
115+ status_text = "Applied" if is_applied else "Pending"
116+
117+ click .echo (f"\n { status_icon } { migration_id } ({ status_text } )" )
118+ click .echo (f" Description: { migration .description } " )
119+
120+ if hasattr (migration , "dependencies" ) and migration .dependencies :
121+ click .echo (f" Dependencies: { ', ' .join (migration .dependencies )} " )
122+ else :
123+ click .echo (" Dependencies: None" )
124+
125+ # Check if migration can run
126+ try :
127+ can_run = run_async (migration .can_run ())
128+ can_run_text = "Yes" if can_run else "No"
129+ click .echo (f" Can run: { can_run_text } " )
130+ except Exception as e :
131+ click .echo (f" Can run: Error checking ({ e } )" )
132+
133+ # Show rollback support
134+ try :
135+ # Try to call down() in dry-run mode to see if it's supported
136+ supports_rollback = hasattr (migration , "down" ) and callable (
137+ migration .down
138+ )
139+ rollback_text = "Yes" if supports_rollback else "No"
140+ click .echo (f" Supports rollback: { rollback_text } " )
141+ except Exception :
142+ click .echo (" Supports rollback: Unknown" )
143+
144+ if verbose :
145+ click .echo (f"\n Raw status data: { status_info } " )
101146
102147
103148@migrate_data .command ()
@@ -112,6 +157,19 @@ def status(migrations_dir: str, module: str):
112157@click .option ("--verbose" , "-v" , is_flag = True , help = "Enable verbose output" )
113158@click .option ("--limit" , type = int , help = "Limit number of migrations to run" )
114159@click .option ("--yes" , "-y" , is_flag = True , help = "Skip confirmation prompt" )
160+ @click .option (
161+ "--failure-mode" ,
162+ type = click .Choice (["skip" , "fail" , "default" , "log_and_skip" ]),
163+ default = "log_and_skip" ,
164+ help = "How to handle conversion failures (default: log_and_skip)" ,
165+ )
166+ @click .option (
167+ "--batch-size" ,
168+ type = int ,
169+ default = 1000 ,
170+ help = "Batch size for processing (default: 1000)" ,
171+ )
172+ @click .option ("--max-errors" , type = int , help = "Maximum errors before stopping migration" )
115173@handle_redis_errors
116174def run (
117175 migrations_dir : str ,
@@ -120,6 +178,9 @@ def run(
120178 verbose : bool ,
121179 limit : int ,
122180 yes : bool ,
181+ failure_mode : str ,
182+ batch_size : int ,
183+ max_errors : int ,
123184):
124185 """Run pending migrations."""
125186 import os
@@ -279,5 +340,240 @@ def rollback(
279340 click .echo (f"Migration '{ migration_id } ' does not support rollback." , err = True )
280341
281342
343+ @migrate_data .command ()
344+ @click .option (
345+ "--migrations-dir" ,
346+ help = "Directory containing migration files (default: <root>/data-migrations)" ,
347+ )
348+ @click .option ("--module" , help = "Python module containing migrations" )
349+ @click .option ("--verbose" , "-v" , is_flag = True , help = "Enable verbose output" )
350+ @click .option ("--check-data" , is_flag = True , help = "Perform data integrity checks" )
351+ @handle_redis_errors
352+ def verify (migrations_dir : str , module : str , verbose : bool , check_data : bool ):
353+ """Verify migration status and optionally check data integrity."""
354+ import os
355+
356+ from ...settings import get_root_migrations_dir
357+
358+ resolved_dir = migrations_dir or os .path .join (
359+ get_root_migrations_dir (), "data-migrations"
360+ )
361+ migrator = DataMigrator (
362+ migrations_dir = resolved_dir if not module else None ,
363+ migration_module = module ,
364+ )
365+
366+ # Get migration status
367+ status_info = run_async (migrator .status ())
368+
369+ click .echo ("Migration Verification Report:" )
370+ click .echo (f" Total migrations: { status_info ['total_migrations' ]} " )
371+ click .echo (f" Applied: { status_info ['applied_count' ]} " )
372+ click .echo (f" Pending: { status_info ['pending_count' ]} " )
373+
374+ if status_info ["pending_migrations" ]:
375+ click .echo ("\n ⚠️ Pending migrations found:" )
376+ for migration_id in status_info ["pending_migrations" ]:
377+ click .echo (f" - { migration_id } " )
378+ click .echo ("\n Run 'om migrate-data run' to apply pending migrations." )
379+ else :
380+ click .echo ("\n ✅ All migrations are applied." )
381+
382+ if status_info ["applied_migrations" ]:
383+ click .echo ("\n Applied migrations:" )
384+ for migration_id in status_info ["applied_migrations" ]:
385+ click .echo (f" ✓ { migration_id } " )
386+
387+ # Perform data integrity checks if requested
388+ if check_data :
389+ click .echo ("\n Performing data integrity checks..." )
390+ verification_result = run_async (migrator .verify_data_integrity (verbose = verbose ))
391+
392+ if verification_result ["success" ]:
393+ click .echo ("✅ Data integrity checks passed." )
394+ else :
395+ click .echo ("❌ Data integrity issues found:" )
396+ for issue in verification_result .get ("issues" , []):
397+ click .echo (f" - { issue } " )
398+
399+ if verbose :
400+ click .echo (f"\n Detailed status: { status_info } " )
401+
402+
403+ @migrate_data .command ()
404+ @click .option (
405+ "--migrations-dir" ,
406+ help = "Directory containing migration files (default: <root>/data-migrations)" ,
407+ )
408+ @click .option ("--module" , help = "Python module containing migrations" )
409+ @click .option ("--verbose" , "-v" , is_flag = True , help = "Enable verbose output" )
410+ @handle_redis_errors
411+ def stats (migrations_dir : str , module : str , verbose : bool ):
412+ """Show migration statistics and data analysis."""
413+ import os
414+
415+ from ...settings import get_root_migrations_dir
416+
417+ resolved_dir = migrations_dir or os .path .join (
418+ get_root_migrations_dir (), "data-migrations"
419+ )
420+ migrator = DataMigrator (
421+ migrations_dir = resolved_dir if not module else None ,
422+ migration_module = module ,
423+ )
424+
425+ click .echo ("Analyzing migration requirements..." )
426+ stats_info = run_async (migrator .get_migration_statistics ())
427+
428+ if "error" in stats_info :
429+ click .echo (f"❌ Error: { stats_info ['error' ]} " )
430+ return
431+
432+ click .echo ("\n Migration Statistics:" )
433+ click .echo (f" Total models in registry: { stats_info ['total_models' ]} " )
434+ click .echo (
435+ f" Models with datetime fields: { stats_info ['models_with_datetime_fields' ]} "
436+ )
437+ click .echo (f" Total datetime fields: { stats_info ['total_datetime_fields' ]} " )
438+ click .echo (
439+ f" Estimated keys to migrate: { stats_info ['estimated_keys_to_migrate' ]} "
440+ )
441+
442+ if stats_info ["model_details" ]:
443+ click .echo ("\n Model Details:" )
444+ for model_detail in stats_info ["model_details" ]:
445+ click .echo (
446+ f"\n 📊 { model_detail ['model_name' ]} ({ model_detail ['model_type' ]} )"
447+ )
448+ click .echo (
449+ f" Datetime fields: { ', ' .join (model_detail ['datetime_fields' ])} "
450+ )
451+ click .echo (f" Keys to migrate: { model_detail ['key_count' ]} " )
452+
453+ if model_detail ["key_count" ] > 10000 :
454+ click .echo (" ⚠️ Large dataset - consider batch processing" )
455+ elif model_detail ["key_count" ] > 1000 :
456+ click .echo (" ℹ️ Medium dataset - monitor progress" )
457+
458+ # Estimate migration time
459+ total_keys = stats_info ["estimated_keys_to_migrate" ]
460+ if total_keys > 0 :
461+ # Rough estimates based on typical performance
462+ estimated_seconds = total_keys / 1000 # Assume ~1000 keys/second
463+ if estimated_seconds < 60 :
464+ time_estimate = f"{ estimated_seconds :.1f} seconds"
465+ elif estimated_seconds < 3600 :
466+ time_estimate = f"{ estimated_seconds / 60 :.1f} minutes"
467+ else :
468+ time_estimate = f"{ estimated_seconds / 3600 :.1f} hours"
469+
470+ click .echo (f"\n Estimated migration time: { time_estimate } " )
471+ click .echo (
472+ "(Actual time may vary based on data complexity and system performance)"
473+ )
474+
475+ if verbose :
476+ click .echo (f"\n Raw statistics: { stats_info } " )
477+
478+
479+ @migrate_data .command ()
480+ @click .option (
481+ "--migrations-dir" ,
482+ help = "Directory containing migration files (default: <root>/data-migrations)" ,
483+ )
484+ @click .option ("--module" , help = "Python module containing migrations" )
485+ @click .option ("--verbose" , "-v" , is_flag = True , help = "Enable verbose output" )
486+ @handle_redis_errors
487+ def progress (migrations_dir : str , module : str , verbose : bool ):
488+ """Show progress of any running or interrupted migrations."""
489+ import os
490+
491+ from ...settings import get_root_migrations_dir
492+ from ..migrations .datetime_migration import MigrationState
493+
494+ resolved_dir = migrations_dir or os .path .join (
495+ get_root_migrations_dir (), "data-migrations"
496+ )
497+ migrator = DataMigrator (
498+ migrations_dir = resolved_dir if not module else None ,
499+ migration_module = module ,
500+ )
501+
502+ # Check for saved progress
503+ click .echo ("Checking for migration progress..." )
504+
505+ # Check the built-in datetime migration
506+ datetime_migration_id = "001_datetime_fields_to_timestamps"
507+ state = MigrationState (migrator .redis , datetime_migration_id )
508+
509+ has_progress = run_async (state .has_saved_progress ())
510+
511+ if has_progress :
512+ progress_data = run_async (state .load_progress ())
513+
514+ click .echo (f"\n 📊 Found saved progress for migration: { datetime_migration_id } " )
515+ click .echo (f" Timestamp: { progress_data .get ('timestamp' , 'Unknown' )} " )
516+ click .echo (f" Current model: { progress_data .get ('current_model' , 'Unknown' )} " )
517+ click .echo (f" Processed keys: { len (progress_data .get ('processed_keys' , []))} " )
518+ click .echo (f" Total keys: { progress_data .get ('total_keys' , 'Unknown' )} " )
519+
520+ if progress_data .get ("stats" ):
521+ stats = progress_data ["stats" ]
522+ click .echo (f" Converted fields: { stats .get ('converted_fields' , 0 )} " )
523+ click .echo (f" Failed conversions: { stats .get ('failed_conversions' , 0 )} " )
524+ click .echo (f" Success rate: { stats .get ('success_rate' , 0 ):.1f} %" )
525+
526+ click .echo ("\n To resume the migration, run: om migrate-data run" )
527+ click .echo ("To clear saved progress, run: om migrate-data clear-progress" )
528+
529+ else :
530+ click .echo ("✅ No saved migration progress found." )
531+
532+ if verbose :
533+ click .echo (f"\n Checked migration: { datetime_migration_id } " )
534+
535+
536+ @migrate_data .command ()
537+ @click .option (
538+ "--migrations-dir" ,
539+ help = "Directory containing migration files (default: <root>/data-migrations)" ,
540+ )
541+ @click .option ("--module" , help = "Python module containing migrations" )
542+ @click .option ("--yes" , "-y" , is_flag = True , help = "Skip confirmation prompt" )
543+ @handle_redis_errors
544+ def clear_progress (migrations_dir : str , module : str , yes : bool ):
545+ """Clear saved migration progress."""
546+ import os
547+
548+ from ...settings import get_root_migrations_dir
549+ from ..migrations .datetime_migration import MigrationState
550+
551+ resolved_dir = migrations_dir or os .path .join (
552+ get_root_migrations_dir (), "data-migrations"
553+ )
554+ migrator = DataMigrator (
555+ migrations_dir = resolved_dir if not module else None ,
556+ migration_module = module ,
557+ )
558+
559+ # Clear progress for datetime migration
560+ datetime_migration_id = "001_datetime_fields_to_timestamps"
561+ state = MigrationState (migrator .redis , datetime_migration_id )
562+
563+ has_progress = run_async (state .has_saved_progress ())
564+
565+ if not has_progress :
566+ click .echo ("No saved migration progress found." )
567+ return
568+
569+ if not yes :
570+ if not click .confirm ("Clear saved migration progress? This cannot be undone." ):
571+ click .echo ("Aborted." )
572+ return
573+
574+ run_async (state .clear_progress ())
575+ click .echo ("✅ Saved migration progress cleared." )
576+
577+
282578if __name__ == "__main__" :
283579 migrate_data ()
0 commit comments