matrx-orm 3.1.65__tar.gz → 3.1.67__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 (328) hide show
  1. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/PKG-INFO +1 -1
  2. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/__init__.py +4 -0
  3. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/associations.py +28 -0
  4. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/FEATURE.md +14 -0
  5. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/base.py +1 -3
  6. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/README.md +29 -0
  7. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/__init__.py +9 -1
  8. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/fallback.py +182 -3
  9. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/pyproject.toml +1 -1
  10. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_derived_table.py +1 -1
  11. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_expression_primitives.py +5 -2
  12. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_join_and_aggregate.py +1 -1
  13. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_managed_write_guard.py +11 -0
  14. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_model_meta.py +1 -1
  15. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_query_builder.py +1 -1
  16. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_relational_backfill.py +4 -4
  17. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_typed_join_projection.py +2 -1
  18. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_typed_predicates.py +10 -7
  19. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/.gitignore +0 -0
  20. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/AGENTS.md +0 -0
  21. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/BASE_CLASS_METHODS.md +0 -0
  22. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/CLAUDE.md +0 -0
  23. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/MODEL_API.md +0 -0
  24. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/README.md +0 -0
  25. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/database/orm/extended/managers/ai_model_base.py +0 -0
  26. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/docs/migrations.md +0 -0
  27. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/MODULE_README.md +0 -0
  28. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/MODULE_README.md +0 -0
  29. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/__init__.py +0 -0
  30. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/async_postgresql.py +0 -0
  31. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/base_adapter.py +0 -0
  32. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/postgresql.py +0 -0
  33. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  34. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/supabase_adapter.py +0 -0
  35. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/admin/__init__.py +0 -0
  36. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/admin/router.py +0 -0
  37. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/__init__.py +0 -0
  38. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/auth.py +0 -0
  39. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/config.py +0 -0
  40. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/handlers.py +0 -0
  41. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/protocol.py +0 -0
  42. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/server.py +0 -0
  43. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/cache_debug.py +0 -0
  44. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/catalog.py +0 -0
  45. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/MODULE_README.md +0 -0
  46. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/__init__.py +0 -0
  47. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/postgres_connection.py +0 -0
  48. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/postgrest.py +0 -0
  49. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/supabase_auth.py +0 -0
  50. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/supabase_config.py +0 -0
  51. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/supabase_manager.py +0 -0
  52. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  53. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/MODULE_README.md +0 -0
  54. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  55. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/TASKS.md +0 -0
  56. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/__init__.py +0 -0
  57. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/async_db_manager.py +0 -0
  58. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/config.py +0 -0
  59. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/connections.py +0 -0
  60. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/db_function.py +0 -0
  61. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/diagnostics.py +0 -0
  62. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/expressions.py +0 -0
  63. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/extended.py +0 -0
  64. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/fields.py +0 -0
  65. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/introspection.py +0 -0
  66. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/listen.py +0 -0
  67. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/loop_filters.py +0 -0
  68. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/model_dto.py +0 -0
  69. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/model_view.py +0 -0
  70. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/one_database.py +0 -0
  71. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/paginator.py +0 -0
  72. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/pool_maintenance.py +0 -0
  73. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/pydantic_bridge.py +0 -0
  74. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/registry.py +0 -0
  75. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/relations.py +0 -0
  76. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/resilience.py +0 -0
  77. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/rls_session.py +0 -0
  78. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/signals.py +0 -0
  79. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/tls.py +0 -0
  80. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/transaction.py +0 -0
  81. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/types.py +0 -0
  82. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/write_queue.py +0 -0
  83. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/entity.py +0 -0
  84. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/error_handling.py +0 -0
  85. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/exceptions.py +0 -0
  86. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/extended/__init__.py +0 -0
  87. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/extended/app_error_handler.py +0 -0
  88. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/identity_rule.py +0 -0
  89. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/middleware/__init__.py +0 -0
  90. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/middleware/base.py +0 -0
  91. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/MODULE_README.md +0 -0
  92. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/__init__.py +0 -0
  93. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/cli.py +0 -0
  94. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/ddl.py +0 -0
  95. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/diff.py +0 -0
  96. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/executor.py +0 -0
  97. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/integration.py +0 -0
  98. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/loader.py +0 -0
  99. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/operations.py +0 -0
  100. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/state.py +0 -0
  101. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/table_filter.py +0 -0
  102. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/MODULE_README.md +0 -0
  103. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/__init__.py +0 -0
  104. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/bulk_update_values.py +0 -0
  105. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/conflict.py +0 -0
  106. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/conflict_writes.py +0 -0
  107. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/create.py +0 -0
  108. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/db_functions.py +0 -0
  109. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/delete.py +0 -0
  110. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/dynamic_admin.py +0 -0
  111. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/dynamic_crud.py +0 -0
  112. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/graph_walk.py +0 -0
  113. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/latest_rows.py +0 -0
  114. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/queue_claim.py +0 -0
  115. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/read.py +0 -0
  116. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/relational_backfill.py +0 -0
  117. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/staging_load.py +0 -0
  118. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/update.py +0 -0
  119. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/platform_db.py +0 -0
  120. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/py.typed +0 -0
  121. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/MODULE_README.md +0 -0
  122. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/__init__.py +0 -0
  123. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/db_objects.py +0 -0
  124. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  125. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  126. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/FEATURE.md +0 -0
  127. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/MODULE_README.md +0 -0
  128. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/__init__.py +0 -0
  129. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/builder.py +0 -0
  130. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/executor.py +0 -0
  131. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  132. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/__init__.py +0 -0
  133. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/code_handler.py +0 -0
  134. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  135. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/columns.py +0 -0
  136. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/common.py +0 -0
  137. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/diff_preview.py +0 -0
  138. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  139. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/generator.py +0 -0
  140. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  141. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  142. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  143. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  144. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/package_wiring.py +0 -0
  145. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/relationships.py +0 -0
  146. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/runner.py +0 -0
  147. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/schema.py +0 -0
  148. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/schema_manager.py +0 -0
  149. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/tables.py +0 -0
  150. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/views.py +0 -0
  151. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  152. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/__init__.py +0 -0
  153. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/attachments.py +0 -0
  154. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/crypto.py +0 -0
  155. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/items.py +0 -0
  156. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/model.py +0 -0
  157. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/org_service.py +0 -0
  158. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/service.py +0 -0
  159. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/coalesce.py +0 -0
  160. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/dag.py +0 -0
  161. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/errors.py +0 -0
  162. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/flush.py +0 -0
  163. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/lifecycle.py +0 -0
  164. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/managed.py +0 -0
  165. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/op.py +0 -0
  166. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/reads.py +0 -0
  167. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/session.py +0 -0
  168. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/telemetry.py +0 -0
  169. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  170. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/__init__.py +0 -0
  171. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/executor.py +0 -0
  172. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/queries.py +0 -0
  173. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/registry.py +0 -0
  174. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/types.py +0 -0
  175. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/utils.py +0 -0
  176. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/state.py +0 -0
  177. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/utils/__init__.py +0 -0
  178. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/utils/sql_utils.py +0 -0
  179. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/utils/type_converters.py +0 -0
  180. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/scripts/git-branches.sh +0 -0
  181. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/scripts/publish.sh +0 -0
  182. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/scripts/release.sh +0 -0
  183. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/MODULE_README.md +0 -0
  184. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/__init__.py +0 -0
  185. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/conftest.py +0 -0
  186. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/MODULE_README.md +0 -0
  187. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/__init__.py +0 -0
  188. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_admin_db_columns.py +0 -0
  189. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_admin_search_negation.py +0 -0
  190. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_admin_search_sentinels.py +0 -0
  191. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_advisory_lock.py +0 -0
  192. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_agent_message.py +0 -0
  193. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_api_auth.py +0 -0
  194. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_api_config.py +0 -0
  195. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_api_handlers.py +0 -0
  196. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_api_protocol.py +0 -0
  197. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_array_agg_order_by.py +0 -0
  198. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_associations.py +0 -0
  199. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_bulk_update_by_pk.py +0 -0
  200. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  201. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_cache_debug.py +0 -0
  202. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_call_function.py +0 -0
  203. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_case_when.py +0 -0
  204. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_catalog.py +0 -0
  205. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_composite_pk_filter.py +0 -0
  206. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_composite_pk_write_paths.py +0 -0
  207. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_config.py +0 -0
  208. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_conflict_target.py +0 -0
  209. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_conflict_writes.py +0 -0
  210. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_connection_codecs.py +0 -0
  211. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_connection_poison_guard.py +0 -0
  212. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_connection_primitives.py +0 -0
  213. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_count_composite_distinct.py +0 -0
  214. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_db_function_field.py +0 -0
  215. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_ddl_generator.py +0 -0
  216. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_dirty_tracking.py +0 -0
  217. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_dynamic_crud.py +0 -0
  218. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_exceptions.py +0 -0
  219. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  220. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_fields.py +0 -0
  221. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  222. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_from_alias.py +0 -0
  223. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_fts.py +0 -0
  224. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_func_expression_args.py +0 -0
  225. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_graph_walk.py +0 -0
  226. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_having_annotate_alias.py +0 -0
  227. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  228. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_introspect_rls_policies.py +0 -0
  229. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_latest_rows.py +0 -0
  230. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_listen_notify.py +0 -0
  231. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_matrx_entity.py +0 -0
  232. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_migration_diff_types.py +0 -0
  233. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_migration_loader.py +0 -0
  234. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_model_instance.py +0 -0
  235. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_optimistic_concurrency.py +0 -0
  236. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_order_by_expression.py +0 -0
  237. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_parameter_name_hydration.py +0 -0
  238. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_patch_jsonb_path.py +0 -0
  239. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_pool_loop_recreation.py +0 -0
  240. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_pool_maintenance.py +0 -0
  241. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_postgrest_filters.py +0 -0
  242. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_pydantic_bridge.py +0 -0
  243. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_query_error_detail.py +0 -0
  244. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_query_executor_sql.py +0 -0
  245. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_query_timeout.py +0 -0
  246. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  247. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_queue_claim.py +0 -0
  248. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_registry.py +0 -0
  249. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_registry_multi_database.py +0 -0
  250. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_registry_multi_schema.py +0 -0
  251. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_relations.py +0 -0
  252. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_resilience.py +0 -0
  253. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_schema_exists.py +0 -0
  254. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_session_advisory_lock.py +0 -0
  255. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_state_cache.py +0 -0
  256. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_subquery_filter_raw.py +0 -0
  257. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_subquery_in_filter.py +0 -0
  258. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_supabase_auth.py +0 -0
  259. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_supabase_config.py +0 -0
  260. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_tls.py +0 -0
  261. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_update_case_expression.py +0 -0
  262. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_update_subquery.py +0 -0
  263. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_update_with_rebase.py +0 -0
  264. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_upsert_default_leak.py +0 -0
  265. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_upsert_with_conflict.py +0 -0
  266. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_values_jsonb_decode.py +0 -0
  267. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_watched_lifecycle.py +0 -0
  268. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_write_retry_semantics.py +0 -0
  269. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/MODULE_README.md +0 -0
  270. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/__init__.py +0 -0
  271. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/conftest.py +0 -0
  272. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_bulk_ops.py +0 -0
  273. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_cache_integration.py +0 -0
  274. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_crud.py +0 -0
  275. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_foreign_keys.py +0 -0
  276. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_m2m.py +0 -0
  277. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_manager.py +0 -0
  278. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_migrations_live.py +0 -0
  279. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_query_execution.py +0 -0
  280. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_schema_diff.py +0 -0
  281. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/.env.example +0 -0
  282. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/README.md +0 -0
  283. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/__init__.py +0 -0
  284. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/generate.py +0 -0
  285. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/generated/.gitkeep +0 -0
  286. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/matrx_orm.yaml +0 -0
  287. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/test_schema_generation.py +0 -0
  288. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/.env.example +0 -0
  289. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/README.md +0 -0
  290. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/__init__.py +0 -0
  291. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/client_example.py +0 -0
  292. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/client_example.ts +0 -0
  293. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  294. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/server.py +0 -0
  295. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/entity_tests.py +0 -0
  296. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_base_generation.py +0 -0
  297. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  298. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_composite_unique_generation.py +0 -0
  299. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_dto_identity_generation.py +0 -0
  300. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_entity_capability_generation.py +0 -0
  301. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_generate_schema.py +0 -0
  302. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_junction_analysis.py +0 -0
  303. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_multi_schema_output.py +0 -0
  304. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_sql_expression_defaults.py +0 -0
  305. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_view_generation.py +0 -0
  306. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  307. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  308. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/__init__.py +0 -0
  309. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/conftest.py +0 -0
  310. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_cache_no_downgrade.py +0 -0
  311. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_capture_poison_proof.py +0 -0
  312. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_coalesce.py +0 -0
  313. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_dag.py +0 -0
  314. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_disk_spill_fallback.py +0 -0
  315. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_flush_individual_fallback.py +0 -0
  316. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_governed_write.py +0 -0
  317. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_managed.py +0 -0
  318. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_op_enqueue_site.py +0 -0
  319. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_reads.py +0 -0
  320. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_session.py +0 -0
  321. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_database_name_alias.py +0 -0
  322. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_db_functions.py +0 -0
  323. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_model_cls_refactor.py +0 -0
  324. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  325. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_one_database.py +0 -0
  326. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_secrets_battery.py +0 -0
  327. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_sql_param_casts.py +0 -0
  328. {matrx_orm-3.1.65 → matrx_orm-3.1.67}/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.65
3
+ Version: 3.1.67
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
@@ -317,10 +317,12 @@ from matrx_orm.session import (
317
317
  COORDINATOR_BYPASS_ACKNOWLEDGEMENT,
318
318
  allow_direct_coordinator_write,
319
319
  coordinator_policy_tables,
320
+ create_or_capture,
320
321
  current_session,
321
322
  declare_managed,
322
323
  drain_spilled_ops,
323
324
  is_in_session,
325
+ WritePreservedError,
324
326
  record_error,
325
327
  record_failures,
326
328
  register_coordinator_owned_model,
@@ -599,6 +601,8 @@ __all__ = [
599
601
  "record_error",
600
602
  "record_failures",
601
603
  "drain_spilled_ops",
604
+ "create_or_capture",
605
+ "WritePreservedError",
602
606
  "run_lifecycle_sweeper",
603
607
  "sweep_lifecycle_once",
604
608
  # ── Adapter layer ──────────────────────────────────────────────────────────
@@ -186,6 +186,7 @@ class Associations:
186
186
  target_id_field: str = "target_id",
187
187
  org_field: str = "org_id",
188
188
  role_field: str = "role",
189
+ soft_delete_field: str = "deleted_at",
189
190
  ) -> None:
190
191
  self._assoc = association_model
191
192
  self._reg = registry
@@ -196,6 +197,14 @@ class Associations:
196
197
  self._org = org_field
197
198
  self._role = role_field
198
199
  self._role_available = True # refined by _validate_field_bindings when introspectable
200
+ # An edge table MAY tombstone edges rather than delete them: the host's
201
+ # entity soft-delete carries its edges down with it, and the entity's
202
+ # restore brings exactly those edges back. Where the column exists EVERY
203
+ # read here is live-only — a tombstoned edge conveys nothing, so handing
204
+ # one back is an access leak. A minimal host edge table without the
205
+ # column keeps the old unfiltered behaviour.
206
+ self._soft_delete = soft_delete_field
207
+ self._soft_delete_available = False # refined by _validate_field_bindings
199
208
  # Optional edge-document columns (platform.associations carries them;
200
209
  # a minimal host edge table may not). Passing the kwarg against a
201
210
  # table without the column raises loudly at call time.
@@ -239,9 +248,14 @@ class Associations:
239
248
  # role is OPTIONAL on the edge table — engines over role-less edge
240
249
  # tables keep working; passing a role against one raises at call time.
241
250
  self._role_available = self._role in fields
251
+ self._soft_delete_available = self._soft_delete in fields
242
252
  for col in self._optional_cols:
243
253
  self._optional_cols[col] = col in fields
244
254
 
255
+ def _live_filter(self) -> dict[str, Any]:
256
+ """Restrict a read to non-tombstoned edges (no-op without the column)."""
257
+ return {self._soft_delete: None} if self._soft_delete_available else {}
258
+
245
259
  # ---- writes ----------------------------------------------------------
246
260
 
247
261
  def _role_filter(self, role: Any) -> dict[str, Any]:
@@ -332,6 +346,14 @@ class Associations:
332
346
  for col, value in {"metadata": metadata, **extras}.items()
333
347
  if value is not None and getattr(edge, col, None) != value
334
348
  }
