plain.postgres 0.108.0__tar.gz → 0.108.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/PKG-INFO +1 -1
  2. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/CHANGELOG.md +11 -0
  3. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/connection.py +13 -1
  4. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/otel.py +30 -19
  5. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/pyproject.toml +1 -1
  6. plain_postgres-0.108.1/tests/conftest.py +26 -0
  7. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_connection_pool.py +0 -14
  8. plain_postgres-0.108.1/tests/internal/test_connection_self_heal.py +131 -0
  9. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_otel_metrics.py +29 -0
  10. plain_postgres-0.108.0/tests/conftest.py +0 -11
  11. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/.gitignore +0 -0
  12. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/CLAUDE.md +0 -0
  13. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/LICENSE +0 -0
  14. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/README.md +0 -0
  15. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/README.md +0 -0
  16. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/__init__.py +0 -0
  17. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/adapters.py +0 -0
  18. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
  19. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  20. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/aggregates.py +0 -0
  21. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/base.py +0 -0
  22. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/cli/__init__.py +0 -0
  23. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/cli/converge.py +0 -0
  24. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/cli/core.py +0 -0
  25. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/cli/decorators.py +0 -0
  26. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/cli/diagnose.py +0 -0
  27. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/cli/migrations.py +0 -0
  28. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/cli/schema.py +0 -0
  29. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/cli/sync.py +0 -0
  30. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/config.py +0 -0
  31. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/constants.py +0 -0
  32. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/constraints.py +0 -0
  33. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/convergence/__init__.py +0 -0
  34. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/convergence/analysis.py +0 -0
  35. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/convergence/fixes.py +0 -0
  36. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/convergence/planning.py +0 -0
  37. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/database_url.py +0 -0
  38. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/db.py +0 -0
  39. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/ddl.py +0 -0
  40. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/default_settings.py +0 -0
  41. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/deletion.py +0 -0
  42. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/dialect.py +0 -0
  43. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/entrypoints.py +0 -0
  44. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/enums.py +0 -0
  45. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/exceptions.py +0 -0
  46. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/expressions.py +0 -0
  47. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/__init__.py +0 -0
  48. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/base.py +0 -0
  49. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/binary.py +0 -0
  50. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/boolean.py +0 -0
  51. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/duration.py +0 -0
  52. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/encrypted.py +0 -0
  53. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/json.py +0 -0
  54. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/mixins.py +0 -0
  55. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/network.py +0 -0
  56. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/numeric.py +0 -0
  57. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/primary_key.py +0 -0
  58. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/related.py +0 -0
  59. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/related_descriptors.py +0 -0
  60. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/related_lookups.py +0 -0
  61. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/related_managers.py +0 -0
  62. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/reverse_descriptors.py +0 -0
  63. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/reverse_related.py +0 -0
  64. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/temporal.py +0 -0
  65. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/text.py +0 -0
  66. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/timezones.py +0 -0
  67. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/fields/uuid.py +0 -0
  68. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/forms.py +0 -0
  69. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/__init__.py +0 -0
  70. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/comparison.py +0 -0
  71. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/datetime.py +0 -0
  72. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/math.py +0 -0
  73. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/mixins.py +0 -0
  74. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/random.py +0 -0
  75. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/text.py +0 -0
  76. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/uuid.py +0 -0
  77. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/functions/window.py +0 -0
  78. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/indexes.py +0 -0
  79. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/__init__.py +0 -0
  80. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/__init__.py +0 -0
  81. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  82. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  83. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/checks_structural.py +0 -0
  84. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/context.py +0 -0
  85. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/helpers.py +0 -0
  86. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/ownership.py +0 -0
  87. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/runner.py +0 -0
  88. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/health/types.py +0 -0
  89. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/introspection/schema.py +0 -0
  90. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/lookups.py +0 -0
  91. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/meta.py +0 -0
  92. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/middleware.py +0 -0
  93. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/__init__.py +0 -0
  94. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/autodetector.py +0 -0
  95. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/exceptions.py +0 -0
  96. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/executor.py +0 -0
  97. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/graph.py +0 -0
  98. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/loader.py +0 -0
  99. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/migration.py +0 -0
  100. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/operations/__init__.py +0 -0
  101. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/operations/base.py +0 -0
  102. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/operations/fields.py +0 -0
  103. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/operations/models.py +0 -0
  104. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/operations/special.py +0 -0
  105. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/optimizer.py +0 -0
  106. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/questioner.py +0 -0
  107. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/recorder.py +0 -0
  108. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/serializer.py +0 -0
  109. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/state.py +0 -0
  110. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/utils.py +0 -0
  111. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/migrations/writer.py +0 -0
  112. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/options.py +0 -0
  113. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/preflight/__init__.py +0 -0
  114. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/preflight/database.py +0 -0
  115. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/preflight/indexes.py +0 -0
  116. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/preflight/models.py +0 -0
  117. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/query.py +0 -0
  118. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/query_utils.py +0 -0
  119. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/registry.py +0 -0
  120. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/schema.py +0 -0
  121. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/sources.py +0 -0
  122. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/sql/__init__.py +0 -0
  123. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/sql/compiler.py +0 -0
  124. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/sql/constants.py +0 -0
  125. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/sql/datastructures.py +0 -0
  126. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/sql/query.py +0 -0
  127. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/sql/where.py +0 -0
  128. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/test/__init__.py +0 -0
  129. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/test/database.py +0 -0
  130. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/test/pytest.py +0 -0
  131. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/transaction.py +0 -0
  132. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/types.py +0 -0
  133. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/types.pyi +0 -0
  134. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/plain/postgres/utils.py +0 -0
  135. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/forms.py +0 -0
  136. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0001_initial.py +0 -0
  137. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  138. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  139. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  140. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  141. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  142. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  143. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  144. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  145. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  146. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  147. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  148. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  149. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  150. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  151. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  152. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  153. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  154. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/migrations/__init__.py +0 -0
  155. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/__init__.py +0 -0
  156. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/constraints.py +0 -0
  157. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/defaults.py +0 -0
  158. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/delete.py +0 -0
  159. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/encrypted.py +0 -0
  160. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/forms.py +0 -0
  161. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/indexes.py +0 -0
  162. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/iteration.py +0 -0
  163. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/mixins.py +0 -0
  164. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/nullability.py +0 -0
  165. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/querysets.py +0 -0
  166. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/relationships.py +0 -0
  167. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/storage_parameters.py +0 -0
  168. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/trees.py +0 -0
  169. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/models/unregistered.py +0 -0
  170. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/urls.py +0 -0
  171. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/examples/views.py +0 -0
  172. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/settings.py +0 -0
  173. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/app/urls.py +0 -0
  174. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/conftest_convergence.py +0 -0
  175. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_autodetector_not_null_errors.py +0 -0
  176. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_autodetector_type_change.py +0 -0
  177. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_connection_isolation.py +0 -0
  178. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_connection_lifecycle.py +0 -0
  179. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_constraint_violation_error.py +0 -0
  180. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_convergence.py +0 -0
  181. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_convergence_constraints.py +0 -0
  182. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_convergence_defaults.py +0 -0
  183. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_convergence_fk.py +0 -0
  184. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_convergence_indexes.py +0 -0
  185. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_convergence_nullability.py +0 -0
  186. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_convergence_storage_parameters.py +0 -0
  187. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_convergence_timeouts.py +0 -0
  188. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_db_expression_defaults.py +0 -0
  189. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_diagnose.py +0 -0
  190. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_executor_connection_hook.py +0 -0
  191. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_fk_characterization.py +0 -0
  192. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_health.py +0 -0
  193. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_introspection.py +0 -0
  194. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_literal_default_persistence.py +0 -0
  195. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_management_connection.py +0 -0
  196. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_migration_executor.py +0 -0
  197. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_no_callable_defaults.py +0 -0
  198. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
  199. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
  200. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_preflight_fk_coverage.py +0 -0
  201. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_schema_normalize_type.py +0 -0
  202. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/internal/test_schema_timeouts.py +0 -0
  203. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_create_update.py +0 -0
  204. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_database_url.py +0 -0
  205. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_deferred_loading.py +0 -0
  206. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_delete_behaviors.py +0 -0
  207. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_encrypted_fields.py +0 -0
  208. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_exceptions.py +0 -0
  209. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_field_defaults.py +0 -0
  210. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_functions_uuid.py +0 -0
  211. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_integrity_error_mapping.py +0 -0
  212. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_iterator.py +0 -0
  213. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_m2m.py +0 -0
  214. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_manager_assignment.py +0 -0
  215. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_manual_pk.py +0 -0
  216. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_mixins.py +0 -0
  217. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_modelform_roundtrip.py +0 -0
  218. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_queryset_repr.py +0 -0
  219. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_random_string_field.py +0 -0
  220. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_raw_query.py +0 -0
  221. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_read_only_transactions.py +0 -0
  222. {plain_postgres-0.108.0 → plain_postgres-0.108.1}/tests/public/test_related.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.108.0
