plain.postgres 0.110.1__tar.gz → 0.111.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 (224) hide show
  1. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/PKG-INFO +1 -1
  2. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/CHANGELOG.md +22 -0
  3. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +1 -10
  4. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/enums.py +1 -1
  5. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/expressions.py +13 -9
  6. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/query.py +48 -26
  7. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/sql/compiler.py +1 -1
  8. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/test/pytest.py +3 -3
  9. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/pyproject.toml +1 -1
  10. plain_postgres-0.111.0/tests/conftest.py +56 -0
  11. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_constraint_violation_error.py +3 -11
  12. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_convergence_constraints.py +7 -8
  13. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_deferred_loading.py +15 -25
  14. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_delete_behaviors.py +4 -12
  15. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_exceptions.py +2 -2
  16. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_queryset_repr.py +6 -18
  17. plain_postgres-0.111.0/tests/public/test_queryset_slicing.py +144 -0
  18. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_related.py +31 -38
  19. plain_postgres-0.110.1/tests/conftest.py +0 -26
  20. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/.gitignore +0 -0
  21. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/CLAUDE.md +0 -0
  22. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/LICENSE +0 -0
  23. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/README.md +0 -0
  24. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/README.md +0 -0
  25. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/__init__.py +0 -0
  26. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/adapters.py +0 -0
  27. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  28. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/aggregates.py +0 -0
  29. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/base.py +0 -0
  30. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/cli/__init__.py +0 -0
  31. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/cli/converge.py +0 -0
  32. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/cli/core.py +0 -0
  33. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/cli/decorators.py +0 -0
  34. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/cli/diagnose.py +0 -0
  35. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/cli/migrations.py +0 -0
  36. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/cli/schema.py +0 -0
  37. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/cli/sync.py +0 -0
  38. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/config.py +0 -0
  39. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/connection.py +0 -0
  40. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/constants.py +0 -0
  41. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/constraints.py +0 -0
  42. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/convergence/__init__.py +0 -0
  43. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/convergence/analysis.py +0 -0
  44. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/convergence/fixes.py +0 -0
  45. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/convergence/planning.py +0 -0
  46. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/database_url.py +0 -0
  47. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/db.py +0 -0
  48. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/ddl.py +0 -0
  49. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/default_settings.py +0 -0
  50. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/deletion.py +0 -0
  51. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/dialect.py +0 -0
  52. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/entrypoints.py +0 -0
  53. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/exceptions.py +0 -0
  54. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/__init__.py +0 -0
  55. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/base.py +0 -0
  56. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/binary.py +0 -0
  57. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/boolean.py +0 -0
  58. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/duration.py +0 -0
  59. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/encrypted.py +0 -0
  60. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/json.py +0 -0
  61. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/mixins.py +0 -0
  62. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/network.py +0 -0
  63. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/numeric.py +0 -0
  64. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/primary_key.py +0 -0
  65. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/related.py +0 -0
  66. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/related_descriptors.py +0 -0
  67. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/related_lookups.py +0 -0
  68. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/related_managers.py +0 -0
  69. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  70. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/reverse_related.py +0 -0
  71. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/temporal.py +0 -0
  72. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/text.py +0 -0
  73. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/timezones.py +0 -0
  74. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/fields/uuid.py +0 -0
  75. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/forms.py +0 -0
  76. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/__init__.py +0 -0
  77. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/comparison.py +0 -0
  78. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/datetime.py +0 -0
  79. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/math.py +0 -0
  80. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/mixins.py +0 -0
  81. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/random.py +0 -0
  82. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/text.py +0 -0
  83. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/uuid.py +0 -0
  84. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/functions/window.py +0 -0
  85. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/indexes.py +0 -0
  86. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/__init__.py +0 -0
  87. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/__init__.py +0 -0
  88. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  89. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  90. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
  91. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/context.py +0 -0
  92. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/helpers.py +0 -0
  93. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/ownership.py +0 -0
  94. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/runner.py +0 -0
  95. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/health/types.py +0 -0
  96. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/introspection/schema.py +0 -0
  97. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/lookups.py +0 -0
  98. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/meta.py +0 -0
  99. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/middleware.py +0 -0
  100. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/__init__.py +0 -0
  101. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/autodetector.py +0 -0
  102. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/exceptions.py +0 -0
  103. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/executor.py +0 -0
  104. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/graph.py +0 -0
  105. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/loader.py +0 -0
  106. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/migration.py +0 -0
  107. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  108. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/operations/base.py +0 -0
  109. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/operations/fields.py +0 -0
  110. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/operations/models.py +0 -0
  111. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/operations/special.py +0 -0
  112. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/optimizer.py +0 -0
  113. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/questioner.py +0 -0
  114. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/recorder.py +0 -0
  115. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/serializer.py +0 -0
  116. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/state.py +0 -0
  117. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/utils.py +0 -0
  118. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/migrations/writer.py +0 -0
  119. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/options.py +0 -0
  120. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/otel.py +0 -0
  121. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/preflight/__init__.py +0 -0
  122. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/preflight/database.py +0 -0
  123. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/preflight/indexes.py +0 -0
  124. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/preflight/models.py +0 -0
  125. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/query_utils.py +0 -0
  126. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/registry.py +0 -0
  127. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/schema.py +0 -0
  128. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/sources.py +0 -0
  129. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/sql/__init__.py +0 -0
  130. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/sql/constants.py +0 -0
  131. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/sql/datastructures.py +0 -0
  132. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/sql/query.py +0 -0
  133. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/sql/where.py +0 -0
  134. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/test/__init__.py +0 -0
  135. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/test/database.py +0 -0
  136. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/transaction.py +0 -0
  137. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/types.py +0 -0
  138. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/types.pyi +0 -0
  139. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/plain/postgres/utils.py +0 -0
  140. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/forms.py +0 -0
  141. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  142. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  143. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  144. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  145. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  146. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  147. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  148. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  149. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  150. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  151. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  152. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  153. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  154. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  155. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  156. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  157. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  158. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  159. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/migrations/__init__.py +0 -0
  160. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/__init__.py +0 -0
  161. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/constraints.py +0 -0
  162. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/defaults.py +0 -0
  163. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/delete.py +0 -0
  164. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/encrypted.py +0 -0
  165. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/forms.py +0 -0
  166. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/indexes.py +0 -0
  167. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/iteration.py +0 -0
  168. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/mixins.py +0 -0
  169. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/nullability.py +0 -0
  170. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/querysets.py +0 -0
  171. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/relationships.py +0 -0
  172. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/storage_parameters.py +0 -0
  173. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/trees.py +0 -0
  174. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/models/unregistered.py +0 -0
  175. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/urls.py +0 -0
  176. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/examples/views.py +0 -0
  177. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/settings.py +0 -0
  178. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/app/urls.py +0 -0
  179. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/conftest_convergence.py +0 -0
  180. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_autodetector_not_null_errors.py +0 -0
  181. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_autodetector_type_change.py +0 -0
  182. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_connection_isolation.py +0 -0
  183. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_connection_lifecycle.py +0 -0
  184. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_connection_pool.py +0 -0
  185. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_connection_self_heal.py +0 -0
  186. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_convergence.py +0 -0
  187. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_convergence_defaults.py +0 -0
  188. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_convergence_fk.py +0 -0
  189. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_convergence_indexes.py +0 -0
  190. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_convergence_nullability.py +0 -0
  191. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
  192. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_convergence_timeouts.py +0 -0
  193. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_db_expression_defaults.py +0 -0
  194. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_diagnose.py +0 -0
  195. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_executor_connection_hook.py +0 -0
  196. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_fk_characterization.py +0 -0
  197. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_health.py +0 -0
  198. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_introspection.py +0 -0
  199. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_literal_default_persistence.py +0 -0
  200. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_management_connection.py +0 -0
  201. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_migration_executor.py +0 -0
  202. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_no_callable_defaults.py +0 -0
  203. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_otel_metrics.py +0 -0
  204. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  205. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
  206. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
  207. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_schema_normalize_type.py +0 -0
  208. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/internal/test_schema_timeouts.py +0 -0
  209. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_create_update.py +0 -0
  210. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_database_url.py +0 -0
  211. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_encrypted_fields.py +0 -0
  212. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_field_defaults.py +0 -0
  213. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_functions_uuid.py +0 -0
  214. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_integrity_error_mapping.py +0 -0
  215. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_iterator.py +0 -0
  216. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_m2m.py +0 -0
  217. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_manager_assignment.py +0 -0
  218. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_manual_pk.py +0 -0
  219. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_mixins.py +0 -0
  220. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_modelform_roundtrip.py +0 -0
  221. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_random_string_field.py +0 -0
  222. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_raw_query.py +0 -0
  223. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_read_only_transactions.py +0 -0
  224. {plain_postgres-0.110.1 → plain_postgres-0.111.0}/tests/public/test_related_instance_filter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.110.1