349
+ # Re-attaching over a TOMBSTONE revives it. The lookup deliberately
350
+ # matches tombstoned edges (the DB's unique index does too), so
351
+ # without this the caller would get back an edge nothing can see.
352
+ if self._soft_delete_available and getattr(edge, self._soft_delete, None) is not None:
353
+ updates[self._soft_delete] = None
354
+ for stamp in ("deleted_via_type", "deleted_via_id"):
355
+ if stamp in getattr(getattr(self._assoc, "_meta", None), "fields", ()):
356
+ updates[stamp] = None
335
357
  if updates:
336
358
  edge = await edge.update(**updates)
337
359
  return edge
@@ -352,6 +374,10 @@ class Associations:
352
374
  self._ti: target.id,
353
375
  self._org: org_id,
354
376
  **self._role_filter(role),
377
+ # Never purge a tombstone: the user is detaching what they can
378
+ # SEE, and the tombstone must survive to be restored with its
379
+ # entity. A hard entity DELETE is what purges tombstones.
380
+ **self._live_filter(),
355
381
  }
356
382
  )
357
383
  for edge in edges:
@@ -374,6 +400,7 @@ class Associations:
374
400
  if target_type is not None:
375
401
  filters[self._tt] = target_type
376
402
  filters.update(self._role_filter(role))
