plain.models 0.50.0__tar.gz → 0.51.1__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 (139) hide show
  1. {plain_models-0.50.0 → plain_models-0.51.1}/PKG-INFO +27 -43
  2. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/CHANGELOG.md +24 -0
  3. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/README.md +26 -42
  4. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/__init__.py +2 -0
  5. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/creation.py +2 -2
  6. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/introspection.py +8 -4
  7. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/schema.py +89 -71
  8. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/validation.py +1 -1
  9. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/compiler.py +1 -1
  10. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/operations.py +1 -1
  11. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/schema.py +4 -4
  12. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/postgresql/operations.py +1 -1
  13. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/postgresql/schema.py +3 -3
  14. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/operations.py +1 -1
  15. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/schema.py +61 -50
  16. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/base.py +116 -163
  17. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/cli.py +4 -4
  18. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/constraints.py +14 -9
  19. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/deletion.py +15 -14
  20. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/expressions.py +21 -5
  21. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/fields/__init__.py +20 -16
  22. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/fields/json.py +3 -3
  23. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/fields/related.py +73 -71
  24. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/fields/related_descriptors.py +2 -2
  25. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/fields/related_lookups.py +1 -1
  26. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/fields/related_managers.py +21 -32
  27. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/fields/reverse_related.py +8 -8
  28. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/forms.py +12 -12
  29. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/indexes.py +5 -4
  30. plain_models-0.50.0/plain/models/options.py → plain_models-0.51.1/plain/models/meta.py +118 -182
  31. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/operations/base.py +1 -1
  32. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/operations/fields.py +6 -6
  33. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/operations/models.py +18 -16
  34. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/recorder.py +9 -5
  35. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/state.py +35 -46
  36. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/utils.py +1 -1
  37. plain_models-0.51.1/plain/models/options.py +233 -0
  38. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/preflight.py +7 -5
  39. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/query.py +119 -65
  40. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/query_utils.py +18 -13
  41. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/registry.py +6 -5
  42. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/sql/compiler.py +51 -37
  43. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/sql/query.py +77 -68
  44. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/sql/subqueries.py +4 -4
  45. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/utils.py +4 -1
  46. {plain_models-0.50.0 → plain_models-0.51.1}/pyproject.toml +1 -1
  47. plain_models-0.51.1/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +68 -0
  48. {plain_models-0.50.0 → plain_models-0.51.1}/tests/app/examples/models.py +24 -6
  49. {plain_models-0.50.0 → plain_models-0.51.1}/tests/test_manager_assignment.py +12 -25
  50. plain_models-0.51.1/tests/test_models.py +43 -0
  51. plain_models-0.50.0/tests/test_models.py +0 -25
  52. {plain_models-0.50.0 → plain_models-0.51.1}/.gitignore +0 -0
  53. {plain_models-0.50.0 → plain_models-0.51.1}/LICENSE +0 -0
  54. {plain_models-0.50.0 → plain_models-0.51.1}/README.md +0 -0
  55. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/AGENTS.md +0 -0
  56. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/aggregates.py +0 -0
  57. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/__init__.py +0 -0
  58. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/__init__.py +0 -0
  59. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/base.py +0 -0
  60. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/client.py +0 -0
  61. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/features.py +0 -0
  62. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/base/operations.py +0 -0
  63. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/ddl_references.py +0 -0
  64. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/__init__.py +0 -0
  65. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/base.py +0 -0
  66. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/client.py +0 -0
  67. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/creation.py +0 -0
  68. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/features.py +0 -0
  69. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/introspection.py +0 -0
  70. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/mysql/validation.py +0 -0
  71. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/postgresql/__init__.py +0 -0
  72. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/postgresql/base.py +0 -0
  73. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/postgresql/client.py +0 -0
  74. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/postgresql/creation.py +0 -0
  75. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/postgresql/features.py +0 -0
  76. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/postgresql/introspection.py +0 -0
  77. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/__init__.py +0 -0
  78. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/_functions.py +0 -0
  79. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/base.py +0 -0
  80. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/client.py +0 -0
  81. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/creation.py +0 -0
  82. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/features.py +0 -0
  83. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/sqlite3/introspection.py +0 -0
  84. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backends/utils.py +0 -0
  85. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backups/__init__.py +0 -0
  86. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backups/cli.py +0 -0
  87. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backups/clients.py +0 -0
  88. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/backups/core.py +0 -0
  89. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/config.py +0 -0
  90. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/connections.py +0 -0
  91. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/constants.py +0 -0
  92. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/database_url.py +0 -0
  93. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/db.py +0 -0
  94. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/default_settings.py +0 -0
  95. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/entrypoints.py +0 -0
  96. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/enums.py +0 -0
  97. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/exceptions.py +0 -0
  98. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/fields/mixins.py +0 -0
  99. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/functions/__init__.py +0 -0
  100. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/functions/comparison.py +0 -0
  101. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/functions/datetime.py +0 -0
  102. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/functions/math.py +0 -0
  103. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/functions/mixins.py +0 -0
  104. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/functions/text.py +0 -0
  105. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/functions/window.py +0 -0
  106. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/lookups.py +0 -0
  107. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/__init__.py +0 -0
  108. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/autodetector.py +0 -0
  109. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/exceptions.py +0 -0
  110. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/executor.py +0 -0
  111. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/graph.py +0 -0
  112. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/loader.py +0 -0
  113. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/migration.py +0 -0
  114. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/operations/__init__.py +0 -0
  115. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/operations/special.py +0 -0
  116. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/optimizer.py +0 -0
  117. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/questioner.py +0 -0
  118. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/serializer.py +0 -0
  119. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/migrations/writer.py +0 -0
  120. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/otel.py +0 -0
  121. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/sql/__init__.py +0 -0
  122. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/sql/constants.py +0 -0
  123. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/sql/datastructures.py +0 -0
  124. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/sql/where.py +0 -0
  125. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/test/__init__.py +0 -0
  126. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/test/pytest.py +0 -0
  127. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/test/utils.py +0 -0
  128. {plain_models-0.50.0 → plain_models-0.51.1}/plain/models/transaction.py +0 -0
  129. {plain_models-0.50.0 → plain_models-0.51.1}/tests/app/examples/migrations/0001_initial.py +0 -0
  130. {plain_models-0.50.0 → plain_models-0.51.1}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  131. {plain_models-0.50.0 → plain_models-0.51.1}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  132. {plain_models-0.50.0 → plain_models-0.51.1}/tests/app/examples/migrations/__init__.py +0 -0
  133. {plain_models-0.50.0 → plain_models-0.51.1}/tests/app/settings.py +0 -0
  134. {plain_models-0.50.0 → plain_models-0.51.1}/tests/app/urls.py +0 -0
  135. {plain_models-0.50.0 → plain_models-0.51.1}/tests/test_database_url.py +0 -0
  136. {plain_models-0.50.0 → plain_models-0.51.1}/tests/test_delete_behaviors.py +0 -0
  137. {plain_models-0.50.0 → plain_models-0.51.1}/tests/test_exceptions.py +0 -0
  138. {plain_models-0.50.0 → plain_models-0.51.1}/tests/test_related_descriptors.py +0 -0
  139. {plain_models-0.50.0 → plain_models-0.51.1}/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.50.0
