matrx-orm 3.1.71__tar.gz → 3.1.72__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 (334) hide show
  1. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/PKG-INFO +2 -2
  2. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/__init__.py +11 -0
  3. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/FEATURE.md +20 -1
  4. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/async_db_manager.py +117 -16
  5. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/pool_maintenance.py +12 -0
  6. matrx_orm-3.1.72/matrx_orm/core/pool_watch.py +318 -0
  7. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/dynamic_admin.py +9 -9
  8. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/staging_load.py +15 -10
  9. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/pyproject.toml +2 -2
  10. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_connection_poison_guard.py +24 -11
  11. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_insert_ignore_and_admin_primitives.py +2 -1
  12. matrx_orm-3.1.72/tests/level1/test_pool_holder_census.py +138 -0
  13. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_pool_maintenance.py +6 -1
  14. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/.gitignore +0 -0
  15. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/AGENTS.md +0 -0
  16. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/BASE_CLASS_METHODS.md +0 -0
  17. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/CLAUDE.md +0 -0
  18. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/MODEL_API.md +0 -0
  19. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/README.md +0 -0
  20. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/database/orm/extended/managers/ai_model_base.py +0 -0
  21. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/docs/migrations.md +0 -0
  22. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/MODULE_README.md +0 -0
  23. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/MODULE_README.md +0 -0
  24. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/__init__.py +0 -0
  25. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/async_postgresql.py +0 -0
  26. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/base_adapter.py +0 -0
  27. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/postgresql.py +0 -0
  28. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  29. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/adapters/supabase_adapter.py +0 -0
  30. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/admin/__init__.py +0 -0
  31. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/admin/router.py +0 -0
  32. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/__init__.py +0 -0
  33. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/auth.py +0 -0
  34. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/config.py +0 -0
  35. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/handlers.py +0 -0
  36. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/protocol.py +0 -0
  37. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/api/server.py +0 -0
  38. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/associations.py +0 -0
  39. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/cache_debug.py +0 -0
  40. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/catalog.py +0 -0
  41. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/catalog_sql.py +0 -0
  42. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/MODULE_README.md +0 -0
  43. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/__init__.py +0 -0
  44. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/postgres_connection.py +0 -0
  45. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/postgrest.py +0 -0
  46. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/supabase_auth.py +0 -0
  47. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/supabase_config.py +0 -0
  48. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/client/supabase_manager.py +0 -0
  49. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  50. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/MODULE_README.md +0 -0
  51. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  52. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/TASKS.md +0 -0
  53. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/__init__.py +0 -0
  54. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/base.py +0 -0
  55. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/config.py +0 -0
  56. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/connections.py +0 -0
  57. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/db_function.py +0 -0
  58. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/diagnostics.py +0 -0
  59. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/expressions.py +0 -0
  60. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/extended.py +0 -0
  61. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/fields.py +0 -0
  62. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/introspection.py +0 -0
  63. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/listen.py +0 -0
  64. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/loop_filters.py +0 -0
  65. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/model_dto.py +0 -0
  66. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/model_view.py +0 -0
  67. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/one_database.py +0 -0
  68. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/paginator.py +0 -0
  69. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/pydantic_bridge.py +0 -0
  70. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/registry.py +0 -0
  71. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/relations.py +0 -0
  72. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/resilience.py +0 -0
  73. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/rls_session.py +0 -0
  74. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/signals.py +0 -0
  75. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/tls.py +0 -0
  76. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/transaction.py +0 -0
  77. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/types.py +0 -0
  78. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/core/write_queue.py +0 -0
  79. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/entity.py +0 -0
  80. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/error_handling.py +0 -0
  81. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/exceptions.py +0 -0
  82. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/extended/__init__.py +0 -0
  83. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/extended/app_error_handler.py +0 -0
  84. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/identity_rule.py +0 -0
  85. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/middleware/__init__.py +0 -0
  86. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/middleware/base.py +0 -0
  87. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/MODULE_README.md +0 -0
  88. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/__init__.py +0 -0
  89. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/cli.py +0 -0
  90. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/ddl.py +0 -0
  91. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/diff.py +0 -0
  92. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/executor.py +0 -0
  93. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/integration.py +0 -0
  94. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/loader.py +0 -0
  95. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/operations.py +0 -0
  96. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/state.py +0 -0
  97. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/migrations/table_filter.py +0 -0
  98. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/MODULE_README.md +0 -0
  99. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/__init__.py +0 -0
  100. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/bulk_update_values.py +0 -0
  101. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/conflict.py +0 -0
  102. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/conflict_writes.py +0 -0
  103. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/create.py +0 -0
  104. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/db_functions.py +0 -0
  105. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/delete.py +0 -0
  106. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/dynamic_crud.py +0 -0
  107. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/graph_walk.py +0 -0
  108. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/latest_rows.py +0 -0
  109. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/queue_claim.py +0 -0
  110. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/read.py +0 -0
  111. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/relational_backfill.py +0 -0
  112. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/operations/update.py +0 -0
  113. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/platform_db.py +0 -0
  114. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/py.typed +0 -0
  115. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/MODULE_README.md +0 -0
  116. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/__init__.py +0 -0
  117. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/db_objects.py +0 -0
  118. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  119. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  120. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/FEATURE.md +0 -0
  121. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/MODULE_README.md +0 -0
  122. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/__init__.py +0 -0
  123. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/builder.py +0 -0
  124. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/query/executor.py +0 -0
  125. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  126. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/__init__.py +0 -0
  127. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/code_handler.py +0 -0
  128. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  129. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/columns.py +0 -0
  130. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/common.py +0 -0
  131. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/diff_preview.py +0 -0
  132. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  133. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/generator.py +0 -0
  134. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  135. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  136. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  137. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  138. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/package_wiring.py +0 -0
  139. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/relationships.py +0 -0
  140. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/runner.py +0 -0
  141. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/schema.py +0 -0
  142. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/schema_manager.py +0 -0
  143. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/tables.py +0 -0
  144. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/schema_builder/views.py +0 -0
  145. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  146. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/__init__.py +0 -0
  147. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/attachments.py +0 -0
  148. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/crypto.py +0 -0
  149. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/items.py +0 -0
  150. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/model.py +0 -0
  151. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/org_service.py +0 -0
  152. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/secrets_battery/service.py +0 -0
  153. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/README.md +0 -0
  154. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/__init__.py +0 -0
  155. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/coalesce.py +0 -0
  156. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/dag.py +0 -0
  157. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/errors.py +0 -0
  158. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/fallback.py +0 -0
  159. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/flush.py +0 -0
  160. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/lifecycle.py +0 -0
  161. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/managed.py +0 -0
  162. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/op.py +0 -0
  163. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/reads.py +0 -0
  164. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/session.py +0 -0
  165. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/session/telemetry.py +0 -0
  166. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  167. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/__init__.py +0 -0
  168. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/executor.py +0 -0
  169. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/queries.py +0 -0
  170. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/registry.py +0 -0
  171. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/types.py +0 -0
  172. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/sql_executor/utils.py +0 -0
  173. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/state.py +0 -0
  174. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/utils/__init__.py +0 -0
  175. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/utils/sql_utils.py +0 -0
  176. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/matrx_orm/utils/type_converters.py +0 -0
  177. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/scripts/git-branches.sh +0 -0
  178. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/scripts/publish.sh +0 -0
  179. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/scripts/release.sh +0 -0
  180. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/MODULE_README.md +0 -0
  181. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/__init__.py +0 -0
  182. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/conftest.py +0 -0
  183. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/MODULE_README.md +0 -0
  184. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/__init__.py +0 -0
  185. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_admin_db_columns.py +0 -0
  186. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_admin_search_negation.py +0 -0
  187. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_admin_search_sentinels.py +0 -0
  188. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_advisory_lock.py +0 -0
  189. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_agent_message.py +0 -0
  190. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_api_auth.py +0 -0
  191. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_api_config.py +0 -0
  192. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_api_handlers.py +0 -0
  193. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_api_protocol.py +0 -0
  194. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_array_agg_order_by.py +0 -0
  195. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_associations.py +0 -0
  196. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_bulk_hydration_offload.py +0 -0
  197. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_bulk_update_by_pk.py +0 -0
  198. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  199. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_cache_debug.py +0 -0
  200. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_call_function.py +0 -0
  201. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_case_when.py +0 -0
  202. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_catalog.py +0 -0
  203. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_composite_pk_filter.py +0 -0
  204. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_composite_pk_write_paths.py +0 -0
  205. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_config.py +0 -0
  206. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_conflict_target.py +0 -0
  207. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_conflict_writes.py +0 -0
  208. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_connection_codecs.py +0 -0
  209. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_connection_primitives.py +0 -0
  210. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_count_composite_distinct.py +0 -0
  211. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_db_function_field.py +0 -0
  212. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_ddl_generator.py +0 -0
  213. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_derived_table.py +0 -0
  214. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_dirty_tracking.py +0 -0
  215. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_dynamic_crud.py +0 -0
  216. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_exceptions.py +0 -0
  217. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  218. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_expression_primitives.py +0 -0
  219. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_fields.py +0 -0
  220. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  221. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_from_alias.py +0 -0
  222. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_fts.py +0 -0
  223. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_func_expression_args.py +0 -0
  224. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_graph_walk.py +0 -0
  225. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_having_annotate_alias.py +0 -0
  226. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_introspect_rls_policies.py +0 -0
  227. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_join_and_aggregate.py +0 -0
  228. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_latest_rows.py +0 -0
  229. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_listen_notify.py +0 -0
  230. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_managed_write_guard.py +0 -0
  231. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_matrx_entity.py +0 -0
  232. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_migration_diff_types.py +0 -0
  233. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_migration_loader.py +0 -0
  234. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_model_instance.py +0 -0
  235. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_model_meta.py +0 -0
  236. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_optimistic_concurrency.py +0 -0
  237. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_order_by_expression.py +0 -0
  238. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_parameter_name_hydration.py +0 -0
  239. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_patch_jsonb_path.py +0 -0
  240. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_pool_loop_recreation.py +0 -0
  241. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_postgrest_filters.py +0 -0
  242. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_pydantic_bridge.py +0 -0
  243. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_query_builder.py +0 -0
  244. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_query_error_detail.py +0 -0
  245. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_query_executor_sql.py +0 -0
  246. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_query_timeout.py +0 -0
  247. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  248. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_queue_claim.py +0 -0
  249. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_registry.py +0 -0
  250. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_registry_multi_database.py +0 -0
  251. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_registry_multi_schema.py +0 -0
  252. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_relational_backfill.py +0 -0
  253. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_relations.py +0 -0
  254. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_resilience.py +0 -0
  255. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_rls_session_scope.py +0 -0
  256. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_schema_exists.py +0 -0
  257. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_session_advisory_lock.py +0 -0
  258. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_state_cache.py +0 -0
  259. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_subquery_filter_raw.py +0 -0
  260. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_subquery_in_filter.py +0 -0
  261. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_supabase_auth.py +0 -0
  262. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_supabase_config.py +0 -0
  263. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_tls.py +0 -0
  264. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_typed_join_projection.py +0 -0
  265. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_typed_predicates.py +0 -0
  266. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_update_case_expression.py +0 -0
  267. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_update_subquery.py +0 -0
  268. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_update_with_rebase.py +0 -0
  269. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_upsert_default_leak.py +0 -0
  270. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_upsert_with_conflict.py +0 -0
  271. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_values_jsonb_decode.py +0 -0
  272. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_watched_lifecycle.py +0 -0
  273. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level1/test_write_retry_semantics.py +0 -0
  274. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/MODULE_README.md +0 -0
  275. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/__init__.py +0 -0
  276. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/conftest.py +0 -0
  277. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_bulk_ops.py +0 -0
  278. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_cache_integration.py +0 -0
  279. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_crud.py +0 -0
  280. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_foreign_keys.py +0 -0
  281. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_m2m.py +0 -0
  282. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_manager.py +0 -0
  283. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_migrations_live.py +0 -0
  284. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_query_execution.py +0 -0
  285. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/level2/test_schema_diff.py +0 -0
  286. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/.env.example +0 -0
  287. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/README.md +0 -0
  288. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/__init__.py +0 -0
  289. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/generate.py +0 -0
  290. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/generated/.gitkeep +0 -0
  291. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/matrx_orm.yaml +0 -0
  292. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project/test_schema_generation.py +0 -0
  293. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/.env.example +0 -0
  294. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/README.md +0 -0
  295. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/__init__.py +0 -0
  296. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/client_example.py +0 -0
  297. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/client_example.ts +0 -0
  298. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  299. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/sample_project_desktop/server.py +0 -0
  300. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/entity_tests.py +0 -0
  301. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_base_generation.py +0 -0
  302. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  303. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_composite_unique_generation.py +0 -0
  304. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_dto_identity_generation.py +0 -0
  305. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_entity_capability_generation.py +0 -0
  306. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_generate_schema.py +0 -0
  307. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_junction_analysis.py +0 -0
  308. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_multi_schema_output.py +0 -0
  309. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_sql_expression_defaults.py +0 -0
  310. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_table_relationships_query.py +0 -0
  311. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema/test_view_generation.py +0 -0
  312. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  313. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  314. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/__init__.py +0 -0
  315. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/conftest.py +0 -0
  316. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_cache_no_downgrade.py +0 -0
  317. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_capture_poison_proof.py +0 -0
  318. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_coalesce.py +0 -0
  319. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_dag.py +0 -0
  320. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_disk_spill_fallback.py +0 -0
  321. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_flush_individual_fallback.py +0 -0
  322. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_governed_write.py +0 -0
  323. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_managed.py +0 -0
  324. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_op_enqueue_site.py +0 -0
  325. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_reads.py +0 -0
  326. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/session/test_session.py +0 -0
  327. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_database_name_alias.py +0 -0
  328. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_db_functions.py +0 -0
  329. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_model_cls_refactor.py +0 -0
  330. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  331. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_one_database.py +0 -0
  332. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_secrets_battery.py +0 -0
  333. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_sql_param_casts.py +0 -0
  334. {matrx_orm-3.1.71 → matrx_orm-3.1.72}/tests/test_sync_twins_guard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matrx-orm
