plain.models 0.51.0__tar.gz → 0.52.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.
Files changed (138) hide show
  1. {plain_models-0.51.0 → plain_models-0.52.0}/PKG-INFO +1 -1
  2. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/CHANGELOG.md +22 -0
  3. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/creation.py +1 -4
  4. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/schema.py +11 -18
  5. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/cli.py +139 -133
  6. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/expressions.py +20 -4
  7. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/executor.py +5 -7
  8. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/loader.py +0 -20
  9. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/migration.py +23 -17
  10. {plain_models-0.51.0 → plain_models-0.52.0}/pyproject.toml +1 -1
  11. {plain_models-0.51.0 → plain_models-0.52.0}/.gitignore +0 -0
  12. {plain_models-0.51.0 → plain_models-0.52.0}/LICENSE +0 -0
  13. {plain_models-0.51.0 → plain_models-0.52.0}/README.md +0 -0
  14. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/AGENTS.md +0 -0
  15. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/README.md +0 -0
  16. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/__init__.py +0 -0
  17. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/aggregates.py +0 -0
  18. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/__init__.py +0 -0
  19. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/__init__.py +0 -0
  20. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/base.py +0 -0
  21. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/client.py +0 -0
  22. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/features.py +0 -0
  23. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/introspection.py +0 -0
  24. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/operations.py +0 -0
  25. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/base/validation.py +0 -0
  26. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/ddl_references.py +0 -0
  27. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/__init__.py +0 -0
  28. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/base.py +0 -0
  29. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/client.py +0 -0
  30. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/compiler.py +0 -0
  31. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/creation.py +0 -0
  32. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/features.py +0 -0
  33. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/introspection.py +0 -0
  34. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/operations.py +0 -0
  35. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/schema.py +0 -0
  36. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/mysql/validation.py +0 -0
  37. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/postgresql/__init__.py +0 -0
  38. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/postgresql/base.py +0 -0
  39. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/postgresql/client.py +0 -0
  40. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/postgresql/creation.py +0 -0
  41. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/postgresql/features.py +0 -0
  42. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/postgresql/introspection.py +0 -0
  43. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/postgresql/operations.py +0 -0
  44. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/postgresql/schema.py +0 -0
  45. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/__init__.py +0 -0
  46. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/_functions.py +0 -0
  47. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/base.py +0 -0
  48. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/client.py +0 -0
  49. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/creation.py +0 -0
  50. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/features.py +0 -0
  51. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/introspection.py +0 -0
  52. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/operations.py +0 -0
  53. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/sqlite3/schema.py +0 -0
  54. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backends/utils.py +0 -0
  55. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backups/__init__.py +0 -0
  56. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backups/cli.py +0 -0
  57. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backups/clients.py +0 -0
  58. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/backups/core.py +0 -0
  59. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/base.py +0 -0
  60. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/config.py +0 -0
  61. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/connections.py +0 -0
  62. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/constants.py +0 -0
  63. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/constraints.py +0 -0
  64. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/database_url.py +0 -0
  65. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/db.py +0 -0
  66. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/default_settings.py +0 -0
  67. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/deletion.py +0 -0
  68. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/entrypoints.py +0 -0
  69. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/enums.py +0 -0
  70. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/exceptions.py +0 -0
  71. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/fields/__init__.py +0 -0
  72. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/fields/json.py +0 -0
  73. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/fields/mixins.py +0 -0
  74. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/fields/related.py +0 -0
  75. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/fields/related_descriptors.py +0 -0
  76. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/fields/related_lookups.py +0 -0
  77. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/fields/related_managers.py +0 -0
  78. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/fields/reverse_related.py +0 -0
  79. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/forms.py +0 -0
  80. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/functions/__init__.py +0 -0
  81. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/functions/comparison.py +0 -0
  82. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/functions/datetime.py +0 -0
  83. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/functions/math.py +0 -0
  84. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/functions/mixins.py +0 -0
  85. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/functions/text.py +0 -0
  86. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/functions/window.py +0 -0
  87. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/indexes.py +0 -0
  88. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/lookups.py +0 -0
  89. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/meta.py +0 -0
  90. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/__init__.py +0 -0
  91. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/autodetector.py +0 -0
  92. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/exceptions.py +0 -0
  93. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/graph.py +0 -0
  94. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/operations/__init__.py +0 -0
  95. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/operations/base.py +0 -0
  96. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/operations/fields.py +0 -0
  97. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/operations/models.py +0 -0
  98. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/operations/special.py +0 -0
  99. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/optimizer.py +0 -0
  100. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/questioner.py +0 -0
  101. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/recorder.py +0 -0
  102. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/serializer.py +0 -0
  103. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/state.py +0 -0
  104. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/utils.py +0 -0
  105. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/migrations/writer.py +0 -0
  106. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/options.py +0 -0
  107. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/otel.py +0 -0
  108. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/preflight.py +0 -0
  109. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/query.py +0 -0
  110. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/query_utils.py +0 -0
  111. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/registry.py +0 -0
  112. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/sql/__init__.py +0 -0
  113. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/sql/compiler.py +0 -0
  114. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/sql/constants.py +0 -0
  115. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/sql/datastructures.py +0 -0
  116. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/sql/query.py +0 -0
  117. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/sql/subqueries.py +0 -0
  118. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/sql/where.py +0 -0
  119. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/test/__init__.py +0 -0
  120. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/test/pytest.py +0 -0
  121. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/test/utils.py +0 -0
  122. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/transaction.py +0 -0
  123. {plain_models-0.51.0 → plain_models-0.52.0}/plain/models/utils.py +0 -0
  124. {plain_models-0.51.0 → plain_models-0.52.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  125. {plain_models-0.51.0 → plain_models-0.52.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  126. {plain_models-0.51.0 → plain_models-0.52.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  127. {plain_models-0.51.0 → plain_models-0.52.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  128. {plain_models-0.51.0 → plain_models-0.52.0}/tests/app/examples/migrations/__init__.py +0 -0
  129. {plain_models-0.51.0 → plain_models-0.52.0}/tests/app/examples/models.py +0 -0
  130. {plain_models-0.51.0 → plain_models-0.52.0}/tests/app/settings.py +0 -0
  131. {plain_models-0.51.0 → plain_models-0.52.0}/tests/app/urls.py +0 -0
  132. {plain_models-0.51.0 → plain_models-0.52.0}/tests/test_database_url.py +0 -0
  133. {plain_models-0.51.0 → plain_models-0.52.0}/tests/test_delete_behaviors.py +0 -0
  134. {plain_models-0.51.0 → plain_models-0.52.0}/tests/test_exceptions.py +0 -0
  135. {plain_models-0.51.0 → plain_models-0.52.0}/tests/test_manager_assignment.py +0 -0
  136. {plain_models-0.51.0 → plain_models-0.52.0}/tests/test_models.py +0 -0
  137. {plain_models-0.51.0 → plain_models-0.52.0}/tests/test_related_descriptors.py +0 -0
  138. {plain_models-0.51.0 → plain_models-0.52.0}/tests/test_related_manager_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.51.0
3
+ Version: 0.52.0
4
4
  Summary: Model your data and store it in a database.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -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
- not self.collect_sql
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
- if self.collect_sql:
211
- ending = "" if sql.rstrip().endswith(";") else ";"
212
- if params is not None:
213
- self.collected_sql.append(
214
- (sql % tuple(map(self.quote_value, params))) + ending
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
- with self.connection.cursor() as cursor:
220
- cursor.execute(sql, params)
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.cli import create_backup
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, migration: Migration | None = None, fake: bool = False
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 verbosity >= 1:
410
- if action == "apply_start":
411
- click.echo(f" Applying {migration}...", nl=False)
412
- elif action == "apply_success":
413
- if fake:
414
- click.echo(click.style(" FAKED", fg="green"))
415
- else:
416
- click.echo(click.style(" OK", fg="green"))
417
- elif action == "render_start":
418
- click.echo(" Rendering model states...", nl=False)
419
- elif action == "render_success":
420
- click.echo(click.style(" DONE", fg="green"))
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 verbosity > 0:
513
- click.secho("Pruning migrations:", fg="cyan")
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
- 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",
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
- 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",
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 verbosity > 0:
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 verbosity > 0:
556
- click.echo(click.style(" OK", fg="green"))
557
- elif verbosity > 0:
558
- click.echo(" No migrations to prune.")
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
- click.secho("Planned operations:", fg="cyan")
564
- if not migration_plan:
565
- click.echo(" No planned migration operations.")
566
- else:
567
- for migration in migration_plan:
568
- click.secho(str(migration), fg="cyan")
569
- for operation in migration.operations:
570
- message, is_error = describe_operation(operation)
571
- if is_error:
572
- click.secho(" " + message, fg="yellow")
573
- else:
574
- click.echo(" " + message)
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 verbosity >= 1:
589
- click.secho("Operations to perform:", fg="cyan")
590
-
603
+ if not quiet:
591
604
  if target_package_labels_only:
592
- click.secho(
593
- " Apply all migrations: "
594
- + (", ".join(sorted({a for a, n in targets})) or "(none)"),
595
- fg="yellow",
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
- f" Target specific migration: {targets[0][1]}, from {targets[0][0]}",
600
- fg="yellow",
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
- if verbosity >= 1:
636
- click.echo(
637
- f" Running {len(migration_plan)} migrations in atomic batch (all-or-nothing)"
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 verbosity >= 1:
643
- click.echo(f" Running {len(migration_plan)} migrations separately")
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
- if verbosity >= 1:
649
- click.echo(
650
- f" Running {len(migration_plan)} migrations in atomic batch (all-or-nothing)"
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 verbosity >= 1:
663
+ if len(migration_plan) > 1:
655
664
  if not can_rollback_ddl:
656
- click.echo(
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
- click.echo(
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
- click.echo(
665
- f" Running {len(migration_plan)} migrations separately"
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
- click.secho(
671
- f"Creating backup before applying migrations: {backup_name}",
672
- bold=True,
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
- if verbosity >= 1:
685
- click.secho("Running migrations:", fg="cyan")
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
- elif verbosity >= 1:
716
- click.echo(" No migrations to apply.")
717
- # If there's changes that aren't in migrations yet, tell them
718
- # how to fix it.
719
- autodetector = MigrationAutodetector(
720
- executor.loader.project_state(),
721
- ProjectState.from_models_registry(models_registry),
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
- click.echo(
733
- click.style(
734
- " Run `plain makemigrations` to make new "
735
- "migrations, and then re-run `plain migrate` to "
736
- "apply them.",
737
- fg="yellow",
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()
@@ -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__(self, queryset: Any, output_field: Field | None = None, **extra: Any):
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
- self.query = getattr(queryset, "query", queryset).clone()
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, queryset: Any, **kwargs: Any):
1692
- super().__init__(queryset, **kwargs)
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(state, schema_editor)
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 typing import Any
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, project_state: Any, schema_editor: Any, collect_sql: bool = False
90
- ) -> Any:
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
- # If this operation cannot be represented as SQL, place a comment
101
- # there instead
102
- if collect_sql:
103
- schema_editor.collected_sql.append("--")
104
- schema_editor.collected_sql.append(f"-- {operation.describe()}")
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 collect_sql and collected_sql_before == len(schema_editor.collected_sql):
132
- schema_editor.collected_sql.append("-- (no-op)")
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.models"
3
- version = "0.51.0"
3
+ version = "0.52.0"
4
4
  description = "Model your data and store it in a database."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes