matrx-orm 3.1.134__tar.gz → 3.1.136__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 (365) hide show
  1. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/CLAUDE.md +1 -0
  2. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/PKG-INFO +2 -2
  3. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/__init__.py +8 -0
  4. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/__init__.py +7 -31
  5. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/async_postgresql.py +7 -1
  6. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/base_adapter.py +3 -1
  7. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/postgrest_client_adapter.py +1 -0
  8. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/supabase_adapter.py +7 -1
  9. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/FEATURE.md +4 -0
  10. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/MODULE_README.md +1 -0
  11. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/async_db_manager.py +30 -1
  12. matrx_orm-3.1.136/matrx_orm/core/session_context.py +94 -0
  13. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/transaction.py +17 -0
  14. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/loader.py +9 -8
  15. matrx_orm-3.1.136/matrx_orm/pytest_plugin.py +200 -0
  16. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/builder.py +9 -0
  17. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/executor.py +8 -1
  18. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/common.py +16 -5
  19. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/package_wiring.py +10 -8
  20. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/runner.py +13 -3
  21. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/README.md +36 -3
  22. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/fallback.py +148 -26
  23. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/session.py +40 -2
  24. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/pyproject.toml +5 -2
  25. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_query_timeout.py +39 -1
  26. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_flush_individual_fallback.py +50 -0
  27. matrx_orm-3.1.136/tests/session/test_record_error_bounded_insert.py +89 -0
  28. matrx_orm-3.1.136/tests/session/test_record_error_storm.py +67 -0
  29. matrx_orm-3.1.136/tests/session/test_spill_guard_plugin.py +231 -0
  30. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/.gitignore +0 -0
  31. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/AGENTS.md +0 -0
  32. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/BASE_CLASS_METHODS.md +0 -0
  33. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/MODEL_API.md +0 -0
  34. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/README.md +0 -0
  35. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/database/orm/extended/managers/ai_model_base.py +0 -0
  36. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/docs/migrations.md +0 -0
  37. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/FEATURE.md +0 -0
  38. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/MODULE_README.md +0 -0
  39. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/MODULE_README.md +0 -0
  40. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/postgresql.py +0 -0
  41. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/admin/FEATURE.md +0 -0
  42. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/admin/__init__.py +0 -0
  43. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/admin/router.py +0 -0
  44. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/__init__.py +0 -0
  45. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/auth.py +0 -0
  46. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/config.py +0 -0
  47. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/handlers.py +0 -0
  48. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/protocol.py +0 -0
  49. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/server.py +0 -0
  50. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/associations.py +0 -0
  51. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/cache_debug.py +0 -0
  52. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/catalog.py +0 -0
  53. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/catalog_sql.py +0 -0
  54. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/MODULE_README.md +0 -0
  55. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/__init__.py +0 -0
  56. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/postgres_connection.py +0 -0
  57. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/postgrest.py +0 -0
  58. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/supabase_auth.py +0 -0
  59. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/supabase_config.py +0 -0
  60. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/supabase_manager.py +0 -0
  61. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  62. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  63. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/TASKS.md +0 -0
  64. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/__init__.py +0 -0
  65. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/base.py +0 -0
  66. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/config.py +0 -0
  67. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/connections.py +0 -0
  68. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/db_function.py +0 -0
  69. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/diagnostics.py +0 -0
  70. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/expressions.py +0 -0
  71. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/extended.py +0 -0
  72. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/fields.py +0 -0
  73. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/introspection.py +0 -0
  74. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/listen.py +0 -0
  75. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/loop_filters.py +0 -0
  76. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/model_dto.py +0 -0
  77. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/model_view.py +0 -0
  78. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/one_database.py +0 -0
  79. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/paginator.py +0 -0
  80. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/pool_maintenance.py +0 -0
  81. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/pool_watch.py +0 -0
  82. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/pooler_role.py +0 -0
  83. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/pydantic_bridge.py +0 -0
  84. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/registry.py +0 -0
  85. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/relations.py +0 -0
  86. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/resilience.py +0 -0
  87. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/rls_session.py +0 -0
  88. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/signals.py +0 -0
  89. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/tls.py +0 -0
  90. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/types.py +0 -0
  91. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/write_queue.py +0 -0
  92. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/entity.py +0 -0
  93. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/error_handling.py +0 -0
  94. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/exceptions.py +0 -0
  95. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/extended/__init__.py +0 -0
  96. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/extended/app_error_handler.py +0 -0
  97. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/identity_rule.py +0 -0
  98. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/local_sqlite.py +0 -0
  99. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/middleware/__init__.py +0 -0
  100. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/middleware/base.py +0 -0
  101. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/MODULE_README.md +0 -0
  102. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/__init__.py +0 -0
  103. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/cli.py +0 -0
  104. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/ddl.py +0 -0
  105. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/diff.py +0 -0
  106. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/executor.py +0 -0
  107. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/integration.py +0 -0
  108. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/operations.py +0 -0
  109. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/state.py +0 -0
  110. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/table_filter.py +0 -0
  111. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/FEATURE.md +0 -0
  112. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/MODULE_README.md +0 -0
  113. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/__init__.py +0 -0
  114. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/bulk_update_values.py +0 -0
  115. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/catalog_select.py +0 -0
  116. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/conflict.py +0 -0
  117. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/conflict_writes.py +0 -0
  118. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/create.py +0 -0
  119. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/db_functions.py +0 -0
  120. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/delete.py +0 -0
  121. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/dynamic_admin.py +0 -0
  122. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/dynamic_crud.py +0 -0
  123. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/graph_walk.py +0 -0
  124. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/latest_rows.py +0 -0
  125. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/queue_claim.py +0 -0
  126. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/read.py +0 -0
  127. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/relational_backfill.py +0 -0
  128. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/staging_load.py +0 -0
  129. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/update.py +0 -0
  130. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/platform_access.py +0 -0
  131. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/platform_db.py +0 -0
  132. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/py.typed +0 -0
  133. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/MODULE_README.md +0 -0
  134. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/__init__.py +0 -0
  135. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/db_objects.py +0 -0
  136. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  137. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  138. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/FEATURE.md +0 -0
  139. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/MODULE_README.md +0 -0
  140. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/__init__.py +0 -0
  141. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/FEATURE.md +0 -0
  142. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  143. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/__init__.py +0 -0
  144. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/code_handler.py +0 -0
  145. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  146. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/columns.py +0 -0
  147. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/diff_preview.py +0 -0
  148. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  149. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/generator.py +0 -0
  150. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  151. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  152. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  153. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  154. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/relationships.py +0 -0
  155. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/schema.py +0 -0
  156. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/schema_manager.py +0 -0
  157. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/tables.py +0 -0
  158. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/views.py +0 -0
  159. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  160. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/__init__.py +0 -0
  161. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/attachments.py +0 -0
  162. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/crypto.py +0 -0
  163. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/host_authorization.py +0 -0
  164. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/items.py +0 -0
  165. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/model.py +0 -0
  166. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/org_service.py +0 -0
  167. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/service.py +0 -0
  168. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/__init__.py +0 -0
  169. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/coalesce.py +0 -0
  170. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/dag.py +0 -0
  171. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/errors.py +0 -0
  172. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/flush.py +0 -0
  173. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/lifecycle.py +0 -0
  174. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/managed.py +0 -0
  175. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/op.py +0 -0
  176. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/reads.py +0 -0
  177. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/telemetry.py +0 -0
  178. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  179. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/__init__.py +0 -0
  180. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/executor.py +0 -0
  181. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/queries.py +0 -0
  182. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/registry.py +0 -0
  183. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/types.py +0 -0
  184. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/utils.py +0 -0
  185. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/state.py +0 -0
  186. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/utils/__init__.py +0 -0
  187. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/utils/sql_utils.py +0 -0
  188. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/utils/type_converters.py +0 -0
  189. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/scripts/git-branches.sh +0 -0
  190. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/scripts/publish.sh +0 -0
  191. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/scripts/release.sh +0 -0
  192. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/MODULE_README.md +0 -0
  193. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/__init__.py +0 -0
  194. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/conftest.py +0 -0
  195. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/MODULE_README.md +0 -0
  196. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/__init__.py +0 -0
  197. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_adapter_registry.py +0 -0
  198. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_admin_db_columns.py +0 -0
  199. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_admin_response_rendering.py +0 -0
  200. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_admin_search_negation.py +0 -0
  201. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_admin_search_sentinels.py +0 -0
  202. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_advisory_lock.py +0 -0
  203. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_agent_message.py +0 -0
  204. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_api_auth.py +0 -0
  205. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_api_config.py +0 -0
  206. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_api_handlers.py +0 -0
  207. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_api_protocol.py +0 -0
  208. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_array_agg_order_by.py +0 -0
  209. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_associations.py +0 -0
  210. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_associations_wiring.py +0 -0
  211. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_bulk_hydration_offload.py +0 -0
  212. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_bulk_update_by_pk.py +0 -0
  213. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  214. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_cache_debug.py +0 -0
  215. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_call_function.py +0 -0
  216. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_case_when.py +0 -0
  217. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_catalog.py +0 -0
  218. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_composite_pk_filter.py +0 -0
  219. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_composite_pk_write_paths.py +0 -0
  220. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_config.py +0 -0
  221. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_conflict_target.py +0 -0
  222. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_conflict_writes.py +0 -0
  223. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_connection_codecs.py +0 -0
  224. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_connection_poison_guard.py +0 -0
  225. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_connection_primitives.py +0 -0
  226. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_count_composite_distinct.py +0 -0
  227. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_db_function_field.py +0 -0
  228. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_ddl_generator.py +0 -0
  229. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_derived_table.py +0 -0
  230. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_dirty_tracking.py +0 -0
  231. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_dynamic_crud.py +0 -0
  232. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_exceptions.py +0 -0
  233. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  234. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_expression_primitives.py +0 -0
  235. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_fields.py +0 -0
  236. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  237. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_from_alias.py +0 -0
  238. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_fts.py +0 -0
  239. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_func_expression_args.py +0 -0
  240. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_graph_walk.py +0 -0
  241. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_having_annotate_alias.py +0 -0
  242. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  243. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_introspect_rls_policies.py +0 -0
  244. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_join_and_aggregate.py +0 -0
  245. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_latest_rows.py +0 -0
  246. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_listen_notify.py +0 -0
  247. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_local_sqlite.py +0 -0
  248. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_managed_write_guard.py +0 -0
  249. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_matrx_entity.py +0 -0
  250. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_migration_diff_types.py +0 -0
  251. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_migration_loader.py +0 -0
  252. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_model_instance.py +0 -0
  253. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_model_meta.py +0 -0
  254. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_optimistic_concurrency.py +0 -0
  255. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_order_by_expression.py +0 -0
  256. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_parameter_name_hydration.py +0 -0
  257. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_patch_jsonb_path.py +0 -0
  258. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_persisted_count.py +0 -0
  259. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_pool_holder_census.py +0 -0
  260. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_pool_loop_recreation.py +0 -0
  261. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_pool_maintenance.py +0 -0
  262. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_postgrest_filters.py +0 -0
  263. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_pydantic_bridge.py +0 -0
  264. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_query_builder.py +0 -0
  265. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_query_error_detail.py +0 -0
  266. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_query_executor_sql.py +0 -0
  267. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  268. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_queue_claim.py +0 -0
  269. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_read_connection_retry.py +0 -0
  270. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_registry.py +0 -0
  271. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_registry_multi_database.py +0 -0
  272. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_registry_multi_schema.py +0 -0
  273. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_relational_backfill.py +0 -0
  274. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_relations.py +0 -0
  275. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_resilience.py +0 -0
  276. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_rls_session_scope.py +0 -0
  277. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_schema_exists.py +0 -0
  278. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_session_advisory_lock.py +0 -0
  279. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_state_cache.py +0 -0
  280. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_subquery_filter_raw.py +0 -0
  281. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_subquery_in_filter.py +0 -0
  282. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_subquery_nested_expressions.py +0 -0
  283. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_supabase_auth.py +0 -0
  284. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_supabase_config.py +0 -0
  285. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_tls.py +0 -0
  286. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_typed_join_projection.py +0 -0
  287. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_typed_predicates.py +0 -0
  288. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_update_case_expression.py +0 -0
  289. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_update_subquery.py +0 -0
  290. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_update_with_rebase.py +0 -0
  291. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_upsert_default_leak.py +0 -0
  292. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_upsert_with_conflict.py +0 -0
  293. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_values_jsonb_decode.py +0 -0
  294. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_watched_lifecycle.py +0 -0
  295. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_write_queue_deadline.py +0 -0
  296. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_write_retry_semantics.py +0 -0
  297. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/MODULE_README.md +0 -0
  298. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/__init__.py +0 -0
  299. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/conftest.py +0 -0
  300. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_bulk_ops.py +0 -0
  301. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_cache_integration.py +0 -0
  302. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_crud.py +0 -0
  303. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_foreign_keys.py +0 -0
  304. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_m2m.py +0 -0
  305. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_manager.py +0 -0
  306. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_migrations_live.py +0 -0
  307. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_query_execution.py +0 -0
  308. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_schema_diff.py +0 -0
  309. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/live_pooler_role.py +0 -0
  310. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/.env.example +0 -0
  311. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/README.md +0 -0
  312. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/__init__.py +0 -0
  313. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/generate.py +0 -0
  314. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/generated/.gitkeep +0 -0
  315. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/matrx_orm.yaml +0 -0
  316. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/test_schema_generation.py +0 -0
  317. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/.env.example +0 -0
  318. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/README.md +0 -0
  319. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/__init__.py +0 -0
  320. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/client_example.py +0 -0
  321. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/client_example.ts +0 -0
  322. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  323. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/server.py +0 -0
  324. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/entity_tests.py +0 -0
  325. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_base_generation.py +0 -0
  326. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  327. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_composite_unique_generation.py +0 -0
  328. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_dto_identity_generation.py +0 -0
  329. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_entity_capability_generation.py +0 -0
  330. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_enum_ordering.py +0 -0
  331. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_generate_schema.py +0 -0
  332. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_junction_analysis.py +0 -0
  333. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_multi_schema_output.py +0 -0
  334. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_schema_analysis.py +0 -0
  335. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_sql_expression_defaults.py +0 -0
  336. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_table_relationships_query.py +0 -0
  337. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_view_generation.py +0 -0
  338. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema_builder/test_array_default_parsing.py +0 -0
  339. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  340. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  341. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/__init__.py +0 -0
  342. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/conftest.py +0 -0
  343. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_cache_no_downgrade.py +0 -0
  344. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_capture_poison_proof.py +0 -0
  345. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_coalesce.py +0 -0
  346. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_dag.py +0 -0
  347. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_disk_spill_fallback.py +0 -0
  348. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_governed_write.py +0 -0
  349. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_managed.py +0 -0
  350. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_op_enqueue_site.py +0 -0
  351. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_reads.py +0 -0
  352. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_session.py +0 -0
  353. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_array_arg_element_type.py +0 -0
  354. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_database_name_alias.py +0 -0
  355. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_db_functions.py +0 -0
  356. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_dynamic_select_unregistered.py +0 -0
  357. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_keyed_digest.py +0 -0
  358. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_model_cls_refactor.py +0 -0
  359. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  360. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_one_database.py +0 -0
  361. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_pooler_role_guard.py +0 -0
  362. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_secrets_battery.py +0 -0
  363. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_secrets_minting_keys.py +0 -0
  364. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_sql_param_casts.py +0 -0
  365. {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_sync_twins_guard.py +0 -0
@@ -52,6 +52,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
52
52
  - **Schema builder** (`matrx_orm.schema_builder`): code generation for Python + TypeScript type definitions from the DB schema. Postgres **VIEWs** are emitted as **read-only `Model` classes** into the same schema models file (on by default; per-entry `output.include_views: false` opts a schema out, `output.exclude_views: [...]` skips individual views). View models get no manager/DTO/base-class/auto_config (views are not writable), synthesize `_primary_keys` from an `id` column (else the first column, for cache-key/`load_by_id` only), and emit plain typed fields with **no `ForeignKey`** (views carry no FK metadata — nothing cross-schema to resolve). Never hand-write a view model again.
53
53
  - **Secrets battery** (`matrx_orm.secrets_battery`): the ONE canonical user-credential vault — `users.user_secrets` CRUD + the platform's single Fernet primitive, keyed by `CREDENTIALS_ENCRYPTION_KEY`. Any package: `configure_secrets(db_config_name=...)` once, then `await require_user_secret(user_id, key)`. No package ever calls another service to resolve a credential. Contract: [`matrx_orm/secrets_battery/FEATURE.md`](matrx_orm/secrets_battery/FEATURE.md).
54
54
  - **Polymorphic associations** (`matrx_orm.associations`): `EntityRegistry` (token→Model) + `Associations` — a registry-driven, **access-gated** edge engine over a host's polymorphic edge table. Every `source→target` hop is org-scoped *and* each resolved endpoint is re-authorized against the ACTOR (fail-closed, screams on a genuine leak), so a hop can't widen access past what the actor may reach. 🚨 **The gate is `access_check` (wire `matrx_orm.platform_access_check` → `iam.has_access_for`), NEVER an org-column comparison** — a person belongs to several orgs on purpose, so org-equality denies her own data across her own workspaces (2026-08-30 incident) while granting any co-member of a private row. The edge's `org_id` is a TENANCY stamp, not a permission. Org-equality survives only for UNATTENDED writes (no actor) as an integrity guard, and **system vocabulary is exempt entirely** (existence still required): a model generated with `_rls_variant="system"` is platform-shared dictionary data (seo.keyword/topic) every tenant legitimately links to. Hardcodes no table names — the host wires its own edge + token-registry models.
55
+ - **The host's declared session context** (`matrx_orm.core.session_context`): `configure_session_context(provider)` once at startup, and every **top-level** `transaction()` — including the one `rls_session` opens, before it drops to the restricted role — emits whatever the provider returns as transaction-local GUCs. This is how an application-level declaration reaches Postgres at all: a GUC set outside a transaction has no meaning, so the BEGIN is the only place it can land. aidream wires it to its provenance declaration (`app.actor_tier` / `app.actor_system`, read by `platform.actor_tier()`, the history capture and the tier rule). A nested entry is a savepoint inside a transaction that already carries them and emits nothing — and a write made **outside** any transaction carries no declaration at all, which is why a single-row write that must record its author still opens one. No provider configured ⇒ no GUCs (standalone behaviour unchanged); a provider that raises fails the transaction loudly rather than committing rows with a false provenance.
55
56
  - **The platform access policy** (`matrx_orm.platform_access`): `platform_access_check(actor, token, id, level)` → `iam.has_access_for`. The ONE answer to "may this actor touch this row?", shared with RLS and the file-access gate. Fails closed. Never re-derive it from `organization_id` or `created_by` — both are strict subsets that deny legitimate access.
56
57
 
57
58
  Public API is defined explicitly in `matrx_orm/__init__.py` (100+ exports). Treat changes there as semver-breaking.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matrx-orm
3
- Version: 3.1.134
3
+ Version: 3.1.136
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.34
28
+ Requires-Dist: matrx-utils>=2.0.36
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
@@ -240,6 +240,11 @@ from .core.registry import (
240
240
  )
241
241
  from .core.relations import ManyToManyField
242
242
  from .core.rls_session import rls_session
243
+ from .core.session_context import (
244
+ SessionContextProvider,
245
+ configure_session_context,
246
+ session_context_gucs,
247
+ )
243
248
  from .core.signals import (
244
249
  Signal,
245
250
  post_create,
@@ -632,6 +637,9 @@ __all__ = [
632
637
  "transaction",
633
638
  "acquire_connection",
634
639
  "set_local_guc",
640
+ "SessionContextProvider",
641
+ "configure_session_context",
642
+ "session_context_gucs",
635
643
  "advisory_xact_lock",
636
644
  "try_advisory_xact_lock",
637
645
  "advisory_lock",
@@ -30,8 +30,6 @@ Register any ``BaseAdapter`` subclass before the first query::
30
30
  from __future__ import annotations
31
31
 
32
32
  import logging
33
- import importlib.util
34
- import sys
35
33
 
36
34
  from matrx_orm.adapters.base_adapter import BaseAdapter
37
35
  from matrx_orm.adapters.async_postgresql import AsyncPostgreSQLAdapter
@@ -143,35 +141,13 @@ class AdapterRegistry:
143
141
  raise ValueError(f"Invalid dotted adapter path: {dotted_path!r}")
144
142
 
145
143
  # A custom adapter is an extension seam, not a closed list of built-in
146
- # modules. Resolve it through the import spec protocol so an unloaded
147
- # third-party adapter remains supported without an opaque dynamic
148
- # ``import_module(module_path)`` call that static provider scans cannot
149
- # inspect.
150
- module = sys.modules.get(module_path)
151
- if module is None:
152
- spec = importlib.util.find_spec(module_path)
153
- # find_spec() imports the PARENT package, and a parent __init__ that
154
- # re-exports this child imports it as a side effect. Re-read the
155
- # cache: executing the spec now would install a SECOND module object
156
- # over the real one, so the adapter class taken from it would be a
157
- # different class than the one everyone else imported.
158
- module = sys.modules.get(module_path)
159
- if module is None:
160
- if spec is None or spec.loader is None:
161
- raise ModuleNotFoundError(f"No importable adapter module named {module_path!r}")
162
- module = importlib.util.module_from_spec(spec)
163
- sys.modules[module_path] = module
164
- try:
165
- spec.loader.exec_module(module)
166
- except Exception:
167
- if sys.modules.get(module_path) is module:
168
- sys.modules.pop(module_path, None)
169
- raise
170
- parent_path, _, child_name = module_path.rpartition(".")
171
- if parent_path:
172
- parent = sys.modules.get(parent_path)
173
- if parent is not None:
174
- setattr(parent, child_name, module)
144
+ # modules. It loads through THE declared-module seam
145
+ # (matrx_utils.module_loading): an unloaded third-party adapter stays
146
+ # supported, a provider SDK name is refused, and the mandate/provider
147
+ # scan sees a fact instead of an opaque computed import.
148
+ from matrx_utils.module_loading import load_declared_module
149
+
150
+ module = load_declared_module(module_path)
175
151
  klass = getattr(module, class_name)
176
152
  instance = klass(config_name)
177
153
  if not isinstance(instance, BaseAdapter):
@@ -41,11 +41,17 @@ class AsyncPostgreSQLAdapter(BaseAdapter):
41
41
  *args: Any,
42
42
  timeout: float = 10.0,
43
43
  statement_timeout: float | None = None,
44
+ verify_empty: bool = True,
44
45
  ) -> list[dict[str, Any]]:
45
46
  from matrx_orm.core.async_db_manager import AsyncDatabaseManager
46
47
 
47
48
  return await AsyncDatabaseManager.execute_query(
48
- self._config_name, sql, *args, timeout=timeout, statement_timeout=statement_timeout
49
+ self._config_name,
50
+ sql,
51
+ *args,
52
+ timeout=timeout,
53
+ statement_timeout=statement_timeout,
54
+ verify_empty=verify_empty,
49
55
  )
50
56
 
51
57
  async def execute_write(
@@ -33,12 +33,14 @@ class BaseAdapter(ABC):
33
33
  *args: Any,
34
34
  timeout: float = 10.0,
35
35
  statement_timeout: float | None = None,
36
+ verify_empty: bool = True,
36
37
  ) -> list[dict[str, Any]]:
37
38
  """Execute a read (SELECT) query and return rows as dicts.
38
39
 
39
40
  ``timeout`` is the connection-acquire timeout; ``statement_timeout``
40
41
  (when set) overrides the pool's ``command_timeout`` for this statement
41
- only (QueryBuilder.timeout()).
42
+ only (QueryBuilder.timeout()). ``verify_empty=False`` is reserved for
43
+ replay-safe polling reads whose cursor does not advance on an empty page.
42
44
  """
43
45
 
44
46
  @abstractmethod
@@ -313,6 +313,7 @@ class PostgRESTClientAdapter(BaseAdapter):
313
313
  *args: Any,
314
314
  timeout: float = 10.0,
315
315
  statement_timeout: float | None = None,
316
+ verify_empty: bool = True,
316
317
  ) -> list[dict[str, Any]]:
317
318
  """Raw SQL is not supported via PostgREST.
318
319
 
@@ -103,11 +103,17 @@ class SupabaseAdapter(BaseAdapter):
103
103
  *args: Any,
104
104
  timeout: float = 10.0,
105
105
  statement_timeout: float | None = None,
106
+ verify_empty: bool = True,
106
107
  ) -> list[dict[str, Any]]:
107
108
  from matrx_orm.core.async_db_manager import AsyncDatabaseManager
108
109
 
109
110
  return await AsyncDatabaseManager.execute_query(
110
- self._config_name, sql, *args, timeout=timeout, statement_timeout=statement_timeout
111
+ self._config_name,
112
+ sql,
113
+ *args,
114
+ timeout=timeout,
115
+ statement_timeout=statement_timeout,
116
+ verify_empty=verify_empty,
111
117
  )
112
118
 
113
119
  async def execute_write(
@@ -221,6 +221,10 @@ Verified against the live database: 2026-08-21.
221
221
  interface errors.
222
222
  - 2026-08-22: Retried read-only mid-query connection loss once on a fresh
223
223
  checkout while preserving no-retry write semantics.
224
+ - 2026-09-11: Replay-safe polling reads may explicitly skip empty-result
225
+ reproof; their unchanged cursor makes an empty-to-nonempty race recover on
226
+ the next poll, avoiding the transaction-pool reproof convoy without relaxing
227
+ the default integrity guard for ordinary reads.
224
228
  - 2026-08-22: Preserved the deepest Postgres SQLSTATE on `AppError` as a
225
229
  lightweight classification (without retaining traceback frames), so an
226
230
  idempotent transactional owner can recover from a database role transition
@@ -617,6 +617,7 @@ Filepath: src/matrx_orm/core/async_db_manager.py [python]
617
617
 
618
618
  class AsyncDatabaseManager:
619
619
  def __new__(cls)
620
+ def pool_snapshot(cls, config_name: str) -> dict[str, int] | None # {size, idle, max, min} of an EXISTING pool; never creates, never awaits — for error paths
620
621
  async def get_pool(cls, config_name)
621
622
  async def get_connection(cls, config_name, timeout = 10.0)
622
623
  async def execute_query(cls, config_name: str, query: str, *args: Any, timeout: float = 10.0) -> list[dict[str, Any]]
@@ -427,6 +427,29 @@ class AsyncDatabaseManager:
427
427
  cls._instance = super().__new__(cls)
428
428
  return cls._instance
429
429
 
430
+ @classmethod
431
+ def pool_snapshot(cls, config_name: str) -> dict[str, int] | None:
432
+ """Read-only sizing of an EXISTING pool: ``{"size", "idle", "max", "min"}``.
433
+
434
+ Never creates a pool (unlike :meth:`get_pool`) and never awaits — safe
435
+ to call from an error path while a deadline is already blown. ``None``
436
+ when no pool exists for the (alias-resolved) name, so a caller can say
437
+ "no pool" instead of inventing zeros.
438
+ """
439
+ name = resolve_database_name(config_name)
440
+ pool = cls._pools.get(name)
441
+ if pool is None:
442
+ return None
443
+ try:
444
+ return {
445
+ "size": int(pool.get_size()),
446
+ "idle": int(pool.get_idle_size()),
447
+ "max": int(pool.get_max_size()),
448
+ "min": int(pool.get_min_size()),
449
+ }
450
+ except Exception: # noqa: BLE001 — a half-closed pool answers nothing, not a lie
451
+ return None
452
+
430
453
  @classmethod
431
454
  async def get_pool(cls, config_name):
432
455
  """Get or create a connection pool for the specified database with error handling.
@@ -1153,6 +1176,7 @@ class AsyncDatabaseManager:
1153
1176
  *args: Any,
1154
1177
  timeout: float = 10.0,
1155
1178
  statement_timeout: float | None = None,
1179
+ verify_empty: bool = True,
1156
1180
  ) -> list[dict[str, Any]]:
1157
1181
  """Execute a query and return results with error handling.
1158
1182
 
@@ -1274,7 +1298,12 @@ class AsyncDatabaseManager:
1274
1298
  read_attempt + 1,
1275
1299
  query[:120],
1276
1300
  )
1277
- if not results and read_only and cls._verifies_empty_reads(config_name):
1301
+ if (
1302
+ not results
1303
+ and verify_empty
1304
+ and read_only
1305
+ and cls._verifies_empty_reads(config_name)
1306
+ ):
1278
1307
  # Layer 2: an empty result from a pooled connection is
1279
1308
  # unproven until a session that can state its own role
1280
1309
  # says so. A borrowed `authenticated` role returns ZERO
@@ -0,0 +1,94 @@
1
+ """The host's DECLARED session context — GUCs every transaction opens with.
2
+
3
+ Some facts about a block of work are known by the application and readable only
4
+ by the database: *which tier of actor* is authoring these writes, for instance.
5
+ Postgres learns them through transaction-local GUCs (``set_config(..., true)``),
6
+ and a GUC set outside a transaction has no meaning — so the ONE place that can
7
+ reliably apply them is the moment this package opens a transaction.
8
+
9
+ The host declares a provider once at startup::
10
+
11
+ from matrx_orm import configure_session_context
12
+ configure_session_context(lambda: {"app.actor_tier": "ai", "app.actor_system": "x"})
13
+
14
+ and from then on every TOP-LEVEL ``transaction()`` (which includes the one
15
+ ``rls_session`` opens, before it drops to the restricted role) emits whatever
16
+ the provider returns at that instant. A savepoint (nested transaction) emits
17
+ nothing: it is already inside the transaction the GUCs were set on.
18
+
19
+ Why a provider and not a value: what is true changes per task — the provider is
20
+ normally a ContextVar read, so a block that declares "an AI is writing this"
21
+ affects exactly the work inside it and nothing running beside it.
22
+
23
+ The package NEVER imports the host: no provider configured ⇒ no GUCs, which is
24
+ exactly how matrx-orm behaves standalone. A provider that raises is a host bug
25
+ and it surfaces — the transaction fails loudly rather than silently committing
26
+ writes whose provenance is a lie.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import re
32
+ from collections.abc import Callable, Mapping
33
+ from typing import Any
34
+
35
+ # Same shape matrx_orm.core.transaction.set_local_guc accepts.
36
+ _GUC_NAME_RE = re.compile(r"^[a-z_][a-z0-9_.]*$")
37
+
38
+ SessionContextProvider = Callable[[], Mapping[str, str] | None]
39
+
40
+ _provider: SessionContextProvider | None = None
41
+
42
+
43
+ def configure_session_context(provider: SessionContextProvider | None) -> None:
44
+ """Bind (or with ``None``, unbind) the host's session-context provider.
45
+
46
+ Call once at startup, alongside the other package ``configure_*`` calls.
47
+ ``provider`` is invoked on every top-level transaction open and must return
48
+ a mapping of GUC name → value (or ``None`` / ``{}`` for "nothing to say").
49
+ """
50
+ global _provider
51
+ if provider is not None and not callable(provider):
52
+ raise TypeError("configure_session_context(provider) requires a callable or None")
53
+ _provider = provider
54
+
55
+
56
+ def session_context_gucs() -> dict[str, str]:
57
+ """The GUCs to set on a transaction being opened right now.
58
+
59
+ Empty when no provider is configured. Raises when a configured provider
60
+ returns something unusable — a malformed declaration is a host defect, and
61
+ writing rows with a wrong (or absent) provenance stamp is worse than a loud
62
+ failure at BEGIN.
63
+ """
64
+ provider = _provider
65
+ if provider is None:
66
+ return {}
67
+ declared = provider()
68
+ if not declared:
69
+ return {}
70
+ if not isinstance(declared, Mapping):
71
+ raise TypeError(
72
+ "the session-context provider must return a mapping of GUC name → value; "
73
+ f"got {type(declared).__name__}"
74
+ )
75
+ out: dict[str, str] = {}
76
+ for name, value in declared.items():
77
+ if not isinstance(name, str) or not _GUC_NAME_RE.match(name):
78
+ raise ValueError(f"invalid session-context GUC name: {name!r}")
79
+ out[name] = "" if value is None else str(value)
80
+ return out
81
+
82
+
83
+ async def apply_session_context(connection: Any) -> None:
84
+ """Emit the declared GUCs on ``connection`` (already inside a BEGIN)."""
85
+ for name, value in session_context_gucs().items():
86
+ await connection.execute("SELECT set_config($1, $2, true)", name, value)
87
+
88
+
89
+ __all__ = [
90
+ "SessionContextProvider",
91
+ "apply_session_context",
92
+ "configure_session_context",
93
+ "session_context_gucs",
94
+ ]
@@ -249,6 +249,23 @@ class TransactionContext:
249
249
  ) from e
