oxyde-postgres 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oxyde/__init__.py +151 -0
- oxyde/_msgpack.py +3 -0
- oxyde/cli/__init__.py +7 -0
- oxyde/cli/app.py +107 -0
- oxyde/cli/codegen.py +66 -0
- oxyde/cli/config.py +84 -0
- oxyde/cli/migrations.py +536 -0
- oxyde/codegen/__init__.py +15 -0
- oxyde/codegen/stub_generator.py +1229 -0
- oxyde/core/__init__.py +90 -0
- oxyde/core/column_types.py +367 -0
- oxyde/core/ir.py +380 -0
- oxyde/core/types.py +69 -0
- oxyde/core/wrapper.py +242 -0
- oxyde/db/__init__.py +230 -0
- oxyde/db/pool.py +305 -0
- oxyde/db/registry.py +113 -0
- oxyde/db/schema.py +51 -0
- oxyde/db/transaction.py +392 -0
- oxyde/exceptions.py +89 -0
- oxyde/migrations/__init__.py +49 -0
- oxyde/migrations/config.py +187 -0
- oxyde/migrations/context.py +598 -0
- oxyde/migrations/executor.py +456 -0
- oxyde/migrations/extract.py +316 -0
- oxyde/migrations/generator.py +534 -0
- oxyde/migrations/replay.py +367 -0
- oxyde/migrations/squash.py +109 -0
- oxyde/migrations/tracker.py +162 -0
- oxyde/migrations/utils.py +145 -0
- oxyde/models/__init__.py +83 -0
- oxyde/models/base.py +871 -0
- oxyde/models/decorators.py +111 -0
- oxyde/models/field.py +204 -0
- oxyde/models/lookups.py +667 -0
- oxyde/models/metadata.py +174 -0
- oxyde/models/registry.py +170 -0
- oxyde/models/serializers.py +144 -0
- oxyde/models/utils.py +116 -0
- oxyde/py.typed +0 -0
- oxyde/queries/__init__.py +79 -0
- oxyde/queries/aggregates.py +145 -0
- oxyde/queries/base.py +249 -0
- oxyde/queries/conditions.py +92 -0
- oxyde/queries/expressions.py +171 -0
- oxyde/queries/insert.py +124 -0
- oxyde/queries/joins.py +66 -0
- oxyde/queries/manager.py +409 -0
- oxyde/queries/mixins/__init__.py +68 -0
- oxyde/queries/mixins/aggregation.py +335 -0
- oxyde/queries/mixins/debug.py +154 -0
- oxyde/queries/mixins/execution.py +701 -0
- oxyde/queries/mixins/filtering.py +144 -0
- oxyde/queries/mixins/joining.py +160 -0
- oxyde/queries/mixins/mutation.py +508 -0
- oxyde/queries/mixins/pagination.py +145 -0
- oxyde/queries/q.py +250 -0
- oxyde/queries/raw.py +114 -0
- oxyde/queries/select.py +345 -0
- oxyde/queries/typed.py +213 -0
- oxyde_postgres-0.1.0.dist-info/METADATA +141 -0
- oxyde_postgres-0.1.0.dist-info/RECORD +66 -0
- oxyde_postgres-0.1.0.dist-info/WHEEL +5 -0
- oxyde_postgres-0.1.0.dist-info/entry_points.txt +2 -0
- oxyde_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
- oxyde_postgres-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""Migration executor - applies migrations to database."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from oxyde._msgpack import msgpack
|
|
9
|
+
from oxyde.core import (
|
|
10
|
+
begin_transaction,
|
|
11
|
+
commit_transaction,
|
|
12
|
+
execute_in_transaction,
|
|
13
|
+
rollback_transaction,
|
|
14
|
+
)
|
|
15
|
+
from oxyde.core.ir import build_raw_sql_ir
|
|
16
|
+
from oxyde.db.pool import _msgpack_encoder
|
|
17
|
+
from oxyde.db.registry import get_connection as _get_connection_async
|
|
18
|
+
from oxyde.migrations.context import MigrationContext
|
|
19
|
+
from oxyde.migrations.replay import (
|
|
20
|
+
SchemaState,
|
|
21
|
+
_get_migration_dependency,
|
|
22
|
+
_topological_sort_migrations,
|
|
23
|
+
)
|
|
24
|
+
from oxyde.migrations.tracker import (
|
|
25
|
+
ensure_migrations_table,
|
|
26
|
+
get_applied_migrations,
|
|
27
|
+
get_pending_migrations,
|
|
28
|
+
record_migration,
|
|
29
|
+
remove_migration,
|
|
30
|
+
)
|
|
31
|
+
from oxyde.migrations.utils import (
|
|
32
|
+
detect_dialect,
|
|
33
|
+
load_migration_module,
|
|
34
|
+
parse_query_result,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from oxyde.db.pool import AsyncDatabase
|
|
39
|
+
|
|
40
|
+
# Advisory lock key for migrations (arbitrary unique number)
|
|
41
|
+
MIGRATION_LOCK_KEY = 0x4F587944 # "OxyD" in hex
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _check_migration_dependency(
|
|
45
|
+
migration_path: Path,
|
|
46
|
+
applied: set[str],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Check that migration's dependency is satisfied.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
migration_path: Path to migration file
|
|
52
|
+
applied: Set of already applied migration names
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
RuntimeError: If dependency is not satisfied
|
|
56
|
+
"""
|
|
57
|
+
module = load_migration_module(migration_path)
|
|
58
|
+
if module is None:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
dependency = _get_migration_dependency(module)
|
|
62
|
+
if dependency is not None and dependency not in applied:
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
f"Migration '{migration_path.stem}' depends on '{dependency}' "
|
|
65
|
+
f"which has not been applied yet."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _acquire_migration_lock(
|
|
70
|
+
db_conn: AsyncDatabase, _dialect: str
|
|
71
|
+
) -> tuple[bool, int | None]:
|
|
72
|
+
"""Acquire advisory lock on a dedicated connection.
|
|
73
|
+
|
|
74
|
+
Uses begin_transaction to pin a connection, ensuring the lock is held
|
|
75
|
+
on the same connection until explicitly released. This prevents the pool
|
|
76
|
+
from routing acquire and release to different connections (advisory locks
|
|
77
|
+
are session-scoped in PostgreSQL).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
db_conn: Database connection
|
|
81
|
+
dialect: Database dialect
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Tuple of (lock_acquired, lock_tx_id). lock_tx_id must be passed
|
|
85
|
+
to _release_migration_lock to release on the same connection.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# Pin a connection via transaction so acquire and release use the same session
|
|
89
|
+
tx_id = await begin_transaction(db_conn.name)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
sql = f"SELECT pg_try_advisory_lock({MIGRATION_LOCK_KEY})"
|
|
93
|
+
ir = build_raw_sql_ir(sql=sql)
|
|
94
|
+
ir_bytes = msgpack.packb(ir, default=_msgpack_encoder)
|
|
95
|
+
result_bytes = await execute_in_transaction(db_conn.name, tx_id, ir_bytes)
|
|
96
|
+
result = parse_query_result(result_bytes)
|
|
97
|
+
if result and result[0].get("pg_try_advisory_lock", False):
|
|
98
|
+
return True, tx_id
|
|
99
|
+
await commit_transaction(tx_id)
|
|
100
|
+
return False, None
|
|
101
|
+
|
|
102
|
+
except Exception:
|
|
103
|
+
try:
|
|
104
|
+
await commit_transaction(tx_id)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _release_migration_lock(
|
|
111
|
+
db_conn: AsyncDatabase, _dialect: str, lock_tx_id: int | None
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Release advisory lock and close the dedicated connection.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
db_conn: Database connection
|
|
117
|
+
dialect: Database dialect
|
|
118
|
+
lock_tx_id: Transaction ID from _acquire_migration_lock
|
|
119
|
+
"""
|
|
120
|
+
if lock_tx_id is None:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
sql = f"SELECT pg_advisory_unlock({MIGRATION_LOCK_KEY})"
|
|
125
|
+
ir = build_raw_sql_ir(sql=sql)
|
|
126
|
+
ir_bytes = msgpack.packb(ir, default=_msgpack_encoder)
|
|
127
|
+
await execute_in_transaction(db_conn.name, lock_tx_id, ir_bytes)
|
|
128
|
+
|
|
129
|
+
await commit_transaction(lock_tx_id)
|
|
130
|
+
|
|
131
|
+
except Exception:
|
|
132
|
+
try:
|
|
133
|
+
await rollback_transaction(lock_tx_id)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _require_migration_module(filepath: Path) -> Any:
|
|
139
|
+
"""Load a migration module, raising on failure.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
filepath: Path to migration file
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Loaded module
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ImportError: If module cannot be loaded
|
|
149
|
+
"""
|
|
150
|
+
module = load_migration_module(filepath)
|
|
151
|
+
if module is None:
|
|
152
|
+
raise ImportError(f"Cannot load migration from {filepath}")
|
|
153
|
+
return module
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def replay_migrations_up_to(
|
|
157
|
+
migrations_dir: str,
|
|
158
|
+
target_migration: Path,
|
|
159
|
+
include_target: bool = False,
|
|
160
|
+
) -> SchemaState:
|
|
161
|
+
"""Replay migrations up to (but not including) target to get schema state.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
migrations_dir: Path to migrations directory
|
|
165
|
+
target_migration: Target migration path
|
|
166
|
+
include_target: If True, include target migration in replay
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
SchemaState after replaying migrations
|
|
170
|
+
"""
|
|
171
|
+
state = SchemaState()
|
|
172
|
+
migration_files = _topological_sort_migrations(
|
|
173
|
+
sorted(Path(migrations_dir).glob("[0-9]*.py"))
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
for filepath in migration_files:
|
|
177
|
+
# Stop before target (or at target if include_target is False)
|
|
178
|
+
if filepath == target_migration and not include_target:
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
module = _require_migration_module(filepath)
|
|
182
|
+
|
|
183
|
+
if hasattr(module, "upgrade"):
|
|
184
|
+
ctx = MigrationContext(mode="collect")
|
|
185
|
+
module.upgrade(ctx)
|
|
186
|
+
|
|
187
|
+
for op in ctx.get_collected_operations():
|
|
188
|
+
state.apply_operation(op)
|
|
189
|
+
|
|
190
|
+
# Stop after target if include_target is True
|
|
191
|
+
if filepath == target_migration and include_target:
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
return state
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def apply_migrations(
|
|
198
|
+
migrations_dir: str = "migrations",
|
|
199
|
+
db_alias: str = "default",
|
|
200
|
+
target: str | None = None,
|
|
201
|
+
fake: bool = False,
|
|
202
|
+
) -> list[str]:
|
|
203
|
+
"""Apply pending migrations to database.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
migrations_dir: Path to migrations directory
|
|
207
|
+
db_alias: Database connection alias
|
|
208
|
+
target: Target migration name (if None, apply all pending)
|
|
209
|
+
fake: If True, mark as applied without executing SQL
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of applied migration names
|
|
213
|
+
"""
|
|
214
|
+
# Ensure migrations table exists
|
|
215
|
+
await ensure_migrations_table(db_alias)
|
|
216
|
+
|
|
217
|
+
# Get applied and pending migrations
|
|
218
|
+
applied = await get_applied_migrations(db_alias)
|
|
219
|
+
pending = get_pending_migrations(migrations_dir, applied)
|
|
220
|
+
|
|
221
|
+
if not pending:
|
|
222
|
+
return []
|
|
223
|
+
|
|
224
|
+
# Filter by target if specified
|
|
225
|
+
if target:
|
|
226
|
+
target_index = None
|
|
227
|
+
for i, migration_path in enumerate(pending):
|
|
228
|
+
# Match by exact name or prefix (e.g. "0001" matches "0001_create_users_table")
|
|
229
|
+
if migration_path.stem == target or migration_path.stem.startswith(
|
|
230
|
+
target + "_"
|
|
231
|
+
):
|
|
232
|
+
target_index = i
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
if target_index is None:
|
|
236
|
+
raise ValueError(
|
|
237
|
+
f"Target migration '{target}' not found in pending migrations"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
pending = pending[: target_index + 1]
|
|
241
|
+
|
|
242
|
+
# Get database connection and detect dialect
|
|
243
|
+
db_conn = await _get_connection_async(db_alias)
|
|
244
|
+
|
|
245
|
+
dialect = detect_dialect(db_conn.url)
|
|
246
|
+
|
|
247
|
+
# Acquire advisory lock to prevent concurrent migrations
|
|
248
|
+
lock_acquired, lock_tx_id = await _acquire_migration_lock(db_conn, dialect)
|
|
249
|
+
if not lock_acquired:
|
|
250
|
+
raise RuntimeError(
|
|
251
|
+
"Cannot acquire migration lock. Another migration process may be running."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Apply each migration
|
|
255
|
+
applied_migrations = []
|
|
256
|
+
applied_set = set(applied) # Track applied migrations for dependency check
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
# Track schema state as we apply migrations
|
|
260
|
+
schema_state = replay_migrations_up_to(
|
|
261
|
+
migrations_dir, pending[0], include_target=False
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
for migration_path in pending:
|
|
265
|
+
migration_name = migration_path.stem
|
|
266
|
+
|
|
267
|
+
# Check dependency is satisfied
|
|
268
|
+
_check_migration_dependency(migration_path, applied_set)
|
|
269
|
+
|
|
270
|
+
if not fake:
|
|
271
|
+
# Import migration module
|
|
272
|
+
module = _require_migration_module(migration_path)
|
|
273
|
+
|
|
274
|
+
if not hasattr(module, "upgrade"):
|
|
275
|
+
raise RuntimeError(
|
|
276
|
+
f"Migration {migration_name} missing upgrade() function"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Create migration context in execute mode with schema state
|
|
280
|
+
ctx = MigrationContext(
|
|
281
|
+
mode="execute",
|
|
282
|
+
dialect=dialect,
|
|
283
|
+
db_conn=db_conn,
|
|
284
|
+
schema_state=schema_state,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Execute upgrade (collects SQL statements)
|
|
288
|
+
module.upgrade(ctx)
|
|
289
|
+
|
|
290
|
+
# Execute collected SQL (wrapped in transaction for postgres)
|
|
291
|
+
await ctx._execute_collected_sql()
|
|
292
|
+
|
|
293
|
+
# Update schema state with this migration's operations
|
|
294
|
+
ctx_collect = MigrationContext(mode="collect")
|
|
295
|
+
module.upgrade(ctx_collect)
|
|
296
|
+
for op in ctx_collect.get_collected_operations():
|
|
297
|
+
schema_state.apply_operation(op)
|
|
298
|
+
|
|
299
|
+
# Record migration as applied
|
|
300
|
+
await record_migration(migration_name, db_alias)
|
|
301
|
+
applied_migrations.append(migration_name)
|
|
302
|
+
applied_set.add(migration_name) # Update for dependency checking
|
|
303
|
+
|
|
304
|
+
finally:
|
|
305
|
+
# Always release the lock
|
|
306
|
+
await _release_migration_lock(db_conn, dialect, lock_tx_id)
|
|
307
|
+
|
|
308
|
+
return applied_migrations
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _check_rollback_dependency(
|
|
312
|
+
migration_name: str,
|
|
313
|
+
migrations_dir: str,
|
|
314
|
+
applied: list[str],
|
|
315
|
+
) -> None:
|
|
316
|
+
"""Check that no applied migration depends on the one being rolled back.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
migration_name: Name of migration to roll back
|
|
320
|
+
migrations_dir: Path to migrations directory
|
|
321
|
+
applied: List of currently applied migration names
|
|
322
|
+
|
|
323
|
+
Raises:
|
|
324
|
+
RuntimeError: If another applied migration depends on this one
|
|
325
|
+
"""
|
|
326
|
+
migrations_path = Path(migrations_dir)
|
|
327
|
+
for applied_name in applied:
|
|
328
|
+
if applied_name == migration_name:
|
|
329
|
+
continue
|
|
330
|
+
migration_path = migrations_path / f"{applied_name}.py"
|
|
331
|
+
if not migration_path.exists():
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
module = load_migration_module(migration_path)
|
|
335
|
+
if module is None:
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
dependency = _get_migration_dependency(module)
|
|
339
|
+
if dependency == migration_name:
|
|
340
|
+
msg = (
|
|
341
|
+
f"Cannot roll back '{migration_name}': "
|
|
342
|
+
f"migration '{applied_name}' depends on it."
|
|
343
|
+
)
|
|
344
|
+
raise RuntimeError(msg)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
async def rollback_migration(
|
|
348
|
+
migration_name: str,
|
|
349
|
+
migrations_dir: str = "migrations",
|
|
350
|
+
db_alias: str = "default",
|
|
351
|
+
fake: bool = False,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Roll back a single migration.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
migration_name: Name of migration to roll back
|
|
357
|
+
migrations_dir: Path to migrations directory
|
|
358
|
+
db_alias: Database connection alias
|
|
359
|
+
fake: If True, remove from history without executing SQL
|
|
360
|
+
"""
|
|
361
|
+
migration_path = Path(migrations_dir) / f"{migration_name}.py"
|
|
362
|
+
if not migration_path.exists():
|
|
363
|
+
raise FileNotFoundError(f"Migration file not found: {migration_path}")
|
|
364
|
+
|
|
365
|
+
# Check that no applied migration depends on this one
|
|
366
|
+
applied = await get_applied_migrations(db_alias)
|
|
367
|
+
_check_rollback_dependency(migration_name, migrations_dir, applied)
|
|
368
|
+
|
|
369
|
+
# Get database connection and detect dialect
|
|
370
|
+
db_conn = await _get_connection_async(db_alias)
|
|
371
|
+
|
|
372
|
+
dialect = detect_dialect(db_conn.url)
|
|
373
|
+
|
|
374
|
+
# Acquire advisory lock
|
|
375
|
+
lock_acquired, lock_tx_id = await _acquire_migration_lock(db_conn, dialect)
|
|
376
|
+
if not lock_acquired:
|
|
377
|
+
raise RuntimeError(
|
|
378
|
+
"Cannot acquire migration lock. Another migration process may be running."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
if not fake:
|
|
383
|
+
# Import migration module
|
|
384
|
+
module = _require_migration_module(migration_path)
|
|
385
|
+
|
|
386
|
+
if not hasattr(module, "downgrade"):
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
f"Migration {migration_name} missing downgrade() function"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Replay migrations up to and including this one to get schema state
|
|
392
|
+
schema_state = replay_migrations_up_to(
|
|
393
|
+
migrations_dir, migration_path, include_target=True
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Create migration context in execute mode with schema state
|
|
397
|
+
ctx = MigrationContext(
|
|
398
|
+
mode="execute",
|
|
399
|
+
dialect=dialect,
|
|
400
|
+
db_conn=db_conn,
|
|
401
|
+
schema_state=schema_state,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Execute downgrade (collects SQL statements)
|
|
405
|
+
module.downgrade(ctx)
|
|
406
|
+
|
|
407
|
+
# Execute collected SQL
|
|
408
|
+
await ctx._execute_collected_sql()
|
|
409
|
+
|
|
410
|
+
# Remove migration from history
|
|
411
|
+
await remove_migration(migration_name, db_alias)
|
|
412
|
+
|
|
413
|
+
finally:
|
|
414
|
+
await _release_migration_lock(db_conn, dialect, lock_tx_id)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
async def rollback_migrations(
|
|
418
|
+
steps: int = 1,
|
|
419
|
+
migrations_dir: str = "migrations",
|
|
420
|
+
db_alias: str = "default",
|
|
421
|
+
fake: bool = False,
|
|
422
|
+
) -> list[str]:
|
|
423
|
+
"""Roll back last N migrations.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
steps: Number of migrations to roll back
|
|
427
|
+
migrations_dir: Path to migrations directory
|
|
428
|
+
db_alias: Database connection alias
|
|
429
|
+
fake: If True, remove from history without executing SQL
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
List of rolled back migration names
|
|
433
|
+
"""
|
|
434
|
+
# Get applied migrations
|
|
435
|
+
applied = await get_applied_migrations(db_alias)
|
|
436
|
+
|
|
437
|
+
if not applied:
|
|
438
|
+
return []
|
|
439
|
+
|
|
440
|
+
# Get last N migrations to roll back
|
|
441
|
+
to_rollback = applied[-steps:] if steps < len(applied) else applied
|
|
442
|
+
to_rollback = list(reversed(to_rollback)) # Roll back in reverse order
|
|
443
|
+
|
|
444
|
+
rolled_back = []
|
|
445
|
+
for migration_name in to_rollback:
|
|
446
|
+
await rollback_migration(migration_name, migrations_dir, db_alias, fake)
|
|
447
|
+
rolled_back.append(migration_name)
|
|
448
|
+
|
|
449
|
+
return rolled_back
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
__all__ = [
|
|
453
|
+
"apply_migrations",
|
|
454
|
+
"rollback_migration",
|
|
455
|
+
"rollback_migrations",
|
|
456
|
+
]
|