plain.models 0.49.1__tar.gz → 0.51.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 (139) hide show
  1. {plain_models-0.49.1 → plain_models-0.51.0}/.gitignore +1 -0
  2. {plain_models-0.49.1 → plain_models-0.51.0}/PKG-INFO +27 -43
  3. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/CHANGELOG.md +37 -0
  4. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/README.md +26 -42
  5. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/__init__.py +2 -0
  6. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/aggregates.py +42 -19
  7. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/base.py +125 -105
  8. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/client.py +11 -3
  9. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/creation.py +24 -14
  10. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/features.py +10 -4
  11. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/introspection.py +37 -20
  12. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/operations.py +187 -91
  13. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/schema.py +338 -218
  14. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/validation.py +13 -4
  15. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/ddl_references.py +85 -43
  16. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/base.py +29 -26
  17. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/client.py +7 -2
  18. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/compiler.py +13 -4
  19. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/creation.py +5 -2
  20. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/features.py +24 -22
  21. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/introspection.py +22 -13
  22. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/operations.py +107 -40
  23. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/schema.py +52 -28
  24. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/validation.py +13 -6
  25. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/postgresql/base.py +41 -34
  26. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/postgresql/client.py +7 -2
  27. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/postgresql/creation.py +10 -5
  28. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/postgresql/introspection.py +15 -8
  29. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/postgresql/operations.py +110 -43
  30. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/postgresql/schema.py +88 -49
  31. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/_functions.py +151 -115
  32. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/base.py +37 -23
  33. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/client.py +7 -1
  34. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/creation.py +9 -5
  35. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/features.py +5 -3
  36. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/introspection.py +32 -16
  37. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/operations.py +126 -43
  38. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/schema.py +127 -92
  39. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/utils.py +52 -29
  40. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backups/cli.py +8 -6
  41. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backups/clients.py +16 -7
  42. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backups/core.py +24 -13
  43. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/base.py +221 -229
  44. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/cli.py +98 -67
  45. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/config.py +1 -1
  46. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/connections.py +23 -7
  47. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/constraints.py +79 -56
  48. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/database_url.py +1 -1
  49. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/db.py +6 -2
  50. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/deletion.py +80 -56
  51. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/entrypoints.py +1 -1
  52. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/enums.py +22 -11
  53. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/exceptions.py +23 -8
  54. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/expressions.py +441 -258
  55. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/fields/__init__.py +272 -217
  56. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/fields/json.py +123 -57
  57. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/fields/mixins.py +12 -8
  58. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/fields/related.py +324 -290
  59. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/fields/related_descriptors.py +36 -27
  60. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/fields/related_lookups.py +24 -12
  61. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/fields/related_managers.py +102 -79
  62. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/fields/reverse_related.py +66 -63
  63. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/forms.py +101 -75
  64. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/functions/comparison.py +71 -18
  65. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/functions/datetime.py +79 -29
  66. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/functions/math.py +43 -10
  67. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/functions/mixins.py +24 -7
  68. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/functions/text.py +104 -25
  69. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/functions/window.py +12 -6
  70. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/indexes.py +57 -32
  71. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/lookups.py +228 -153
  72. plain_models-0.49.1/plain/models/options.py → plain_models-0.51.0/plain/models/meta.py +150 -207
  73. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/autodetector.py +86 -43
  74. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/exceptions.py +7 -3
  75. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/executor.py +33 -7
  76. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/graph.py +79 -50
  77. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/loader.py +45 -22
  78. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/migration.py +23 -18
  79. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/operations/base.py +38 -20
  80. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/operations/fields.py +95 -48
  81. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/operations/models.py +246 -142
  82. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/operations/special.py +82 -25
  83. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/optimizer.py +7 -2
  84. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/questioner.py +58 -31
  85. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/recorder.py +27 -16
  86. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/serializer.py +50 -39
  87. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/state.py +232 -156
  88. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/utils.py +30 -14
  89. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/writer.py +17 -14
  90. plain_models-0.51.0/plain/models/options.py +233 -0
  91. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/otel.py +16 -6
  92. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/preflight.py +42 -17
  93. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/query.py +400 -251
  94. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/query_utils.py +109 -69
  95. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/registry.py +40 -21
  96. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/sql/compiler.py +190 -127
  97. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/sql/datastructures.py +38 -25
  98. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/sql/query.py +320 -225
  99. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/sql/subqueries.py +36 -25
  100. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/sql/where.py +54 -29
  101. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/test/pytest.py +15 -11
  102. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/test/utils.py +4 -2
  103. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/transaction.py +20 -7
  104. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/utils.py +17 -6
  105. {plain_models-0.49.1 → plain_models-0.51.0}/pyproject.toml +1 -1
  106. plain_models-0.51.0/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +68 -0
  107. {plain_models-0.49.1 → plain_models-0.51.0}/tests/app/examples/models.py +24 -6
  108. {plain_models-0.49.1 → plain_models-0.51.0}/tests/test_delete_behaviors.py +1 -1
  109. {plain_models-0.49.1 → plain_models-0.51.0}/tests/test_exceptions.py +1 -1
  110. {plain_models-0.49.1 → plain_models-0.51.0}/tests/test_manager_assignment.py +13 -26
  111. plain_models-0.51.0/tests/test_models.py +43 -0
  112. {plain_models-0.49.1 → plain_models-0.51.0}/tests/test_related_descriptors.py +1 -1
  113. {plain_models-0.49.1 → plain_models-0.51.0}/tests/test_related_manager_api.py +1 -1
  114. plain_models-0.49.1/tests/test_models.py +0 -25
  115. {plain_models-0.49.1 → plain_models-0.51.0}/LICENSE +0 -0
  116. {plain_models-0.49.1 → plain_models-0.51.0}/README.md +0 -0
  117. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/AGENTS.md +0 -0
  118. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/__init__.py +0 -0
  119. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/base/__init__.py +0 -0
  120. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/mysql/__init__.py +0 -0
  121. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/postgresql/__init__.py +0 -0
  122. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/postgresql/features.py +0 -0
  123. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backends/sqlite3/__init__.py +0 -0
  124. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/backups/__init__.py +0 -0
  125. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/constants.py +0 -0
  126. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/default_settings.py +0 -0
  127. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/functions/__init__.py +0 -0
  128. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/__init__.py +0 -0
  129. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/migrations/operations/__init__.py +0 -0
  130. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/sql/__init__.py +0 -0
  131. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/sql/constants.py +0 -0
  132. {plain_models-0.49.1 → plain_models-0.51.0}/plain/models/test/__init__.py +0 -0
  133. {plain_models-0.49.1 → plain_models-0.51.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  134. {plain_models-0.49.1 → plain_models-0.51.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  135. {plain_models-0.49.1 → plain_models-0.51.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  136. {plain_models-0.49.1 → plain_models-0.51.0}/tests/app/examples/migrations/__init__.py +0 -0
  137. {plain_models-0.49.1 → plain_models-0.51.0}/tests/app/settings.py +0 -0
  138. {plain_models-0.49.1 → plain_models-0.51.0}/tests/app/urls.py +0 -0
  139. {plain_models-0.49.1 → plain_models-0.51.0}/tests/test_database_url.py +0 -0
@@ -16,3 +16,4 @@ plain*/tests/.plain
16
16
 
17
17
  .vscode
18
18
  /.claude
19
+ /.benchmarks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.49.1
3
+ Version: 0.51.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
@@ -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,42 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.51.0](https://github.com/dropseed/plain/releases/plain-models@0.51.0) (2025-10-07)
4
+
5
+ ### What's changed
6
+
7
+ - 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))
8
+ - 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))
9
+ - 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))
10
+ - Added comprehensive type improvements to model metadata and related fields for better IDE support ([3b477a0](https://github.com/dropseed/plain/commit/3b477a0d43))
11
+
12
+ ### Upgrade instructions
13
+
14
+ - Replace `Meta.queryset_class = CustomQuerySet` with `query = CustomQuerySet()` as a class attribute on your models
15
+ - Replace `class Meta:` with `model_options = models.Options(...)` in your models
16
+
17
+ ## [0.50.0](https://github.com/dropseed/plain/releases/plain-models@0.50.0) (2025-10-06)
18
+
19
+ ### What's changed
20
+
21
+ - Added comprehensive type annotations throughout plain-models, improving IDE support and type checking capabilities ([ea1a7df](https://github.com/dropseed/plain/commit/ea1a7df622), [f49ee32](https://github.com/dropseed/plain/commit/f49ee32a90), [369353f](https://github.com/dropseed/plain/commit/369353f9d6), [13b7d16](https://github.com/dropseed/plain/commit/13b7d16f8d), [e23a0ca](https://github.com/dropseed/plain/commit/e23a0cae7c), [02d8551](https://github.com/dropseed/plain/commit/02d85518f0))
22
+ - The `QuerySet` class is now generic and the `model` parameter is now required in the `__init__` method ([719e792](https://github.com/dropseed/plain/commit/719e792c96))
23
+ - Database wrapper classes have been renamed for consistency: `DatabaseWrapper` classes are now named `MySQLDatabaseWrapper`, `PostgreSQLDatabaseWrapper`, and `SQLiteDatabaseWrapper` ([5a39e85](https://github.com/dropseed/plain/commit/5a39e851e5))
24
+ - The plain-models package now has 100% type annotation coverage and is validated in CI to prevent regressions
25
+
26
+ ### Upgrade instructions
27
+
28
+ - No changes required
29
+
30
+ ## [0.49.2](https://github.com/dropseed/plain/releases/plain-models@0.49.2) (2025-10-02)
31
+
32
+ ### What's changed
33
+
34
+ - Updated dependency to use the latest plain package version
35
+
36
+ ### Upgrade instructions
37
+
38
+ - No changes required
39
+
3
40
  ## [0.49.1](https://github.com/dropseed/plain/releases/plain-models@0.49.1) (2025-09-29)
4
41
 
5
42
  ### 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",
@@ -1,6 +1,6 @@
1
- """
2
- Classes to represent the definitions of aggregate functions.
3
- """
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from plain.models.exceptions import FieldError, FullResultSet
6
6
  from plain.models.expressions import Case, Func, Star, Value, When
@@ -11,6 +11,12 @@ from plain.models.functions.mixins import (
11
11
  NumericOutputFieldMixin,
12
12
  )
13
13
 
14
+ if TYPE_CHECKING:
15
+ from plain.models.backends.base.base import BaseDatabaseWrapper
16
+ from plain.models.expressions import Expression
17
+ from plain.models.query_utils import Q
18
+ from plain.models.sql.compiler import SQLCompiler
19
+
14
20
  __all__ = [
15
21
  "Aggregate",
16
22
  "Avg",
@@ -33,8 +39,13 @@ class Aggregate(Func):
33
39
  empty_result_set_value = None
34
40
 
35
41
  def __init__(
36
- self, *expressions, distinct=False, filter=None, default=None, **extra
37
- ):
42
+ self,
43
+ *expressions: Any,
44
+ distinct: bool = False,
45
+ filter: Q | Expression | None = None,
46
+ default: Any = None,
47
+ **extra: Any,
48
+ ) -> None:
38
49
  if distinct and not self.allow_distinct:
39
50
  raise TypeError(f"{self.__class__.__name__} does not allow distinct.")
40
51
  if default is not None and self.empty_result_set_value is not None:
@@ -44,23 +55,28 @@ class Aggregate(Func):
44
55
  self.default = default
45
56
  super().__init__(*expressions, **extra)
46
57
 
47
- def get_source_fields(self):
58
+ def get_source_fields(self) -> list[Any]:
48
59
  # Don't return the filter expression since it's not a source field.
49
60
  return [e._output_field_or_none for e in super().get_source_expressions()]
50
61
 
51
- def get_source_expressions(self):
62
+ def get_source_expressions(self) -> list[Expression]:
52
63
  source_expressions = super().get_source_expressions()
53
64
  if self.filter:
54
65
  return source_expressions + [self.filter]
55
66
  return source_expressions
56
67
 
57
- def set_source_expressions(self, exprs):
68
+ def set_source_expressions(self, exprs: list[Expression]) -> list[Expression]:
58
69
  self.filter = self.filter and exprs.pop()
59
70
  return super().set_source_expressions(exprs)
60
71
 
61
72
  def resolve_expression(
62
- self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
63
- ):
73
+ self,
74
+ query: Any = None,
75
+ allow_joins: bool = True,
76
+ reuse: Any = None,
77
+ summarize: bool = False,
78
+ for_save: bool = False,
79
+ ) -> Expression:
64
80
  # Aggregates are not allowed in UPDATE queries, so ignore for_save
65
81
  c = super().resolve_expression(query, allow_joins, reuse, summarize)
66
82
  c.filter = c.filter and c.filter.resolve_expression(
@@ -95,16 +111,21 @@ class Aggregate(Func):
95
111
  return coalesce
96
112
 
97
113
  @property
98
- def default_alias(self):
114
+ def default_alias(self) -> str:
99
115
  expressions = self.get_source_expressions()
100
116
  if len(expressions) == 1 and hasattr(expressions[0], "name"):
101
117
  return f"{expressions[0].name}__{self.name.lower()}"
102
118
  raise TypeError("Complex expressions require an alias")
103
119
 
104
- def get_group_by_cols(self):
120
+ def get_group_by_cols(self) -> list[Any]:
105
121
  return []
106
122
 
107
- def as_sql(self, compiler, connection, **extra_context):
123
+ def as_sql(
124
+ self,
125
+ compiler: SQLCompiler,
126
+ connection: BaseDatabaseWrapper,
127
+ **extra_context: Any,
128
+ ) -> tuple[str, tuple[Any, ...]]:
108
129
  extra_context["distinct"] = "DISTINCT " if self.distinct else ""
109
130
  if self.filter:
110
131
  if connection.features.supports_aggregate_filter_clause:
@@ -135,7 +156,7 @@ class Aggregate(Func):
135
156
  )
136
157
  return super().as_sql(compiler, connection, **extra_context)
137
158
 
138
- def _get_repr_options(self):
159
+ def _get_repr_options(self) -> dict[str, Any]:
139
160
  options = super()._get_repr_options()
140
161
  if self.distinct:
141
162
  options["distinct"] = self.distinct
@@ -157,7 +178,9 @@ class Count(Aggregate):
157
178
  allow_distinct = True
158
179
  empty_result_set_value = 0
159
180
 
160
- def __init__(self, expression, filter=None, **extra):
181
+ def __init__(
182
+ self, expression: Any, filter: Q | Expression | None = None, **extra: Any
183
+ ) -> None:
161
184
  if expression == "*":
162
185
  expression = Star()
163
186
  if isinstance(expression, Star) and filter is not None:
@@ -178,11 +201,11 @@ class Min(Aggregate):
178
201
  class StdDev(NumericOutputFieldMixin, Aggregate):
179
202
  name = "StdDev"
180
203
 
181
- def __init__(self, expression, sample=False, **extra):
204
+ def __init__(self, expression: Any, sample: bool = False, **extra: Any) -> None:
182
205
  self.function = "STDDEV_SAMP" if sample else "STDDEV_POP"
183
206
  super().__init__(expression, **extra)
184
207
 
185
- def _get_repr_options(self):
208
+ def _get_repr_options(self) -> dict[str, Any]:
186
209
  return {**super()._get_repr_options(), "sample": self.function == "STDDEV_SAMP"}
187
210
 
188
211
 
@@ -195,9 +218,9 @@ class Sum(FixDurationInputMixin, Aggregate):
195
218
  class Variance(NumericOutputFieldMixin, Aggregate):
196
219
  name = "Variance"
197
220
 
198
- def __init__(self, expression, sample=False, **extra):
221
+ def __init__(self, expression: Any, sample: bool = False, **extra: Any) -> None:
199
222
  self.function = "VAR_SAMP" if sample else "VAR_POP"
200
223
  super().__init__(expression, **extra)
201
224
 
202
- def _get_repr_options(self):
225
+ def _get_repr_options(self) -> dict[str, Any]:
203
226
  return {**super()._get_repr_options(), "sample": self.function == "VAR_SAMP"}