250
250
  self._conn_token = _active_connection.set(conn)
251
251
  self._depth_token = _savepoint_depth.set(0)
252
+ # THE HOST'S DECLARED SESSION CONTEXT (provenance and friends). A
253
+ # transaction-local GUC only has meaning inside a transaction, so
254
+ # this — the moment one opens — is the only place a declaration made
255
+ # in application code can reach Postgres. Set while still the
256
+ # privileged role: `rls_session` opens through here and drops to the
257
+ # restricted role afterwards. Nested entries are savepoints inside a
258
+ # transaction that already carries them, so they emit nothing.
259
+ from matrx_orm.core.session_context import apply_session_context
260
+
261
+ try:
262
+ await apply_session_context(conn)
263
+ except Exception as e:
264
+ await self.__aexit__(type(e), e, e.__traceback__)
265
+ raise DatabaseError(
266
+ message=f"Failed to apply the declared session context: {e}",
267
+ details={"database": self.database},
268
+ ) from e
252
269
  return self
253
270
 
254
271
  async def __aexit__(
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import importlib.util
6
5
  import re
7
6
  from dataclasses import dataclass
8
7
  from pathlib import Path
@@ -124,15 +123,17 @@ class MigrationLoader:
124
123
 
125
124
  def _load_module(self, filepath: Path, name: str) -> Any:
126
125
  """Dynamically import a migration file as a module."""
127
- spec = importlib.util.spec_from_file_location(f"matrx_migrations.{name}", filepath)
128
- if spec is None or spec.loader is None:
126
+ from matrx_utils.module_loading import ProviderModuleRefused, load_module_from_path
127
+
128
+ # Only "no such file" / "refused" become MigrationError; an error the
129
+ # migration raises while executing propagates raw, exactly as before.
130
+ try:
131
+ return load_module_from_path(filepath, module_name=f"matrx_migrations.{name}")
132
+ except (FileNotFoundError, ProviderModuleRefused) as exc:
129
133
  raise MigrationError(
130
134
  migration=name,
131
- original_error=f"Could not load migration file: {filepath}",
132
- )
133
- module = importlib.util.module_from_spec(spec)
134
- spec.loader.exec_module(module)
135
- return module
135
+ original_error=f"Could not load migration file: {filepath} ({exc})",
136
+ ) from exc
136
137
 