3
+ Version: 0.51.1
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
@@ -211,31 +211,32 @@ class User(models.Model):
211
211
  username = models.CharField(max_length=150)
212
212
  age = models.IntegerField()
213
213
 
214
- class Meta:
215
- indexes = [
214
+ model_options = models.Options(
215
+ indexes=[
216
216
  models.Index(fields=["email"]),
217
217
  models.Index(fields=["-created_at"], name="user_created_idx"),
218
- ]
219
- constraints = [
218
+ ],
219
+ constraints=[
220
220
  models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
221
221
  models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
222
- ]
222
+ ],
223
+ )
223
224
  ```
224
225
 
225
226
  ## Custom QuerySets
226
227
 
227
- With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods. There are several ways to use custom QuerySets:
228
-
229
- ### Setting a default QuerySet for a model
228
+ With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods.
230
229
 
231
- Use `Meta.queryset_class` to set a custom QuerySet that will be used by `Model.query`:
230
+ Define a custom QuerySet and assign it to your model's `query` attribute:
232
231
 
233
232
  ```python
234
- class PublishedQuerySet(models.QuerySet):
235
- def published_only(self):
233
+ from typing import Self
234
+
235
+ class PublishedQuerySet(models.QuerySet["Article"]):
236
+ def published_only(self) -> Self:
236
237
  return self.filter(status="published")
