plain.models 0.52.0__tar.gz → 0.53.0__tar.gz
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.
- {plain_models-0.52.0 → plain_models-0.53.0}/PKG-INFO +1 -1
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/CHANGELOG.md +13 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/creation.py +0 -1
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/cli.py +102 -90
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/executor.py +3 -1
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/loader.py +5 -2
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/recorder.py +2 -1
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/preflight.py +80 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/pyproject.toml +1 -1
- {plain_models-0.52.0 → plain_models-0.53.0}/.gitignore +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/LICENSE +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/README.md +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/AGENTS.md +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/README.md +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/aggregates.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/base.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/client.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/features.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/introspection.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/operations.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/schema.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/base/validation.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/ddl_references.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/base.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/client.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/compiler.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/creation.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/features.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/introspection.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/operations.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/schema.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/mysql/validation.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/base.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/client.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/creation.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/features.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/introspection.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/operations.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/schema.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/_functions.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/base.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/client.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/creation.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/features.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/introspection.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/operations.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/sqlite3/schema.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/utils.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backups/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backups/cli.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backups/clients.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backups/core.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/base.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/config.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/connections.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/constants.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/constraints.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/database_url.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/db.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/default_settings.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/deletion.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/entrypoints.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/enums.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/exceptions.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/expressions.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/fields/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/fields/json.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/fields/mixins.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/fields/related.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/fields/related_descriptors.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/fields/related_lookups.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/fields/related_managers.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/fields/reverse_related.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/forms.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/functions/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/functions/comparison.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/functions/datetime.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/functions/math.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/functions/mixins.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/functions/text.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/functions/window.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/indexes.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/lookups.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/meta.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/autodetector.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/exceptions.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/graph.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/migration.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/operations/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/operations/base.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/operations/fields.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/operations/models.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/operations/special.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/optimizer.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/questioner.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/serializer.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/state.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/utils.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/migrations/writer.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/options.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/otel.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/query.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/query_utils.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/registry.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/sql/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/sql/compiler.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/sql/constants.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/sql/datastructures.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/sql/query.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/sql/subqueries.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/sql/where.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/test/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/test/pytest.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/test/utils.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/transaction.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/plain/models/utils.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/app/examples/models.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/app/settings.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/app/urls.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/test_database_url.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/test_delete_behaviors.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/test_exceptions.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/test_manager_assignment.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/test_models.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/test_related_descriptors.py +0 -0
- {plain_models-0.52.0 → plain_models-0.53.0}/tests/test_related_manager_api.py +0 -0
@@ -1,5 +1,18 @@
|
|
1
1
|
# plain-models changelog
|
2
2
|
|
3
|
+
## [0.53.0](https://github.com/dropseed/plain/releases/plain-models@0.53.0) (2025-10-12)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Added new `plain models prune-migrations` command to identify and remove stale migration records from the database ([998aa49](https://github.com/dropseed/plain/commit/998aa49140))
|
8
|
+
- The `--prune` option has been removed from `plain migrate` command in favor of the dedicated `prune-migrations` command ([998aa49](https://github.com/dropseed/plain/commit/998aa49140))
|
9
|
+
- Added new preflight check `models.prunable_migrations` that warns about stale migration records in the database ([9b43617](https://github.com/dropseed/plain/commit/9b4361765c))
|
10
|
+
- The `show-migrations` command no longer displays prunable migrations in its output ([998aa49](https://github.com/dropseed/plain/commit/998aa49140))
|
11
|
+
|
12
|
+
### Upgrade instructions
|
13
|
+
|
14
|
+
- Replace any usage of `plain migrate --prune` with the new `plain models prune-migrations` command
|
15
|
+
|
3
16
|
## [0.52.0](https://github.com/dropseed/plain/releases/plain-models@0.52.0) (2025-10-10)
|
4
17
|
|
5
18
|
### What's changed
|
@@ -59,7 +59,6 @@ class BaseDatabaseCreation:
|
|
59
59
|
plan=False,
|
60
60
|
check_unapplied=False,
|
61
61
|
backup=False,
|
62
|
-
prune=False,
|
63
62
|
no_input=True,
|
64
63
|
atomic_batch=False, # No need for atomic batch when creating test database
|
65
64
|
quiet=verbosity < 2, # Show migration output when verbosity is 2+
|
@@ -366,11 +366,6 @@ def makemigrations(
|
|
366
366
|
default=None,
|
367
367
|
help="Explicitly enable/disable pre-migration backups.",
|
368
368
|
)
|
369
|
-
@click.option(
|
370
|
-
"--prune",
|
371
|
-
is_flag=True,
|
372
|
-
help="Delete nonexistent migrations from the plainmigrations table.",
|
373
|
-
)
|
374
369
|
@click.option(
|
375
370
|
"--no-input",
|
376
371
|
"--noinput",
|
@@ -395,7 +390,6 @@ def migrate(
|
|
395
390
|
plan: bool,
|
396
391
|
check_unapplied: bool,
|
397
392
|
backup: bool | None,
|
398
|
-
prune: bool,
|
399
393
|
no_input: bool,
|
400
394
|
atomic_batch: bool | None,
|
401
395
|
quiet: bool,
|
@@ -519,58 +513,6 @@ def migrate(
|
|
519
513
|
else:
|
520
514
|
targets = list(executor.loader.graph.leaf_nodes())
|
521
515
|
|
522
|
-
if prune:
|
523
|
-
if not package_label:
|
524
|
-
raise click.ClickException(
|
525
|
-
"Migrations can be pruned only when a package is specified."
|
526
|
-
)
|
527
|
-
if not quiet:
|
528
|
-
click.secho("Pruning migrations:", bold=True)
|
529
|
-
to_prune = set(executor.loader.applied_migrations) - set( # type: ignore[arg-type]
|
530
|
-
executor.loader.disk_migrations # type: ignore[arg-type]
|
531
|
-
)
|
532
|
-
squashed_migrations_with_deleted_replaced_migrations = [
|
533
|
-
migration_key
|
534
|
-
for migration_key, migration_obj in executor.loader.replacements.items()
|
535
|
-
if any(replaced in to_prune for replaced in migration_obj.replaces)
|
536
|
-
]
|
537
|
-
if squashed_migrations_with_deleted_replaced_migrations:
|
538
|
-
if not quiet:
|
539
|
-
click.echo(
|
540
|
-
click.style(
|
541
|
-
" Cannot use --prune because the following squashed "
|
542
|
-
"migrations have their 'replaces' attributes and may not "
|
543
|
-
"be recorded as applied:",
|
544
|
-
fg="yellow",
|
545
|
-
)
|
546
|
-
)
|
547
|
-
for migration in squashed_migrations_with_deleted_replaced_migrations:
|
548
|
-
package, name = migration
|
549
|
-
click.echo(f" {package}.{name}")
|
550
|
-
click.echo(
|
551
|
-
click.style(
|
552
|
-
" Re-run `plain migrate` if they are not marked as "
|
553
|
-
"applied, and remove 'replaces' attributes in their "
|
554
|
-
"Migration classes.",
|
555
|
-
fg="yellow",
|
556
|
-
)
|
557
|
-
)
|
558
|
-
else:
|
559
|
-
to_prune = sorted(
|
560
|
-
migration for migration in to_prune if migration[0] == package_label
|
561
|
-
)
|
562
|
-
if to_prune:
|
563
|
-
for migration in to_prune:
|
564
|
-
package, name = migration
|
565
|
-
if not quiet:
|
566
|
-
click.echo(f" Pruning {package}.{name}...", nl=False)
|
567
|
-
executor.recorder.record_unapplied(package, name)
|
568
|
-
if not quiet:
|
569
|
-
click.echo(" OK")
|
570
|
-
else:
|
571
|
-
if not quiet:
|
572
|
-
click.echo(" No migrations to prune.")
|
573
|
-
|
574
516
|
migration_plan = executor.migration_plan(targets)
|
575
517
|
|
576
518
|
if plan:
|
@@ -596,9 +538,6 @@ def migrate(
|
|
596
538
|
sys.exit(1)
|
597
539
|
return
|
598
540
|
|
599
|
-
if prune:
|
600
|
-
return
|
601
|
-
|
602
541
|
# Print some useful info
|
603
542
|
if not quiet:
|
604
543
|
if target_package_labels_only:
|
@@ -825,35 +764,6 @@ def show_migrations(
|
|
825
764
|
if not shown:
|
826
765
|
click.secho(" (no migrations)", fg="red")
|
827
766
|
|
828
|
-
# Find recorded migrations that aren't in the graph (prunable)
|
829
|
-
prunable_migrations = [
|
830
|
-
migration
|
831
|
-
for migration in recorded_migrations
|
832
|
-
if (
|
833
|
-
migration not in loader.disk_migrations # type: ignore[operator]
|
834
|
-
and (not package_names_list or migration[0] in package_names_list)
|
835
|
-
)
|
836
|
-
]
|
837
|
-
|
838
|
-
if prunable_migrations:
|
839
|
-
click.echo()
|
840
|
-
click.secho(
|
841
|
-
"Recorded migrations not in migration files (candidates for pruning):",
|
842
|
-
fg="yellow",
|
843
|
-
bold=True,
|
844
|
-
)
|
845
|
-
prunable_by_package = {}
|
846
|
-
for migration in prunable_migrations:
|
847
|
-
package, name = migration
|
848
|
-
if package not in prunable_by_package:
|
849
|
-
prunable_by_package[package] = []
|
850
|
-
prunable_by_package[package].append(name)
|
851
|
-
|
852
|
-
for package in sorted(prunable_by_package.keys()):
|
853
|
-
click.secho(f" {package}:", fg="yellow")
|
854
|
-
for name in sorted(prunable_by_package[package]):
|
855
|
-
click.echo(f" - {name}")
|
856
|
-
|
857
767
|
def show_plan(db_connection: Any, package_names: tuple[str, ...]) -> None:
|
858
768
|
"""
|
859
769
|
Show all known migrations (or only those of the specified package_names)
|
@@ -906,6 +816,108 @@ def show_migrations(
|
|
906
816
|
show_list(db_connection, package_labels)
|
907
817
|
|
908
818
|
|
819
|
+
@cli.command()
|
820
|
+
@click.option(
|
821
|
+
"--yes",
|
822
|
+
is_flag=True,
|
823
|
+
help="Skip confirmation prompt (for non-interactive use).",
|
824
|
+
)
|
825
|
+
def prune_migrations(yes: bool) -> None:
|
826
|
+
"""Show and optionally remove stale migration records from the database."""
|
827
|
+
# Load migrations from disk and database
|
828
|
+
loader = MigrationLoader(db_connection, ignore_no_migrations=True)
|
829
|
+
recorder = MigrationRecorder(db_connection)
|
830
|
+
recorded_migrations = recorder.applied_migrations()
|
831
|
+
|
832
|
+
# Find all prunable migrations (recorded but not on disk)
|
833
|
+
all_prunable = [
|
834
|
+
migration
|
835
|
+
for migration in recorded_migrations
|
836
|
+
if migration not in loader.disk_migrations # type: ignore[operator]
|
837
|
+
]
|
838
|
+
|
839
|
+
if not all_prunable:
|
840
|
+
click.echo("No stale migration records found.")
|
841
|
+
return
|
842
|
+
|
843
|
+
# Separate into existing packages vs orphaned packages
|
844
|
+
existing_packages = set(loader.migrated_packages)
|
845
|
+
prunable_existing: dict[str, list[str]] = {}
|
846
|
+
prunable_orphaned: dict[str, list[str]] = {}
|
847
|
+
|
848
|
+
for migration in all_prunable:
|
849
|
+
package, name = migration
|
850
|
+
if package in existing_packages:
|
851
|
+
if package not in prunable_existing:
|
852
|
+
prunable_existing[package] = []
|
853
|
+
prunable_existing[package].append(name)
|
854
|
+
else:
|
855
|
+
if package not in prunable_orphaned:
|
856
|
+
prunable_orphaned[package] = []
|
857
|
+
prunable_orphaned[package].append(name)
|
858
|
+
|
859
|
+
# Display what was found
|
860
|
+
if prunable_existing:
|
861
|
+
click.secho(
|
862
|
+
"Stale migration records (from existing packages):",
|
863
|
+
fg="yellow",
|
864
|
+
bold=True,
|
865
|
+
)
|
866
|
+
for package in sorted(prunable_existing.keys()):
|
867
|
+
click.secho(f" {package}:", fg="yellow")
|
868
|
+
for name in sorted(prunable_existing[package]):
|
869
|
+
click.echo(f" - {name}")
|
870
|
+
click.echo()
|
871
|
+
|
872
|
+
if prunable_orphaned:
|
873
|
+
click.secho(
|
874
|
+
"Orphaned migration records (from removed packages):",
|
875
|
+
fg="red",
|
876
|
+
bold=True,
|
877
|
+
)
|
878
|
+
for package in sorted(prunable_orphaned.keys()):
|
879
|
+
click.secho(f" {package}:", fg="red")
|
880
|
+
for name in sorted(prunable_orphaned[package]):
|
881
|
+
click.echo(f" - {name}")
|
882
|
+
click.echo()
|
883
|
+
|
884
|
+
total_count = sum(len(migs) for migs in prunable_existing.values()) + sum(
|
885
|
+
len(migs) for migs in prunable_orphaned.values()
|
886
|
+
)
|
887
|
+
|
888
|
+
if not yes:
|
889
|
+
click.echo(
|
890
|
+
f"Found {total_count} stale migration record{'s' if total_count != 1 else ''}."
|
891
|
+
)
|
892
|
+
click.echo()
|
893
|
+
|
894
|
+
# Prompt for confirmation if interactive
|
895
|
+
if not click.confirm(
|
896
|
+
"Do you want to remove these migrations from the database?"
|
897
|
+
):
|
898
|
+
return
|
899
|
+
|
900
|
+
# Actually prune the migrations
|
901
|
+
click.secho("Pruning migrations...", bold=True)
|
902
|
+
|
903
|
+
for package, migration_names in prunable_existing.items():
|
904
|
+
for name in sorted(migration_names):
|
905
|
+
click.echo(f" Pruning {package}.{name}...", nl=False)
|
906
|
+
recorder.record_unapplied(package, name)
|
907
|
+
click.echo(" OK")
|
908
|
+
|
909
|
+
for package, migration_names in prunable_orphaned.items():
|
910
|
+
for name in sorted(migration_names):
|
911
|
+
click.echo(f" Pruning {package}.{name} (orphaned)...", nl=False)
|
912
|
+
recorder.record_unapplied(package, name)
|
913
|
+
click.echo(" OK")
|
914
|
+
|
915
|
+
click.secho(
|
916
|
+
f"✓ Removed {total_count} stale migration record{'s' if total_count != 1 else ''}.",
|
917
|
+
fg="green",
|
918
|
+
)
|
919
|
+
|
920
|
+
|
909
921
|
@cli.command()
|
910
922
|
@click.argument("package_label")
|
911
923
|
@click.argument("start_migration_name", required=False)
|
@@ -4,6 +4,8 @@ from collections.abc import Callable
|
|
4
4
|
from contextlib import nullcontext
|
5
5
|
from typing import TYPE_CHECKING, Any
|
6
6
|
|
7
|
+
from plain.models.connections import DatabaseConnection
|
8
|
+
|
7
9
|
from ..transaction import atomic
|
8
10
|
from .loader import MigrationLoader
|
9
11
|
from .migration import Migration
|
@@ -22,7 +24,7 @@ class MigrationExecutor:
|
|
22
24
|
|
23
25
|
def __init__(
|
24
26
|
self,
|
25
|
-
connection: BaseDatabaseWrapper,
|
27
|
+
connection: BaseDatabaseWrapper | DatabaseConnection,
|
26
28
|
progress_callback: Callable[..., Any] | None = None,
|
27
29
|
) -> None:
|
28
30
|
self.connection = connection
|
@@ -5,6 +5,7 @@ import sys
|
|
5
5
|
from importlib import import_module, reload
|
6
6
|
from typing import TYPE_CHECKING, Any
|
7
7
|
|
8
|
+
from plain.models.connections import DatabaseConnection
|
8
9
|
from plain.models.migrations.graph import MigrationGraph
|
9
10
|
from plain.models.migrations.recorder import MigrationRecorder
|
10
11
|
from plain.packages import packages_registry
|
@@ -50,7 +51,7 @@ class MigrationLoader:
|
|
50
51
|
|
51
52
|
def __init__(
|
52
53
|
self,
|
53
|
-
connection: BaseDatabaseWrapper | None,
|
54
|
+
connection: BaseDatabaseWrapper | DatabaseConnection | None,
|
54
55
|
load: bool = True,
|
55
56
|
ignore_no_migrations: bool = False,
|
56
57
|
replace_migrations: bool = True,
|
@@ -316,7 +317,9 @@ class MigrationLoader:
|
|
316
317
|
raise
|
317
318
|
self.graph.ensure_not_cyclic()
|
318
319
|
|
319
|
-
def check_consistent_history(
|
320
|
+
def check_consistent_history(
|
321
|
+
self, connection: BaseDatabaseWrapper | DatabaseConnection
|
322
|
+
) -> None:
|
320
323
|
"""
|
321
324
|
Raise InconsistentMigrationHistory if any applied migrations have
|
322
325
|
unapplied dependencies.
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
4
4
|
|
5
5
|
from plain import models
|
6
|
+
from plain.models.connections import DatabaseConnection
|
6
7
|
from plain.models.db import DatabaseError
|
7
8
|
from plain.models.meta import Meta
|
8
9
|
from plain.models.registry import ModelsRegistry
|
@@ -59,7 +60,7 @@ class MigrationRecorder:
|
|
59
60
|
cls._migration_class = Migration
|
60
61
|
return cls._migration_class
|
61
62
|
|
62
|
-
def __init__(self, connection: BaseDatabaseWrapper) -> None:
|
63
|
+
def __init__(self, connection: BaseDatabaseWrapper | DatabaseConnection) -> None:
|
63
64
|
self.connection = connection
|
64
65
|
|
65
66
|
@property
|
@@ -244,3 +244,83 @@ class CheckDatabaseTables(PreflightCheck):
|
|
244
244
|
)
|
245
245
|
|
246
246
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{plain_models-0.52.0 → plain_models-0.53.0}/plain/models/backends/postgresql/introspection.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{plain_models-0.52.0 → plain_models-0.53.0}/tests/app/examples/migrations/0002_test_field_removed.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|