plain.postgres 0.97.0__tar.gz → 0.99.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 (210) hide show
  1. plain_postgres-0.97.0/plain/postgres/README.md → plain_postgres-0.99.0/PKG-INFO +162 -44
  2. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/CHANGELOG.md +63 -0
  3. plain_postgres-0.97.0/PKG-INFO → plain_postgres-0.99.0/plain/postgres/README.md +148 -56
  4. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/__init__.py +3 -0
  5. plain_postgres-0.99.0/plain/postgres/adapters.py +41 -0
  6. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/core.py +16 -7
  7. plain_postgres-0.99.0/plain/postgres/cli/diagnose.py +312 -0
  8. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/config.py +4 -0
  9. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/connection.py +39 -303
  10. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/database_url.py +36 -9
  11. plain_postgres-0.99.0/plain/postgres/db.py +158 -0
  12. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/default_settings.py +7 -3
  13. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/introspection/__init__.py +2 -0
  14. plain_postgres-0.99.0/plain/postgres/introspection/health/__init__.py +34 -0
  15. plain_postgres-0.99.0/plain/postgres/introspection/health/checks_cumulative.py +670 -0
  16. plain_postgres-0.99.0/plain/postgres/introspection/health/checks_snapshot.py +330 -0
  17. plain_postgres-0.99.0/plain/postgres/introspection/health/checks_structural.py +292 -0
  18. plain_postgres-0.99.0/plain/postgres/introspection/health/context.py +223 -0
  19. plain_postgres-0.99.0/plain/postgres/introspection/health/helpers.py +164 -0
  20. plain_postgres-0.99.0/plain/postgres/introspection/health/ownership.py +65 -0
  21. plain_postgres-0.99.0/plain/postgres/introspection/health/runner.py +158 -0
  22. plain_postgres-0.99.0/plain/postgres/introspection/health/types.py +54 -0
  23. plain_postgres-0.99.0/plain/postgres/middleware.py +37 -0
  24. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/otel.py +192 -11
  25. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/preflight.py +31 -1
  26. plain_postgres-0.99.0/plain/postgres/sources.py +221 -0
  27. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/test/database.py +47 -24
  28. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/test/pytest.py +26 -19
  29. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/transaction.py +8 -12
  30. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/utils.py +22 -8
  31. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/pyproject.toml +3 -3
  32. plain_postgres-0.99.0/tests/conftest.py +11 -0
  33. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_connection_isolation.py +16 -16
  34. plain_postgres-0.99.0/tests/test_connection_lifecycle.py +383 -0
  35. plain_postgres-0.99.0/tests/test_connection_pool.py +162 -0
  36. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_timeouts.py +2 -1
  37. plain_postgres-0.99.0/tests/test_diagnose.py +244 -0
  38. plain_postgres-0.99.0/tests/test_executor_connection_hook.py +103 -0
  39. plain_postgres-0.99.0/tests/test_health.py +47 -0
  40. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_management_connection.py +3 -3
  41. plain_postgres-0.99.0/tests/test_otel_metrics.py +243 -0
  42. plain_postgres-0.99.0/tests/test_read_only_transactions.py +87 -0
  43. plain_postgres-0.97.0/plain/postgres/cli/diagnose.py +0 -206
  44. plain_postgres-0.97.0/plain/postgres/connections.py +0 -126
  45. plain_postgres-0.97.0/plain/postgres/db.py +0 -38
  46. plain_postgres-0.97.0/plain/postgres/introspection/health.py +0 -737
  47. plain_postgres-0.97.0/tests/test_connection_lifecycle.py +0 -354
  48. plain_postgres-0.97.0/tests/test_read_only_transactions.py +0 -116
  49. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/.gitignore +0 -0
  50. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/CLAUDE.md +0 -0
  51. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/LICENSE +0 -0
  52. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/README.md +0 -0
  53. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
  54. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  55. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/aggregates.py +0 -0
  56. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/base.py +0 -0
  57. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/__init__.py +0 -0
  58. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/converge.py +0 -0
  59. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/decorators.py +0 -0
  60. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/migrations.py +0 -0
  61. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/schema.py +0 -0
  62. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/cli/sync.py +0 -0
  63. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/constants.py +0 -0
  64. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/constraints.py +0 -0
  65. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/convergence/__init__.py +0 -0
  66. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/convergence/analysis.py +0 -0
  67. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/convergence/fixes.py +0 -0
  68. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/convergence/planning.py +0 -0
  69. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/ddl.py +0 -0
  70. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/deletion.py +0 -0
  71. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/dialect.py +0 -0
  72. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/entrypoints.py +0 -0
  73. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/enums.py +0 -0
  74. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/exceptions.py +0 -0
  75. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/expressions.py +0 -0
  76. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/__init__.py +0 -0
  77. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/base.py +0 -0
  78. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/binary.py +0 -0
  79. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/boolean.py +0 -0
  80. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/duration.py +0 -0
  81. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/encrypted.py +0 -0
  82. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/json.py +0 -0
  83. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/mixins.py +0 -0
  84. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/network.py +0 -0
  85. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/numeric.py +0 -0
  86. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/primary_key.py +0 -0
  87. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/related.py +0 -0
  88. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/related_descriptors.py +0 -0
  89. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/related_lookups.py +0 -0
  90. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/related_managers.py +0 -0
  91. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
  92. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/reverse_related.py +0 -0
  93. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/temporal.py +0 -0
  94. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/text.py +0 -0
  95. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/timezones.py +0 -0
  96. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/fields/uuid.py +0 -0
  97. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/forms.py +0 -0
  98. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/__init__.py +0 -0
  99. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/comparison.py +0 -0
  100. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/datetime.py +0 -0
  101. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/math.py +0 -0
  102. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/mixins.py +0 -0
  103. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/random.py +0 -0
  104. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/text.py +0 -0
  105. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/uuid.py +0 -0
  106. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/functions/window.py +0 -0
  107. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/indexes.py +0 -0
  108. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/introspection/schema.py +0 -0
  109. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/lookups.py +0 -0
  110. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/meta.py +0 -0
  111. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/__init__.py +0 -0
  112. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/autodetector.py +0 -0
  113. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/exceptions.py +0 -0
  114. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/executor.py +0 -0
  115. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/graph.py +0 -0
  116. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/loader.py +0 -0
  117. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/migration.py +0 -0
  118. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/__init__.py +0 -0
  119. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/base.py +0 -0
  120. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/fields.py +0 -0
  121. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/models.py +0 -0
  122. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/operations/special.py +0 -0
  123. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/optimizer.py +0 -0
  124. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/questioner.py +0 -0
  125. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/recorder.py +0 -0
  126. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/serializer.py +0 -0
  127. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/state.py +0 -0
  128. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/utils.py +0 -0
  129. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/migrations/writer.py +0 -0
  130. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/options.py +0 -0
  131. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/query.py +0 -0
  132. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/query_utils.py +0 -0
  133. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/registry.py +0 -0
  134. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/schema.py +0 -0
  135. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/__init__.py +0 -0
  136. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/compiler.py +0 -0
  137. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/constants.py +0 -0
  138. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/datastructures.py +0 -0
  139. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/query.py +0 -0
  140. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/sql/where.py +0 -0
  141. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/test/__init__.py +0 -0
  142. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/types.py +0 -0
  143. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/plain/postgres/types.pyi +0 -0
  144. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/forms.py +0 -0
  145. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  146. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  147. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  148. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  149. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  150. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  151. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  152. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  153. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  154. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  155. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  156. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  157. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  158. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  159. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  160. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  161. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  162. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/migrations/__init__.py +0 -0
  163. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/__init__.py +0 -0
  164. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/constraints.py +0 -0
  165. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/defaults.py +0 -0
  166. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/delete.py +0 -0
  167. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/encrypted.py +0 -0
  168. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/forms.py +0 -0
  169. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/indexes.py +0 -0
  170. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/iteration.py +0 -0
  171. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/mixins.py +0 -0
  172. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/nullability.py +0 -0
  173. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/querysets.py +0 -0
  174. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/relationships.py +0 -0
  175. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/trees.py +0 -0
  176. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/models/unregistered.py +0 -0
  177. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/urls.py +0 -0
  178. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/examples/views.py +0 -0
  179. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/settings.py +0 -0
  180. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/app/urls.py +0 -0
  181. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/conftest_convergence.py +0 -0
  182. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_autodetector_not_null_errors.py +0 -0
  183. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_autodetector_type_change.py +0 -0
  184. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence.py +0 -0
  185. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_constraints.py +0 -0
  186. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_defaults.py +0 -0
  187. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_fk.py +0 -0
  188. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_indexes.py +0 -0
  189. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_convergence_nullability.py +0 -0
  190. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_database_url.py +0 -0
  191. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_db_expression_defaults.py +0 -0
  192. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_delete_behaviors.py +0 -0
  193. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_encrypted_fields.py +0 -0
  194. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_exceptions.py +0 -0
  195. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_field_defaults.py +0 -0
  196. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_functions_uuid.py +0 -0
  197. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_introspection.py +0 -0
  198. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_iterator.py +0 -0
  199. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_literal_default_persistence.py +0 -0
  200. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_m2m.py +0 -0
  201. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_manager_assignment.py +0 -0
  202. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_migration_executor.py +0 -0
  203. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_mixins.py +0 -0
  204. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_modelform_roundtrip.py +0 -0
  205. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_no_callable_defaults.py +0 -0
  206. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_random_string_field.py +0 -0
  207. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_raw_query.py +0 -0
  208. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_related.py +0 -0
  209. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_schema_normalize_type.py +0 -0
  210. {plain_postgres-0.97.0 → plain_postgres-0.99.0}/tests/test_schema_timeouts.py +0 -0