237
238
 
238
- def draft_only(self):
239
+ def draft_only(self) -> Self:
239
240
  return self.filter(status="draft")
240
241
 
241
242
  @models.register_model
@@ -243,50 +244,33 @@ class Article(models.Model):
243
244
  title = models.CharField(max_length=200)
244
245
  status = models.CharField(max_length=20)
245
246
 
246
- class Meta:
247
- queryset_class = PublishedQuerySet
247
+ query = PublishedQuerySet()
248
248
 
249
- # Usage - all methods available on Article.objects
249
+ # Usage - all methods available on Article.query
250
250
  all_articles = Article.query.all()
251
251
  published_articles = Article.query.published_only()
252
252
  draft_articles = Article.query.draft_only()
253
253
  ```
254
254
 
255
- ### Using custom QuerySets without formal attachment
256
-
257
- You can also use custom QuerySets manually without setting them as the default:
255
+ Custom methods can be chained with built-in QuerySet methods:
258
256
 
259
257
  ```python
260
- class SpecialQuerySet(models.QuerySet):
261
- def special_filter(self):
262
- return self.filter(special=True)
263
-
264
- # Create and use the QuerySet manually
265
- special_qs = SpecialQuerySet(model=Article)
266
- special_articles = special_qs.special_filter()
258
+ # Chaining works naturally
259
+ recent_published = Article.query.published_only().order_by("-created_at")[:10]
267
260
  ```
268
261
 
269
- ### Using classmethods for convenience
262
+ ### Programmatic QuerySet usage
270
263
 
271
- For even cleaner API, add classmethods to your model:
264
+ For internal code that needs to create QuerySet instances programmatically, use `from_model()`:
272
265
 
273
266
  ```python
274
- @models.register_model
275
- class Article(models.Model):
276
- title = models.CharField(max_length=200)
277
- status = models.CharField(max_length=20)
278
-
279
- @classmethod
280
- def published(cls):
281
- return PublishedQuerySet(model=cls).published_only()
282
-
283
- @classmethod
284
- def drafts(cls):
285
- return PublishedQuerySet(model=cls).draft_only()
267
+ class SpecialQuerySet(models.QuerySet["Article"]):
268
+ def special_filter(self) -> Self:
269
+ return self.filter(special=True)
286
270
 
