plain.models 0.51.0__py3-none-any.whl → 0.52.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 +22 -0
- plain/models/backends/base/creation.py +1 -4
- plain/models/backends/base/schema.py +11 -18
- plain/models/cli.py +139 -133
- plain/models/expressions.py +20 -4
- plain/models/migrations/executor.py +5 -7
- plain/models/migrations/loader.py +0 -20
- plain/models/migrations/migration.py +23 -17
- {plain_models-0.51.0.dist-info → plain_models-0.52.0.dist-info}/METADATA +1 -1
- {plain_models-0.51.0.dist-info → plain_models-0.52.0.dist-info}/RECORD +13 -13
- {plain_models-0.51.0.dist-info → plain_models-0.52.0.dist-info}/WHEEL +0 -0
- {plain_models-0.51.0.dist-info → plain_models-0.52.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.51.0.dist-info → plain_models-0.52.0.dist-info}/licenses/LICENSE +0 -0
plain/models/CHANGELOG.md
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
# plain-models changelog
|
2
2
|
|
3
|
+
## [0.52.0](https://github.com/dropseed/plain/releases/plain-models@0.52.0) (2025-10-10)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- 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))
|
8
|
+
- 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))
|
9
|
+
- 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))
|
10
|
+
|
11
|
+
### Upgrade instructions
|
12
|
+
|
13
|
+
- Replace any usage of `-v` or `--verbosity` flags in `plain migrate` commands with `--quiet` if you want to suppress migration output
|
14
|
+
|
15
|
+
## [0.51.1](https://github.com/dropseed/plain/releases/plain-models@0.51.1) (2025-10-08)
|
16
|
+
|
17
|
+
### What's changed
|
18
|
+
|
19
|
+
- Fixed a bug in `Subquery` and `Exists` expressions that was using the old `query` attribute name instead of `sql_query` when extracting the SQL query from a QuerySet ([79ca52d](https://github.com/dropseed/plain/commit/79ca52d32e))
|
20
|
+
|
21
|
+
### Upgrade instructions
|
22
|
+
|
23
|
+
- No changes required
|
24
|
+
|
3
25
|
## [0.51.0](https://github.com/dropseed/plain/releases/plain-models@0.51.0) (2025-10-07)
|
4
26
|
|
5
27
|
### 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,
|
@@ -64,8 +61,8 @@ class BaseDatabaseCreation:
|
|
64
61
|
backup=False,
|
65
62
|
prune=False,
|
66
63
|
no_input=True,
|
67
|
-
verbosity=max(verbosity - 1, 0),
|
68
64
|
atomic_batch=False, # No need for atomic batch when creating test database
|
65
|
+
quiet=verbosity < 2, # Show migration output when verbosity is 2+
|
69
66
|
)
|
70
67
|
|
71
68
|
# 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:
|
@@ -377,18 +378,16 @@ def makemigrations(
|
|
377
378
|
is_flag=True,
|
378
379
|
help="Tells Plain to NOT prompt the user for input of any kind.",
|
379
380
|
)
|
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
381
|
@click.option(
|
388
382
|
"--atomic-batch/--no-atomic-batch",
|
389
383
|
default=None,
|
390
384
|
help="Run migrations in a single transaction (auto-detected by default)",
|
391
385
|
)
|
386
|
+
@click.option(
|
387
|
+
"--quiet",
|
388
|
+
is_flag=True,
|
389
|
+
help="Suppress migration output (used for test database creation).",
|
390
|
+
)
|
392
391
|
def migrate(
|
393
392
|
package_label: str | None,
|
394
393
|
migration_name: str | None,
|
@@ -398,26 +397,42 @@ def migrate(
|
|
398
397
|
backup: bool | None,
|
399
398
|
prune: bool,
|
400
399
|
no_input: bool,
|
401
|
-
verbosity: int,
|
402
400
|
atomic_batch: bool | None,
|
401
|
+
quiet: bool,
|
403
402
|
) -> None:
|
404
403
|
"""Updates database schema. Manages both packages with migrations and those without."""
|
405
404
|
|
406
405
|
def migration_progress_callback(
|
407
|
-
action: str,
|
406
|
+
action: str,
|
407
|
+
*,
|
408
|
+
migration: Migration | None = None,
|
409
|
+
fake: bool = False,
|
410
|
+
operation: Operation | None = None,
|
411
|
+
sql_statements: list[str] | None = None,
|
408
412
|
) -> None:
|
409
|
-
if
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
413
|
+
if quiet:
|
414
|
+
return
|
415
|
+
|
416
|
+
if action == "apply_start":
|
417
|
+
click.echo() # Always add newline between migrations
|
418
|
+
if fake:
|
419
|
+
click.secho(f"{migration} (faked)", fg="cyan")
|
420
|
+
else:
|
421
|
+
click.secho(f"{migration}", fg="cyan")
|
422
|
+
elif action == "apply_success":
|
423
|
+
pass # Already shown via operations
|
424
|
+
elif action == "operation_start":
|
425
|
+
click.echo(f" {operation.describe()}", nl=False)
|
426
|
+
click.secho("... ", dim=True, nl=False)
|
427
|
+
elif action == "operation_success":
|
428
|
+
# Show SQL statements (no OK needed, SQL implies success)
|
429
|
+
if sql_statements:
|
430
|
+
click.echo() # newline after "..."
|
431
|
+
for sql in sql_statements:
|
432
|
+
click.secho(f" {sql}", dim=True)
|
433
|
+
else:
|
434
|
+
# No SQL: just add a newline
|
435
|
+
click.echo()
|
421
436
|
|
422
437
|
def describe_operation(operation: Any) -> tuple[str, bool]:
|
423
438
|
"""Return a string that describes a migration operation for --plan."""
|
@@ -509,8 +524,8 @@ def migrate(
|
|
509
524
|
raise click.ClickException(
|
510
525
|
"Migrations can be pruned only when a package is specified."
|
511
526
|
)
|
512
|
-
if
|
513
|
-
click.secho("Pruning migrations:",
|
527
|
+
if not quiet:
|
528
|
+
click.secho("Pruning migrations:", bold=True)
|
514
529
|
to_prune = set(executor.loader.applied_migrations) - set( # type: ignore[arg-type]
|
515
530
|
executor.loader.disk_migrations # type: ignore[arg-type]
|
516
531
|
)
|
@@ -520,25 +535,26 @@ def migrate(
|
|
520
535
|
if any(replaced in to_prune for replaced in migration_obj.replaces)
|
521
536
|
]
|
522
537
|
if squashed_migrations_with_deleted_replaced_migrations:
|
523
|
-
|
524
|
-
click.
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
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
|
+
)
|
529
546
|
)
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
click.echo(
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
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
|
+
)
|
540
557
|
)
|
541
|
-
)
|
542
558
|
else:
|
543
559
|
to_prune = sorted(
|
544
560
|
migration for migration in to_prune if migration[0] == package_label
|
@@ -546,32 +562,31 @@ def migrate(
|
|
546
562
|
if to_prune:
|
547
563
|
for migration in to_prune:
|
548
564
|
package, name = migration
|
549
|
-
if
|
550
|
-
click.echo(
|
551
|
-
click.style(f" Pruning {package}.{name}", fg="yellow"),
|
552
|
-
nl=False,
|
553
|
-
)
|
565
|
+
if not quiet:
|
566
|
+
click.echo(f" Pruning {package}.{name}...", nl=False)
|
554
567
|
executor.recorder.record_unapplied(package, name)
|
555
|
-
if
|
556
|
-
click.echo(
|
557
|
-
|
558
|
-
|
568
|
+
if not quiet:
|
569
|
+
click.echo(" OK")
|
570
|
+
else:
|
571
|
+
if not quiet:
|
572
|
+
click.echo(" No migrations to prune.")
|
559
573
|
|
560
574
|
migration_plan = executor.migration_plan(targets)
|
561
575
|
|
562
576
|
if plan:
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
577
|
+
if not quiet:
|
578
|
+
click.secho("Planned operations:", fg="cyan")
|
579
|
+
if not migration_plan:
|
580
|
+
click.echo(" No planned migration operations.")
|
581
|
+
else:
|
582
|
+
for migration in migration_plan:
|
583
|
+
click.secho(str(migration), fg="cyan")
|
584
|
+
for operation in migration.operations:
|
585
|
+
message, is_error = describe_operation(operation)
|
586
|
+
if is_error:
|
587
|
+
click.secho(" " + message, fg="yellow")
|
588
|
+
else:
|
589
|
+
click.echo(" " + message)
|
575
590
|
if check_unapplied:
|
576
591
|
sys.exit(1)
|
577
592
|
return
|
@@ -585,29 +600,23 @@ def migrate(
|
|
585
600
|
return
|
586
601
|
|
587
602
|
# Print some useful info
|
588
|
-
if
|
589
|
-
click.secho("Operations to perform:", fg="cyan")
|
590
|
-
|
603
|
+
if not quiet:
|
591
604
|
if target_package_labels_only:
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
)
|
605
|
+
packages = ", ".join(sorted({a for a, n in targets})) or "(none)"
|
606
|
+
click.secho("Packages: ", bold=True, nl=False)
|
607
|
+
click.secho(packages, dim=True)
|
608
|
+
click.echo() # Add newline after packages
|
597
609
|
else:
|
598
|
-
click.secho(
|
599
|
-
|
600
|
-
|
601
|
-
)
|
610
|
+
click.secho("Target: ", bold=True, nl=False)
|
611
|
+
click.secho(f"{targets[0][1]} from {targets[0][0]}", dim=True)
|
612
|
+
click.echo() # Add newline after target
|
602
613
|
|
603
614
|
pre_migrate_state = executor._create_project_state(with_applied_migrations=True)
|
604
615
|
|
605
|
-
# sql = executor.loader.collect_sql(migration_plan)
|
606
|
-
# pprint(sql)
|
607
|
-
|
608
616
|
if migration_plan:
|
609
617
|
# Determine whether to use atomic batch
|
610
618
|
use_atomic_batch = False
|
619
|
+
atomic_batch_message = None
|
611
620
|
if len(migration_plan) > 1:
|
612
621
|
# Check database capabilities
|
613
622
|
can_rollback_ddl = db_connection.features.can_rollback_ddl
|
@@ -632,58 +641,62 @@ def migrate(
|
|
632
641
|
f"--atomic-batch requested but these migrations have atomic=False: {names}"
|
633
642
|
)
|
634
643
|
use_atomic_batch = True
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
)
|
644
|
+
atomic_batch_message = (
|
645
|
+
f"Running {len(migration_plan)} migrations in atomic batch"
|
646
|
+
)
|
639
647
|
elif atomic_batch is False:
|
640
648
|
# User explicitly disabled atomic batch
|
641
649
|
use_atomic_batch = False
|
642
|
-
if
|
643
|
-
|
650
|
+
if len(migration_plan) > 1:
|
651
|
+
atomic_batch_message = (
|
652
|
+
f"Running {len(migration_plan)} migrations separately"
|
653
|
+
)
|
644
654
|
else:
|
645
655
|
# Auto-detect (atomic_batch is None)
|
646
656
|
if can_rollback_ddl and not non_atomic_migrations:
|
647
657
|
use_atomic_batch = True
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
)
|
658
|
+
atomic_batch_message = (
|
659
|
+
f"Running {len(migration_plan)} migrations in atomic batch"
|
660
|
+
)
|
652
661
|
else:
|
653
662
|
use_atomic_batch = False
|
654
|
-
if
|
663
|
+
if len(migration_plan) > 1:
|
655
664
|
if not can_rollback_ddl:
|
656
|
-
|
657
|
-
f" Running {len(migration_plan)} migrations separately ({db_connection.vendor} doesn't support batch transactions)"
|
658
|
-
)
|
665
|
+
atomic_batch_message = f"Running {len(migration_plan)} migrations separately ({db_connection.vendor} doesn't support batch)"
|
659
666
|
elif non_atomic_migrations:
|
660
|
-
|
661
|
-
f" Running {len(migration_plan)} migrations separately (some migrations have atomic=False)"
|
662
|
-
)
|
667
|
+
atomic_batch_message = f"Running {len(migration_plan)} migrations separately (some have atomic=False)"
|
663
668
|
else:
|
664
|
-
|
665
|
-
f"
|
669
|
+
atomic_batch_message = (
|
670
|
+
f"Running {len(migration_plan)} migrations separately"
|
666
671
|
)
|
667
672
|
|
668
673
|
if backup or (backup is None and settings.DEBUG):
|
669
674
|
backup_name = f"migrate_{time.strftime('%Y%m%d_%H%M%S')}"
|
670
|
-
|
671
|
-
|
672
|
-
|
675
|
+
if not quiet:
|
676
|
+
click.secho("Creating backup: ", bold=True, nl=False)
|
677
|
+
click.secho(f"{backup_name}", dim=True, nl=False)
|
678
|
+
click.secho("... ", dim=True, nl=False)
|
679
|
+
|
680
|
+
backups_handler = DatabaseBackups()
|
681
|
+
backups_handler.create(
|
682
|
+
backup_name,
|
683
|
+
pg_dump=os.environ.get("PG_DUMP", "pg_dump"),
|
673
684
|
)
|
674
|
-
# Can't use ctx.invoke because this is called by the test db creation currently,
|
675
|
-
# which doesn't have a context.
|
676
|
-
create_backup.callback(
|
677
|
-
backup_name=backup_name,
|
678
|
-
pg_dump=os.environ.get(
|
679
|
-
"PG_DUMP", "pg_dump"
|
680
|
-
), # Have to pass this in manually
|
681
|
-
)
|
682
|
-
print()
|
683
685
|
|
684
|
-
|
685
|
-
|
686
|
+
if not quiet:
|
687
|
+
click.echo(click.style("OK", fg="green"))
|
688
|
+
click.echo() # Add blank line after backup output
|
689
|
+
else:
|
690
|
+
if not quiet:
|
691
|
+
click.echo() # Add blank line after packages/target info
|
686
692
|
|
693
|
+
if not quiet:
|
694
|
+
if atomic_batch_message:
|
695
|
+
click.secho(
|
696
|
+
f"Applying migrations ({atomic_batch_message.lower()}):", bold=True
|
697
|
+
)
|
698
|
+
else:
|
699
|
+
click.secho("Applying migrations:", bold=True)
|
687
700
|
post_migrate_state = executor.migrate(
|
688
701
|
targets,
|
689
702
|
plan=migration_plan,
|
@@ -712,31 +725,24 @@ def migrate(
|
|
712
725
|
]
|
713
726
|
)
|
714
727
|
|
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
|
-
)
|
728
|
+
else:
|
729
|
+
if not quiet:
|
730
|
+
click.echo("No migrations to apply.")
|
731
|
+
# If there's changes that aren't in migrations yet, tell them
|
732
|
+
# how to fix it.
|
733
|
+
autodetector = MigrationAutodetector(
|
734
|
+
executor.loader.project_state(),
|
735
|
+
ProjectState.from_models_registry(models_registry),
|
731
736
|
)
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
"
|
737
|
-
|
737
|
+
changes = autodetector.changes(graph=executor.loader.graph)
|
738
|
+
if changes:
|
739
|
+
packages = ", ".join(sorted(changes))
|
740
|
+
click.echo(
|
741
|
+
f"Your models have changes that are not yet reflected in migrations ({packages})."
|
742
|
+
)
|
743
|
+
click.echo(
|
744
|
+
"Run 'plain makemigrations' to create migrations for these changes."
|
738
745
|
)
|
739
|
-
)
|
740
746
|
|
741
747
|
|
742
748
|
@cli.command()
|
plain/models/expressions.py
CHANGED
@@ -28,7 +28,9 @@ if TYPE_CHECKING:
|
|
28
28
|
|
29
29
|
from plain.models.backends.base.base import BaseDatabaseWrapper
|
30
30
|
from plain.models.fields import Field
|
31
|
+
from plain.models.query import QuerySet
|
31
32
|
from plain.models.sql.compiler import SQLCompiler
|
33
|
+
from plain.models.sql.query import Query
|
32
34
|
|
33
35
|
|
34
36
|
class SQLiteNumericMixin:
|
@@ -1635,9 +1637,23 @@ class Subquery(BaseExpression, Combinable):
|
|
1635
1637
|
contains_aggregate = False
|
1636
1638
|
empty_result_set_value = None
|
1637
1639
|
|
1638
|
-
def __init__(
|
1640
|
+
def __init__(
|
1641
|
+
self,
|
1642
|
+
query: QuerySet[Any] | Query,
|
1643
|
+
output_field: Field | None = None,
|
1644
|
+
**extra: Any,
|
1645
|
+
):
|
1646
|
+
# Import here to avoid circular import
|
1647
|
+
from plain.models.sql.query import Query
|
1648
|
+
|
1639
1649
|
# Allow the usage of both QuerySet and sql.Query objects.
|
1640
|
-
|
1650
|
+
if isinstance(query, Query):
|
1651
|
+
# It's already a Query object, use it directly
|
1652
|
+
sql_query = query
|
1653
|
+
else:
|
1654
|
+
# It's a QuerySet, extract the sql.Query
|
1655
|
+
sql_query = query.sql_query
|
1656
|
+
self.query = sql_query.clone()
|
1641
1657
|
self.query.subquery = True
|
1642
1658
|
self.extra = extra
|
1643
1659
|
super().__init__(output_field)
|
@@ -1688,8 +1704,8 @@ class Exists(Subquery):
|
|
1688
1704
|
output_field = fields.BooleanField()
|
1689
1705
|
empty_result_set_value = False
|
1690
1706
|
|
1691
|
-
def __init__(self,
|
1692
|
-
super().__init__(
|
1707
|
+
def __init__(self, query: QuerySet[Any] | Query, **kwargs: Any):
|
1708
|
+
super().__init__(query, **kwargs)
|
1693
1709
|
self.query = self.query.exists()
|
1694
1710
|
|
1695
1711
|
def select_format(
|
@@ -126,11 +126,7 @@ class MigrationExecutor:
|
|
126
126
|
break
|
127
127
|
if migration in migrations_to_run:
|
128
128
|
if "models_registry" not in state.__dict__:
|
129
|
-
if self.progress_callback:
|
130
|
-
self.progress_callback("render_start")
|
131
129
|
state.models_registry # Render all -- performance critical
|
132
|
-
if self.progress_callback:
|
133
|
-
self.progress_callback("render_success")
|
134
130
|
state = self.apply_migration(state, migration, fake=fake)
|
135
131
|
migrations_to_run.remove(migration)
|
136
132
|
|
@@ -145,13 +141,15 @@ class MigrationExecutor:
|
|
145
141
|
"""Run a migration forwards."""
|
146
142
|
migration_recorded = False
|
147
143
|
if self.progress_callback:
|
148
|
-
self.progress_callback("apply_start", migration, fake)
|
144
|
+
self.progress_callback("apply_start", migration=migration, fake=fake)
|
149
145
|
if not fake:
|
150
146
|
# Alright, do it normally
|
151
147
|
with self.connection.schema_editor(
|
152
148
|
atomic=migration.atomic
|
153
149
|
) as schema_editor:
|
154
|
-
state = migration.apply(
|
150
|
+
state = migration.apply(
|
151
|
+
state, schema_editor, operation_callback=self.progress_callback
|
152
|
+
)
|
155
153
|
if not schema_editor.deferred_sql:
|
156
154
|
self.record_migration(migration)
|
157
155
|
migration_recorded = True
|
@@ -159,7 +157,7 @@ class MigrationExecutor:
|
|
159
157
|
self.record_migration(migration)
|
160
158
|
# Report progress
|
161
159
|
if self.progress_callback:
|
162
|
-
self.progress_callback("apply_success", migration, fake)
|
160
|
+
self.progress_callback("apply_success", migration=migration, fake=fake)
|
163
161
|
return state
|
164
162
|
|
165
163
|
def record_migration(self, migration: Migration) -> None:
|
@@ -370,23 +370,3 @@ class MigrationLoader:
|
|
370
370
|
return self.graph.make_state(
|
371
371
|
nodes=nodes, at_end=at_end, real_packages=self.unmigrated_packages
|
372
372
|
)
|
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:
|
@@ -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=ytDTrb6495Nue9PudyuT_JmaqscsoV-U8IntQs9rS1s,24147
|
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=kFJqqCOIVBF5gL3T_obIxjxMbw83gUcOFUdQanvSlTc,41478
|
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
|
@@ -16,7 +16,7 @@ plain/models/deletion.py,sha256=xQ1X_2W45h0L16R-8HAYceb0q4XhEidY6lCRPi8vvJ0,1908
|
|
16
16
|
plain/models/entrypoints.py,sha256=8IyQlCia0c9ZZaWHOA9FrqmN0iG_z9EeFoqEZifsdH4,199
|
17
17
|
plain/models/enums.py,sha256=IOqdywY_TNNJIDp-reaRlbTL-1uVw3jwQ4NpVT8yjDs,3049
|
18
18
|
plain/models/exceptions.py,sha256=DOnRGvbEHEUDr3ioafhK1rSBnMeJ_Q4VW7gZXEIj7SU,5492
|
19
|
-
plain/models/expressions.py,sha256=
|
19
|
+
plain/models/expressions.py,sha256=5Bf8V0wMMVzLueDEpTH5f_8skRspq6nNYi5y1S3fNAo,71223
|
20
20
|
plain/models/forms.py,sha256=dkX3or5TROgKAzkLayek63pngYrcIYdijZQS0LcYhhI,27428
|
21
21
|
plain/models/indexes.py,sha256=fiQ1F-zDVJPSSGb_zaH4Kj1H_dBkMpXM2znHWA2KLjk,12761
|
22
22
|
plain/models/lookups.py,sha256=A-3rs3a2Obb-gQPs5RsQiCq-Shj21tznk2LwOeD4RXs,29447
|
@@ -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=zrXFYRNc0GJkmwIYYZ3UP8kyFo5u611ixqpxg0PgFWk,9692
|
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,10 +90,10 @@ 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=ZWCkiW9BLK3S0uia_Vj4U0piE971JSLMn3QbVjTsZqk,7468
|
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=Ojr42-swwqsiJt88K-xj5glQqqnGl-3wYN0lgJcxIi8,16725
|
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
99
|
plain/models/migrations/recorder.py,sha256=4rxOUmwJRkyKt5WVTQtnfsXnesV5Ue--1MpXYFltbto,4175
|
@@ -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.52.0.dist-info/METADATA,sha256=b-TEpaQ3ZGed7rk0_UliwNwaekE0DJxLWgCaL47VS7w,8502
|
120
|
+
plain_models-0.52.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
121
|
+
plain_models-0.52.0.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
|
122
|
+
plain_models-0.52.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
123
|
+
plain_models-0.52.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|