137
138
  def _validate_graph(self) -> None:
138
139
  """Ensure all declared dependencies exist."""
@@ -0,0 +1,200 @@
1
+ """Session-wide guard: a test that reaches the durable-write spill lane.
2
+
3
+ Registered as a ``pytest11`` entry point so pytest auto-loads it in every suite
4
+ **regardless of rootdir** — same mechanism, same reason, as
5
+ ``matrx_connect.pytest_plugin`` and ``matrx_utils.pytest_plugin``.
6
+
7
+ WHY IT EXISTS. The write-durability chain is: Session flush → on failure
8
+ ``record_failures`` → on failure ``_spill_ops_to_disk``. The spill directory is
9
+ resolved from ``MATRX_SPILL_DIR`` → a packaged default → the OS temp dir, and
10
+ that last rung is SHARED by every process on the machine — including the local
11
+ API server, whose auto-replay loop drains it every minute back into
12
+ ``ops.system_write_failure`` and then re-applies the rows to the real tables.
13
+
14
+ Measured 2026-09-11: a matrx-ai unit test drove the orchestrator with a
15
+ fabricated user and conversation. Its collaborators were doubled, but the
16
+ coordinator queue helpers were not, and another suite in the same process had
17
+ already registered REAL database credentials and left a request lane open. The
18
+ flush hit production Postgres, the ``_stamp_org_default`` trigger refused the
19
+ row (the user does not exist), the failure capture itself failed (the same
20
+ user is a foreign key on the capture table), the rows were spilled to
21
+ ``$TMPDIR/matrx-write-spill`` — and the developer's local server drained them
22
+ into the live failure table and tried to replay test fixtures against
23
+ production forty times, landing a ``system_error`` row per attempt.
24
+
25
+ THE CONTRACT. No automated test may spill. Under pytest this plugin:
26
+
27
+ 1. points ``MATRX_SPILL_DIR`` at a fresh, session-private temp dir BEFORE any
28
+ test runs, so a spill can never reach a directory another process drains;
29
+ 2. FAILS the session if anything was spilled there, naming every file — a
30
+ spill under pytest is proof that a test wrote through to a real database
31
+ and could not even record its failure. Fix the test's isolation (double the
32
+ write seam, or install ``aidream.testing.offline`` placeholders); never
33
+ delete the evidence to make the run green;
34
+ 3. re-confines the cascade after EVERY test: a test that assigns
35
+ ``MATRX_SPILL_DIR`` or ``fallback._spill_dir_cache`` directly (no
36
+ monkeypatch revert) would otherwise leave every later spill landing in its
37
+ directory, unseen. Such a test is named in the summary and its directory is
38
+ swept too (hole found by the adversarial review of the first version).
39
+
40
+ Suites that exercise the spill lane on purpose (``matrx-orm``'s
41
+ ``test_disk_spill_fallback``) point the cascade at their own ``tmp_path`` via
42
+ ``monkeypatch`` and reset the cache; those files never land in the session dir
43
+ and the revert happens before this plugin looks.
44
+
45
+ Opt out for one run with ``-p no:matrx_write_spill_guard``.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import json
51
+ import os
52
+ import tempfile
53
+ from pathlib import Path
54
+
55
+ import pytest
56
+
57
+ _SESSION_SPILL_DIR: Path | None = None
58
+ _PREVIOUS_ENV: str | None = None
59
+ # Tests that repointed the spill cascade (env var or the module cache) and left
60
+ # it repointed. Each is re-confined at its teardown; its directory is still
61
+ # swept for spills at session end, so hijacking the cascade cannot hide one.
62
+ _HIJACKS: dict[str, Path] = {}
63
+
64
+
65
+ def session_spill_dir() -> Path | None:
66
+ """The directory this session's spills are confined to (None if not configured)."""
67
+ return _SESSION_SPILL_DIR
68
+
69
+
70
+ def spilled_files() -> list[Path]:
71
+ """Every op spilled to disk during this session, oldest first — in the
72
+ session dir and in any directory a test repointed the cascade to."""
73
+ if _SESSION_SPILL_DIR is None:
74
+ return []
75
+ seen: set[Path] = set()
76
+ files: list[Path] = []
77
+ for directory in (_SESSION_SPILL_DIR, *_HIJACKS.values()):
78
+ if directory in seen or not directory.exists():
79
+ continue
80
+ seen.add(directory)
81
+ files.extend(directory.glob("*.json"))
82
+ return sorted(files, key=lambda f: (f.stat().st_mtime, f.name))
83
+
84
+
85
+ def _describe(path: Path) -> str:
86
+ try:
87
+ row = json.loads(path.read_text())
88
+ except Exception: # noqa: BLE001 — a half-written file is still evidence
89
+ return f"{path.name}: (unreadable)"
90
+ return (
91
+ f"{path.name}: {row.get('table_target')} {row.get('op_type')} "
92
+ f"request_id={row.get('request_id')} user_id={row.get('user_id')} "
93
+ f"error={str(row.get('error_text') or '')[:160]!r}"
94
+ )
95
+
96
+
97
+ def _reconfine(nodeid: str) -> None:
98
+ """After a test: if it left the cascade pointing elsewhere, record where and
99
+ point it back at the session dir. A test that repoints via ``monkeypatch``
100
+ (auto-reverted before this runs) is never recorded; one that assigns
101
+ ``os.environ`` or the module cache directly is."""
102
+ if _SESSION_SPILL_DIR is None:
103
+ return
104
+ try:
105
+ from matrx_orm.session import fallback
106
+ except Exception: # noqa: BLE001 — the ORM is not imported in every suite
107
+ return
108
+ cache: Path | None = fallback._spill_dir_cache
109
+ env = os.environ.get("MATRX_SPILL_DIR")
110
+ hijacked: Path | None = None
111
+ if cache is not None and cache != _SESSION_SPILL_DIR:
112
+ hijacked = cache
113
+ elif env != str(_SESSION_SPILL_DIR):
114
+ hijacked = Path(env) if env else None
115
+ if hijacked is not None:
116
+ _HIJACKS.setdefault(nodeid, hijacked)
117
+ if hijacked is not None or env != str(_SESSION_SPILL_DIR):
118
+ fallback._spill_dir_cache = None
119
+ os.environ["MATRX_SPILL_DIR"] = str(_SESSION_SPILL_DIR)
120
+
121
+
122
+ def pytest_configure(config: pytest.Config) -> None: # noqa: ARG001 — hook signature
123
+ global _SESSION_SPILL_DIR, _PREVIOUS_ENV
124
+ _PREVIOUS_ENV = os.environ.get("MATRX_SPILL_DIR")
125
+ _SESSION_SPILL_DIR = Path(tempfile.mkdtemp(prefix="matrx-test-spill-"))
126
+ os.environ["MATRX_SPILL_DIR"] = str(_SESSION_SPILL_DIR)
127
+ # The cascade caches its first resolution for the life of the process. If
128
+ # something already resolved it (a conftest that imported the ORM and
129
+ # spilled at import time would be its own scandal), drop the cache so the
130
+ # session dir wins from here on.
131
+ try:
132
+ from matrx_orm.session import fallback
133
+
134
+ fallback._spill_dir_cache = None
135
+ except Exception: # noqa: BLE001 — never block a session over the guard's own import
136
+ pass
137
+
138
+
139
+ @pytest.hookimpl(hookwrapper=True, trylast=True)
140
+ def pytest_runtest_teardown(item: pytest.Item):
141
+ # Runs after every fixture finalizer (monkeypatch reverts included), so what
142
+ # is left pointing elsewhere is what the test itself leaked.
143
+ yield
144
+ _reconfine(item.nodeid)
145
+
146
+
147
+ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # noqa: ARG001
148
+ files = spilled_files()
149
+ if not files:
150
+ return
151
+ # A spilled op means a real database write was attempted AND failed AND the
152
+ # failure could not even be recorded. That is never what a test meant to do.
153
+ if session.exitstatus in (pytest.ExitCode.OK, pytest.ExitCode.NO_TESTS_COLLECTED):
154
+ session.exitstatus = pytest.ExitCode.TESTS_FAILED
155
+
156
+
157
+ def pytest_terminal_summary(terminalreporter, exitstatus: int, config: pytest.Config) -> None: # noqa: ARG001
158
+ files = spilled_files()
159
+ if not files:
160
+ return
161
+ lines = [
162
+ f"{len(files)} durable-write op(s) were SPILLED TO DISK during this test run.",
163
+ "A test wrote through to a real database, the write was refused, and the",
164
+ "failure capture itself failed — so the fallback chain parked the rows on",
165
+ "disk. Under pytest that is a test-isolation defect, not a persistence event:",
166
+ "double the coordinator/queue seam the test reaches, or install",
167
+ "aidream.testing.offline placeholders in the suite's conftest.",
168
+ f"Evidence (left in place, in {_SESSION_SPILL_DIR}):",
169
+ ]
170
+ lines.extend(f" - {_describe(path)}" for path in files[:20])
171
+ if len(files) > 20:
172
+ lines.append(f" ... and {len(files) - 20} more")
173
+ if _HIJACKS:
174
+ lines.append(
175
+ "These tests repointed the spill cascade (MATRX_SPILL_DIR or the module "
176
+ "cache) without reverting it; each was re-confined at its teardown and "
177
+ "its directory swept — use monkeypatch so the change reverts itself:"
178
+ )
179
+ lines.extend(f" - {nodeid} -> {path}" for nodeid, path in _HIJACKS.items())
180
+ lines.append("The session is marked FAILED. Opt out once with -p no:matrx_write_spill_guard.")
181
+ terminalreporter.write_sep("=", "matrx write-spill guard", red=True, bold=True)
182
+ for line in lines:
183
+ terminalreporter.write_line(line, red=True)
184
+
185
+
186
+ def pytest_unconfigure(config: pytest.Config) -> None: # noqa: ARG001
187
+ global _SESSION_SPILL_DIR, _PREVIOUS_ENV
188
+ if _PREVIOUS_ENV is None:
189
+ os.environ.pop("MATRX_SPILL_DIR", None)
190
+ else:
191
+ os.environ["MATRX_SPILL_DIR"] = _PREVIOUS_ENV
192
+ # Remove ONLY an empty session dir; spilled evidence is left for the human.
193
+ if _SESSION_SPILL_DIR is not None and not spilled_files():
194
+ try:
195
+ _SESSION_SPILL_DIR.rmdir()
196
+ except OSError:
197
+ pass
198
+ _SESSION_SPILL_DIR = None
199
+ _PREVIOUS_ENV = None
200
+ _HIJACKS.clear()
@@ -213,6 +213,7 @@ class QueryBuilder(Generic[ModelT]):
213
213
  vector_null_guard: bool # auto-add IS NOT NULL guard for vector column