287
- # Usage
288
- published_articles = Article.published()
289
- draft_articles = Article.drafts()
271
+ # Create and use the QuerySet programmatically
272
+ special_qs = SpecialQuerySet.from_model(Article)
273
+ special_articles = special_qs.special_filter()
290
274
  ```
291
275
 
292
276
  ## Forms
@@ -1,5 +1,29 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.51.1](https://github.com/dropseed/plain/releases/plain-models@0.51.1) (2025-10-08)
4
+
5
+ ### What's changed
6
+
7
+ - 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))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required
12
+
13
+ ## [0.51.0](https://github.com/dropseed/plain/releases/plain-models@0.51.0) (2025-10-07)
14
+
15
+ ### What's changed
16
+
17
+ - Model metadata has been split into two separate descriptors: `model_options` for user-defined configuration and `_model_meta` for internal metadata ([73ba469](https://github.com/dropseed/plain/commit/73ba469ba0), [17a378d](https://github.com/dropseed/plain/commit/17a378dcfb))
18
+ - The `_meta` attribute has been replaced with `model_options` for user-defined options like indexes, constraints, and database settings ([17a378d](https://github.com/dropseed/plain/commit/17a378dcfb))
19
+ - Custom QuerySets are now assigned directly to the `query` class attribute instead of using `Meta.queryset_class` ([2578301](https://github.com/dropseed/plain/commit/2578301819))
20
+ - Added comprehensive type improvements to model metadata and related fields for better IDE support ([3b477a0](https://github.com/dropseed/plain/commit/3b477a0d43))
21
+
22
+ ### Upgrade instructions
23
+
24
+ - Replace `Meta.queryset_class = CustomQuerySet` with `query = CustomQuerySet()` as a class attribute on your models
25
+ - Replace `class Meta:` with `model_options = models.Options(...)` in your models
26
+
3
27
  ## [0.50.0](https://github.com/dropseed/plain/releases/plain-models@0.50.0) (2025-10-06)
4
28
 
5
29
  ### What's changed
@@ -200,31 +200,32 @@ class User(models.Model):
200
200
  username = models.CharField(max_length=150)
201
201
  age = models.IntegerField()
202
202
 
203
- class Meta:
204
- indexes = [
203
+ model_options = models.Options(
204
+ indexes=[
205
205
  models.Index(fields=["email"]),
206
206
  models.Index(fields=["-created_at"], name="user_created_idx"),
207
- ]
208
- constraints = [
207
+ ],
208
+ constraints=[
209
209
  models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
210
210
  models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
211
- ]
211
+ ],
212
+ )
212
213
  ```
213
214
 
214
215
  ## Custom QuerySets
215
216
 
216
- With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods. There are several ways to use custom QuerySets:
217
-
218
- ### Setting a default QuerySet for a model
217
+ With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods.
219
218
 
220
- Use `Meta.queryset_class` to set a custom QuerySet that will be used by `Model.query`:
219
+ Define a custom QuerySet and assign it to your model's `query` attribute:
221
220
 
222
221
  ```python
223
- class PublishedQuerySet(models.QuerySet):
224
- def published_only(self):
222
+ from typing import Self
223
+
224
+ class PublishedQuerySet(models.QuerySet["Article"]):
225
+ def published_only(self) -> Self:
225
226
  return self.filter(status="published")
226
227
 
227
- def draft_only(self):
228
+ def draft_only(self) -> Self:
228
229
  return self.filter(status="draft")
229
230
 
230
231
  @models.register_model
@@ -232,50 +233,33 @@ class Article(models.Model):
232
233
  title = models.CharField(max_length=200)
233
234
  status = models.CharField(max_length=20)
234
235
 
235
- class Meta:
236
- queryset_class = PublishedQuerySet
236
+ query = PublishedQuerySet()
237
237
 
238
- # Usage - all methods available on Article.objects
238
+ # Usage - all methods available on Article.query
239
239
  all_articles = Article.query.all()
240
240
  published_articles = Article.query.published_only()
241
241
  draft_articles = Article.query.draft_only()
242
242
  ```
243
243
 
244
- ### Using custom QuerySets without formal attachment
245
-
246
- You can also use custom QuerySets manually without setting them as the default:
244
+ Custom methods can be chained with built-in QuerySet methods:
247
245
 
248
246
  ```python
249
- class SpecialQuerySet(models.QuerySet):
250
- def special_filter(self):
251
- return self.filter(special=True)
252
-
253
- # Create and use the QuerySet manually
254
- special_qs = SpecialQuerySet(model=Article)
255
- special_articles = special_qs.special_filter()
247
+ # Chaining works naturally
248
+ recent_published = Article.query.published_only().order_by("-created_at")[:10]
256
249
  ```
257
250
 
258
- ### Using classmethods for convenience
251
+ ### Programmatic QuerySet usage
259
252
 
260
- For even cleaner API, add classmethods to your model:
253
+ For internal code that needs to create QuerySet instances programmatically, use `from_model()`:
261
254
 