@@ -1,9 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: plain.postgres
3
+ Version: 0.99.0
4
+ Summary: Model your data and store it in a database.
5
+ Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
+ License-Expression: BSD-3-Clause
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.13
9
+ Requires-Dist: plain<1.0.0,>=0.134.0
10
+ Requires-Dist: psycopg-pool>=3.2
11
+ Requires-Dist: psycopg>=3.2
12
+ Requires-Dist: sqlparse>=0.3.1
13
+ Description-Content-Type: text/markdown
14
+
1
15
  # plain.postgres
2
16
 
3
17
  **Model your data and store it in a database.**
4
18
 
5
19
  - [Overview](#overview)
6
20
  - [Database connection](#database-connection)
21
+ - [Middleware](#middleware)
22
+ - [Bypassing a connection pooler for management operations](#bypassing-a-connection-pooler-for-management-operations)
7
23
  - [Querying](#querying)
8
24
  - [Schema management](#schema-management)
9
25
  - [Syncing](#syncing)
@@ -99,6 +115,24 @@ To explicitly disable the database (e.g. during Docker builds where no database
99
115
  PLAIN_POSTGRES_URL=none
100
116
  ```
101
117
 
118
+ ### Middleware
119
+
120
+ Connections are checked out lazily on first use and returned to the pool when the HTTP request finishes. That's handled by [`DatabaseConnectionMiddleware`](./middleware.py#DatabaseConnectionMiddleware) — add it to `MIDDLEWARE` once you install `plain.postgres`:
121
+
122
+ ```python
123
+ # app/settings.py
124
+ MIDDLEWARE = [
125
+ "plain.postgres.DatabaseConnectionMiddleware",
126
+ # ...other middleware
127
+ ]
128
+ ```
129
+
130
+ Place it near the top so downstream middleware can use the database inside `before_request` / `after_response` and still have the connection returned cleanly at the end.
131
+
132
+ For `StreamingResponse` / `AsyncStreamingResponse`, the connection is returned after the body is fully drained (not when the view returns), so generators that lazily query the database — for example `Model.query.iterator()` or raw cursor loops — keep their cursor alive until the last chunk is sent.
133
+
134
+ Without the middleware, connections keep living on their thread until something explicitly calls `plain.postgres.db.return_database_connection()` (or the process exits). That's fine for short-lived scripts but wastes a connection per thread in long-running servers.
135
+
102
136
  ### Bypassing a connection pooler for management operations
103
137
 
104
138
  Transaction-mode poolers (PlanetScale, Supabase's pooler, Neon's pooler, standalone pgbouncer in transaction mode) can't run DDL, long transactions, or `pg_dump`. To work around this, set a second URL that management commands use to reach Postgres directly:
@@ -128,14 +162,6 @@ with use_management_connection():
128
162
 
129
163
  You _can_ point the two URLs at different Postgres roles — e.g. a least-privilege DML role for runtime and a DDL-capable role for management. Plain does not currently automate the grant/ownership plumbing that split requires (default privileges for newly-created tables, ownership reassignment, preflight checks that the runtime role can see the schema). If you adopt that pattern, you're responsible for wiring those up yourself.
130
164
 
131
- **PostgreSQL is the only supported database.** You need to install a PostgreSQL driver separately — [psycopg](https://www.psycopg.org/) is recommended:
132
-
133
- ```bash
134
- uv add psycopg[binary] # Pre-built wheels, easiest for local development
135
- # or
136
- uv add psycopg[c] # Compiled against your system's libpq, recommended for production
137
- ```
138
-
139
165
  ## Querying
140
166
 
141
167
  Models come with a powerful query API through their [`QuerySet`](./query.py#QuerySet) interface:
@@ -443,32 +469,34 @@ with transaction.atomic():
443
469
  safe_operation() # This still runs in the outer transaction
444
470
  ```
445
471
 
446
- ### Read-only connections
472
+ ### Read-only transactions
447
473
 
448
- Enforce read-only mode on the current database connection using `read_only()`. Any write (INSERT, UPDATE, DELETE, DDL) raises `psycopg.errors.ReadOnlySqlTransaction`:
474
+ Run a block of code in a read-only transaction using `read_only()`. Any write (INSERT, UPDATE, DELETE, DDL) raises `psycopg.errors.ReadOnlySqlTransaction`:
449
475
 
450
476
  ```python
451
- from plain.postgres.connections import read_only
477
+ from plain.postgres.db import read_only
452
478
 
453
479
  with read_only():
454
480
  users = User.query.all() # reads work
455
481
  User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
456
482
  ```
457
483
 
458
- This works with both autocommit queries and explicit `atomic()` blocks.
484
+ `read_only()` opens a single `BEGIN READ ONLY` transaction for the block. Nested `atomic()` blocks inside become savepoints of the outer read-only transaction and inherit read-only.
459
485
 
460
- For sticky read-only mode (e.g., a shell session), use `set_read_only()` on the connection directly:
486
+ Because it opens its own transaction, `read_only()` cannot be entered inside an existing `atomic()` block doing so raises `TransactionManagementError`.
461
487
 
462
- ```python
463
- from plain.postgres.db import get_connection
488
+ Because the whole block is one transaction, catching a database error inside `read_only()` and trying to keep reading will fail — the transaction is aborted and any further query raises `TransactionManagementError`. Wrap the write in a nested `atomic()` savepoint if you need to recover and continue:
464
489
 
465
- conn = get_connection()
466
- conn.set_read_only(True) # all subsequent queries are read-only
467
- conn.set_read_only(False) # back to normal
490
+ ```python
491
+ with read_only():
492
+ try:
493
+ with atomic():
494
+ User.query.create(name="x") # raises, savepoint rolls back
495
+ except psycopg.errors.ReadOnlySqlTransaction:
496
+ pass
497
+ User.query.count() # still works — outer txn is healthy
468
498
  ```
469
499
 
470
- Read-only mode must be set outside a transaction — calling it inside `atomic()` raises `TransactionManagementError`.
471
-
472
500
  ## Schema management
473
501
 
474
502
  Schema changes fall into three categories, each with a different author and apply model:
@@ -1170,37 +1198,110 @@ graph TB
1170
1198
 
1171
1199
  ## Diagnostics
1172
1200
 
1173
- You can run health checks against your database to find issues like missing indexes, redundant indexes, and configuration problems.
1201
+ Run health checks against your database. `diagnose` is designed to produce
1202
+ only actionable findings — every warning has a copy-paste fix or specific
1203
+ resource to investigate, and noisy one-off signals (hit ratios, XID age) are
1204
+ surfaced as informational context rather than as warnings.
1174
1205
 
1175
1206
  ```bash
1176
1207
  uv run plain postgres diagnose
1177
1208
  ```
1178
1209
 
1179
- Use `--json` for structured output (useful for scripting and AI agents):
1210
+ Output modes:
1180
1211
 
1181
1212
  ```bash
1182
- uv run plain postgres diagnose --json
1213
+ uv run plain postgres diagnose --json # structured output for scripts/agents
1214
+ uv run plain postgres diagnose --verbose # expand to show every check, including passing
1215
+ uv run plain postgres diagnose --all # include findings on installed-package tables
1183
1216
  ```
1184
1217
 
1185
- Use `--all` to include issues in installed packages (by default, only your app's issues are shown):
1218
+ ### Guiding principle
1186
1219
 
1187
- ```bash
1188
- uv run plain postgres diagnose --all
1189
- ```
1220
+ `diagnose` emits a **warning** only if the remedy fits in the user's codebase
1221
+ or is an app-level action they own. If the remedy is "run SQL against your
1222
+ DB" or "configure your Postgres server," the check emits **operational
1223
+ context** or an **informational number**, not a warning. This keeps the
1224
+ warning surface high-trust — every warning has an edit-to-make — and prevents
1225
+ `diagnose` from bleeding into DB-host concerns.
1226
+
1227
+ ### Warning-tier checks
1228
+
1229
+ Things the user can fix by editing code + running `plain postgres sync`, or
1230
+ app-level incidents they must act on.
1231
+
1232
+ **Structural — always-real; a fix is possible immediately.**
1233
+
1234
+ | Check | What it finds | Severity |
1235
+ | ----------------------- | --------------------------------------------------------------------------------------------------- | ---------------- |
1236
+ | **Invalid indexes** | Broken indexes from failed `CREATE INDEX CONCURRENTLY` — maintained on writes, never used for reads | Warning |
1237
+ | **Duplicate indexes** | One index is a column-prefix of another on the same table | Warning |
1238
+ | **Missing FK indexes** | Foreign key columns without any index coverage | Warning |
1239
+ | **Sequence exhaustion** | Identity sequences approaching their type max | Warning/Critical |
1190
1240
 
1191
- ### Checks
1241
+ **Cumulative — depends on stats since the last reset.**
1192
1242
 
1193
- | Check | What it finds | Severity |
1194
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
1195
- | **Invalid indexes** | Broken indexes from failed `CREATE INDEX CONCURRENTLY` maintained on writes, never used for reads | Warning |
1196
- | **Duplicate indexes** | Indexes where one is a column-prefix of another on the same table (e.g., an auto FK index that's redundant with a composite index) | Warning |
1197
- | **Unused indexes** | Indexes with zero scans since stats reset (>1 MB). Excludes unique indexes, constraint-backing indexes, and indexes that are the sole coverage for a FK column | Warning |
1198
- | **Missing FK indexes** | Foreign key columns without any index coverage parent DELETE/UPDATE operations will sequentially scan the child table | Warning |
1199
- | **Sequence exhaustion** | Identity sequences approaching their type max (>50% warning, >90% critical) | Warning/Critical |
1200
- | **XID wraparound** | Transaction ID age approaching the 2 billion wraparound limit (>25% warning, >40% critical) | Warning/Critical |
1201
- | **Cache hit ratio** | Heap buffer hit ratio below 98.5% — indicates insufficient `shared_buffers` or RAM | Warning |
1202
- | **Index hit ratio** | Index buffer hit ratio below 98.5% | Warning |
1203
- | **Vacuum health** | Tables with significant dead tuple accumulation (>10% of live rows) where autovacuum may be falling behind | Warning |
1243
+ | Check | What it finds | Severity |
1244
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- |
1245
+ | **Unused indexes** | Indexes with zero scans since stats reset (>1 MB). Excludes unique, constraint-backing, and sole-FK-coverage indexes | Warning |
1246
+ | **Missing index candidates** | Tables with seq-scan activity suggesting a missing index. Includes top contributing queries from pg_stat_statements | Warning |
1247
+
1248
+ **Snapshotpoint-in-time incidents.**
1249
+
1250
+ | Check | What it finds | Severity |
1251
+ | ---------------------------- | ----------------------------------------------------------------------- | ---------------- |
1252
+ | **Long-running connections** | Client backends idle-in-transaction or running a query past a threshold | Warning/Critical |
1253
+ | **Blocking queries** | Queries currently blocking other queries via held locks | Warning/Critical |
1254
+
1255
+ ### Operational-context findings
1256
+
1257
+ These are facts about the database whose remedies live outside Plain today
1258
+ (`ANALYZE`, `VACUUM`, `REINDEX`, autovacuum server tuning). They're surfaced
1259
+ so agents and humans can interpret findings correctly, but the CLI renders
1260
+ them as context rather than alarming warnings — the user can't express the
1261
+ fix in their model code. (In JSON output each finding still carries
1262
+ `status: "warning"`; the `tier: "operational"` field is what distinguishes
1263
+ it.) Each finding still carries the exact SQL in its suggestion for anyone
1264
+ who wants to act.
1265
+
1266
+ | Finding | What it reports |
1267
+ | ------------------- | -------------------------------------------------------------------------------- |
1268
+ | **Stats freshness** | Tables whose planner statistics are missing (never analyzed) or stale |
1269
+ | **Vacuum health** | Tables with >10% dead tuples |
1270
+ | **Index bloat** | btree indexes with significant estimated wasted space (≥10 MB, ioguix estimator) |
1271
+
1272
+ If a future release exposes per-table autovacuum / fillfactor parameters in
1273
+ `model_options` (see the `postgres-model-storage-parameters` arc), these
1274
+ findings can graduate back to the warning tier — because the remedy will be
1275
+ expressible in code.
1276
+
1277
+ ### Informational context
1278
+
1279
+ Alongside checks, `diagnose` surfaces context an agent or human may want to read but that isn't actionable on its own:
1280
+
1281
+ - **Cache hit ratio**, **Index hit ratio** — buffer hit rates (volatile after restart; not a warning in themselves)
1282
+ - **XID wraparound** — transaction ID age as a percent of the 2B limit. Autovacuum usually keeps this low; long-running transactions can block the freeze process even on managed Postgres
1283
+ - **Connection saturation** — active/max connections at this moment
1284
+ - **Stats reset** — when cumulative stats were last reset (affects the confidence of operational checks)
1285
+ - **pg_stat_statements** — whether the extension is installed
1286
+
1287
+ ### Cross-check caveats
1288
+
1289
+ Findings whose confidence depends on another check are tagged with a caveat. For example:
1290
+
1291
+ - `unused_indexes` on a table flagged by `stats_freshness` → caveat: "this table has never been analyzed — the planner may not yet use this index; re-check after running ANALYZE"
1292
+ - `missing_index_candidates` on a never-analyzed table → caveat: "planner statistics are absent — running ANALYZE may change query plans and make this finding moot"
1293
+
1294
+ This prevents false confidence: dropping an "unused" index on a never-analyzed table is often the wrong move.
1295
+
1296
+ ### Model-aware suggestions
1297
+
1298
+ Findings on app-owned tables include the Plain model class and its source file. Suggestions reference the exact edit point:
1299
+
1300
+ ```
1301
+ app/processing/models.py :: ProcessingResult — Add an Index on ["is_processing"] to the model, then run plain postgres sync
1302
+ ```
1303
+
1304
+ This closes the loop from detection to fix — agents can draft the model edit without guessing.
1204
1305
 
1205
1306
  ### App vs package issues
1206
1307
 
@@ -1220,6 +1321,8 @@ heroku run -a your-app "plain postgres diagnose --json"
1220
1321
 
1221
1322
  The `--json` flag must be quoted so Heroku passes it through to the command.
1222
1323
 
1324
+ Cumulative-stat checks (`stats_freshness`, `vacuum_health`, `unused_indexes`, `missing_index_candidates`, `index_bloat`) need cumulative stat history after the last reset to be reliable. Check the `stats_reset` informational to see how much history you have. (Note: this list spans both the warning and operational tiers — the common thread is that all five depend on counters that `pg_stat_reset()` wipes.)
1325
+
1223
1326
  ### Preflight checks
1224
1327
 
1225
1328
  Two related checks run automatically during `uv run plain preflight` (and `uv run plain check`):
@@ -1229,6 +1332,12 @@ Two related checks run automatically during `uv run plain preflight` (and `uv ru
1229
1332
 
1230
1333
  These are static, code-level checks that catch issues before you deploy. The `diagnose` command complements them with runtime stats from the actual database.
1231
1334
 
1335
+ ### What diagnose deliberately doesn't do
1336
+
1337
+ - **LLM-powered column recommendations for missing indexes** — `missing_index_candidates` shows the culprit queries and lets you decide. For precise column-level suggestions, use a platform tool (PlanetScale Insights, Dexter, pg_qualstats + hypopg).
1338
+ - **Historical trending** — `diagnose` is stateless; it reports on the current state of cumulative stats. Continuous monitoring is out of scope.
1339
+ - **Niche server checks** (WAL bloat, replication slot age, etc.) — better covered by your Postgres provider's monitoring or a dedicated tool; users on self-hosted setups that need them typically have their own tooling.
1340
+
1232
1341
  ## Settings
1233
1342
 
1234
1343
  The connection is configured with a single URL (`POSTGRES_URL`). `DATABASE_URL` is read as a platform-compat fallback. Set the URL to `none` to explicitly disable the database (e.g. during Docker image builds).
@@ -1237,8 +1346,10 @@ The connection is configured with a single URL (`POSTGRES_URL`). `DATABASE_URL`
1237
1346
  | ---------------------------------------- | ------------- | ----------------------- | ---------------------------------------------- |
1238
1347
  | `POSTGRES_URL` | `Secret[str]` | `$DATABASE_URL` or `""` | `PLAIN_POSTGRES_URL` |
1239
1348
  | `POSTGRES_MANAGEMENT_URL` | `Secret[str]` | `""` | `PLAIN_POSTGRES_MANAGEMENT_URL` |
1240
- | `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
1241
- | `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
1349
+ | `POSTGRES_POOL_MIN_SIZE` | `int` | `4` | `PLAIN_POSTGRES_POOL_MIN_SIZE` |
1350
+ | `POSTGRES_POOL_MAX_SIZE` | `int` | `20` | `PLAIN_POSTGRES_POOL_MAX_SIZE` |
1351
+ | `POSTGRES_POOL_MAX_LIFETIME` | `float` | `3600.0` | `PLAIN_POSTGRES_POOL_MAX_LIFETIME` |
1352
+ | `POSTGRES_POOL_TIMEOUT` | `float` | `30.0` | `PLAIN_POSTGRES_POOL_TIMEOUT` |
1242
1353
  | `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
1243
1354
  | `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
1244
1355
  | `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
@@ -1282,13 +1393,15 @@ Currently, Plain supports a single database connection per application. For appl
1282
1393
 
1283
1394
  ## Installation
1284
1395
 
1285
- Install the `plain.postgres` package from [PyPI](https://pypi.org/project/plain.postgres/):
1396
+ Install the `plain.postgres` package from [PyPI](https://pypi.org/project/plain.postgres/). You must also pick a `psycopg` implementation — `plain.postgres` depends on `psycopg` but does not pick one for you, so installing `plain.postgres` alone will not be able to connect.
1286
1397
 
1287
1398
  ```bash
1288
- uv add plain.postgres psycopg[binary]
1399
+ uv add plain.postgres psycopg[binary] # Pre-built wheels, easiest for local development
1400
+ # or
1401
+ uv add plain.postgres psycopg[c] # Compiled against your system's libpq, recommended for production
1289
1402
  ```
1290
1403
 
1291
- Then add to your `INSTALLED_PACKAGES`:
1404
+ Then add to your `INSTALLED_PACKAGES` and register [`DatabaseConnectionMiddleware`](#middleware) so pooled connections are returned at the end of each request:
1292
1405
 
1293
1406
  ```python
1294
1407
  # app/settings.py
@@ -1296,4 +1409,9 @@ INSTALLED_PACKAGES = [
1296
1409
  ...
1297
1410
  "plain.postgres",
1298
1411
  ]
1412
+
1413
+ MIDDLEWARE = [
1414
+ "plain.postgres.DatabaseConnectionMiddleware",
1415
+ ...
1416
+ ]
1299
1417
  ```
@@ -1,5 +1,68 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.99.0](https://github.com/dropseed/plain/releases/plain-postgres@0.99.0) (2026-04-23)
4
+
5
+ ### What's changed
6
+
7
+ - **Reworked `plain postgres diagnose` around tiered findings.** Warnings are now reserved for things the user can fix by editing model code or taking an app-level action — every warning carries a copy-paste fix or a model-file pointer (`app/path.py :: ModelName`). Noisy one-off signals (cache/index hit ratios, XID wraparound, connection saturation, pg_stat_statements availability, stats reset age) render as **informational context**; DB-state facts whose remedies live outside Plain (stats freshness, vacuum health, index bloat) render as **operational context** instead of warnings. Added `--verbose` to expand every check, and `--all` still includes installed-package tables. ([26abb6cbc075](https://github.com/dropseed/plain/commit/26abb6cbc075))
8
+ - **New diagnostic checks:** `stats_freshness` (uses `pg_class.reltuples` so it survives `pg_stat_reset`), `index_bloat` (ioguix btree estimator, public schema only), `missing_index_candidates` (seq-scan heuristics with per-query drill-down from `pg_stat_statements`), `blocking_queries` (wait age from `pg_locks.waitstart`, PG 14+), and `long_running_connections` (xact age for idle-in-transaction). Findings include **cross-check caveats** — e.g. an `unused_indexes` finding on a table that's also flagged by `stats_freshness` or `vacuum_health` now carries a warning that dropping the index may be premature. ([26abb6cbc075](https://github.com/dropseed/plain/commit/26abb6cbc075))
9
+ - **Permission-safe probes.** Checks that may hit permission errors (`pg_stat_statements`, `pg_stat_activity`, `pg_locks`) now wrap their queries in `cursor.connection.transaction()` so a failure rolls back cleanly in either autocommit or transaction mode without cascade-failing later checks. ([26abb6cbc075](https://github.com/dropseed/plain/commit/26abb6cbc075))
10
+ - **Refactored internals.** The 1800+ line `introspection/health.py` split into an `introspection/health/` package along natural seams (types, ownership, context, helpers, checks grouped by `structural`/`cumulative`/`snapshot`, and a runner). Public re-exports are unchanged. ([26abb6cbc075](https://github.com/dropseed/plain/commit/26abb6cbc075))
11
+ - Adapter annotations use `Response` after plain 0.135.0 merged `ResponseBase` into `Response`. ([f5007281d7fa](https://github.com/dropseed/plain/commit/f5007281d7fa))
12
+
13
+ ### Upgrade instructions
14
+
15
+ - Requires `plain>=0.135.0`.
16
+ - No code changes required. If you parse `plain postgres diagnose --json`, note the new `tier` field on each finding (`"structural"`, `"cumulative"`, `"snapshot"`, or `"operational"`) — operational findings still carry `status: "warning"` but the CLI renders them as context rather than as alarming warnings.
17
+
18
+ ## [0.98.0](https://github.com/dropseed/plain/releases/plain-postgres@0.98.0) (2026-04-22)
19
+
20
+ ### What's changed
21
+
22
+ - **Pool-backed connections via `psycopg_pool.ConnectionPool`.** A new `sources` abstraction routes `DatabaseConnection` through either a long-lived `PoolSource` (runtime) or a `DirectSource` (management / one-shot). Each request checks a connection out of the pool on first use and returns it when the HTTP request finishes. `psycopg>=3.2` and `psycopg-pool>=3.2` are now declared as hard dependencies. ([2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
23
+ - **New `DatabaseConnectionMiddleware` (required).** Add `"plain.postgres.DatabaseConnectionMiddleware"` to `MIDDLEWARE` — it's what returns the pooled connection at the end of each request. For `StreamingResponse` / `AsyncStreamingResponse` the connection is returned after the body fully drains, so generators that lazily query the database (e.g. `Model.query.iterator()`) keep their cursor alive until the last chunk is sent. A new `postgres.middleware_installed` preflight check errors if the middleware is missing. ([2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
24
+ - **Connection settings replaced with pool settings.** `POSTGRES_CONN_MAX_AGE` and `POSTGRES_CONN_HEALTH_CHECKS` are gone. Tune the pool with `POSTGRES_POOL_MIN_SIZE` (default `4`), `POSTGRES_POOL_MAX_SIZE` (default `20`), `POSTGRES_POOL_MAX_LIFETIME` seconds (default `3600.0`), and `POSTGRES_POOL_TIMEOUT` seconds (default `30.0`). Each is also available as a `PLAIN_POSTGRES_POOL_*` environment variable. ([2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
25
+ - **`plain.postgres.connections` module removed.** `get_connection`, `has_connection`, `use_management_connection`, and `read_only` now live in `plain.postgres.db` (the underscore-less counterpart). ([2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
26
+ - **`read_only()` is now pgbouncer-safe.** It opens a single `BEGIN READ ONLY` transaction for the block (previously a session-level `SET default_transaction_read_only = on`). Nested `atomic()` blocks become savepoints of the outer read-only transaction. Entering `read_only()` inside an existing `atomic()` block now raises `TransactionManagementError`. The old `DatabaseConnection.set_read_only()` method is removed. ([ebdec30](https://github.com/dropseed/plain/commit/ebdec30))
27
+ - **Added OTel pool + rowcount metrics and semconv polish.** Wires the `db.client.connection.*` metric family (count, max, idle.min/max, pending_requests, wait_time, use_time, timeouts) from the pool's stats and the acquire/release path, plus `db.client.response.returned_rows` for SELECT queries including streamed iterators. Query spans now carry `server.address` / `server.port` alongside `network.peer.*`, and the tracer/meter are tagged with the `plain.postgres` package version for `InstrumentationScope`. ([61278d5](https://github.com/dropseed/plain/commit/61278d5))
28
+ - **Moved `psql` CLI orchestration off `DatabaseConnection`.** New `postgres_cli_args` / `postgres_cli_env` helpers in `plain.postgres.database_url` build the arguments and environment for `psql`, `pg_dump`, etc.; `plain postgres shell` and the `plain-dev` backup client both use them. `DatabaseConnection.runshell()` and `executable_name` are gone. ([5b4a488](https://github.com/dropseed/plain/commit/5b4a488))
29
+ - **Removed dead connection-lifecycle plumbing.** `close_if_unusable_or_obsolete`, `close_if_health_check_failed`, `closed_in_transaction`, `is_usable`, `health_check_enabled`, `health_check_done`, `close_at`, `_maintenance_cursor`, and `DatabaseConnection.from_url` are gone — the pool handles recycling, health checks, and URL parsing. `close()` now validates there's no open atomic block instead of silently deferring. ([044e942](https://github.com/dropseed/plain/commit/044e942), [2a51b25](https://github.com/dropseed/plain/commit/2a51b25))
30
+ - **Inlined `pg_version` and removed `temporary_connection()`.** The single caller now reads `connection.info.server_version` directly; `temporary_connection()` has no remaining users. ([319f6ac](https://github.com/dropseed/plain/commit/319f6ac))
31
+ - **`APIResult` shorthand returns moved out of `View`.** Any internal views that relied on dict/int shorthand now wrap their returns in `JsonResponse` / `Response(status_code=...)` to match plain 0.134.0's narrower `View` handler return type. ([1935f3f](https://github.com/dropseed/plain/commit/1935f3f))
32
+ - **Adapter registration extracted to `plain.postgres.adapters`.** `PlainRangeDumper` and `get_adapters_template()` moved out of `connection.py` into their own module.
33
+
34
+ ### Upgrade instructions
35
+
36
+ - Requires `plain>=0.134.0`.
37
+ - **Add the middleware** to `app/settings.py`:
38
+
39
+ ```python
40
+ MIDDLEWARE = [
41
+ "plain.postgres.DatabaseConnectionMiddleware",
42
+ # ...the rest of your middleware
43
+ ]
44
+ ```
45
+
46
+ Place it near the top so downstream middleware can use the database inside `before_request` / `after_response` and still have the connection returned cleanly. Preflight will error if it's missing.
47
+
48
+ - **Replace `POSTGRES_CONN_MAX_AGE` / `POSTGRES_CONN_HEALTH_CHECKS`** with the pool settings (`POSTGRES_POOL_MIN_SIZE`, `POSTGRES_POOL_MAX_SIZE`, `POSTGRES_POOL_MAX_LIFETIME`, `POSTGRES_POOL_TIMEOUT`) or remove them to take the defaults.
49
+
50
+ - **Update imports from `plain.postgres.connections`** to `plain.postgres.db`:
51
+
52
+ ```python
53
+ # Before
54
+ from plain.postgres.connections import get_connection, read_only, use_management_connection
55
+
56
+ # After
57
+ from plain.postgres.db import get_connection, read_only, use_management_connection
58
+ ```
59
+
60
+ - **If you called `DatabaseConnection.set_read_only(True)`** for a sticky read-only session, switch to the `read_only()` context manager around the block you want read-only. If you need session-level enforcement outside a transaction, open a `DirectSource` connection yourself and issue `SET default_transaction_read_only = on` on it.
61
+
62
+ - **If you entered `read_only()` inside an `atomic()` block**, move `read_only()` to the outer position — it now owns the transaction. Nested `atomic()` blocks inside `read_only()` are fine (they become savepoints).
63
+
64
+ - **If you pinned `psycopg` via your own dependency**, make sure it's `>=3.2`, and add `psycopg-pool>=3.2` if you were installing psycopg without extras.
65
+
3
66
  ## [0.97.0](https://github.com/dropseed/plain/releases/plain-postgres@0.97.0) (2026-04-21)
4
67
 
5
68
  ### What's changed