403
+ filters.update(self._live_filter())
377
404
  return await self._assoc.filter_items(**filters)
378
405
 
379
406
  async def edges_to(
@@ -390,6 +417,7 @@ class Associations:
390
417
  if source_type is not None:
391
418
  filters[self._st] = source_type
392
419
  filters.update(self._role_filter(role))
420
+ filters.update(self._live_filter())
393
421
  return await self._assoc.filter_items(**filters)
394
422
 
395
423
  async def targets_from(
@@ -13,6 +13,18 @@ columns present in the row as loaded.
13
13
  Pinned by `tests/level1/test_parameter_name_hydration.py` and
14
14
  `tests/level1/test_dirty_tracking.py`.
15
15
 
16
+ ## Relation identity
17
+
18
+ **Every model renders an exact `schema.table` SQL target.** Models whose
19
+ generated metadata omits `_db_schema` are public-schema models, so
20
+ `ModelOptions.qualified_table_name` returns `public.<table>` rather than a bare
21
+ name. This keeps ordinary public-model writes compatible with the managed-write
22
+ guard and ensures diagnostic sinks such as `public.app_log` and
23
+ `public.system_write_failure` remain writable after schema-identity hardening.
24
+
25
+ Pinned by `tests/level1/test_model_meta.py` and
26
+ `tests/level1/test_managed_write_guard.py`.
27
+
16
28
  ## Database execution errors
17
29
 
18
30
  `AsyncDatabaseManager.execute_query` must classify every PostgreSQL integrity
@@ -45,6 +57,8 @@ Verified against code: 2026-08-13.
45
57
 
46
58
  ## Change log
47
59
 
60
+ - 2026-08-14: Public models now render `public.table` SQL targets; bare public
61
+ relation names can no longer disable application logs or write-failure capture.
48
62
  - 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.
49
63
  - 2026-08-13: Added atomic recovery from a pool left at zero connections after an upstream database restart.
50
64
  - 2026-08-09: Pinned positional-mapping hydration for parameter-shaped column names.
@@ -176,9 +176,7 @@ class ModelOptions:
176
176
 
177
177
  @property
178
178
  def qualified_table_name(self) -> str:
179
- if self.db_schema:
180
- return f"{self.db_schema}.{self.table_name}"
181
- return self.table_name
179
+ return f"{self.db_schema or 'public'}.{self.table_name}"
182
180
 
183
181
 
184
182
  class ModelMeta(type):
@@ -231,6 +231,35 @@ matrx-ai/persistence module.
231
231
  See [docs/persistence/ORM_SESSION_VISION.md](../../../../docs/persistence/ORM_SESSION_VISION.md)
232
232
  for the full migration plan.
233
233
 
234
+ ## Keep-the-work: `create_or_capture` for DIRECT writes
235
+
236
+ A Session-queued write that fails is captured to `system_write_failure` and
237
+ replayed automatically. A **direct** write (`Model.create_item(...)`) had no
238
+ such tier — when it raised, the payload evaporated. That destroyed a paid agent
239
+ result in production (matrx-frontend FOUND_DEFECTS D183): the model had already
240
+ answered, the user had already been billed, and a 10s statement timeout on the
241
+ single INSERT deleted the only copy.
242
+
243
+ `create_or_capture(model_cls, payload, ...)` closes it, in this order:
244
+
245
+ 1. **Mint the PK first**, so one id spans the attempt, the verification, and any
246
+ replay.
247
+ 2. **Fire** `create_item`.
248
+ 3. **Verify before declaring loss** — a statement timeout can fire *after* the
249
+ row committed, so re-read by PK; if it is there the write SUCCEEDED (logged
250
+ loudly: a client gave up on a landed write).
251
+ 4. **Preserve** — capture the intended INSERT to `system_write_failure` stamped
252
+ `DirectWritePreserved`, which `matrx_ai`'s auto-replay loop treats as
253
+ self-healing and re-applies with no human in the loop.
254
+ 5. **Raise `WritePreservedError`** carrying the op handle, so the caller's
255
+ failure record can say *your work survived* (`as_payload()` is the JSON-safe
256
+ shape for a run row's `error` column and for a stream event).
257
+
258
+ **Reach for it only where a lost row means lost money or lost human effort** —
259
+ an agent's structured output, a provider's response, a rendered document.
260
+ Ordinary writes stay on the Session/Coordinator path, which already has this
261
+ guarantee. Live proof: `scripts/_verify_keep_the_work_live.py`.
262
+
234
263
  ## Doctrine references
235
264
 
236
265
  - `docs/persistence/DOCTRINE.md` — the persistence rules
@@ -56,7 +56,13 @@ from matrx_orm.session.errors import (
56
56
  SessionClosedError,
57
57
  SessionError,
58
58
  )
59
- from matrx_orm.session.fallback import drain_spilled_ops, record_error, record_failures
59
+ from matrx_orm.session.fallback import (
60
+ WritePreservedError,
61
+ create_or_capture,
62
+ drain_spilled_ops,
63
+ record_error,
64
+ record_failures,
65
+ )
60
66
  from matrx_orm.session.lifecycle import (
61
67
  SWEEP_INTERVAL_SECS_DEFAULT,
62
68
  SWEEP_LIMIT_DEFAULT,
@@ -131,6 +137,8 @@ __all__ = [
131
137
  "record_error",
132
138
  "record_failures",
133
139
  "drain_spilled_ops",
140
+ "create_or_capture",
141
+ "WritePreservedError",
134
142
  "merge_pending_with_db_one",
135
143
  "run_sweeper",
136
144
  "session_stack",
@@ -50,6 +50,15 @@ _SPILL_PACKAGED_DEFAULT = "/var/lib/matrx/spill"
50
50
  # which is definitionally recoverable once the DB returns). matrx-ai's replay
51
51
  # loop adds this to its RECOVERABLE_RETRY_ERRORS set — keep the string in lockstep.
52
52
  DISK_SPILL_RECOVERED_MARKER = "DiskSpillRecovered"
53
+ # Signature stamped into a row captured by :func:`create_or_capture` — a DIRECT
54
+ # (non-Session) write that failed after work was already done and paid for. It
55
+ # is stamped for the same reason as the disk-spill marker: the failure classes
56
+ # that kill a direct write (statement timeout, connection reset, pool
57
+ # exhaustion) are ENVIRONMENTAL and self-heal, so the auto-replay loop should
58
+ # re-apply the row rather than leave a paid artifact sitting in a failure table.
59
+ # matrx-ai's replay loop adds this to its RECOVERABLE_RETRY_ERRORS set — keep
60
+ # the string in lockstep.
61
+ DIRECT_WRITE_PRESERVED_MARKER = "DirectWritePreserved"
53
62
  # Resolved-writable spill dir, cached after first success so we don't re-probe
54
63
  # the cascade on every failure. None until first resolution.
55
64
  _spill_dir_cache: Path | None = None
@@ -300,19 +309,31 @@ async def record_failures(
300
309
  user_id: str | None = None,
301
310
  conversation_id: str | None = None,
302
311
  database: str | None = None,
303
- ) -> None:
312
+ error_prefix: str | None = None,
313
+ ) -> list[str]:
304
314
  """Write every dropped op to ``system_write_failure`` on a fresh connection.
305
315
 
306
316
  Never raises. If the fresh-connection write itself fails, dumps every
307
317
  op to stdout in red and to the structured logger.
318
+
319
+ ``error_prefix`` stamps a recoverability marker onto ``error_text`` (the
320
+ same mechanism ``drain_spilled_ops`` uses for ``DISK_SPILL_RECOVERED_MARKER``)
321
+ so the auto-replay loop can classify this capture without pattern-matching
322
+ a driver message.
323
+
324
+ Returns the ``op_id`` of every op it captured — the handle a caller quotes
325
+ back to a human ("your work is preserved as <id>") and the key the admin
326
+ persistence surface replays by.
308
327
  """
309
328
  if not ops:
310
- return
329
+ return []
311
330
 
312
331
  # Never blank: resolve the deepest driver error (SQLSTATE + message + diag)
313
332
  # so the system_write_failure row records WHY the write was dropped, even
314
333
  # when the exception stringifies to "".
315
334
  error_text = describe_db_exception(exc).as_text()
335
+ if error_prefix:
336
+ error_text = f"{error_prefix}: {error_text}"
316
337
  tb_text = "".join(_tb.format_exception(type(exc), exc, exc.__traceback__))
317
338
 
318
339
  # UUID-typed columns must never crash the capture itself (request_id is
@@ -351,7 +372,7 @@ async def record_failures(
351
372
  f"system_write_failure (flush exc: {error_text})",
352
373
  color="yellow",
353
374
  )
354
- return
375
+ return [op.op_id for op in ops]
355
376
 
356
377
  # Anything that didn't reach the table is spilled to disk — last resort
357
378
  # before data loss. drain_spilled_ops() re-lands it when the DB returns.
@@ -376,6 +397,10 @@ async def record_failures(
376
397
  spilled=spilled,
377
398
  total=len(failed_rows),
378
399
  )
400
+ # A spilled op is still preserved (drain_spilled_ops re-lands it), so it
401
+ # counts as captured. Only ops that reached neither the table nor the disk
402
+ # are lost, and those already screamed above.
403
+ return [op.op_id for op in ops]
379
404
 
380
405
 
381
406
  async def _bulk_insert_failures(
@@ -657,3 +682,157 @@ def _scream_to_stdout(
657
682
  )
658
683
  except Exception:
659
684
  vcprint(f" LOST OP {op.op_id} (could not serialize)", color="red")
685
+
686
+
687
+ # ── Keep-the-work tier: DIRECT writes that must never lose their payload ─────
688
+
689
+
690
+ class WritePreservedError(RuntimeError):
691
+ """A direct write failed, but its payload was PRESERVED for replay.
692
+
693
+ Raised by :func:`create_or_capture` so a caller can tell a human the truth
694
+ that matters: the expensive work is not gone. ``op_id`` is the
695
+ ``system_write_failure`` handle the auto-replay loop retries and the admin
696
+ persistence surface replays by.
697
+
698
+ ``captured=False`` means even the capture failed (the op was screamed to
699
+ stdout/structlog) — the ONLY case where a caller should say the work is
700
+ genuinely at risk.
701
+ """
702
+
703
+ def __init__(
704
+ self,
705
+ message: str,
706
+ *,
707
+ table: str,
708
+ pk_value: Any,
709
+ op_id: str | None,
710
+ captured: bool,
711
+ cause: BaseException,
712
+ ) -> None:
713
+ super().__init__(message)
714
+ self.table = table
715
+ self.pk_value = pk_value
716
+ self.op_id = op_id
717
+ self.captured = captured
718
+ self.cause = cause
719
+
720
+ def as_payload(self) -> dict[str, Any]:
721
+ """JSON-safe description for a run row's ``error`` column / a stream
722
+ event. Never a bare traceback — this is what a SURFACE renders."""
723
+ return {
724
+ "type": "WritePreservedError",
725
+ "message": str(self),
726
+ "work_preserved": self.captured,
727
+ "preserved_op_id": self.op_id,
728
+ "preserved_table": self.table,
729
+ "preserved_row_id": str(self.pk_value),
730
+ "user_message": (
731
+ "The result was generated successfully but could not be saved "
732
+ "to its final home. It has been preserved and will be recovered "
733
+ "automatically — you will not be charged to produce it again."
734
+ if self.captured
735
+ else "The result was generated but could not be saved, and the "
736
+ "preservation write also failed. Support has the full payload "
737
+ "in the server logs."
738
+ ),
739
+ }
740
+
741
+
742
+ async def create_or_capture(
743
+ model_cls: type,
744
+ payload: dict[str, Any],
745
+ *,
746
+ pk_field: str = "id",
747
+ request_id: str | None = None,
748
+ user_id: str | None = None,
749
+ conversation_id: str | None = None,
750
+ database: str | None = None,
751
+ ) -> Any:
752
+ """Create ONE row, and never lose its payload if the write fails.
753
+
754
+ THE HOLE THIS CLOSES: a Session-queued write that fails is captured to
755
+ ``system_write_failure`` and replayed automatically. A DIRECT write
756
+ (``Model.create_item(...)``) had no such tier — when it raised, the payload
757
+ simply evaporated. That is fine for a row we can trivially recompute and
758
+ catastrophic for the ONE class of write this function exists for: **the
759
+ durable home of an artifact that was already generated and already paid
760
+ for** — an agent's structured output, a provider's response, a rendered
761
+ document. Losing that write does not just fail the request, it destroys
762
+ work the user was billed for and forces them to buy it again (D183).
763
+
764
+ The contract, in order:
765
+
766
+ 1. **Stable identity first.** The PK is minted here when absent, so the
767
+ same id spans the attempt, the verification, and any later replay.
768
+ 2. **Fire.** ``model_cls.create_item(**payload)``.
769
+ 3. **Verify on failure.** A statement timeout can fire AFTER the row
770
+ committed, so a failed attempt is not proof of a missing row. Re-read by
771
+ PK on a fresh read before declaring anything lost; if it is there, the
772
+ write SUCCEEDED and we return it (loudly — a recovery firing means a
773
+ real defect got past the proactive layer).
774
+ 4. **Preserve.** Otherwise capture the intended INSERT into
775
+ ``system_write_failure`` stamped ``DirectWritePreserved``, so the
776
+ existing auto-replay loop re-applies it with no human in the loop.
777
+ 5. **Tell the truth.** Raise :class:`WritePreservedError` carrying the
778
+ handle, so the caller's failure record says "your work survived" instead
779
+ of only "it broke".
780
+
781
+ This is a KEEP-THE-WORK primitive, not a general write path: reach for it
782
+ where a lost row means lost money or lost human effort. Ordinary writes
783
+ stay on the Session/Coordinator path, which already has this guarantee.
784
+ """
785
+ from matrx_orm.session.op import make_insert
786
+
787
+ row = dict(payload)
788
+ if not row.get(pk_field):
789
+ row[pk_field] = str(_uuid.uuid4())
790
+ pk_value = row[pk_field]
791
+ table = f"{getattr(model_cls._meta, 'schema', 'public')}.{model_cls._meta.table_name}"
792
+
793
+ try:
794
+ return await model_cls.create_item(**row)
795
+ except Exception as exc:
796
+ # (3) The attempt failing is NOT proof the row is missing.
797
+ try:
798
+ landed = await model_cls.filter(**{pk_field: pk_value}).limit(1).all()
799
+ except Exception:
800
+ landed = []
801
+ if landed:
802
+ vcprint(
803
+ f"[matrx_orm.create_or_capture] {table} {pk_value} raised "
804
+ f"{type(exc).__name__} but the row IS committed — the write "
805
+ f"succeeded and the error was the acknowledgement, not the write. "
806
+ f"Investigate: this means a client gave up on a landed write.",
807
+ color="yellow",
808
+ )
809
+ logger.warning("create_or_capture_landed_despite_error", exc_info=exc)
810
+ return landed[0]
811
+
812
+ # (4) Preserve the payload where the replay loop will find it.
813
+ op = make_insert(model_cls, row, pk_value=pk_value)
814
+ op_ids = await record_failures(
815
+ [op],
816
+ exc,
817
+ request_id=request_id,
818
+ user_id=user_id,
819
+ conversation_id=conversation_id,
820
+ database=database,
821
+ error_prefix=DIRECT_WRITE_PRESERVED_MARKER,
822
+ )
823
+ op_id = op_ids[0] if op_ids else None
824
+ vcprint(
825
+ f"[matrx_orm.create_or_capture] {table} {pk_value} FAILED "
826
+ f"({type(exc).__name__}) — payload preserved as system_write_failure "
827
+ f"op {op_id}; the auto-replay loop will re-apply it.",
828
+ color="yellow",
829
+ )
830
+ raise WritePreservedError(
831
+ f"{table} write failed ({type(exc).__name__}: {exc}); the payload was "
832
+ f"preserved for automatic replay",
833
+ table=table,
834
+ pk_value=pk_value,
835
+ op_id=op_id,
836
+ captured=op_id is not None,
837
+ cause=exc,
838
+ ) from exc
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.65"
3
+ version = "3.1.67"
4
4
  description = "Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -86,7 +86,7 @@ def test_correlated_subquery_aggregate_params_shift_past_outer_params() -> None:
86
86
  sql, params = QueryExecutor(query._build_query())._build_query()
87
87
 
88
88
  assert "COALESCE(SUM(amount), $1)" in sql
89
- assert "id = parents.id" in sql
89
+ assert "id = public.parents.id" in sql
90
90
  assert "kind = $2" in sql
91
91
  assert "WHERE kind = $3" in sql
92
92
  assert params == [0, "child", "parent"]
@@ -137,7 +137,7 @@ def test_exists_in_filter_now_works_after_list_fix(_dummy_adapter):
137
137
  Site, Run = _models()
138
138
  # Before the fix this raised AttributeError (.items() on a list).
139
139
  sql = _sql(Site.filter(Exists(Run.filter(site_id=OuterRef("id")))))
140
- assert "EXISTS (SELECT 1 FROM runs WHERE site_id = sites.id)" in sql
140
+ assert "EXISTS (SELECT 1 FROM public.runs WHERE site_id = public.sites.id)" in sql
141
141
  finally:
142
142
  ModelRegistry.clear()
143
143
 
@@ -151,7 +151,10 @@ def test_correlated_count_subquery_in_annotate(_dummy_adapter):
151
151
  inner = Run.filter(site_id=OuterRef("id")).annotate(c=Count())
152
152
  sql = _sql(Site.filter().annotate(run_count=Subquery(inner)))
153
153
  # The correlated scalar rollup: (SELECT COUNT(*) FROM runs WHERE ...) AS run_count
154
- assert "(SELECT COUNT(*) FROM runs WHERE site_id = sites.id) AS run_count" in sql
154
+ assert (
155
+ "(SELECT COUNT(*) FROM public.runs WHERE site_id = public.sites.id) AS run_count"
156
+ in sql
157
+ )
155
158
  finally:
156
159
  ModelRegistry.clear()
157
160
 
@@ -310,7 +310,7 @@ class TestBaseManagerAggregate:
310
310
  assert "SELECT *" not in sql.split(" FROM ")[0]
311
311
  assert "SUM(amount) AS total" in sql
312
312
  assert "COUNT(id) AS n" in sql
313
- assert "FROM orders" in sql
313
+ assert "FROM public.orders" in sql
314
314
  assert "WHERE status = $1" in sql
315
315
  assert params == ["paid"]
316
316
 
@@ -95,6 +95,17 @@ class TestStrictGuard:
95
95
  M._STRICT_TABLES.add("public.cx_message")
96
96
  M.assert_managed_write_allowed("WITH t AS (...) UPDATE cx_message ...") # no raise
97
97
 
98
+ def test_public_model_sql_is_schema_qualified_at_the_source(self):
99
+ model = _make_model(
100
+ "PublicDiagnostic",
101
+ {"id": UUIDField(primary_key=True), "_table_name": "diagnostic_sink"},
102
+ )
103
+ assert model._meta.qualified_table_name == "public.diagnostic_sink"
104
+ M._STRICT_TABLES.add("chat.message")
105
+ M.assert_managed_write_allowed(
106
+ f"INSERT INTO {model._meta.qualified_table_name} (id) VALUES ($1)"
107
+ )
108
+
98
109
 
99
110
  class TestDeclareManagedRegistersStrictTable:
100
111
  def test_strict_declaration_registers_table(self):
@@ -32,7 +32,7 @@ class TestModelOptions:
32
32
  unique_together=[],
33
33
  constraints=[],
34
34
  )
35
- assert opts.qualified_table_name == "my_table"
35
+ assert opts.qualified_table_name == "public.my_table"
36
36
 
37
37
  def test_qualified_table_name_with_schema(self):
38
38
  opts = ModelOptions(
@@ -110,7 +110,7 @@ class TestBuildQuery:
110
110
  qb = QueryBuilder(M).filter(name="Alice").limit(10)
111
111
  q = qb._build_query()
112
112
  assert q["model"] is M
113
- assert q["table"] == "qb_table"
113
+ assert q["table"] == "public.qb_table"
114
114
  # filters is now a list of dicts/Q objects — find the dict that contains "name"
115
115
  filter_items = q["filters"]
116
116
  combined = {}
@@ -100,13 +100,13 @@ async def test_set_based_backfills_render_model_owned_statements(
100
100
  ) == 1
101
101
 
102
102
  assert "ensure_personal_organization(target.owner_id)" in statements[0]
103
- assert "UPDATE mentions AS target" in statements[1]
104
- assert "FROM chunks AS source" in statements[1]
103
+ assert "UPDATE public.mentions AS target" in statements[1]
104
+ assert "FROM public.chunks AS source" in statements[1]
105
105
  dominant = statements[2]
106
- assert "JOIN chunks AS authority" in dominant
106
+ assert "JOIN public.chunks AS authority" in dominant
107
107
  assert "authority.organization_id AS value" in dominant
108
108
  assert "ORDER BY n DESC, value ASC" in dominant
109
- assert "UPDATE entities AS target" in dominant
109
+ assert "UPDATE public.entities AS target" in dominant
110
110
 
111
111
 
112
112
  @pytest.mark.asyncio
@@ -79,7 +79,8 @@ def test_join_accepts_composed_typed_predicate() -> None:
79
79
  sql, params = executor.query, executor.params
80
80
 
81
81
  assert (
82
- "JOIN joined ON (public.base.id = public.joined.base_id AND public.joined.active IS TRUE)"
82
+ "JOIN public.joined ON (public.base.id = public.joined.base_id "
83
+ "AND public.joined.active IS TRUE)"
83
84
  ) in sql
84
85
  assert params == []
85
86
 
@@ -104,7 +104,10 @@ def test_exists_is_composable_and_outer_ref_is_correlated() -> None:
104
104
  sql, params = _sql(Parent.filter(Is(F("enabled"), True) | ~child_exists))
105
105
 
106
106
  assert "enabled IS TRUE" in sql
107
- assert "NOT (EXISTS (SELECT 1 FROM children WHERE parent_id = parents.id))" in sql
107
+ assert (
108
+ "NOT (EXISTS (SELECT 1 FROM public.children "
109
+ "WHERE parent_id = public.parents.id))"
110
+ ) in sql
108
111
  assert params == []
109
112
 
110
113
 
@@ -129,9 +132,9 @@ def test_nested_exists_correlates_to_immediately_enclosing_query() -> None:
129
132
 
130
133
  sql, _ = _sql(Parent.filter(nested))
131
134
 
132
- assert "parent_id = parents.id" in sql
135
+ assert "parent_id = public.parents.id" in sql
133
136
  assert "child_id = c.id" in sql
134
- assert "child_id = parents.id" not in sql
137
+ assert "child_id = public.parents.id" not in sql
135
138
 
136
139
 
137
140
  def test_nested_exists_shifts_parameters_at_every_query_level() -> None:
@@ -249,10 +252,10 @@ def test_outer_ref_is_preserved_through_cast_json_path_and_function() -> None:
249
252
 
250
253
  sql, params = _sql(Parent.filter(nested))
251
254
 
252
- assert "CAST(parents.id AS text) = parent_id" in sql
253
- assert "LOWER(parents.name) = LOWER(id)" in sql
254
- assert "((parents.metadata -> $1) ->> $2) IS NOT NULL" in sql
255
- assert "public.can_read_parent(parents.id, id) = $3" in sql
255
+ assert "CAST(public.parents.id AS text) = parent_id" in sql
256
+ assert "LOWER(public.parents.name) = LOWER(id)" in sql
257
+ assert "((public.parents.metadata -> $1) ->> $2) IS NOT NULL" in sql
258
+ assert "public.can_read_parent(public.parents.id, id) = $3" in sql
256
259
  assert params == ["source", "id", True]
257
260
 
258
261
 
File without changes
File without changes
File without changes
File without changes
File without changes