262
255
  ```python
263
- @models.register_model
264
- class Article(models.Model):
265
- title = models.CharField(max_length=200)
266
- status = models.CharField(max_length=20)
267
-
268
- @classmethod
269
- def published(cls):
270
- return PublishedQuerySet(model=cls).published_only()
271
-
272
- @classmethod
273
- def drafts(cls):
274
- return PublishedQuerySet(model=cls).draft_only()
256
+ class SpecialQuerySet(models.QuerySet["Article"]):
257
+ def special_filter(self) -> Self:
258
+ return self.filter(special=True)
275
259
 
276
- # Usage
277
- published_articles = Article.published()
278
- draft_articles = Article.drafts()
260
+ # Create and use the QuerySet programmatically
261
+ special_qs = SpecialQuerySet.from_model(Article)
262
+ special_articles = special_qs.special_filter()
279
263
  ```
280
264
 
281
265
  ## Forms
@@ -63,6 +63,7 @@ from .registry import models_registry, register_model
63
63
 
64
64
  # Imports that would create circular imports if sorted
65
65
  from .base import DEFERRED, Model # isort:skip
66
+ from .options import Options # isort:skip
66
67
  from .fields.related import ( # isort:skip
67
68
  ForeignKey,
68
69
  ManyToManyField,
@@ -104,6 +105,7 @@ __all__ += [
104
105
  "JSONField",
105
106
  "Lookup",
106
107
  "Transform",
108
+ "Options",
107
109
  "Prefetch",
108
110
  "Q",
109
111
  "QuerySet",
@@ -98,7 +98,7 @@ class BaseDatabaseCreation:
98
98
  # and package_config.package_label in loader.migrated_packages
99
99
  # ):
100
100
  # for model in package_config.get_models():
101
- # if model._meta.can_migrate(
101
+ # if model.model_options.can_migrate(
102
102
  # self.connection
103
103
  # ) and router.allow_migrate_model(self.connection.alias, model):
104
104
  # queryset = model._base_manager.using(
@@ -127,7 +127,7 @@ class BaseDatabaseCreation:
127
127
  # "json", data, using=self.connection.alias
128
128
  # ):
129
129
  # obj.save()
130
- # table_names.add(obj.object.__class__._meta.db_table)
130
+ # table_names.add(obj.object.model_options.db_table)
131
131
  # # Manually check for any invalid keys that might have been added,
132
132
  # # because constraint checks were disabled.
133
133
  # self.connection.check_constraints(table_names=table_names)
@@ -94,7 +94,7 @@ class BaseDatabaseIntrospection:
94
94
  for model in models_registry.get_models(
95
95
  package_label=package_config.package_label
96
96
  )
97
- if model._meta.can_migrate(self.connection)
97
+ if model.model_options.can_migrate(self.connection)
98
98
  )
99
99
 
100
100
  def plain_table_names(
@@ -108,8 +108,10 @@ class BaseDatabaseIntrospection:
108
108
  """
109
109
  tables = set()
110
110
  for model in self.get_migratable_models():
111
- tables.add(model._meta.db_table)
112
- tables.update(f.m2m_db_table() for f in model._meta.local_many_to_many)
111
+ tables.add(model.model_options.db_table)
112
+ tables.update(
113
+ f.m2m_db_table() for f in model._model_meta.local_many_to_many
114
+ )
113
115
  tables = list(tables)
114
116
  if only_existing:
115
117
  existing_tables = set(self.table_names(include_views=include_views))
@@ -128,7 +130,9 @@ class BaseDatabaseIntrospection:
128
130
  for model in self.get_migratable_models():
129
131
  sequence_list.extend(
130
132
  self.get_sequences(
131
- cursor, model._meta.db_table, model._meta.local_fields
133
+ cursor,
134
+ model.model_options.db_table,
135
+ model._model_meta.local_fields,
132
136
  )
133
137
  )
134
138
  return sequence_list