|
| 1 | +import asyncio |
| 2 | +import os |
| 3 | + |
1 | 4 | import click
|
2 | 5 |
|
3 |
| -from aredis_om.model.migrations.migrator import Migrator |
| 6 | +from aredis_om.model.migrations.schema_migrator import SchemaMigrator |
| 7 | +from aredis_om.settings import get_root_migrations_dir |
| 8 | + |
| 9 | + |
| 10 | +def run_async(coro): |
| 11 | + """Run an async coroutine in an isolated event loop to avoid interfering with pytest loops.""" |
| 12 | + import concurrent.futures |
| 13 | + |
| 14 | + with concurrent.futures.ThreadPoolExecutor() as executor: |
| 15 | + future = executor.submit(asyncio.run, coro) |
| 16 | + return future.result() |
| 17 | + |
| 18 | + |
| 19 | +@click.group() |
| 20 | +def migrate(): |
| 21 | + """Manage schema migrations for Redis OM models.""" |
| 22 | + pass |
| 23 | + |
| 24 | + |
| 25 | +@migrate.command() |
| 26 | +@click.option("--migrations-dir", help="Directory containing schema migration files") |
| 27 | +def status(migrations_dir: str | None): |
| 28 | + """Show current schema migration status from files.""" |
| 29 | + |
| 30 | + async def _status(): |
| 31 | + dir_path = migrations_dir or os.path.join( |
| 32 | + get_root_migrations_dir(), "schema-migrations" |
| 33 | + ) |
| 34 | + migrator = SchemaMigrator(migrations_dir=dir_path) |
| 35 | + status_info = await migrator.status() |
| 36 | + |
| 37 | + click.echo("Schema Migration Status:") |
| 38 | + click.echo(f" Total migrations: {status_info['total_migrations']}") |
| 39 | + click.echo(f" Applied: {status_info['applied_count']}") |
| 40 | + click.echo(f" Pending: {status_info['pending_count']}") |
| 41 | + |
| 42 | + if status_info["pending_migrations"]: |
| 43 | + click.echo("\nPending migrations:") |
| 44 | + for migration_id in status_info["pending_migrations"]: |
| 45 | + click.echo(f"- {migration_id}") |
| 46 | + |
| 47 | + if status_info["applied_migrations"]: |
| 48 | + click.echo("\nApplied migrations:") |
| 49 | + for migration_id in status_info["applied_migrations"]: |
| 50 | + click.echo(f"- {migration_id}") |
| 51 | + |
| 52 | + run_async(_status()) |
| 53 | + |
| 54 | + |
| 55 | +@migrate.command() |
| 56 | +@click.option("--migrations-dir", help="Directory containing schema migration files") |
| 57 | +@click.option( |
| 58 | + "--dry-run", is_flag=True, help="Show what would be done without applying changes" |
| 59 | +) |
| 60 | +@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") |
| 61 | +@click.option("--limit", type=int, help="Limit number of migrations to run") |
| 62 | +@click.option( |
| 63 | + "--yes", |
| 64 | + "-y", |
| 65 | + is_flag=True, |
| 66 | + help="Skip confirmation prompt to create directory or run", |
| 67 | +) |
| 68 | +def run( |
| 69 | + migrations_dir: str | None, |
| 70 | + dry_run: bool, |
| 71 | + verbose: bool, |
| 72 | + limit: int | None, |
| 73 | + yes: bool, |
| 74 | +): |
| 75 | + """Run pending schema migrations from files.""" |
| 76 | + |
| 77 | + async def _run(): |
| 78 | + dir_path = migrations_dir or os.path.join( |
| 79 | + get_root_migrations_dir(), "schema-migrations" |
| 80 | + ) |
| 81 | + |
| 82 | + if not os.path.exists(dir_path): |
| 83 | + if yes or click.confirm( |
| 84 | + f"Create schema migrations directory at '{dir_path}'?" |
| 85 | + ): |
| 86 | + os.makedirs(dir_path, exist_ok=True) |
| 87 | + else: |
| 88 | + click.echo("Aborted.") |
| 89 | + return |
| 90 | + |
| 91 | + migrator = SchemaMigrator(migrations_dir=dir_path) |
| 92 | + |
| 93 | + # Show list for confirmation |
| 94 | + if not dry_run and not yes: |
| 95 | + status_info = await migrator.status() |
| 96 | + if status_info["pending_migrations"]: |
| 97 | + listing = "\n".join( |
| 98 | + f"- {m}" |
| 99 | + for m in status_info["pending_migrations"][ |
| 100 | + : (limit or len(status_info["pending_migrations"])) |
| 101 | + ] |
| 102 | + ) |
| 103 | + if not click.confirm( |
| 104 | + f"Run {min(limit or len(status_info['pending_migrations']), len(status_info['pending_migrations']))} migration(s)?\n{listing}" |
| 105 | + ): |
| 106 | + click.echo("Aborted.") |
| 107 | + return |
| 108 | + |
| 109 | + count = await migrator.run(dry_run=dry_run, limit=limit, verbose=verbose) |
| 110 | + if verbose and not dry_run: |
| 111 | + click.echo(f"Successfully applied {count} migration(s).") |
| 112 | + |
| 113 | + run_async(_run()) |
| 114 | + |
| 115 | + |
| 116 | +@migrate.command() |
| 117 | +@click.argument("name") |
| 118 | +@click.option("--migrations-dir", help="Directory to create migration in") |
| 119 | +@click.option( |
| 120 | + "--yes", "-y", is_flag=True, help="Skip confirmation prompt to create directory" |
| 121 | +) |
| 122 | +def create(name: str, migrations_dir: str | None, yes: bool): |
| 123 | + """Create a new schema migration snapshot file from current pending operations.""" |
| 124 | + |
| 125 | + async def _create(): |
| 126 | + dir_path = migrations_dir or os.path.join( |
| 127 | + get_root_migrations_dir(), "schema-migrations" |
| 128 | + ) |
| 129 | + |
| 130 | + if not os.path.exists(dir_path): |
| 131 | + if yes or click.confirm( |
| 132 | + f"Create schema migrations directory at '{dir_path}'?" |
| 133 | + ): |
| 134 | + os.makedirs(dir_path, exist_ok=True) |
| 135 | + else: |
| 136 | + click.echo("Aborted.") |
| 137 | + return |
| 138 | + |
| 139 | + migrator = SchemaMigrator(migrations_dir=dir_path) |
| 140 | + filepath = await migrator.create_migration_file(name) |
| 141 | + if filepath: |
| 142 | + click.echo(f"Created migration: {filepath}") |
| 143 | + else: |
| 144 | + click.echo("No pending schema changes detected. Nothing to snapshot.") |
| 145 | + |
| 146 | + run_async(_create()) |
| 147 | + |
| 148 | + |
| 149 | +@migrate.command() |
| 150 | +@click.argument("migration_id") |
| 151 | +@click.option("--migrations-dir", help="Directory containing schema migration files") |
| 152 | +@click.option( |
| 153 | + "--dry-run", is_flag=True, help="Show what would be done without applying changes" |
| 154 | +) |
| 155 | +@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") |
| 156 | +@click.option( |
| 157 | + "--yes", |
| 158 | + "-y", |
| 159 | + is_flag=True, |
| 160 | + help="Skip confirmation prompt to create directory or run", |
| 161 | +) |
| 162 | +def rollback( |
| 163 | + migration_id: str, |
| 164 | + migrations_dir: str | None, |
| 165 | + dry_run: bool, |
| 166 | + verbose: bool, |
| 167 | + yes: bool, |
| 168 | +): |
| 169 | + """Rollback a specific schema migration by ID.""" |
| 170 | + |
| 171 | + async def _rollback(): |
| 172 | + dir_path = migrations_dir or os.path.join( |
| 173 | + get_root_migrations_dir(), "schema-migrations" |
| 174 | + ) |
| 175 | + |
| 176 | + if not os.path.exists(dir_path): |
| 177 | + if yes or click.confirm( |
| 178 | + f"Create schema migrations directory at '{dir_path}'?" |
| 179 | + ): |
| 180 | + os.makedirs(dir_path, exist_ok=True) |
| 181 | + else: |
| 182 | + click.echo("Aborted.") |
| 183 | + return |
4 | 184 |
|
| 185 | + migrator = SchemaMigrator(migrations_dir=dir_path) |
5 | 186 |
|
6 |
| -@click.command() |
7 |
| -@click.option("--module", default="aredis_om") |
8 |
| -def migrate(module: str): |
9 |
| - migrator = Migrator(module) |
10 |
| - migrator.detect_migrations() |
| 187 | + if not yes and not dry_run: |
| 188 | + if not click.confirm(f"Rollback migration '{migration_id}'?"): |
| 189 | + click.echo("Aborted.") |
| 190 | + return |
11 | 191 |
|
12 |
| - if migrator.migrations: |
13 |
| - print("Pending migrations:") |
14 |
| - for migration in migrator.migrations: |
15 |
| - print(migration) |
| 192 | + success = await migrator.rollback( |
| 193 | + migration_id, dry_run=dry_run, verbose=verbose |
| 194 | + ) |
| 195 | + if success: |
| 196 | + if verbose: |
| 197 | + click.echo(f"Successfully rolled back migration: {migration_id}") |
| 198 | + else: |
| 199 | + click.echo( |
| 200 | + f"Migration '{migration_id}' does not support rollback or is not applied.", |
| 201 | + err=True, |
| 202 | + ) |
16 | 203 |
|
17 |
| - if input("Run migrations? (y/n) ") == "y": |
18 |
| - migrator.run() |
| 204 | + run_async(_rollback()) |
0 commit comments