3
- Version: 3.1.71
3
+ Version: 3.1.72
4
4
  Summary: Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching
5
5
  Project-URL: Homepage, https://github.com/AI-Matrix-Engine/aidream-current
6
6
  Project-URL: Repository, https://github.com/AI-Matrix-Engine/aidream-current
@@ -25,7 +25,7 @@ Requires-Python: >=3.13
25
25
  Requires-Dist: asyncpg>=0.31.0
26
26
  Requires-Dist: cryptography>=43.0
27
27
  Requires-Dist: gitpython>=3.1.40
28
- Requires-Dist: matrx-utils>=2.0.14
28
+ Requires-Dist: matrx-utils>=2.0.15
29
29
  Requires-Dist: psycopg-pool>=3.2.5
30
30
  Requires-Dist: psycopg[binary]>=3.2.5
31
31
  Requires-Dist: python-dotenv>=1.0
@@ -197,6 +197,12 @@ from .core.pydantic_bridge import (
197
197
  clear_schema_cache,
198
198
  model_json_schema,
199
199
  )
200
+ from .core.pool_watch import (
201
+ PoolHolderSnapshot,
202
+ PoolSaturationEvent,
203
+ connection_holder_census,
204
+ register_pool_saturation_sink,
205
+ )
200
206
  from .core.pydantic_bridge import (
201
207
  validate_input as validate_model_input,
202
208
  )