214
214
  from_subquery_builder: QueryBuilder[Any] | None
215
215
  timeout_val: float | None # timeout() — per-query statement timeout override (seconds)
216
+ verify_empty_val: bool
216
217
 
217
218
  def __init__(self, model_cls: type[ModelT], database: str | None = None) -> None:
218
219
  self.model = model_cls
@@ -239,6 +240,7 @@ class QueryBuilder(Generic[ModelT]):
239
240
  self.from_subquery_builder = None
240
241
  self.from_alias_name: str | None = None
241
242
  self.timeout_val: float | None = None
243
+ self.verify_empty_val = True
242
244
 
243
245
  def _set_database(self, model_cls: type[ModelT]) -> str:
244
246
  if hasattr(model_cls, "_database") and model_cls._database:
@@ -566,6 +568,12 @@ class QueryBuilder(Generic[ModelT]):
566
568
  new.timeout_val = seconds
567
569
  return new
568
570
 
571
+ def replay_safe_empty(self) -> QueryBuilder[ModelT]:
572
+ """Skip empty-read reproof for a replay-safe poll whose cursor does not advance on empty."""
573
+ new = self._clone()
574
+ new.verify_empty_val = False
575
+ return new
576
+
569
577
  def from_alias(self, alias: str) -> QueryBuilder[ModelT]:
570
578
  """Alias the base (FROM) table — renders ``FROM <table> AS <alias>``.
571
579
 
@@ -737,6 +745,7 @@ class QueryBuilder(Generic[ModelT]):
737
745
  "from_subquery": self.from_subquery_builder,
738
746
  "from_alias": self.from_alias_name,
739
747
  "timeout": self.timeout_val,
748
+ "verify_empty": self.verify_empty_val,
740
749
  }
741
750
  return query
742
751
 
@@ -1028,10 +1028,17 @@ class QueryExecutor:
1028
1028
  # Passed only when set so adapters that don't accept the kwarg
1029
1029
  # keep working for un-overridden queries.
1030
1030
  statement_timeout = self._full_query_dict.get("timeout")
1031
- if statement_timeout is not None:
1031
+ verify_empty = self._full_query_dict.get("verify_empty", True)
1032
+ if statement_timeout is not None and not verify_empty:
1033
+ results = await self.db.execute_query(
1034
+ self.query, *self.params, statement_timeout=statement_timeout, verify_empty=verify_empty
1035
+ )
1036
+ elif statement_timeout is not None:
1032
1037
  results = await self.db.execute_query(
1033
1038
  self.query, *self.params, statement_timeout=statement_timeout
1034
1039
  )
1040
+ elif not verify_empty:
1041
+ results = await self.db.execute_query(self.query, *self.params, verify_empty=False)
1035
1042
  else:
1036
1043
  results = await self.db.execute_query(self.query, *self.params)
1037
1044
  return results