3
+ Version: 0.108.1
4
4
  Summary: Model your data and store it in a database.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -1,5 +1,16 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.108.1](https://github.com/dropseed/plain/releases/plain-postgres@0.108.1) (2026-06-09)
4
+
5
+ ### What's changed
6
+
7
+ - `ensure_connection()` now detects a pooled connection the server has closed while it was held (Postgres restart, failover, idle timeout) and discards it before establishing a fresh one — logging a "Discarding dead database connection" warning — instead of repeatedly failing on the dead connection. Inside an atomic block the dead connection is intentionally left in place so the transaction's normal error handling runs, rather than silently continuing the rest of the block outside its transaction. ([9e82cb4454](https://github.com/dropseed/plain/commit/9e82cb4454))
8
+ - Query span instrumentation no longer pays for expensive attributes when the span is sampled out: the per-query Python stack walk (for `code.*` attributes) and the `DEBUG`-mode stringification of query parameters now only happen when the span is actually recording. The cheap connection attributes are still passed at span creation so attribute-aware samplers can see them in `should_sample()`. ([af736043b0](https://github.com/dropseed/plain/commit/af736043b0))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required.
13
+
3
14
  ## [0.108.0](https://github.com/dropseed/plain/releases/plain-postgres@0.108.0) (2026-06-09)
4
15
 
5
16
  ### What's changed
@@ -170,7 +170,19 @@ class DatabaseConnection:
170
170
  self.set_autocommit(True)
171
171
 
172
172
  def ensure_connection(self) -> None:
173
- """Guarantee that a connection to the database is established."""
173
+ """Guarantee that a live connection to the database is established."""
174
+ if (
175
+ self.connection is not None
176
+ and self.connection.closed
177
+ and not self.in_atomic_block
178
+ ):
179
+ # The server closed this connection while we held it (restart,
180
+ # failover, idle timeout) — psycopg marks it closed once an
181
+ # operation fails on it. Mid-atomic, swapping it would silently
182
+ # run the rest of the block outside its transaction — leave the
183
+ # dead connection for Atomic.__exit__'s error recovery instead.
184
+ logger.warning("Discarding dead database connection")
185
+ self.close()
174
186
  if self.connection is None:
175
187
  self.connect()
176
188
 
@@ -305,7 +305,10 @@ def db_span(
305
305
  # Single settings_dict read — the property delegates to source.config.
306
306
  cfg = db.settings_dict
307
307
 
308
- # Build attribute set following semantic conventions
308
+ # Cheap attributes are passed at span creation so attribute-aware
309
+ # samplers can see them in should_sample(). Expensive ones (the
310
+ # per-query stack walk, DEBUG params) are added after, only when the
311
+ # span actually records.
309
312
  attrs: dict[str, Any] = {
310
313
  DB_SYSTEM_NAME: DB_SYSTEM,
311
314
  DB_NAMESPACE: cfg.get("DATABASE"),
@@ -314,8 +317,6 @@ def db_span(
314
317
  DB_OPERATION_NAME: operation,
315
318
  }
316
319
 
317
- attrs.update(_get_code_attributes())
318
-
319
320
  # Add collection name if detected
320
321
  if collection_name:
321
322
  attrs[DB_COLLECTION_NAME] = collection_name
@@ -335,31 +336,41 @@ def db_span(
335
336
  attrs[SERVER_PORT] = port_int
336
337
  attrs[NETWORK_PEER_PORT] = port_int
337
338
 
338
- # Add query parameters as attributes when DEBUG is True
339
- if settings.DEBUG and params is not None:
340
- # Convert params to appropriate format based on type
341
- if isinstance(params, dict):
342
- # Dictionary params (e.g., for named placeholders)
343
- for key, value in params.items():
344
- attrs[f"{DB_QUERY_PARAMETER_TEMPLATE}.{key}"] = str(value)
345
- elif isinstance(params, list | tuple):
346
- # Sequential params (e.g., for %s or ? placeholders)
347
- for i, value in enumerate(params):
348
- attrs[f"{DB_QUERY_PARAMETER_TEMPLATE}.{i + 1}"] = str(value)
349
- else:
350
- # Single param (rare but possible)
351
- attrs[f"{DB_QUERY_PARAMETER_TEMPLATE}.1"] = str(params)
352
-
353
339
  with tracer.start_as_current_span(
354
340
  span_name, kind=SpanKind.CLIENT, attributes=attrs
355
341
  ) as span:
342
+ if span.is_recording():
343
+ expensive_attrs = _get_code_attributes()
344
+
345
+ # Add query parameters as attributes when DEBUG is True
346
+ if settings.DEBUG and params is not None:
347
+ # Convert params to appropriate format based on type
348
+ if isinstance(params, dict):
349
+ # Dictionary params (e.g., for named placeholders)
350
+ for key, value in params.items():
351
+ expensive_attrs[f"{DB_QUERY_PARAMETER_TEMPLATE}.{key}"] = str(
352
+ value
353
+ )
354
+ elif isinstance(params, list | tuple):
355
+ # Sequential params (e.g., for %s or ? placeholders)
356
+ for i, value in enumerate(params):
357
+ expensive_attrs[f"{DB_QUERY_PARAMETER_TEMPLATE}.{i + 1}"] = str(
358
+ value
359
+ )
360
+ else:
361
+ # Single param (rare but possible)
362
+ expensive_attrs[f"{DB_QUERY_PARAMETER_TEMPLATE}.1"] = str(params)
363
+
364
+ span.set_attributes(expensive_attrs)
365
+
356
366
  start = time.perf_counter()
357
367
  try:
358
368
  yield span
359
369
  except Exception as exc:
360
370
  # record_exception + set_status(ERROR) handled by
361
371
  # start_as_current_span when the exception propagates out.
362
- span.set_attribute(ERROR_TYPE, format_exception_type(exc))
372
+ if span.is_recording():
373
+ span.set_attribute(ERROR_TYPE, format_exception_type(exc))
363
374
  raise
364
375
  duration_s = time.perf_counter() - start
365
376
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.postgres"
3
- version = "0.108.0"
3
+ version = "0.108.1"
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,26 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from plain.postgres.connection import DatabaseConnection
6
+ from plain.postgres.db import _db_conn
7
+
8
+
9
+ @pytest.fixture
10
+ def _unblock_cursor() -> None:
11
+ """Restore the real cursor method (blocked by the autouse _db_disabled fixture)."""
12
+ DatabaseConnection.cursor = getattr(DatabaseConnection, "_enabled_cursor")
13
+
14
+
15
+ @pytest.fixture
16
+ def _clean_connection():
17
+ """Ensure the ContextVar starts empty and clean up any connection afterward."""
18
+ token = _db_conn.set(None)
19
+ yield
20
+ conn = _db_conn.get()
21
+ if conn is not None:
22
+ try:
23
+ conn.close()
24
+ except Exception:
25
+ pass
26
+ _db_conn.reset(token)
@@ -18,20 +18,6 @@ from plain.postgres.sources import runtime_pool_source
18
18
  from plain.runtime import settings
19
19
 
20
20
 
21
- @pytest.fixture
22
- def _clean_connection():
23
- """Ensure the ContextVar starts empty and clean up any connection afterward."""
24
- token = _db_conn.set(None)
25
- yield
26
- conn = _db_conn.get()
27
- if conn is not None:
28
- try:
29
- conn.close()
30
- except Exception:
31
- pass
32
- _db_conn.reset(token)
33
-
34
-
35
21
  class TestPoolCheckoutReturn:
36
22
  @pytest.mark.usefixtures("_unblock_cursor", "_clean_connection")
37
23
  def test_checkout_and_return(self, setup_db):
@@ -0,0 +1,131 @@
1
+ """Tests for self-healing after a server-side connection close.
2
+
3
+ When Postgres closes a connection we're holding (restart, failover, idle
4
+ timeout), psycopg only notices on the next I/O — the operation fails and the
5
+ connection is marked closed. `ensure_connection()` must then discard the dead
6
+ connection and acquire a fresh one, instead of reusing the dead object on
7
+ every subsequent call.
8
+
9
+ The exception is inside an atomic block: swapping there would silently run
10
+ the rest of the block outside its transaction, so the dead connection stays
11
+ put and `Atomic.__exit__`'s error recovery drops it instead.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import psycopg
17
+ import pytest
18
+
19
+ from plain.postgres import transaction
20
+ from plain.postgres.db import get_connection
21
+ from plain.postgres.sources import runtime_pool_source
22
+
23
+
24
+ def _terminate_backend(pid: int) -> None:
25
+ """Kill a backend from a separate pool connection."""
26
+ terminator = runtime_pool_source.acquire()
27
+ try:
28
+ with terminator.cursor() as cursor:
29
+ cursor.execute("SELECT pg_terminate_backend(%s)", (pid,))
30
+ finally:
31
+ runtime_pool_source.release(terminator)
32
+
33
+
34
+ def _backend_pid(conn) -> int:
35
+ with conn.cursor() as cursor:
36
+ cursor.execute("SELECT pg_backend_pid()")
37
+ row = cursor.fetchone()
38
+ assert row is not None
39
+ return row[0]
40
+
41
+
42
+ class TestDeadConnectionSelfHeal:
43
+ @pytest.mark.usefixtures("_unblock_cursor", "_clean_connection")
44
+ def test_closed_connection_replaced_on_next_use(self, setup_db):
45
+ """A wrapper holding a closed psycopg connection acquires a fresh one."""
46
+ conn = get_connection()
47
+ with conn.cursor() as cursor:
48
+ cursor.execute("SELECT 1")
49
+
50
+ # Simulate the aftermath of a server-side close: the wrapper still
51
+ # holds a psycopg connection whose pgconn status is BAD.
52
+ assert conn.connection is not None
53
+ conn.connection.close()
54
+ assert conn.connection.closed
55
+
56
+ with conn.cursor() as cursor:
57
+ cursor.execute("SELECT 1")
58
+ assert cursor.fetchone() == (1,)
59
+ assert conn.connection is not None
60
+ assert not conn.connection.closed
61
+
62
+ @pytest.mark.usefixtures("_unblock_cursor", "_clean_connection")
63
+ def test_terminated_backend_errors_once_then_heals(self, setup_db):
64
+ """A server-side kill fails the in-flight query, then self-heals."""
65
+ conn = get_connection()
66
+ pid = _backend_pid(conn)
67
+
68
+ _terminate_backend(pid)
69
+
70
+ # psycopg only detects the dead socket on I/O — the first use fails
71
+ # and marks the connection closed.
72
+ with pytest.raises(psycopg.OperationalError):
73
+ with conn.cursor() as cursor:
74
+ cursor.execute("SELECT 1")
75
+ assert conn.connection is not None
76
+ assert conn.connection.closed
77
+
78
+ # The next use discards the dead connection and acquires a fresh one.
79
+ with conn.cursor() as cursor:
80
+ cursor.execute("SELECT 1")
81
+ assert cursor.fetchone() == (1,)
82
+ assert _backend_pid(conn) != pid
83
+
84
+ @pytest.mark.usefixtures("_unblock_cursor", "_clean_connection")
85
+ def test_dead_connection_not_swapped_mid_atomic(self, setup_db):
86
+ """Mid-atomic, the dead connection stays put — a silent swap would run
87
+ the rest of the block outside its transaction. Atomic.__exit__'s error
88
+ recovery drops it, and the next use heals."""
89
+ conn = get_connection()
90
+
91
+ def killed_mid_atomic() -> None:
92
+ with transaction.atomic():
93
+ pid = _backend_pid(conn)
94
+ _terminate_backend(pid)
95
+ with conn.cursor() as cursor:
96
+ cursor.execute("SELECT 1")
97
+
98
+ with pytest.raises(psycopg.OperationalError):
99
+ killed_mid_atomic()
100
+
101
+ # Atomic.__exit__ dropped the dead connection during rollback recovery.
102
+ assert conn.connection is None
103
+
104
+ with conn.cursor() as cursor:
105
+ cursor.execute("SELECT 1")
106
+ assert cursor.fetchone() == (1,)
107
+
108
+ @pytest.mark.usefixtures("_unblock_cursor", "_clean_connection")
109
+ def test_dead_connection_in_nested_atomic(self, setup_db):
110
+ """Nested atomics: the inner savepoint rollback fails on the dead
111
+ connection and marks needs_rollback; the outer rollback then fails
112
+ too and drops the connection. The next use heals."""
113
+ conn = get_connection()
114
+
115
+ def killed_in_nested_atomic() -> None:
116
+ with transaction.atomic():
117
+ with transaction.atomic():
118
+ pid = _backend_pid(conn)
119
+ _terminate_backend(pid)
120
+ with conn.cursor() as cursor:
121
+ cursor.execute("SELECT 1")
122
+
123
+ with pytest.raises(psycopg.OperationalError):
124
+ killed_in_nested_atomic()
125
+
126
+ # The outer Atomic.__exit__ dropped the dead connection.
127
+ assert conn.connection is None
128
+
129
+ with conn.cursor() as cursor:
130
+ cursor.execute("SELECT 1")
131
+ assert cursor.fetchone() == (1,)
@@ -15,8 +15,10 @@ from opentelemetry.sdk.metrics.export import InMemoryMetricReader
15
15
  from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
16
16
  InMemorySpanExporter,
17
17
  )
18
+ from opentelemetry.trace import NoOpTracer
18
19
  from psycopg_pool import PoolTimeout
19
20
 
21
+ from plain.postgres import otel as postgres_otel
20
22
  from plain.postgres.db import get_connection
21
23
  from plain.postgres.otel import register_pool_observables
22
24
  from plain.postgres.sources import runtime_pool_source
@@ -60,6 +62,33 @@ class TestQuerySpanAttributes:
60
62
  assert "server.port" in attrs
61
63
  assert attrs["server.port"] == attrs.get("network.peer.port")
62
64
 
65
+ def test_not_recording_skips_stack_walk(
66
+ self, monkeypatch: pytest.MonkeyPatch
67
+ ) -> None:
68
+ # Cheap attributes still build unconditionally (attribute-aware
69
+ # samplers see them at span creation), but the per-query stack walk
70
+ # must only happen when the span actually records.
71
+ stack_walks: list[int] = []
72
+
73
+ def counting_code_attributes() -> dict[str, Any]:
74
+ stack_walks.append(1)
75
+ return {}
76
+
77
+ monkeypatch.setattr(
78
+ postgres_otel, "_get_code_attributes", counting_code_attributes
79
+ )
80
+ monkeypatch.setattr(postgres_otel, "tracer", NoOpTracer())
81
+
82
+ class StubDb:
83
+ settings_dict: dict[str, Any] = {}
84
+
85
+ with postgres_otel.db_span(StubDb(), "SELECT 1") as span: # ty: ignore[invalid-argument-type]
86
+ pass
87
+
88
+ assert span is not None
89
+ assert not span.is_recording()
90
+ assert not stack_walks
91
+
63
92
 
64
93
  class TestReturnedRowsMetric:
65
94
  @pytest.mark.usefixtures("db")
@@ -1,11 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import pytest
4
-
5
- from plain.postgres.connection import DatabaseConnection
6
-
7
-
8
- @pytest.fixture
9
- def _unblock_cursor() -> None:
10
- """Restore the real cursor method (blocked by the autouse _db_disabled fixture)."""
11
- DatabaseConnection.cursor = getattr(DatabaseConnection, "_enabled_cursor")