plain.models 0.51.1__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.51.1 → plain_models-0.53.0}/PKG-INFO +1 -1
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/CHANGELOG.md +25 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/creation.py +1 -5
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/schema.py +11 -18
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/cli.py +214 -196
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/executor.py +8 -8
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/loader.py +5 -22
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/migration.py +23 -17
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/recorder.py +2 -1
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/preflight.py +80 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/pyproject.toml +1 -1
- {plain_models-0.51.1 → plain_models-0.53.0}/.gitignore +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/LICENSE +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/README.md +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/AGENTS.md +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/README.md +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/aggregates.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/base.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/client.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/features.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/introspection.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/operations.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/base/validation.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/ddl_references.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/base.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/client.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/compiler.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/creation.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/features.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/introspection.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/operations.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/schema.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/mysql/validation.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/postgresql/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/postgresql/base.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/postgresql/client.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/postgresql/creation.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/postgresql/features.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/postgresql/introspection.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/postgresql/operations.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/postgresql/schema.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/_functions.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/base.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/client.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/creation.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/features.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/introspection.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/operations.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/sqlite3/schema.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backends/utils.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backups/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backups/cli.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backups/clients.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/backups/core.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/base.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/config.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/connections.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/constants.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/constraints.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/database_url.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/db.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/default_settings.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/deletion.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/entrypoints.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/enums.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/exceptions.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/expressions.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/fields/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/fields/json.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/fields/mixins.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/fields/related.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/fields/related_descriptors.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/fields/related_lookups.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/fields/related_managers.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/fields/reverse_related.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/forms.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/functions/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/functions/comparison.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/functions/datetime.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/functions/math.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/functions/mixins.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/functions/text.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/functions/window.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/indexes.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/lookups.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/meta.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/autodetector.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/exceptions.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/graph.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/operations/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/operations/base.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/operations/fields.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/operations/models.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/operations/special.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/optimizer.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/questioner.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/serializer.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/state.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/utils.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/migrations/writer.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/options.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/otel.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/query.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/query_utils.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/registry.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/sql/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/sql/compiler.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/sql/constants.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/sql/datastructures.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/sql/query.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/sql/subqueries.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/sql/where.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/test/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/test/pytest.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/test/utils.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/transaction.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/plain/models/utils.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/app/examples/models.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/app/settings.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/app/urls.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/test_database_url.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/test_delete_behaviors.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/test_exceptions.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/test_manager_assignment.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/test_models.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/test_related_descriptors.py +0 -0
- {plain_models-0.51.1 → plain_models-0.53.0}/tests/test_related_manager_api.py +0 -0
@@ -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)
|
@@ -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)
|