@@ -562,6 +568,11 @@ __all__ = [
562
568
  # ── Pagination ─────────────────────────────────────────────────────────────
563
569
  "Paginator",
564
570
  "Page",
571
+ # ── Pool saturation observability ───────────────────────────────────────────
572
+ "PoolHolderSnapshot",
573
+ "PoolSaturationEvent",
574
+ "connection_holder_census",
575
+ "register_pool_saturation_sink",
565
576
  # ── Signals ────────────────────────────────────────────────────────────────
566
577
  "Signal",
567
578
  "pre_create",
@@ -53,7 +53,25 @@ connection and can strand old holders in release/reset.
53
53
 
54
54
  Pinned by `tests/level1/test_pool_maintenance.py`.
55
55
 
56
- Verified against code: 2026-08-13.
56
+ **Every checkout is attributable.** `AsyncDatabaseManager.get_connection` and
57
+ the sanctioned caller-supplied-pool helper register the holder's asyncio task,
58
+ checkout call site/stack, current suspended call site/stack, and age. On the
59
+ first acquire timeout in an incident, `pool_watch` emits a critical red census
60
+ and invokes host sinks; subsequent retry timeouts do not spam another incident.
61
+ Once a release returns an idle slot, the alarm re-arms. The final
62
+ `PoolAcquireTimeoutError.details` carries both the first snapshot and the current
63
+ census, so retry/persistence casualties preserve the diagnosis.
64
+
65
+ **Release is bounded and cancellation-safe.** Return/reset runs in a dedicated,
66
+ shielded task with a 10-second asyncpg release timeout. A holder remains visible
67
+ as `releasing` until reset really finishes. If reset exceeds the bound, asyncpg
68
+ discards the physical connection and the failure is logged loudly rather than
69
+ leaving an invisible checkout forever.
70
+
71
+ Pinned by `tests/level1/test_pool_holder_census.py` and
72
+ `tests/level1/test_pool_maintenance.py`.
73
+
74
+ Verified against code: 2026-08-15.
57
75
 
58
76
  ## Transaction-pooled role scope
59
77
 
@@ -73,6 +91,7 @@ Verified against code: 2026-08-15.
73
91
 
74
92
  - 2026-08-15: Pinned transaction-local RLS role cleanup across success,
75
93
  exception, and early-return paths.
94
+ - 2026-08-15: Added first-timeout pool holder census, host alarm seam, tracked raw-pool helper, and bounded cancellation-safe release.
76
95
  - 2026-08-14: Public models now render `public.table` SQL targets; bare public
77
96
  relation names can no longer disable application logs or write-failure capture.
78
97
  - 2026-08-13: Registry, admin, recovery, catalog, and diagnostic identities are now exact `schema.table`; bare names are accepted only as uncached discovery terms and ambiguity fails loudly.
@@ -91,6 +91,7 @@ def _integrity_constraint_kind(exc: asyncpg.exceptions.IntegrityConstraintViolat
91
91
  # read path survives the same burst.
92
92
  POOL_ACQUIRE_RETRIES = 2 # total acquire attempts = 1 + POOL_ACQUIRE_RETRIES
93
93
  POOL_ACQUIRE_RETRY_BASE_BACKOFF = 0.25 # seconds; doubled per attempt
94
+ POOL_RELEASE_TIMEOUT_SECONDS = 10.0
94
95
 
95
96
  # ── read-statement retry (command-timeout self-heal) ──────────────────────────
96
97
  # A ``command_timeout`` on a READ (SELECT) is provably side-effect-free to retry
@@ -576,8 +577,7 @@ class AsyncDatabaseManager:
576
577
  functions, so `call_function` cannot reach them; this is the sanctioned
577
578
  accessor.
578
579
  """
579
- pool = await cls.get_pool(config_name)
580
- async with pool.acquire() as conn:
580
+ async with cls.get_connection(config_name) as conn:
581
581
  row = await conn.fetchrow(
582
582
  "SELECT current_user AS current_user, session_user AS session_user, "
583
583
  "current_database() AS database, "
@@ -643,7 +643,9 @@ class AsyncDatabaseManager:
643
643
 
644
644
  growth_lock = cls._growth_locks.setdefault(config_name, asyncio.Lock())
645
645
  async with growth_lock:
646
- await maintain_one_connection(pool, target_size=pool_min)
646
+ await maintain_one_connection(
647
+ pool, target_size=pool_min, config_name=config_name
648
+ )
647
649
  failures = 0
648
650
  sweep_count = max(pool.get_size(), pool_min, 1)
649
651
  interval = max(
@@ -718,6 +720,7 @@ class AsyncDatabaseManager:
718
720
  config_name = resolve_database_name(config_name)
719
721
  pool = await cls.get_pool(config_name)
720
722
  conn = None
723
+ first_timeout_event = None
721
724
  try:
722
725
  # Bounded acquire-retry: a timeout here means NO SQL was sent (the
723
726
  # connection was never handed out), so retrying is provably
@@ -731,13 +734,42 @@ class AsyncDatabaseManager:
731
734
  conn = await cls._acquire_bounded_growth(config_name, pool)
732
735
  break
733
736
  except TimeoutError:
734
- if _acquire_attempt < POOL_ACQUIRE_RETRIES:
735
- backoff = POOL_ACQUIRE_RETRY_BASE_BACKOFF * (2**_acquire_attempt)
737
+ from matrx_orm.core.pool_watch import emit_first_pool_timeout
738
+
739
+ try:
740
+ _snap_size = pool.get_size()
741
+ except Exception:
742
+ _snap_size = -1
743
+ try:
744
+ _snap_idle = pool.get_idle_size()
745
+ except Exception:
746
+ _snap_idle = -1
747
+ try:
748
+ _snap_max = pool.get_max_size()
749
+ except Exception:
750
+ _snap_max = -1
751
+ try:
752
+ _snap_min = get_database_config(config_name).get(
753
+ "pool_min", pool.get_min_size()
754
+ )
755
+ except Exception:
736
756
  try:
737
- _snap_size = pool.get_size()
738
- _snap_idle = pool.get_idle_size()
757
+ _snap_min = pool.get_min_size()
739
758
  except Exception:
740
- _snap_size = _snap_idle = -1
759
+ _snap_min = -1
760
+ timeout_event = emit_first_pool_timeout(
761
+ config_name=config_name,
762
+ timeout_seconds=timeout,
763
+ acquire_attempt=_acquire_attempt + 1,
764
+ pool_size=_snap_size,
765
+ pool_max=_snap_max,
766
+ pool_idle=_snap_idle,
767
+ pool_min=_snap_min,
768
+ )
769
+ if first_timeout_event is None:
770
+ first_timeout_event = timeout_event
771
+ if _acquire_attempt < POOL_ACQUIRE_RETRIES:
772
+ backoff = POOL_ACQUIRE_RETRY_BASE_BACKOFF * (2**_acquire_attempt)
741
773
  logger.warning(
742
774
  "[matrx_orm] Acquire timeout for '%s' (attempt %d/%d, "
743
775
  "size=%s idle=%s) — retrying in %.2fs (no SQL sent, safe).",
@@ -782,8 +814,7 @@ class AsyncDatabaseManager:
782
814
 
783
815
  if conn is not None:
784
816
  logger.warning(
785
- "[matrx_orm] Acquire healed for '%s' after replacing a "
786
- "zero-connection pool.",
817
+ "[matrx_orm] Acquire healed for '%s' after replacing a zero-connection pool.",
787
818
  config_name,
788
819
  )
789
820
  else:
@@ -791,13 +822,28 @@ class AsyncDatabaseManager:
791
822
 
792
823
  try:
793
824
  _pool_size = pool.get_size()
825
+ except Exception:
826
+ _pool_size = -1
827
+ try:
794
828
  _pool_idle = pool.get_idle_size()
829
+ except Exception:
830
+ _pool_idle = -1
831
+ try:
795
832
  _pool_max = pool.get_max_size()
833
+ except Exception:
834
+ _pool_max = -1
835
+ try:
796
836
  _pool_min = get_database_config(config_name).get(
797
837
  "pool_min", pool.get_min_size()
798
838
  )
799
839
  except Exception:
800
- _pool_size = _pool_idle = _pool_max = _pool_min = -1
840
+ try:
841
+ _pool_min = pool.get_min_size()
842
+ except Exception:
843
+ _pool_min = -1
844
+ from matrx_orm.core.pool_watch import connection_holder_census
845
+
846
+ holders = connection_holder_census(config_name)
801
847
  _timeout_err = PoolAcquireTimeoutError(
802
848
  model=None,
803
849
  config_name=config_name,
@@ -808,10 +854,14 @@ class AsyncDatabaseManager:
808
854
  "pool_idle": _pool_idle,
809
855
  "pool_min": _pool_min,
810
856
  "acquire_attempts": (
811
- 2 + POOL_ACQUIRE_RETRIES
812
- if rebuilt
813
- else 1 + POOL_ACQUIRE_RETRIES
857
+ 2 + POOL_ACQUIRE_RETRIES if rebuilt else 1 + POOL_ACQUIRE_RETRIES
858
+ ),
859
+ "first_timeout": (
860
+ first_timeout_event.model_dump(mode="json")
861
+ if first_timeout_event is not None
862
+ else None
814
863
  ),
864
+ "holders": [holder.model_dump(mode="json") for holder in holders],
815
865
  },
816
866
  )
817
867
  maybe_activate_queue(config_name, _timeout_err)
@@ -852,6 +902,17 @@ class AsyncDatabaseManager:
852
902
  if classified is None:
853
903
  raise
854
904
  raise classified from None
905
+ from matrx_orm.core.pool_watch import (
906
+ mark_connection_releasing,
907
+ note_pool_recovered,
908
+ track_connection_holder,
909
+ untrack_connection_holder,
910
+ )
911
+
912
+ # Attribute the checkout immediately. Poison cleanup below is database
913
+ # work too; if it stalls, the holder census must show who owns the slot.
914
+ track_connection_holder(config_name, conn)
915
+
855
916
  # Acquired successfully — yield outside the timeout scope so the
856
917
  # user's work isn't bounded by it, and release in finally so leaks
857
918
  # are impossible regardless of how the consumer exits.
@@ -888,7 +949,11 @@ class AsyncDatabaseManager:
888
949
  conn.terminate()
889
950
  except Exception:
890
951
  pass
891
- await pool.release(conn)
952
+ mark_connection_releasing(config_name, conn)
953
+ try:
954
+ await pool.release(conn, timeout=POOL_RELEASE_TIMEOUT_SECONDS)
955
+ finally:
956
+ untrack_connection_holder(config_name, conn)
892
957
  raise StateError(
893
958
  model=None,
894
959
  operation="acquire_connection",
@@ -900,7 +965,43 @@ class AsyncDatabaseManager:
900
965
  try:
901
966
  yield conn
902
967
  finally:
903
- await pool.release(conn)
968
+ mark_connection_releasing(config_name, conn)
969
+
970
+ async def _release() -> None:
971
+ try:
972
+ await pool.release(conn, timeout=POOL_RELEASE_TIMEOUT_SECONDS)
973
+ finally:
974
+ untrack_connection_holder(config_name, conn)
975
+
976
+ # A caller cancellation must not strand a checkout while asyncpg
977
+ # resets it. The release has its own hard bound; asyncpg terminates
978
+ # the physical connection when reset exceeds that bound.
979
+ release_task = asyncio.create_task(_release(), name=f"matrx-orm-release:{config_name}")
980
+
981
+ def _release_done(task: asyncio.Task[None]) -> None:
982
+ if task.cancelled():
983
+ return
984
+ release_exc = task.exception()
985
+ if release_exc is not None:
986
+ logger.error(
987
+ "[matrx_orm] Connection release failed for '%s': %s",
988
+ config_name,
989
+ release_exc,
990
+ )
991
+ return
992
+ try:
993
+ if pool.get_idle_size() > 0:
994
+ note_pool_recovered(config_name)
995
+ except Exception:
996
+ pass
997
+
998
+ release_task.add_done_callback(_release_done)
999
+ try:
1000
+ await asyncio.shield(release_task)
1001
+ except asyncio.CancelledError:
1002
+ # The dedicated release task keeps running to completion and its
1003
+ # body/callback own census cleanup and recovery-latch reset.
1004
+ raise
904
1005
 
905
1006
  @classmethod
906
1007
  async def execute_query(
@@ -59,6 +59,7 @@ async def maintain_one_connection(
59
59
  *,
60
60
  target_size: int,
61
61
  timeout_seconds: float = 5.0,
62
+ config_name: str = "<pool-maintenance>",
62
63
  ) -> bool:
63
64
  """Probe one idle connection or grow one disconnected holder, never more."""
64
65
  queue, raw_queue = _pool_queue(pool)
@@ -83,10 +84,19 @@ async def maintain_one_connection(
83
84
  try:
84
85
  async with asyncio.timeout(timeout_seconds):
85
86
  proxy = await holder.acquire()
87
+ from matrx_orm.core.pool_watch import track_connection_holder
88
+
89
+ track_connection_holder(config_name, proxy)
86
90
  await proxy.fetchval("SELECT 1")
87
91
  return True
88
92
  finally:
89
93
  if proxy is not None:
94
+ from matrx_orm.core.pool_watch import (
95
+ mark_connection_releasing,
96
+ untrack_connection_holder,
97
+ )
98
+
99
+ mark_connection_releasing(config_name, proxy)
90
100
  try:
91
101
  await pool.release(proxy, timeout=timeout_seconds)
92
102
  except Exception:
@@ -95,6 +105,8 @@ async def maintain_one_connection(
95
105
  if holder not in raw_queue and getattr(holder, "_in_use", None) is None:
96
106
  queue.put_nowait(holder)
97
107
  raise
108
+ finally:
109
+ untrack_connection_holder(config_name, proxy)
98
110
  elif holder not in raw_queue:
99
111
  # connect/setup failed before a proxy was established.
100
112
  queue.put_nowait(holder)
@@ -0,0 +1,318 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+ import traceback
7
+ from collections.abc import AsyncIterator, Callable
8
+ from contextlib import asynccontextmanager
9
+ from dataclasses import dataclass
10
+ from datetime import UTC, datetime
11
+ from typing import Any, Literal
12
+
13
+ from pydantic import BaseModel, ConfigDict
14
+
15
+ from matrx_utils import vcprint
16
+
17
+ logger = logging.getLogger("matrx_orm.pool_watch")
18
+
19
+ _STACK_LIMIT = 24
20
+ _DISPLAY_STACK_LIMIT = 8
21
+
22
+
23
+ class PoolHolderSnapshot(BaseModel):
24
+ """One connection currently checked out from a watched ORM pool."""
25
+
26
+ model_config = ConfigDict(extra="forbid", frozen=True)
27
+
28
+ task_name: str
29
+ task_id: int
30
+ state: Literal["held", "releasing"]
31
+ age_seconds: float
32
+ acquired_at: datetime
33
+ checkout_site: str
34
+ checkout_stack: tuple[str, ...]
35
+ current_site: str | None
36
+ current_stack: tuple[str, ...]
37
+ task_done: bool
38
+ task_cancelled: bool
39
+
40
+
41
+ class PoolSaturationEvent(BaseModel):
42
+ """Diagnostic snapshot captured on a pool's first acquire timeout."""
43
+
44
+ model_config = ConfigDict(extra="forbid", frozen=True)
45
+
46
+ config_name: str
47
+ timeout_seconds: float
48
+ acquire_attempt: int
49
+ occurred_at: datetime
50
+ pool_size: int
51
+ pool_max: int
52
+ pool_idle: int
53
+ pool_min: int
54
+ waiter_task_name: str
55
+ waiter_site: str
56
+ holders: tuple[PoolHolderSnapshot, ...]
57
+
58
+
59
+ PoolSaturationSink = Callable[[PoolSaturationEvent], None]
60
+
61
+
62
+ @dataclass(slots=True)
63
+ class _TrackedHolder:
64
+ config_name: str
65
+ connection: Any
66
+ task: asyncio.Task[Any]
67
+ acquired_monotonic: float
68
+ acquired_at: datetime
69
+ checkout_site: str
70
+ checkout_stack: tuple[str, ...]
71
+ state: Literal["held", "releasing"] = "held"
72
+
73
+
74
+ _holders: dict[str, dict[int, _TrackedHolder]] = {}
75
+ _sinks: list[PoolSaturationSink] = []
76
+ _open_saturation_incidents: set[str] = set()
77
+
78
+
79
+ def _format_frame(frame: traceback.FrameSummary) -> str:
80
+ return f"{frame.filename}:{frame.lineno} in {frame.name}"
81
+
82
+
83
+ def _is_orm_frame(frame: traceback.FrameSummary) -> bool:
84
+ normalized = frame.filename.replace("\\", "/")
85
+ return "/matrx_orm/" in normalized
86
+
87
+
88
+ def _is_context_wrapper_frame(frame: traceback.FrameSummary) -> bool:
89
+ normalized = frame.filename.replace("\\", "/")
90
+ return normalized.endswith("/contextlib.py") or "/asyncio/" in normalized
91
+
92
+
93
+ def _capture_checkout_stack() -> tuple[str, tuple[str, ...]]:
94
+ frames = traceback.extract_stack(limit=_STACK_LIMIT)[:-1]
95
+ formatted = tuple(_format_frame(frame) for frame in frames[-_DISPLAY_STACK_LIMIT:])
96
+ external = next(
97
+ (
98
+ frame
99
+ for frame in reversed(frames)
100
+ if not _is_orm_frame(frame) and not _is_context_wrapper_frame(frame)
101
+ ),
102
+ None,
103
+ )
104
+ site = _format_frame(external or frames[-1]) if frames else "<stack unavailable>"
105
+ return site, formatted
106
+
107
+
108
+ def _task_stack(task: asyncio.Task[Any]) -> tuple[str | None, tuple[str, ...]]:
109
+ try:
110
+ frames = task.get_stack(limit=_DISPLAY_STACK_LIMIT)
111
+ except Exception:
112
+ return None, ()
113
+ formatted = tuple(
114
+ f"{frame.f_code.co_filename}:{frame.f_lineno} in {frame.f_code.co_name}" for frame in frames
115
+ )
116
+ return (formatted[-1] if formatted else None), formatted
117
+
118
+
119
+ def track_connection_holder(config_name: str, connection: Any) -> None:
120
+ """Register the current task as the holder of ``connection``."""
121
+ task = asyncio.current_task()
122
+ if task is None:
123
+ return
124
+ checkout_site, checkout_stack = _capture_checkout_stack()
125
+ tracked = _TrackedHolder(
126
+ config_name=config_name,
127
+ connection=connection,
128
+ task=task,
129
+ acquired_monotonic=time.monotonic(),
130
+ acquired_at=datetime.now(UTC),
131
+ checkout_site=checkout_site,
132
+ checkout_stack=checkout_stack,
133
+ )
134
+ _holders.setdefault(config_name, {})[id(connection)] = tracked
135
+
136
+
137
+ def mark_connection_releasing(config_name: str, connection: Any) -> None:
138
+ """Mark a checkout as resetting/returning rather than caller-held."""
139
+ tracked = _holders.get(config_name, {}).get(id(connection))
140
+ if tracked is not None:
141
+ tracked.state = "releasing"
142
+
143
+
144
+ def untrack_connection_holder(config_name: str, connection: Any) -> None:
145
+ """Remove a connection after its pool release actually completes."""
146
+ config_holders = _holders.get(config_name)
147
+ if config_holders is None:
148
+ return
149
+ config_holders.pop(id(connection), None)
150
+ if not config_holders:
151
+ _holders.pop(config_name, None)
152
+
153
+
154
+ def connection_holder_census(config_name: str) -> tuple[PoolHolderSnapshot, ...]:
155
+ """Return oldest-first snapshots of every tracked checkout for a pool."""
156
+ now = time.monotonic()
157
+ snapshots: list[PoolHolderSnapshot] = []
158
+ for tracked in list(_holders.get(config_name, {}).values()):
159
+ current_site, current_stack = _task_stack(tracked.task)
160
+ snapshots.append(
161
+ PoolHolderSnapshot(
162
+ task_name=tracked.task.get_name(),
163
+ task_id=id(tracked.task),
164
+ state=tracked.state,
165
+ age_seconds=round(max(0.0, now - tracked.acquired_monotonic), 3),
166
+ acquired_at=tracked.acquired_at,
167
+ checkout_site=tracked.checkout_site,
168
+ checkout_stack=tracked.checkout_stack,
169
+ current_site=current_site,
170
+ current_stack=current_stack,
171
+ task_done=tracked.task.done(),
172
+ task_cancelled=tracked.task.cancelled(),
173
+ )
174
+ )
175
+ snapshots.sort(key=lambda holder: holder.age_seconds, reverse=True)
176
+ return tuple(snapshots)
177
+
178
+
179
+ def register_pool_saturation_sink(sink: PoolSaturationSink) -> None:
180
+ """Register a synchronous first-timeout observer (normally a task scheduler)."""
181
+ if sink not in _sinks:
182
+ _sinks.append(sink)
183
+
184
+
185
+ def reset_pool_watch_state() -> None:
186
+ """Clear process state for isolated tests."""
187
+ _holders.clear()
188
+ _sinks.clear()
189
+ _open_saturation_incidents.clear()
190
+
191
+
192
+ def note_pool_recovered(config_name: str) -> None:
193
+ """Re-arm the first-timeout alarm after the pool has an idle connection."""
194
+ _open_saturation_incidents.discard(config_name)
195
+
196
+
197
+ @asynccontextmanager
198
+ async def watched_pool_acquire(
199
+ pool: Any,
200
+ *,
201
+ config_name: str = "<external-pool>",
202
+ timeout_seconds: float = 10.0,
203
+ ) -> AsyncIterator[Any]:
204
+ """Track a caller-supplied pool checkout and alarm if its wait times out."""
205
+ acquired = False
206
+ connection: Any = None
207
+ try:
208
+ async with pool.acquire(timeout=timeout_seconds) as connection:
209
+ acquired = True
210
+ track_connection_holder(config_name, connection)
211
+ try:
212
+ yield connection
213
+ finally:
214
+ mark_connection_releasing(config_name, connection)
215
+ except TimeoutError:
216
+ # A TimeoutError from the caller's body is not an acquire timeout.
217
+ if acquired:
218
+ raise
219
+
220
+ def _stat(method: str) -> int:
221
+ try:
222
+ return int(getattr(pool, method)())
223
+ except Exception:
224
+ return -1
225
+
226
+ emit_first_pool_timeout(
227
+ config_name=config_name,
228
+ timeout_seconds=timeout_seconds,
229
+ acquire_attempt=1,
230
+ pool_size=_stat("get_size"),
231
+ pool_max=_stat("get_max_size"),
232
+ pool_idle=_stat("get_idle_size"),
233
+ pool_min=_stat("get_min_size"),
234
+ )
235
+ raise
236
+ finally:
237
+ if acquired:
238
+ untrack_connection_holder(config_name, connection)
239
+ try:
240
+ if pool.get_idle_size() > 0:
241
+ note_pool_recovered(config_name)
242
+ except Exception:
243
+ pass
244
+
245
+
246
+ def emit_first_pool_timeout(
247
+ *,
248
+ config_name: str,
249
+ timeout_seconds: float,
250
+ acquire_attempt: int,
251
+ pool_size: int,
252
+ pool_max: int,
253
+ pool_idle: int,
254
+ pool_min: int,
255
+ ) -> PoolSaturationEvent:
256
+ """Scream once per saturation incident and return its holder census."""
257
+ waiter = asyncio.current_task()
258
+ waiter_site, _ = _capture_checkout_stack()
259
+ event = PoolSaturationEvent(
260
+ config_name=config_name,
261
+ timeout_seconds=timeout_seconds,
262
+ acquire_attempt=acquire_attempt,
263
+ occurred_at=datetime.now(UTC),
264
+ pool_size=pool_size,
265
+ pool_max=pool_max,
266
+ pool_idle=pool_idle,
267
+ pool_min=pool_min,
268
+ waiter_task_name=waiter.get_name() if waiter is not None else "<no task>",
269
+ waiter_site=waiter_site,
270
+ holders=connection_holder_census(config_name),
271
+ )
272
+ if config_name in _open_saturation_incidents:
273
+ return event
274
+ _open_saturation_incidents.add(config_name)
275
+
276
+ holder_lines = [
277
+ (
278
+ f" - task={holder.task_name!r} state={holder.state} "
279
+ f"age={holder.age_seconds:.3f}s checkout={holder.checkout_site} "
280
+ f"current={holder.current_site or '<done>'}"
281
+ )
282
+ for holder in event.holders
283
+ ]
284
+ if not holder_lines:
285
+ holder_lines.append(" - <no tracked holders; inspect raw pool bypasses or pool growth>")
286
+ message = (
287
+ "\n🚨 MATRX ORM POOL SATURATION — FIRST ACQUIRE TIMEOUT\n"
288
+ f"config={config_name} attempt={acquire_attempt} timeout={timeout_seconds:.3f}s "
289
+ f"pool={pool_idle} idle / {pool_size} open / {pool_max} max (min={pool_min})\n"
290
+ f"waiter={event.waiter_task_name!r} at {event.waiter_site}\n"
291
+ f"tracked_holders={len(event.holders)}\n" + "\n".join(holder_lines)
292
+ )
293
+ vcprint(message, color="red")
294
+ logger.critical(
295
+ "pool_saturation_first_timeout",
296
+ extra={"pool_saturation": event.model_dump(mode="json")},
297
+ )
298
+ for sink in list(_sinks):
299
+ try:
300
+ sink(event)
301
+ except Exception as exc: # noqa: BLE001 - observability never masks the timeout
302
+ logger.exception("pool saturation sink %r failed: %s", sink, exc)
303
+ return event
304
+
305
+
306
+ __all__ = [
307
+ "PoolHolderSnapshot",
308
+ "PoolSaturationEvent",
309
+ "connection_holder_census",
310
+ "emit_first_pool_timeout",
311
+ "mark_connection_releasing",
312
+ "note_pool_recovered",
313
+ "register_pool_saturation_sink",
314
+ "reset_pool_watch_state",
315
+ "track_connection_holder",
316
+ "untrack_connection_holder",
317
+ "watched_pool_acquire",
318
+ ]