Skip to content

Commit 9b43617

Browse files
committed
Prunable migrations preflight check
1 parent 998aa49 commit 9b43617

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed

plain-models/plain/models/migrations/loader.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from importlib import import_module, reload
66
from typing import TYPE_CHECKING, Any
77

8+
from plain.models.connections import DatabaseConnection
89
from plain.models.migrations.graph import MigrationGraph
910
from plain.models.migrations.recorder import MigrationRecorder
1011
from plain.packages import packages_registry
@@ -50,7 +51,7 @@ class MigrationLoader:
5051

5152
def __init__(
5253
self,
53-
connection: BaseDatabaseWrapper | None,
54+
connection: BaseDatabaseWrapper | DatabaseConnection | None,
5455
load: bool = True,
5556
ignore_no_migrations: bool = False,
5657
replace_migrations: bool = True,

plain-models/plain/models/migrations/recorder.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import TYPE_CHECKING, Any
44

55
from plain import models
6+
from plain.models.connections import DatabaseConnection
67
from plain.models.db import DatabaseError
78
from plain.models.meta import Meta
89
from plain.models.registry import ModelsRegistry
@@ -59,7 +60,7 @@ def __str__(self) -> str:
5960
cls._migration_class = Migration
6061
return cls._migration_class
6162

62-
def __init__(self, connection: BaseDatabaseWrapper) -> None:
63+
def __init__(self, connection: BaseDatabaseWrapper | DatabaseConnection) -> None:
6364
self.connection = connection
6465

6566
@property

plain-models/plain/models/preflight.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,83 @@ def run(self) -> list[PreflightResult]:
244244
)
245245

246246
return errors
247+
248+
249+
@register_check("models.prunable_migrations")
250+
class CheckPrunableMigrations(PreflightCheck):
251+
"""Warns about stale migration records in the database."""
252+
253+
def run(self) -> list[PreflightResult]:
254+
# Import here to avoid circular import issues
255+
from plain.models.migrations.loader import MigrationLoader
256+
from plain.models.migrations.recorder import MigrationRecorder
257+
258+
errors = []
259+
260+
# Load migrations from disk and database
261+
loader = MigrationLoader(db_connection, ignore_no_migrations=True)
262+
recorder = MigrationRecorder(db_connection)
263+
recorded_migrations = recorder.applied_migrations()
264+
265+
# disk_migrations should not be None after MigrationLoader initialization,
266+
# but check to satisfy type checker
267+
if loader.disk_migrations is None:
268+
return errors
269+
270+
# Find all prunable migrations (recorded but not on disk)
271+
all_prunable = [
272+
migration
273+
for migration in recorded_migrations
274+
if migration not in loader.disk_migrations
275+
]
276+
277+
if not all_prunable:
278+
return errors
279+
280+
# Separate into existing packages vs orphaned packages
281+
existing_packages = set(loader.migrated_packages)
282+
prunable_existing: list[tuple[str, str]] = []
283+
prunable_orphaned: list[tuple[str, str]] = []
284+
285+
for migration in all_prunable:
286+
package, name = migration
287+
if package in existing_packages:
288+
prunable_existing.append(migration)
289+
else:
290+
prunable_orphaned.append(migration)
291+
292+
# Build the warning message
293+
total_count = len(all_prunable)
294+
message_parts = [
295+
f"Found {total_count} stale migration record{'s' if total_count != 1 else ''} in the database."
296+
]
297+
298+
if prunable_existing:
299+
existing_list = ", ".join(
300+
f"{pkg}.{name}" for pkg, name in prunable_existing[:3]
301+
)
302+
if len(prunable_existing) > 3:
303+
existing_list += f" (and {len(prunable_existing) - 3} more)"
304+
message_parts.append(f"From existing packages: {existing_list}.")
305+
306+
if prunable_orphaned:
307+
orphaned_list = ", ".join(
308+
f"{pkg}.{name}" for pkg, name in prunable_orphaned[:3]
309+
)
310+
if len(prunable_orphaned) > 3:
311+
orphaned_list += f" (and {len(prunable_orphaned) - 3} more)"
312+
message_parts.append(f"From removed packages: {orphaned_list}.")
313+
314+
message_parts.append(
315+
"Run 'plain models prune-migrations' to review and remove them."
316+
)
317+
318+
errors.append(
319+
PreflightResult(
320+
fix=" ".join(message_parts),
321+
id="models.prunable_migrations",
322+
warning=True,
323+
)
324+
)
325+
326+
return errors

0 commit comments

Comments
 (0)