3
+ Version: 0.111.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-Expression: BSD-3-Clause
@@ -1,5 +1,27 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.111.0](https://github.com/dropseed/plain/releases/plain-postgres@0.111.0) (2026-06-30)
4
+
5
+ ### What's changed
6
+
7
+ - Slicing a `QuerySet` now always returns a `QuerySet`, even when its results are already cached — previously, slicing an already-evaluated queryset returned a plain `list`. The returned queryset carries the sliced cache, so iterating it won't re-query, but re-chaining it (adding a `.filter()`, ordering, etc.) drops the cache and applies the slice as SQL `LIMIT`/`OFFSET` so the rows stay correct. ([d60e07d6ad](https://github.com/dropseed/plain/commit/d60e07d6ad))
8
+ - Step slicing (e.g. `qs[::2]`) now raises `ValueError` instead of silently evaluating the queryset and returning a stepped `list`. Negative indexing and negative slice bounds continue to raise, as before. `RawQuerySet` is unaffected — it is always fully materialized, so its slicing keeps plain `list` semantics, where step and negative slicing still work. ([d60e07d6ad](https://github.com/dropseed/plain/commit/d60e07d6ad), [a873a521d4](https://github.com/dropseed/plain/commit/a873a521d4))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - If you relied on `queryset[a:b]` returning a `list` — for example checking `isinstance(result, list)`, calling list methods like `.append()` on it, or re-slicing it with a step — wrap it in `list(queryset[a:b])` to materialize it explicitly.
13
+ - Replace any step slicing of a queryset (`queryset[::2]`) with a `list(...)` conversion first, since it now raises `ValueError`.
14
+
15
+ ## [0.110.2](https://github.com/dropseed/plain/releases/plain-postgres@0.110.2) (2026-06-26)
16
+
17
+ ### What's changed
18
+
19
+ - Internal type-annotation cleanups (`Func` expression attributes, enum choices) and a dev-dependency bump (pytest 9.1.1) to clear a security advisory. No public API changes. ([288bbf0bb1](https://github.com/dropseed/plain/commit/288bbf0bb1))
20
+
21
+ ### Upgrade instructions
22
+
23
+ - No changes required.
24
+
3
25
  ## [0.110.1](https://github.com/dropseed/plain/releases/plain-postgres@0.110.1) (2026-06-22)
4
26
 
5
27
  ### What's changed
@@ -39,16 +39,7 @@ Do NOT import field classes directly from `plain.postgres` or `plain.postgres.fi
39
39
 
40
40
  ## Schema Changes
41
41
 
42
- When creating new models or modifying existing model fields/relationships, always enter plan mode first. Database schema is hard to change after the fact, so get the design right before writing code.
43
-
44
- In your plan, present:
45
-
46
- - Proposed schema as a table (model, field, type, constraints)
47
- - Relationship cardinality (1:1, 1:N, M:N)
48
- - Key decisions: nullable vs default, indexing, cascade behavior
49
- - Whether the data could live on an existing model instead of a new one
50
-
51
- Get approval before writing any model code or generating migrations.
42
+ Migrations are annoying to revise after the fact; convergence is cheap. So before making a batch of migration-generating changes — new models, new columns, or column-type changes think the design through first. Nullability, defaults, indexes, constraints, `on_delete`, and `choices` aren't migrations; they're convergence (edit the model and re-sync), so they stay cheap to revise. For a large set of migration-dependent changes, surfacing the design first (plan mode fits) is worth it.
52
43
 
53
44
  ## Migrations vs Convergence
54
45
 
@@ -58,7 +58,7 @@ class ChoicesMeta(enum.EnumMeta):
58
58
  @property
59
59
  def choices(cls) -> list[tuple[Any, str]]:
60
60
  empty = [(None, cls.__empty__)] if hasattr(cls, "__empty__") else []
61
- return empty + [(member.value, member.label) for member in cls] # ty: ignore[unresolved-attribute, invalid-return-type]
61
+ return empty + [(member.value, member.label) for member in cls] # ty: ignore[unresolved-attribute]
62
62
 
63
63
  @property
64
64
  def labels(cls) -> list[str]:
@@ -8,7 +8,7 @@ from collections import defaultdict
8
8
  from decimal import Decimal
9
9
  from functools import cached_property
10
10
  from types import NoneType
11
- from typing import TYPE_CHECKING, Any, Protocol, Self, runtime_checkable
11
+ from typing import TYPE_CHECKING, Any, Protocol, Self, cast, runtime_checkable
12
12
  from uuid import UUID
13
13
 
14
14
  import psycopg
@@ -911,10 +911,10 @@ class OuterRef(F):
911
911
  class Func(Expression):
912
912
  """An SQL function call."""
913
913
 
914
- function = None
915
- template = "%(function)s(%(expressions)s)"
916
- arg_joiner = ", "
917
- arity = None # The number of arguments the function accepts.
914
+ function: str | None = None
915
+ template: str = "%(function)s(%(expressions)s)"
916
+ arg_joiner: str = ", "
917
+ arity: int | None = None # The number of arguments the function accepts.
918
918
 
919
919
  def __init__(
920
920
  self, *expressions: Any, output_field: Field | None = None, **extra: Any
@@ -1001,10 +1001,14 @@ class Func(Expression):
1001
1001
  data["function"] = function
1002
1002
  else:
1003
1003
  data.setdefault("function", self.function)
1004
- template = template or data.get("template", self.template)
1005
- arg_joiner = arg_joiner or data.get("arg_joiner", self.arg_joiner)
1006
- data["expressions"] = data["field"] = arg_joiner.join(sql_parts)
1007
- return template % data, params
1004
+ # `data` is typed dict[str, Any], so the override values come back as
1005
+ # Any; they are always strings, so cast to keep the `or` fallback typed.
1006
+ resolved_template = template or cast(str, data.get("template", self.template))
1007
+ resolved_joiner = arg_joiner or cast(
1008
+ str, data.get("arg_joiner", self.arg_joiner)
1009
+ )
1010
+ data["expressions"] = data["field"] = resolved_joiner.join(sql_parts)
1011
+ return resolved_template % data, params
1008
1012
 
1009
1013
  def copy(self) -> Self:
1010
1014
  clone = super().copy()
@@ -423,38 +423,47 @@ class QuerySet[T: "Model"]:
423
423
  def __getitem__(self, k: int) -> T: ...
424
424
 
425
425
  @overload
426
- def __getitem__(self, k: slice) -> QuerySet[T] | list[T]: ...
426
+ def __getitem__(self, k: slice) -> QuerySet[T]: ...
427
427
 
428
- def __getitem__(self, k: int | slice) -> T | QuerySet[T] | list[T]:
429
- """Retrieve an item or slice from the set of results."""
428
+ def __getitem__(self, k: int | slice) -> T | QuerySet[T]:
429
+ """Retrieve an item or slice from the set of results.
430
+
431
+ Slicing always returns a QuerySet, even when the results are
432
+ already cached. The returned QuerySet behaves exactly like one
433
+ sliced before evaluation — same allowed operations — except it
434
+ carries the sliced cache so iterating it won't re-query. The slice
435
+ is also applied as SQL limits, so any operation that re-chains the
436
+ QuerySet drops the cache and re-queries the correct rows.
437
+
438
+ Negative indexing and step slicing both raise.
439
+ """
430
440
  if not isinstance(k, int | slice):
431
441
  raise TypeError(
432
442
  f"QuerySet indices must be integers or slices, not {type(k).__name__}."
433
443
  )
434
- if (isinstance(k, int) and k < 0) or (
435
- isinstance(k, slice)
436
- and (
437
- (k.start is not None and k.start < 0)
438
- or (k.stop is not None and k.stop < 0)
439
- )
440
- ):
441
- raise ValueError("Negative indexing is not supported.")
442
-
443
- if self._result_cache is not None:
444
- return self._result_cache[k]
445
444
 
446
445
  if isinstance(k, slice):
446
+ if (k.start is not None and k.start < 0) or (
447
+ k.stop is not None and k.stop < 0
448
+ ):
449
+ raise ValueError("Negative indexing is not supported.")
450
+ if k.step is not None:
451
+ raise ValueError("Step slicing is not supported.")
447
452
  qs = self._chain()
448
- if k.start is not None:
449
- start = int(k.start)
450
- else:
451
- start = None
452
- if k.stop is not None:
453
- stop = int(k.stop)
454
- else:
455
- stop = None
453
+ start = int(k.start) if k.start is not None else None
454
+ stop = int(k.stop) if k.stop is not None else None
456
455
  qs.sql_query.set_limits(start, stop)
457
- return list(qs)[:: k.step] if k.step else qs
456
+ if self._result_cache is not None:
457
+ # Carry the sliced cache so the new QuerySet won't re-query on
458
+ # iteration. The SQL limits above keep it correct if it's later
459
+ # re-chained, which drops the cache.
460
+ self._attach_result_cache(qs, self._result_cache[k])
461
+ return qs
462
+
463
+ if k < 0:
464
+ raise ValueError("Negative indexing is not supported.")
465
+ if self._result_cache is not None:
466
+ return self._result_cache[k]
458
467
 
459
468
  qs = self._chain()
460
469
  qs.sql_query.set_limits(k, k + 1)
@@ -1136,9 +1145,8 @@ class QuerySet[T: "Model"]:
1136
1145
  """
1137
1146
  obj = self._chain()
1138
1147
  # Preserve cache since all() doesn't modify the query.
1139
- # This is important for prefetch_related() to work correctly.
1140
- obj._result_cache = self._result_cache
1141
- obj._prefetch_done = self._prefetch_done
1148
+ if self._result_cache is not None:
1149
+ self._attach_result_cache(obj, self._result_cache)
1142
1150
  return obj
1143
1151
 
1144
1152
  def filter(self, *args: Any, **kwargs: Any) -> Self:
@@ -1511,6 +1519,17 @@ class QuerySet[T: "Model"]:
1511
1519
  c._fields = self._fields
1512
1520
  return c
1513
1521
 
1522
+ def _attach_result_cache(self, obj: Self, cache: list[T]) -> None:
1523
+ """Carry a result cache onto a chained QuerySet.
1524
+
1525
+ Whenever a cache is moved onto a new QuerySet, the prefetch state
1526
+ must ride along with it — otherwise prefetch_related() would re-run
1527
+ (or be skipped). Keep both writes together here so callers can't
1528
+ forget the pairing.
1529
+ """
1530
+ obj._result_cache = cache
1531
+ obj._prefetch_done = self._prefetch_done
1532
+
1514
1533
  def _fetch_all(self) -> None:
1515
1534
  if self._result_cache is None:
1516
1535
  self._result_cache = list(self._iterable_class(self))
@@ -1691,6 +1710,9 @@ class RawQuerySet:
1691
1710
  return f"<{self.__class__.__name__}: {self.sql_query}>"
1692
1711
 
1693
1712
  def __getitem__(self, k: int | slice) -> Model | list[Model]:
1713
+ # Unlike QuerySet, a RawQuerySet is always fully materialized — there's
1714
+ # no lazy query to push a slice into — so indexing keeps plain list
1715
+ # semantics: a slice returns a list (and step/negative slicing work).
1694
1716
  return list(self)[k]
1695
1717
 
1696
1718
  @cached_property
@@ -344,7 +344,7 @@ class SQLCompiler:
344
344
  if self.query.select_related:
345
345
  related_klass_infos = self.get_related_selections(select, select_mask)
346
346
  if klass_info is not None:
347
- klass_info["related_klass_infos"] = related_klass_infos # ty: ignore[invalid-assignment] (heterogeneous klass_info dict)
347
+ klass_info["related_klass_infos"] = related_klass_infos
348
348
 
349
349
  ret = []
350
350
  col_idx = 1
@@ -24,7 +24,7 @@ def _db_disabled() -> Generator[None]:
24
24
  """
25
25
 
26
26
  def cursor_disabled(self: Any) -> None:
27
- pytest.fail("Database access not allowed without the `db` fixture") # ty: ignore[invalid-argument-type]
27
+ pytest.fail("Database access not allowed without the `db` fixture")
28
28
 
29
29
  # Save original cursor method and replace with disabled version
30
30
  setattr(DatabaseConnection, "_enabled_cursor", DatabaseConnection.cursor)
@@ -63,7 +63,7 @@ def setup_db(request: Any) -> Generator[None]:
63
63
  @pytest.fixture
64
64
  def db(setup_db: Any, request: Any) -> Generator[None]:
65
65
  if "isolated_db" in request.fixturenames:
66
- pytest.fail("The 'db' and 'isolated_db' fixtures cannot be used together") # ty: ignore[invalid-argument-type]
66
+ pytest.fail("The 'db' and 'isolated_db' fixtures cannot be used together")
67
67
 
68
68
  # Set .cursor() back to the original implementation to unblock it
69
69
  DatabaseConnection.cursor = getattr(DatabaseConnection, "_enabled_cursor")
@@ -101,7 +101,7 @@ def isolated_db(request: Any) -> Generator[None]:
101
101
  test database.
102
102
  """
103
103
  if "db" in request.fixturenames:
104
- pytest.fail("The 'db' and 'isolated_db' fixtures cannot be used together") # ty: ignore[invalid-argument-type]
104
+ pytest.fail("The 'db' and 'isolated_db' fixtures cannot be used together")
105
105
  # Set .cursor() back to the original implementation to unblock it
106
106
  DatabaseConnection.cursor = getattr(DatabaseConnection, "_enabled_cursor")
107
107
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.postgres"
3
- version = "0.110.1"
3
+ version = "0.111.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"
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+
5
+ import pytest
6
+
7
+ from plain.postgres.connection import DatabaseConnection
8
+ from plain.postgres.db import _db_conn, get_connection
9
+
10
+
11
+ @pytest.fixture
12
+ def capture_queries():
13
+ """Return a context manager that records the SQL run within its block.
14
+
15
+ with capture_queries() as queries:
16
+ list(qs)
17
+ assert len(queries) == 1
18
+
19
+ ``queries`` is populated when the block exits with the executed query
20
+ dicts (each has a ``"sql"`` key), so inspect it after the ``with``.
21
+ """
22
+
23
+ @contextmanager
24
+ def _capture():
25
+ conn = get_connection()
26
+ previous = conn.force_debug_cursor
27
+ conn.force_debug_cursor = True
28
+ conn.queries_log.clear()
29
+ captured: list[dict] = []
30
+ try:
31
+ yield captured
32
+ finally:
33
+ captured.extend(conn.queries_log)
34
+ conn.force_debug_cursor = previous
35
+
36
+ return _capture
37
+
38
+
39
+ @pytest.fixture
40
+ def _unblock_cursor() -> None:
41
+ """Restore the real cursor method (blocked by the autouse _db_disabled fixture)."""
42
+ DatabaseConnection.cursor = getattr(DatabaseConnection, "_enabled_cursor")
43
+
44
+
45
+ @pytest.fixture
46
+ def _clean_connection():
47
+ """Ensure the ContextVar starts empty and clean up any connection afterward."""
48
+ token = _db_conn.set(None)
49
+ yield
50
+ conn = _db_conn.get()
51
+ if conn is not None:
52
+ try:
53
+ conn.close()
54
+ except Exception:
55
+ pass
56
+ _db_conn.reset(token)
@@ -12,7 +12,6 @@ from app.examples.models.constraints import ConstraintExample
12
12
  from plain.exceptions import NON_FIELD_ERRORS, ValidationError
13
13
  from plain.postgres import CheckConstraint, Q, UniqueConstraint
14
14
  from plain.postgres.constraints import BaseConstraint
15
- from plain.postgres.db import get_connection
16
15
  from plain.postgres.expressions import F
17
16
  from plain.postgres.forms import ModelForm
18
17
  from plain.test import RequestFactory
@@ -172,20 +171,13 @@ def test_save_runs_full_clean_by_default(
172
171
  assert ConstraintExample.query.filter(name="bad").count() == 0
173
172
 
174
173
 
175
- def test_save_skips_constraint_pre_check_select(db: None) -> None:
174
+ def test_save_skips_constraint_pre_check_select(db: None, capture_queries) -> None:
176
175
  """A default save() issues only the INSERT — the per-unique-constraint
177
176
  pre-check SELECT is gone. The database enforces the constraint and
178
177
  save_base maps any violation."""
179
- conn = get_connection()
180
- previous = conn.force_debug_cursor
181
- conn.force_debug_cursor = True
182
- conn.queries_log.clear()
183
- try:
178
+ with capture_queries() as queries:
184
179
  ConstraintExample(name="solo", description="row").create()
185
- query_count = len(conn.queries_log)
186
- finally:
187
- conn.force_debug_cursor = previous
188
- assert query_count == 1, [q["sql"] for q in conn.queries_log]
180
+ assert len(queries) == 1, [q["sql"] for q in queries]
189
181
 
190
182
 
191
183
  def test_save_clean_and_validate_false_skips_validation(
@@ -1379,7 +1379,9 @@ class TestProbeTableReuse:
1379
1379
  """analyze_model creates the probe temp table once per model and reuses it
1380
1380
  for every round-trip, instead of churning one CREATE/DROP per comparison."""
1381
1381
 
1382
- def test_analyze_model_shares_one_temp_table_across_round_trips(self, db):
1382
+ def test_analyze_model_shares_one_temp_table_across_round_trips(
1383
+ self, db, capture_queries
1384
+ ):
1383
1385
  from plain.postgres.convergence.analysis import _PROBE_TABLE
1384
1386
 
1385
1387
  # ConstraintExample already has a converged UNIQUE constraint (one
@@ -1398,15 +1400,12 @@ class TestProbeTableReuse:
1398
1400
  )
1399
1401
 
1400
1402
  conn = get_connection()
1401
- previous = conn.force_debug_cursor
1402
- conn.force_debug_cursor = True
1403
- conn.queries_log.clear()
1404
1403
  try:
1405
- with conn.cursor() as cursor:
1406
- analyze_model(conn, cursor, ConstraintExample)
1407
- sqls = [q["sql"] for q in conn.queries_log]
1404
+ with capture_queries() as queries:
1405
+ with conn.cursor() as cursor:
1406
+ analyze_model(conn, cursor, ConstraintExample)
1407
+ sqls = [q["sql"] for q in queries]
1408
1408
  finally:
1409
- conn.force_debug_cursor = previous
1410
1409
  ConstraintExample.model_options.constraints = original
1411
1410
  execute(
1412
1411
  'ALTER TABLE "examples_constraintexample" DROP CONSTRAINT '
@@ -9,20 +9,6 @@ from __future__ import annotations
9
9
 
10
10
  from app.examples.models.relationships import Tag, Widget, WidgetTag
11
11
 
12
- from plain.postgres.db import get_connection
13
-
14
-
15
- def _count_queries(fn):
16
- conn = get_connection()
17
- previous = conn.force_debug_cursor
18
- conn.force_debug_cursor = True
19
- conn.queries_log.clear()
20
- try:
21
- result = fn()
22
- return result, len(conn.queries_log)
23
- finally:
24
- conn.force_debug_cursor = previous
25
-
26
12
 
27
13
  def test_only_defers_unlisted_fields(db):
28
14
  Widget.query.create(name="W", size="L")
@@ -43,11 +29,12 @@ def test_deferred_field_loads_correct_value(db):
43
29
  assert widget.size == "L"
44
30
 
45
31
 
46
- def test_listed_field_is_available_without_a_query(db):
32
+ def test_listed_field_is_available_without_a_query(db, capture_queries):
47
33
  Widget.query.create(name="W", size="L")
48
34
  widget = Widget.query.only("id", "name").get()
49
- _, queries = _count_queries(lambda: widget.name)
50
- assert queries == 0
35
+ with capture_queries() as queries:
36
+ _ = widget.name
37
+ assert len(queries) == 0
51
38
 
52
39
 
53
40
  def test_refresh_from_db_reloads_values(db):
@@ -57,20 +44,22 @@ def test_refresh_from_db_reloads_values(db):
57
44
  assert widget.name == "changed"
58
45
 
59
46
 
60
- def test_deferred_field_access_query_count(db):
47
+ def test_deferred_field_access_query_count(db, capture_queries):
61
48
  # WAS: each deferred field loaded in its own query (two fields = two
62
49
  # queries). NOW: the first deferred access hydrates every still-missing
63
50
  # field, so the second field needs no further query.
64
51
  Widget.query.create(name="W", size="L")
65
52
  widget = Widget.query.only("id").get()
66
53
 
67
- _, first = _count_queries(lambda: widget.name)
68
- assert first == 1
69
- _, second = _count_queries(lambda: widget.size)
70
- assert second == 0
54
+ with capture_queries() as first:
55
+ _ = widget.name
56
+ assert len(first) == 1
57
+ with capture_queries() as second:
58
+ _ = widget.size
59
+ assert len(second) == 0
71
60
 
72
61
 
73
- def test_deferred_fk_column_loads_only_the_fk(db):
62
+ def test_deferred_fk_column_loads_only_the_fk(db, capture_queries):
74
63
  # The foreign key descriptor is the partial-related-instance fast path:
75
64
  # hydrating other deferred columns just to materialize a related-object
76
65
  # handle would defeat the optimization. So accessing a deferred FK loads
@@ -84,8 +73,9 @@ def test_deferred_fk_column_loads_only_the_fk(db):
84
73
  assert widget_tag.get_deferred_fields() == {"widget", "tag"}
85
74
 
86
75
  # Accessing the deferred FK loads exactly one column (the FK column).
87
- _, queries = _count_queries(lambda: widget_tag.widget)
88
- assert queries == 1
76
+ with capture_queries() as queries:
77
+ _ = widget_tag.widget
78
+ assert len(queries) == 1
89
79
  # The other deferred FK column is still deferred -- not hydrated as a
90
80
  # side effect of the first FK access.
91
81
  assert "tag" in widget_tag.get_deferred_fields()
@@ -376,7 +376,7 @@ def test_delete_rejects_sliced_queryset(db):
376
376
  _create_parents()
377
377
  qs = DeleteParent.query.all()[:1]
378
378
  with pytest.raises(TypeError):
379
- qs.delete() # ty: ignore[unresolved-attribute]
379
+ qs.delete()
380
380
 
381
381
 
382
382
  def test_delete_rejects_distinct_queryset(db):
@@ -485,25 +485,17 @@ def test_delete_already_deleted_instance_raises(db):
485
485
  # ===========================================================================
486
486
 
487
487
 
488
- def test_cascade_delete_issues_one_query(db):
488
+ def test_cascade_delete_issues_one_query(db, capture_queries):
489
489
  """Single-level CASCADE fires exactly one DELETE — Postgres handles the
490
490
  cascade internally. This is the headline win of the DB-level rewrite."""
491
- from plain.postgres.db import get_connection
492
-
493
491
  _create_parents()
494
492
  parent = DeleteParent.query.get(name="parent")
495
493
  for _ in range(5):
496
494
  ChildCascade.query.create(parent=parent)
497
495
 
498
- conn = get_connection()
499
- prev_force = conn.force_debug_cursor
500
- conn.force_debug_cursor = True
501
- conn.queries_log.clear()
502
- try:
496
+ with capture_queries() as queries:
503
497
  parent.delete()
504
- query_count = len(conn.queries_log)
505
- finally:
506
- conn.force_debug_cursor = prev_force
498
+ query_count = len(queries)
507
499
 
508
500
  assert query_count == 1, (
509
501
  f"Expected one DELETE; got {query_count} queries — Collector may have "
@@ -62,11 +62,11 @@ def test_base_exceptions_from_plain_exceptions():
62
62
  except ObjectDoesNotExist:
63
63
  pass # Should work due to inheritance
64
64
  except Exception:
65
- pytest.fail("Should have caught with base ObjectDoesNotExist") # ty: ignore[invalid-argument-type]
65
+ pytest.fail("Should have caught with base ObjectDoesNotExist")
66
66
 
67
67
  try:
68
68
  raise IterationExample.MultipleObjectsReturned("model-specific exception")
69
69
  except MultipleObjectsReturned:
70
70
  pass # Should work due to inheritance
71
71
  except Exception:
72
- pytest.fail("Should have caught with base MultipleObjectsReturned") # ty: ignore[invalid-argument-type]
72
+ pytest.fail("Should have caught with base MultipleObjectsReturned")
@@ -2,22 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from app.examples.models.iteration import IterationExample
4
4
 
5
- from plain.postgres.db import get_connection
6
5
 
7
-
8
- def _capture_queries(callable_):
9
- conn = get_connection()
10
- prev_force = conn.force_debug_cursor
11
- conn.force_debug_cursor = True
12
- conn.queries_log.clear()
13
- try:
14
- callable_()
15
- return list(conn.queries_log)
16
- finally:
17
- conn.force_debug_cursor = prev_force
18
-
19
-
20
- def test_repr_does_not_execute_sql_when_unevaluated(db):
6
+ def test_repr_does_not_execute_sql_when_unevaluated(db, capture_queries):
21
7
  """repr() of an unevaluated queryset must not issue a SQL query.
22
8
 
23
9
  Error reporters (Sentry, pdb, exception templates) call repr() on
@@ -26,13 +12,14 @@ def test_repr_does_not_execute_sql_when_unevaluated(db):
26
12
  """
27
13
  qs = IterationExample.query.all()
28
14
 
29
- queries = _capture_queries(lambda: repr(qs))
15
+ with capture_queries() as queries:
16
+ repr(qs)
30
17
 
31
18
  assert queries == []
32
19
  assert repr(qs) == "<QuerySet [unevaluated]>"
33
20
 
34
21
 
35
- def test_repr_uses_cache_when_evaluated(db):
22
+ def test_repr_uses_cache_when_evaluated(db, capture_queries):
36
23
  """Once a queryset is evaluated, repr() reflects its rows without re-querying."""
37
24
  IterationExample.query.create(name="alpha", tag="a")
38
25
  IterationExample.query.create(name="beta", tag="b")
@@ -40,7 +27,8 @@ def test_repr_uses_cache_when_evaluated(db):
40
27
  qs = IterationExample.query.all()
41
28
  list(qs) # force evaluation, populates _result_cache
42
29
 
43
- queries = _capture_queries(lambda: repr(qs))
30
+ with capture_queries() as queries:
31
+ repr(qs)
44
32
 
45
33
  assert queries == []
46
34
  rendered = repr(qs)