plain.models 0.51.1__py3-none-any.whl → 0.53.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.
- plain/models/CHANGELOG.md +25 -0
- plain/models/backends/base/creation.py +1 -5
- plain/models/backends/base/schema.py +11 -18
- plain/models/cli.py +214 -196
- plain/models/migrations/executor.py +8 -8
- plain/models/migrations/loader.py +5 -22
- plain/models/migrations/migration.py +23 -17
- plain/models/migrations/recorder.py +2 -1
- plain/models/preflight.py +80 -0
- {plain_models-0.51.1.dist-info → plain_models-0.53.0.dist-info}/METADATA +1 -1
- {plain_models-0.51.1.dist-info → plain_models-0.53.0.dist-info}/RECORD +14 -14
- {plain_models-0.51.1.dist-info → plain_models-0.53.0.dist-info}/WHEEL +0 -0
- {plain_models-0.51.1.dist-info → plain_models-0.53.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.51.1.dist-info → plain_models-0.53.0.dist-info}/licenses/LICENSE +0 -0
plain/models/CHANGELOG.md
CHANGED
@@ -1,5 +1,30 @@
|
|
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
|
+
|
16
|
+
## [0.52.0](https://github.com/dropseed/plain/releases/plain-models@0.52.0) (2025-10-10)
|
17
|
+
|
18
|
+
### What's changed
|
19
|
+
|
20
|
+
- The `plain migrate` command now shows detailed operation descriptions and SQL statements for each migration step, replacing the previous verbosity levels with a cleaner `--quiet` flag ([d6b041bd24](https://github.com/dropseed/plain/commit/d6b041bd24))
|
21
|
+
- Migration output format has been improved to display each operation's description and the actual SQL being executed, making it easier to understand what changes are being made to the database ([d6b041bd24](https://github.com/dropseed/plain/commit/d6b041bd24))
|
22
|
+
- The `-v/--verbosity` option has been removed from `plain migrate` in favor of the simpler `--quiet` flag for suppressing output ([d6b041bd24](https://github.com/dropseed/plain/commit/d6b041bd24))
|
23
|
+
|
24
|
+
### Upgrade instructions
|
25
|
+
|
26
|
+
- Replace any usage of `-v` or `--verbosity` flags in `plain migrate` commands with `--quiet` if you want to suppress migration output
|
27
|
+
|
3
28
|
## [0.51.1](https://github.com/dropseed/plain/releases/plain-models@0.51.1) (2025-10-08)
|
4
29
|
|
5
30
|
### What's changed
|
@@ -52,9 +52,6 @@ class BaseDatabaseCreation:
|
|
52
52
|
settings.DATABASE["NAME"] = test_database_name
|
53
53
|
self.connection.settings_dict["NAME"] = test_database_name
|
54
54
|
|
55
|
-
# We report migrate messages at one level lower than that
|
56
|
-
# requested. This ensures we don't get flooded with messages during
|
57
|
-
# testing (unless you really ask to be flooded).
|
58
55
|
migrate.callback(
|
59
56
|
package_label=None,
|
60
57
|
migration_name=None,
|
@@ -62,10 +59,9 @@ class BaseDatabaseCreation:
|
|
62
59
|
plan=False,
|
63
60
|
check_unapplied=False,
|
64
61
|
backup=False,
|
65
|
-
prune=False,
|
66
62
|
no_input=True,
|
67
|
-
verbosity=max(verbosity - 1, 0),
|
68
63
|
atomic_batch=False, # No need for atomic batch when creating test database
|
64
|
+
quiet=verbosity < 2, # Show migration output when verbosity is 2+
|
69
65
|
)
|
70
66
|
|
71
67
|
# Ensure a connection for the side effect of initializing the test database.
|
@@ -159,19 +159,16 @@ class BaseDatabaseSchemaEditor:
|
|
159
159
|
def __init__(
|
160
160
|
self,
|
161
161
|
connection: BaseDatabaseWrapper,
|
162
|
-
collect_sql: bool = False,
|
163
162
|
atomic: bool = True,
|
164
163
|
):
|
165
164
|
self.connection = connection
|
166
|
-
self.collect_sql = collect_sql
|
167
|
-
if self.collect_sql:
|
168
|
-
self.collected_sql: list[str] = []
|
169
165
|
self.atomic_migration = self.connection.features.can_rollback_ddl and atomic
|
170
166
|
|
171
167
|
# State-managing methods
|
172
168
|
|
173
169
|
def __enter__(self) -> BaseDatabaseSchemaEditor:
|
174
170
|
self.deferred_sql: list[Any] = []
|
171
|
+
self.executed_sql: list[str] = []
|
175
172
|
if self.atomic_migration:
|
176
173
|
self.atomic = atomic()
|
177
174
|
self.atomic.__enter__()
|
@@ -190,11 +187,8 @@ class BaseDatabaseSchemaEditor:
|
|
190
187
|
self, sql: str | Statement, params: tuple[Any, ...] | list[Any] | None = ()
|
191
188
|
) -> None:
|
192
189
|
"""Execute the given SQL statement, with optional parameters."""
|
193
|
-
# Don't perform the transactional DDL check if SQL is being collected
|
194
|
-
# as it's not going to be executed anyway.
|
195
190
|
if (
|
196
|
-
|
197
|
-
and self.connection.in_atomic_block
|
191
|
+
self.connection.in_atomic_block
|
198
192
|
and not self.connection.features.can_rollback_ddl
|
199
193
|
):
|
200
194
|
raise TransactionManagementError(
|
@@ -207,17 +201,16 @@ class BaseDatabaseSchemaEditor:
|
|
207
201
|
logger.debug(
|
208
202
|
"%s; (params %r)", sql, params, extra={"params": params, "sql": sql}
|
209
203
|
)
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
)
|
216
|
-
else:
|
217
|
-
self.collected_sql.append(sql + ending)
|
204
|
+
|
205
|
+
# Track executed SQL for display in migration output
|
206
|
+
# Store the SQL for display (interpolate params for readability)
|
207
|
+
if params:
|
208
|
+
self.executed_sql.append(sql % tuple(map(self.quote_value, params)))
|
218
209
|
else:
|
219
|
-
|
220
|
-
|
210
|
+
self.executed_sql.append(sql)
|
211
|
+
|
212
|
+
with self.connection.cursor() as cursor:
|
213
|
+
cursor.execute(sql, params)
|
221
214
|
|
222
215
|
def quote_name(self, name: str) -> str:
|
223
216
|
return self.connection.ops.quote_name(name)
|
plain/models/cli.py
CHANGED
@@ -15,7 +15,7 @@ from plain.utils.text import Truncator
|
|
15
15
|
|
16
16
|
from . import migrations
|
17
17
|
from .backups.cli import cli as backups_cli
|
18
|
-
from .backups.
|
18
|
+
from .backups.core import DatabaseBackups
|
19
19
|
from .db import OperationalError
|
20
20
|
from .db import db_connection as _db_connection
|
21
21
|
from .migrations.autodetector import MigrationAutodetector
|
@@ -34,6 +34,7 @@ from .registry import models_registry
|
|
34
34
|
|
35
35
|
if TYPE_CHECKING:
|
36
36
|
from .backends.base.base import BaseDatabaseWrapper
|
37
|
+
from .migrations.operations.base import Operation
|
37
38
|
|
38
39
|
db_connection = cast("BaseDatabaseWrapper", _db_connection)
|
39
40
|
else:
|
@@ -365,11 +366,6 @@ def makemigrations(
|
|
365
366
|
default=None,
|
366
367
|
help="Explicitly enable/disable pre-migration backups.",
|
367
368
|
)
|
368
|
-
@click.option(
|
369
|
-
"--prune",
|
370
|
-
is_flag=True,
|
371
|
-
help="Delete nonexistent migrations from the plainmigrations table.",
|
372
|
-
)
|
373
369
|
@click.option(
|
374
370
|
"--no-input",
|
375
371
|
"--noinput",
|
@@ -377,18 +373,16 @@ def makemigrations(
|
|
377
373
|
is_flag=True,
|
378
374
|
help="Tells Plain to NOT prompt the user for input of any kind.",
|
379
375
|
)
|
380
|
-
@click.option(
|
381
|
-
"-v",
|
382
|
-
"--verbosity",
|
383
|
-
type=int,
|
384
|
-
default=1,
|
385
|
-
help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
|
386
|
-
)
|
387
376
|
@click.option(
|
388
377
|
"--atomic-batch/--no-atomic-batch",
|
389
378
|
default=None,
|
390
379
|
help="Run migrations in a single transaction (auto-detected by default)",
|
391
380
|
)
|
381
|
+
@click.option(
|
382
|
+
"--quiet",
|
383
|
+
is_flag=True,
|
384
|
+
help="Suppress migration output (used for test database creation).",
|
385
|
+
)
|
392
386
|
def migrate(
|
393
387
|
package_label: str | None,
|
394
388
|
migration_name: str | None,
|
@@ -396,28 +390,43 @@ def migrate(
|
|
396
390
|
plan: bool,
|
397
391
|
check_unapplied: bool,
|
398
392
|
backup: bool | None,
|
399
|
-
prune: bool,
|
400
393
|
no_input: bool,
|
401
|
-
verbosity: int,
|
402
394
|
atomic_batch: bool | None,
|
395
|
+
quiet: bool,
|
403
396
|
) -> None:
|
404
397
|
"""Updates database schema. Manages both packages with migrations and those without."""
|
405
398
|
|
406
399
|
def migration_progress_callback(
|
407
|
-
action: str,
|
400
|
+
action: str,
|
401
|
+
*,
|
402
|
+
migration: Migration | None = None,
|
403
|
+
fake: bool = False,
|
404
|
+
operation: Operation | None = None,
|
405
|
+
sql_statements: list[str] | None = None,
|
408
406
|
) -> None:
|
409
|
-
if
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
407
|
+
if quiet:
|
408
|
+
return
|
409
|
+
|
410
|
+
if action == "apply_start":
|
411
|
+
click.echo() # Always add newline between migrations
|
412
|
+
if fake:
|
413
|
+
click.secho(f"{migration} (faked)", fg="cyan")
|
414
|
+
else:
|
415
|
+
click.secho(f"{migration}", fg="cyan")
|
416
|
+
elif action == "apply_success":
|
417
|
+
pass # Already shown via operations
|
418
|
+
elif action == "operation_start":
|
419
|
+
click.echo(f" {operation.describe()}", nl=False)
|
420
|
+
click.secho("... ", dim=True, nl=False)
|
421
|
+
elif action == "operation_success":
|
422
|
+
# Show SQL statements (no OK needed, SQL implies success)
|
423
|
+
if sql_statements:
|
424
|
+
click.echo() # newline after "..."
|
425
|
+
for sql in sql_statements:
|
426
|
+
click.secho(f" {sql}", dim=True)
|
427
|
+
else:
|
428
|
+
# No SQL: just add a newline
|
429
|
+
click.echo()
|
421
430
|
|
422
431
|
def describe_operation(operation: Any) -> tuple[str, bool]:
|
423
432
|
"""Return a string that describes a migration operation for --plan."""
|
@@ -504,74 +513,22 @@ def migrate(
|
|
504
513
|
else:
|
505
514
|
targets = list(executor.loader.graph.leaf_nodes())
|
506
515
|
|
507
|
-
if prune:
|
508
|
-
if not package_label:
|
509
|
-
raise click.ClickException(
|
510
|
-
"Migrations can be pruned only when a package is specified."
|
511
|
-
)
|
512
|
-
if verbosity > 0:
|
513
|
-
click.secho("Pruning migrations:", fg="cyan")
|
514
|
-
to_prune = set(executor.loader.applied_migrations) - set( # type: ignore[arg-type]
|
515
|
-
executor.loader.disk_migrations # type: ignore[arg-type]
|
516
|
-
)
|
517
|
-
squashed_migrations_with_deleted_replaced_migrations = [
|
518
|
-
migration_key
|
519
|
-
for migration_key, migration_obj in executor.loader.replacements.items()
|
520
|
-
if any(replaced in to_prune for replaced in migration_obj.replaces)
|
521
|
-
]
|
522
|
-
if squashed_migrations_with_deleted_replaced_migrations:
|
523
|
-
click.echo(
|
524
|
-
click.style(
|
525
|
-
" Cannot use --prune because the following squashed "
|
526
|
-
"migrations have their 'replaces' attributes and may not "
|
527
|
-
"be recorded as applied:",
|
528
|
-
fg="yellow",
|
529
|
-
)
|
530
|
-
)
|
531
|
-
for migration in squashed_migrations_with_deleted_replaced_migrations:
|
532
|
-
package, name = migration
|
533
|
-
click.echo(f" {package}.{name}")
|
534
|
-
click.echo(
|
535
|
-
click.style(
|
536
|
-
" Re-run `plain migrate` if they are not marked as "
|
537
|
-
"applied, and remove 'replaces' attributes in their "
|
538
|
-
"Migration classes.",
|
539
|
-
fg="yellow",
|
540
|
-
)
|
541
|
-
)
|
542
|
-
else:
|
543
|
-
to_prune = sorted(
|
544
|
-
migration for migration in to_prune if migration[0] == package_label
|
545
|
-
)
|
546
|
-
if to_prune:
|
547
|
-
for migration in to_prune:
|
548
|
-
package, name = migration
|
549
|
-
if verbosity > 0:
|
550
|
-
click.echo(
|
551
|
-
click.style(f" Pruning {package}.{name}", fg="yellow"),
|
552
|
-
nl=False,
|
553
|
-
)
|
554
|
-
executor.recorder.record_unapplied(package, name)
|
555
|
-
if verbosity > 0:
|
556
|
-
click.echo(click.style(" OK", fg="green"))
|
557
|
-
elif verbosity > 0:
|
558
|
-
click.echo(" No migrations to prune.")
|
559
|
-
|
560
516
|
migration_plan = executor.migration_plan(targets)
|
561
517
|
|
562
518
|
if plan:
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
519
|
+
if not quiet:
|
520
|
+
click.secho("Planned operations:", fg="cyan")
|
521
|
+
if not migration_plan:
|
522
|
+
click.echo(" No planned migration operations.")
|
523
|
+
else:
|
524
|
+
for migration in migration_plan:
|
525
|
+
click.secho(str(migration), fg="cyan")
|
526
|
+
for operation in migration.operations:
|
527
|
+
message, is_error = describe_operation(operation)
|
528
|
+
if is_error:
|
529
|
+
click.secho(" " + message, fg="yellow")
|
530
|
+
else:
|
531
|
+
click.echo(" " + message)
|
575
532
|
if check_unapplied:
|
576
533
|
sys.exit(1)
|
577
534
|
return
|
@@ -581,33 +538,24 @@ def migrate(
|
|
581
538
|
sys.exit(1)
|
582
539
|
return
|
583
540
|
|
584
|
-
if prune:
|
585
|
-
return
|
586
|
-
|
587
541
|
# Print some useful info
|
588
|
-
if
|
589
|
-
click.secho("Operations to perform:", fg="cyan")
|
590
|
-
|
542
|
+
if not quiet:
|
591
543
|
if target_package_labels_only:
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
)
|
544
|
+
packages = ", ".join(sorted({a for a, n in targets})) or "(none)"
|
545
|
+
click.secho("Packages: ", bold=True, nl=False)
|
546
|
+
click.secho(packages, dim=True)
|
547
|
+
click.echo() # Add newline after packages
|
597
548
|
else:
|
598
|
-
click.secho(
|
599
|
-
|
600
|
-
|
601
|
-
)
|
549
|
+
click.secho("Target: ", bold=True, nl=False)
|
550
|
+
click.secho(f"{targets[0][1]} from {targets[0][0]}", dim=True)
|
551
|
+
click.echo() # Add newline after target
|
602
552
|
|
603
553
|
pre_migrate_state = executor._create_project_state(with_applied_migrations=True)
|
604
554
|
|
605
|
-
# sql = executor.loader.collect_sql(migration_plan)
|
606
|
-
# pprint(sql)
|
607
|
-
|
608
555
|
if migration_plan:
|
609
556
|
# Determine whether to use atomic batch
|
610
557
|
use_atomic_batch = False
|
558
|
+
atomic_batch_message = None
|
611
559
|
if len(migration_plan) > 1:
|
612
560
|
# Check database capabilities
|
613
561
|
can_rollback_ddl = db_connection.features.can_rollback_ddl
|
@@ -632,58 +580,62 @@ def migrate(
|
|
632
580
|
f"--atomic-batch requested but these migrations have atomic=False: {names}"
|
633
581
|
)
|
634
582
|
use_atomic_batch = True
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
)
|
583
|
+
atomic_batch_message = (
|
584
|
+
f"Running {len(migration_plan)} migrations in atomic batch"
|
585
|
+
)
|
639
586
|
elif atomic_batch is False:
|
640
587
|
# User explicitly disabled atomic batch
|
641
588
|
use_atomic_batch = False
|
642
|
-
if
|
643
|
-
|
589
|
+
if len(migration_plan) > 1:
|
590
|
+
atomic_batch_message = (
|
591
|
+
f"Running {len(migration_plan)} migrations separately"
|
592
|
+
)
|
644
593
|
else:
|
645
594
|
# Auto-detect (atomic_batch is None)
|
646
595
|
if can_rollback_ddl and not non_atomic_migrations:
|
647
596
|
use_atomic_batch = True
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
)
|
597
|
+
atomic_batch_message = (
|
598
|
+
f"Running {len(migration_plan)} migrations in atomic batch"
|
599
|
+
)
|
652
600
|
else:
|
653
601
|
use_atomic_batch = False
|
654
|
-
if
|
602
|
+
if len(migration_plan) > 1:
|
655
603
|
if not can_rollback_ddl:
|
656
|
-
|
657
|
-
f" Running {len(migration_plan)} migrations separately ({db_connection.vendor} doesn't support batch transactions)"
|
658
|
-
)
|
604
|
+
atomic_batch_message = f"Running {len(migration_plan)} migrations separately ({db_connection.vendor} doesn't support batch)"
|
659
605
|
elif non_atomic_migrations:
|
660
|
-
|
661
|
-
f" Running {len(migration_plan)} migrations separately (some migrations have atomic=False)"
|
662
|
-
)
|
606
|
+
atomic_batch_message = f"Running {len(migration_plan)} migrations separately (some have atomic=False)"
|
663
607
|
else:
|
664
|
-
|
665
|
-
f"
|
608
|
+
atomic_batch_message = (
|
609
|
+
f"Running {len(migration_plan)} migrations separately"
|
666
610
|
)
|
667
611
|
|
668
612
|
if backup or (backup is None and settings.DEBUG):
|
669
613
|
backup_name = f"migrate_{time.strftime('%Y%m%d_%H%M%S')}"
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
backup_name
|
678
|
-
pg_dump=os.environ.get(
|
679
|
-
"PG_DUMP", "pg_dump"
|
680
|
-
), # Have to pass this in manually
|
614
|
+
if not quiet:
|
615
|
+
click.secho("Creating backup: ", bold=True, nl=False)
|
616
|
+
click.secho(f"{backup_name}", dim=True, nl=False)
|
617
|
+
click.secho("... ", dim=True, nl=False)
|
618
|
+
|
619
|
+
backups_handler = DatabaseBackups()
|
620
|
+
backups_handler.create(
|
621
|
+
backup_name,
|
622
|
+
pg_dump=os.environ.get("PG_DUMP", "pg_dump"),
|
681
623
|
)
|
682
|
-
print()
|
683
624
|
|
684
|
-
|
685
|
-
|
625
|
+
if not quiet:
|
626
|
+
click.echo(click.style("OK", fg="green"))
|
627
|
+
click.echo() # Add blank line after backup output
|
628
|
+
else:
|
629
|
+
if not quiet:
|
630
|
+
click.echo() # Add blank line after packages/target info
|
686
631
|
|
632
|
+
if not quiet:
|
633
|
+
if atomic_batch_message:
|
634
|
+
click.secho(
|
635
|
+
f"Applying migrations ({atomic_batch_message.lower()}):", bold=True
|
636
|
+
)
|
637
|
+
else:
|
638
|
+
click.secho("Applying migrations:", bold=True)
|
687
639
|
post_migrate_state = executor.migrate(
|
688
640
|
targets,
|
689
641
|
plan=migration_plan,
|
@@ -712,31 +664,24 @@ def migrate(
|
|
712
664
|
]
|
713
665
|
)
|
714
666
|
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
changes = autodetector.changes(graph=executor.loader.graph)
|
724
|
-
if changes:
|
725
|
-
click.echo(
|
726
|
-
click.style(
|
727
|
-
f" Your models in package(s): {', '.join(repr(package) for package in sorted(changes))} "
|
728
|
-
"have changes that are not yet reflected in a migration, and so won't be applied.",
|
729
|
-
fg="yellow",
|
730
|
-
)
|
667
|
+
else:
|
668
|
+
if not quiet:
|
669
|
+
click.echo("No migrations to apply.")
|
670
|
+
# If there's changes that aren't in migrations yet, tell them
|
671
|
+
# how to fix it.
|
672
|
+
autodetector = MigrationAutodetector(
|
673
|
+
executor.loader.project_state(),
|
674
|
+
ProjectState.from_models_registry(models_registry),
|
731
675
|
)
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
"
|
737
|
-
|
676
|
+
changes = autodetector.changes(graph=executor.loader.graph)
|
677
|
+
if changes:
|
678
|
+
packages = ", ".join(sorted(changes))
|
679
|
+
click.echo(
|
680
|
+
f"Your models have changes that are not yet reflected in migrations ({packages})."
|
681
|
+
)
|
682
|
+
click.echo(
|
683
|
+
"Run 'plain makemigrations' to create migrations for these changes."
|
738
684
|
)
|
739
|
-
)
|
740
685
|
|
741
686
|
|
742
687
|
@cli.command()
|
@@ -819,35 +764,6 @@ def show_migrations(
|
|
819
764
|
if not shown:
|
820
765
|
click.secho(" (no migrations)", fg="red")
|
821
766
|
|
822
|
-
# Find recorded migrations that aren't in the graph (prunable)
|
823
|
-
prunable_migrations = [
|
824
|
-
migration
|
825
|
-
for migration in recorded_migrations
|
826
|
-
if (
|
827
|
-
migration not in loader.disk_migrations # type: ignore[operator]
|
828
|
-
and (not package_names_list or migration[0] in package_names_list)
|
829
|
-
)
|
830
|
-
]
|
831
|
-
|
832
|
-
if prunable_migrations:
|
833
|
-
click.echo()
|
834
|
-
click.secho(
|
835
|
-
"Recorded migrations not in migration files (candidates for pruning):",
|
836
|
-
fg="yellow",
|
837
|
-
bold=True,
|
838
|
-
)
|
839
|
-
prunable_by_package = {}
|
840
|
-
for migration in prunable_migrations:
|
841
|
-
package, name = migration
|
842
|
-
if package not in prunable_by_package:
|
843
|
-
prunable_by_package[package] = []
|
844
|
-
prunable_by_package[package].append(name)
|
845
|
-
|
846
|
-
for package in sorted(prunable_by_package.keys()):
|
847
|
-
click.secho(f" {package}:", fg="yellow")
|
848
|
-
for name in sorted(prunable_by_package[package]):
|
849
|
-
click.echo(f" - {name}")
|
850
|
-
|
851
767
|
def show_plan(db_connection: Any, package_names: tuple[str, ...]) -> None:
|
852
768
|
"""
|
853
769
|
Show all known migrations (or only those of the specified package_names)
|
@@ -900,6 +816,108 @@ def show_migrations(
|
|
900
816
|
show_list(db_connection, package_labels)
|
901
817
|
|
902
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
|
+
|
903
921
|
@cli.command()
|
904
922
|
@click.argument("package_label")
|
905
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
|
@@ -126,11 +128,7 @@ class MigrationExecutor:
|
|
126
128
|
break
|
127
129
|
if migration in migrations_to_run:
|
128
130
|
if "models_registry" not in state.__dict__:
|
129
|
-
if self.progress_callback:
|
130
|
-
self.progress_callback("render_start")
|
131
131
|
state.models_registry # Render all -- performance critical
|
132
|
-
if self.progress_callback:
|
133
|
-
self.progress_callback("render_success")
|
134
132
|
state = self.apply_migration(state, migration, fake=fake)
|
135
133
|
migrations_to_run.remove(migration)
|
136
134
|
|
@@ -145,13 +143,15 @@ class MigrationExecutor:
|
|
145
143
|
"""Run a migration forwards."""
|
146
144
|
migration_recorded = False
|
147
145
|
if self.progress_callback:
|
148
|
-
self.progress_callback("apply_start", migration, fake)
|
146
|
+
self.progress_callback("apply_start", migration=migration, fake=fake)
|
149
147
|
if not fake:
|
150
148
|
# Alright, do it normally
|
151
149
|
with self.connection.schema_editor(
|
152
150
|
atomic=migration.atomic
|
153
151
|
) as schema_editor:
|
154
|
-
state = migration.apply(
|
152
|
+
state = migration.apply(
|
153
|
+
state, schema_editor, operation_callback=self.progress_callback
|
154
|
+
)
|
155
155
|
if not schema_editor.deferred_sql:
|
156
156
|
self.record_migration(migration)
|
157
157
|
migration_recorded = True
|
@@ -159,7 +159,7 @@ class MigrationExecutor:
|
|
159
159
|
self.record_migration(migration)
|
160
160
|
# Report progress
|
161
161
|
if self.progress_callback:
|
162
|
-
self.progress_callback("apply_success", migration, fake)
|
162
|
+
self.progress_callback("apply_success", migration=migration, fake=fake)
|
163
163
|
return state
|
164
164
|
|
165
165
|
def record_migration(self, migration: Migration) -> None:
|
@@ -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.
|
@@ -370,23 +373,3 @@ class MigrationLoader:
|
|
370
373
|
return self.graph.make_state(
|
371
374
|
nodes=nodes, at_end=at_end, real_packages=self.unmigrated_packages
|
372
375
|
)
|
373
|
-
|
374
|
-
def collect_sql(self, plan: list[Migration]) -> list[str]:
|
375
|
-
"""
|
376
|
-
Take a migration plan and return a list of collected SQL statements
|
377
|
-
that represent the best-efforts version of that plan.
|
378
|
-
"""
|
379
|
-
statements = []
|
380
|
-
state = None
|
381
|
-
for migration in plan:
|
382
|
-
with self.connection.schema_editor(
|
383
|
-
collect_sql=True, atomic=migration.atomic
|
384
|
-
) as schema_editor:
|
385
|
-
if state is None:
|
386
|
-
state = self.project_state(
|
387
|
-
(migration.package_label, migration.name), at_end=False
|
388
|
-
)
|
389
|
-
|
390
|
-
state = migration.apply(state, schema_editor, collect_sql=True)
|
391
|
-
statements.extend(schema_editor.collected_sql)
|
392
|
-
return statements
|
@@ -1,11 +1,16 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
-
from
|
4
|
+
from collections.abc import Callable
|
5
|
+
from typing import TYPE_CHECKING, Any
|
5
6
|
|
6
7
|
from plain.models.migrations.utils import get_migration_name_timestamp
|
7
8
|
from plain.models.transaction import atomic
|
8
9
|
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from plain.models.backends.base.schema import BaseDatabaseSchemaEditor
|
12
|
+
from plain.models.migrations.state import ProjectState
|
13
|
+
|
9
14
|
|
10
15
|
class Migration:
|
11
16
|
"""
|
@@ -86,8 +91,11 @@ class Migration:
|
|
86
91
|
return new_state
|
87
92
|
|
88
93
|
def apply(
|
89
|
-
self,
|
90
|
-
|
94
|
+
self,
|
95
|
+
project_state: ProjectState,
|
96
|
+
schema_editor: BaseDatabaseSchemaEditor,
|
97
|
+
operation_callback: Callable[..., Any] | None = None,
|
98
|
+
) -> ProjectState:
|
91
99
|
"""
|
92
100
|
Take a project_state representing all migrations prior to this one
|
93
101
|
and a schema_editor for a live database and apply the migration
|
@@ -97,18 +105,11 @@ class Migration:
|
|
97
105
|
Migrations.
|
98
106
|
"""
|
99
107
|
for operation in self.operations:
|
100
|
-
#
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
schema_editor.collected_sql.append("--")
|
106
|
-
if not operation.reduces_to_sql:
|
107
|
-
schema_editor.collected_sql.append(
|
108
|
-
"-- THIS OPERATION CANNOT BE WRITTEN AS SQL"
|
109
|
-
)
|
110
|
-
continue
|
111
|
-
collected_sql_before = len(schema_editor.collected_sql)
|
108
|
+
# Clear any previous SQL statements before starting this operation
|
109
|
+
schema_editor.executed_sql = []
|
110
|
+
|
111
|
+
if operation_callback:
|
112
|
+
operation_callback("operation_start", operation=operation)
|
112
113
|
# Save the state before the operation has run
|
113
114
|
old_state = project_state.clone()
|
114
115
|
operation.state_forwards(self.package_label, project_state)
|
@@ -128,8 +129,13 @@ class Migration:
|
|
128
129
|
operation.database_forwards(
|
129
130
|
self.package_label, schema_editor, old_state, project_state
|
130
131
|
)
|
131
|
-
if
|
132
|
-
|
132
|
+
if operation_callback:
|
133
|
+
# Pass the accumulated SQL statements for this operation
|
134
|
+
operation_callback(
|
135
|
+
"operation_success",
|
136
|
+
operation=operation,
|
137
|
+
sql_statements=schema_editor.executed_sql,
|
138
|
+
)
|
133
139
|
return project_state
|
134
140
|
|
135
141
|
def suggest_name(self) -> str:
|
@@ -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
|
plain/models/preflight.py
CHANGED
@@ -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
|
@@ -1,10 +1,10 @@
|
|
1
1
|
plain/models/AGENTS.md,sha256=xQQW-z-DehnCUyjiGSBfLqUjoSUdo_W1b0JmwYmWieA,209
|
2
|
-
plain/models/CHANGELOG.md,sha256=
|
2
|
+
plain/models/CHANGELOG.md,sha256=opeamIL3UOaX7RoG3reojlYb9W0O9k6Yf6SWCuj0cwc,25083
|
3
3
|
plain/models/README.md,sha256=BW4a56bKkf2r-fkfK4SIU92th8h1geBNZ6j-XCv9yE4,8190
|
4
4
|
plain/models/__init__.py,sha256=S0HNxIS4PQ0mSNpo3PNOXExVnHXFmODQvdPhTCVrW-E,2903
|
5
5
|
plain/models/aggregates.py,sha256=z6AjlPlMI-henws9DYPwylL91sfrBPPHR7f9MNb2cjw,8043
|
6
6
|
plain/models/base.py,sha256=uB1OHfyd9hDDqEHck481MCUeXsiwuYmyM7lkS2uQ7ts,64448
|
7
|
-
plain/models/cli.py,sha256=
|
7
|
+
plain/models/cli.py,sha256=SUcDnRL6QT9nCduK_F6cdD4wgoe3TVcCpw8XK5mVGCc,41376
|
8
8
|
plain/models/config.py,sha256=vrrGZnmT0TCR_B-YF0VWZgG3iQ5syijaab2BW7Xfr7A,390
|
9
9
|
plain/models/connections.py,sha256=8DVH6Rl47wbfY_4wO6bGSoTuyJKoKz3tMB8fpe1cpqE,2937
|
10
10
|
plain/models/constants.py,sha256=ndnj9TOTKW0p4YcIPLOLEbsH6mOgFi6B1-rIzr_iwwU,210
|
@@ -23,7 +23,7 @@ plain/models/lookups.py,sha256=A-3rs3a2Obb-gQPs5RsQiCq-Shj21tznk2LwOeD4RXs,29447
|
|
23
23
|
plain/models/meta.py,sha256=alEKxzgwxmCRddq9Wonl1bjglDk1-x3CtEwWL3cGy6A,19942
|
24
24
|
plain/models/options.py,sha256=gTso-rRXxV6qbuAprsA1qflkQfsRqmcoETEQJmP0dGk,8593
|
25
25
|
plain/models/otel.py,sha256=6xsu5BhNhGXWRPNQVj0yzhsn1SryOhtCq_qzP8XL-qo,8010
|
26
|
-
plain/models/preflight.py,sha256=
|
26
|
+
plain/models/preflight.py,sha256=QmLr51J2GNtvm5y45SQ7oxdqq585VoifgHGgsm1unSM,12695
|
27
27
|
plain/models/query.py,sha256=-ZDJlt7aM5d3Sms_1Eu53h1UGnQvtKinFIhhLBgnfkY,98481
|
28
28
|
plain/models/query_utils.py,sha256=m_3FCbFZJjqkN6tcDuLrgA2n63RxUHOaXb_bJyQsS9k,16171
|
29
29
|
plain/models/registry.py,sha256=dIsgZolh7senbPil6xW_qLFIeIXkCtzysPcrpWdGAAY,8903
|
@@ -35,11 +35,11 @@ plain/models/backends/utils.py,sha256=wn4U_qrzGQxecJtcXS5VFds2Tg6VoJrs1TB_l8LN9A
|
|
35
35
|
plain/models/backends/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
36
36
|
plain/models/backends/base/base.py,sha256=465KAc8J2R8eIy3NmJSnrgMPK0K2DwDsylGKEzUkpsI,29099
|
37
37
|
plain/models/backends/base/client.py,sha256=xuMk4iXQjDju5cUspmOSpv6w2JnsB-gJa3RDvoP5I9I,1230
|
38
|
-
plain/models/backends/base/creation.py,sha256=
|
38
|
+
plain/models/backends/base/creation.py,sha256=xNrYqxheUiK1-CaKEQa-CGFcVzme0CpApqU--SY9a-4,9667
|
39
39
|
plain/models/backends/base/features.py,sha256=iRfB346Z9Ng3LIMvJAwr-8KxQthlSuvcdo9zBklE1-8,8067
|
40
40
|
plain/models/backends/base/introspection.py,sha256=3yeluBm6UHOlKikgVzGlKnDqBhyaqP-1FoSO4y_c-Hc,7613
|
41
41
|
plain/models/backends/base/operations.py,sha256=8m5XDZ5LtALaXbUX10QPSGDFKce17V5UeJDMkbSZdCs,29324
|
42
|
-
plain/models/backends/base/schema.py,sha256=
|
42
|
+
plain/models/backends/base/schema.py,sha256=H8Fdl4Gqyhc6aLifwYa5PsuPK3i-4oN6NTCR_61tBRs,70206
|
43
43
|
plain/models/backends/base/validation.py,sha256=Ok-TbVVi84zdPprKI--tUxYgoDl2PaxfNDiuYqZQBLM,1341
|
44
44
|
plain/models/backends/mysql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
45
|
plain/models/backends/mysql/base.py,sha256=6yNw8m4LpnNOOBVGzGZDPZGDAZKDnDbAWBO62Xg4yJY,14786
|
@@ -90,13 +90,13 @@ plain/models/functions/window.py,sha256=7rzVWpRMZ-1FptAWN_xPt-onh-tErL5sGfss63IL
|
|
90
90
|
plain/models/migrations/__init__.py,sha256=ZAQUGrfr_OxYMIO7vUBIHLs_M3oZ4iQSjDzCHRFUdtI,96
|
91
91
|
plain/models/migrations/autodetector.py,sha256=Aack1d0nGPx8UPRea0ZcSouMwE9hsXltOK6LIdlPAVs,62565
|
92
92
|
plain/models/migrations/exceptions.py,sha256=yfimKFZJpyw4IpH2LqZR7hL3J41PBSmXd1HbuWWayWY,1143
|
93
|
-
plain/models/migrations/executor.py,sha256=
|
93
|
+
plain/models/migrations/executor.py,sha256=6_BzYIxSf2ayN7ITFo5Yxj0xdoiLAu1kv8TpE-dTttw,7546
|
94
94
|
plain/models/migrations/graph.py,sha256=SFSk2QxG8ni9LxtnfuMHeN2qQeI3lv4jD-R-y_i2VL0,14101
|
95
|
-
plain/models/migrations/loader.py,sha256=
|
96
|
-
plain/models/migrations/migration.py,sha256=
|
95
|
+
plain/models/migrations/loader.py,sha256=vr717xlnahSzmEkAGnIJp9_FFoOIilS_2nwH7Ww-vd4,16837
|
96
|
+
plain/models/migrations/migration.py,sha256=I2Nsu-8YlTKXZBGCufzK50XeCIsEMSz-I5Vg3r6Hf9s,6953
|
97
97
|
plain/models/migrations/optimizer.py,sha256=Kk8-KBB51mYiR96hp8F7ZhFvdVg7DYyujpKmp8b9T6A,3385
|
98
98
|
plain/models/migrations/questioner.py,sha256=1uhQqPooxAaJuXe_W_B9X5df8MOoO_G7IFcubQxd6lE,12980
|
99
|
-
plain/models/migrations/recorder.py,sha256=
|
99
|
+
plain/models/migrations/recorder.py,sha256=e268sBTptmYVqRitlkza_mTdmWklUk-jjpMCMrn6zIY,4252
|
100
100
|
plain/models/migrations/serializer.py,sha256=QWz5PvDSWKgHjae1McEIPwahQTm17ZES2Kf6aVebrr0,13410
|
101
101
|
plain/models/migrations/state.py,sha256=3wmARJZ8XgCnkW-xvg2r2VWrhl1GoLRQmBFIh2EFa3w,35795
|
102
102
|
plain/models/migrations/utils.py,sha256=MkrO_ZHCLQVlhgJPVxIlkZ7tqpQ_BDa6i07T-OCORnE,4796
|
@@ -116,8 +116,8 @@ plain/models/sql/where.py,sha256=GeTopzVmvZTqm2NTS32ok0rHbNgoEREUVtsD7usrlCA,138
|
|
116
116
|
plain/models/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
117
117
|
plain/models/test/pytest.py,sha256=sZtHzmNoqIFb7csZ8fqbRwljQ0vWrcMcDm6Wk_0g-uk,3924
|
118
118
|
plain/models/test/utils.py,sha256=eduH039cMVixWORfsUr7qkk0YDkTHPXFZklm9lzY474,540
|
119
|
-
plain_models-0.
|
120
|
-
plain_models-0.
|
121
|
-
plain_models-0.
|
122
|
-
plain_models-0.
|
123
|
-
plain_models-0.
|
119
|
+
plain_models-0.53.0.dist-info/METADATA,sha256=EiyfdHYM2rkhwORfwuTzZQBmq7js5m7vqWmITs43UHA,8502
|
120
|
+
plain_models-0.53.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
121
|
+
plain_models-0.53.0.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
|
122
|
+
plain_models-0.53.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
123
|
+
plain_models-0.53.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|