matrx-orm 3.1.73__tar.gz → 3.1.75__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 (339) hide show
  1. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/.gitignore +2 -0
  2. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/CLAUDE.md +2 -2
  3. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/PKG-INFO +2 -2
  4. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/__init__.py +11 -0
  5. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/catalog.py +46 -0
  6. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/FEATURE.md +4 -0
  7. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/async_db_manager.py +44 -0
  8. matrx_orm-3.1.75/matrx_orm/core/pooler_role.py +290 -0
  9. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/exceptions.py +62 -2
  10. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/update.py +128 -23
  11. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/columns.py +106 -3
  12. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/package_wiring.py +5 -2
  13. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/README.md +6 -1
  14. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/lifecycle.py +61 -22
  15. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/pyproject.toml +2 -2
  16. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_catalog.py +17 -0
  17. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_optimistic_concurrency.py +197 -0
  18. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_watched_lifecycle.py +43 -0
  19. matrx_orm-3.1.75/tests/live_pooler_role.py +104 -0
  20. matrx_orm-3.1.75/tests/schema_builder/test_array_default_parsing.py +56 -0
  21. matrx_orm-3.1.75/tests/test_pooler_role_guard.py +142 -0
  22. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/AGENTS.md +0 -0
  23. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/BASE_CLASS_METHODS.md +0 -0
  24. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/MODEL_API.md +0 -0
  25. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/README.md +0 -0
  26. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/database/orm/extended/managers/ai_model_base.py +0 -0
  27. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/docs/migrations.md +0 -0
  28. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/MODULE_README.md +0 -0
  29. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/adapters/MODULE_README.md +0 -0
  30. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/adapters/__init__.py +0 -0
  31. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/adapters/async_postgresql.py +0 -0
  32. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/adapters/base_adapter.py +0 -0
  33. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/adapters/postgresql.py +0 -0
  34. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  35. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/adapters/supabase_adapter.py +0 -0
  36. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/admin/__init__.py +0 -0
  37. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/admin/router.py +0 -0
  38. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/api/__init__.py +0 -0
  39. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/api/auth.py +0 -0
  40. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/api/config.py +0 -0
  41. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/api/handlers.py +0 -0
  42. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/api/protocol.py +0 -0
  43. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/api/server.py +0 -0
  44. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/associations.py +0 -0
  45. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/cache_debug.py +0 -0
  46. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/catalog_sql.py +0 -0
  47. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/client/MODULE_README.md +0 -0
  48. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/client/__init__.py +0 -0
  49. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/client/postgres_connection.py +0 -0
  50. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/client/postgrest.py +0 -0
  51. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/client/supabase_auth.py +0 -0
  52. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/client/supabase_config.py +0 -0
  53. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/client/supabase_manager.py +0 -0
  54. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  55. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/MODULE_README.md +0 -0
  56. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  57. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/TASKS.md +0 -0
  58. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/__init__.py +0 -0
  59. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/base.py +0 -0
  60. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/config.py +0 -0
  61. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/connections.py +0 -0
  62. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/db_function.py +0 -0
  63. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/diagnostics.py +0 -0
  64. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/expressions.py +0 -0
  65. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/extended.py +0 -0
  66. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/fields.py +0 -0
  67. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/introspection.py +0 -0
  68. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/listen.py +0 -0
  69. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/loop_filters.py +0 -0
  70. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/model_dto.py +0 -0
  71. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/model_view.py +0 -0
  72. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/one_database.py +0 -0
  73. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/paginator.py +0 -0
  74. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/pool_maintenance.py +0 -0
  75. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/pool_watch.py +0 -0
  76. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/pydantic_bridge.py +0 -0
  77. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/registry.py +0 -0
  78. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/relations.py +0 -0
  79. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/resilience.py +0 -0
  80. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/rls_session.py +0 -0
  81. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/signals.py +0 -0
  82. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/tls.py +0 -0
  83. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/transaction.py +0 -0
  84. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/types.py +0 -0
  85. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/core/write_queue.py +0 -0
  86. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/entity.py +0 -0
  87. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/error_handling.py +0 -0
  88. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/extended/__init__.py +0 -0
  89. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/extended/app_error_handler.py +0 -0
  90. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/identity_rule.py +0 -0
  91. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/middleware/__init__.py +0 -0
  92. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/middleware/base.py +0 -0
  93. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/MODULE_README.md +0 -0
  94. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/__init__.py +0 -0
  95. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/cli.py +0 -0
  96. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/ddl.py +0 -0
  97. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/diff.py +0 -0
  98. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/executor.py +0 -0
  99. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/integration.py +0 -0
  100. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/loader.py +0 -0
  101. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/operations.py +0 -0
  102. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/state.py +0 -0
  103. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/migrations/table_filter.py +0 -0
  104. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/MODULE_README.md +0 -0
  105. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/__init__.py +0 -0
  106. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/bulk_update_values.py +0 -0
  107. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/conflict.py +0 -0
  108. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/conflict_writes.py +0 -0
  109. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/create.py +0 -0
  110. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/db_functions.py +0 -0
  111. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/delete.py +0 -0
  112. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/dynamic_admin.py +0 -0
  113. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/dynamic_crud.py +0 -0
  114. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/graph_walk.py +0 -0
  115. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/latest_rows.py +0 -0
  116. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/queue_claim.py +0 -0
  117. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/read.py +0 -0
  118. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/relational_backfill.py +0 -0
  119. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/operations/staging_load.py +0 -0
  120. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/platform_db.py +0 -0
  121. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/py.typed +0 -0
  122. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/python_sql/MODULE_README.md +0 -0
  123. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/python_sql/__init__.py +0 -0
  124. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/python_sql/db_objects.py +0 -0
  125. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  126. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  127. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/query/FEATURE.md +0 -0
  128. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/query/MODULE_README.md +0 -0
  129. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/query/__init__.py +0 -0
  130. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/query/builder.py +0 -0
  131. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/query/executor.py +0 -0
  132. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/FEATURE.md +0 -0
  133. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  134. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/__init__.py +0 -0
  135. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/code_handler.py +0 -0
  136. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  137. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/common.py +0 -0
  138. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/diff_preview.py +0 -0
  139. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  140. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/generator.py +0 -0
  141. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  142. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  143. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  144. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  145. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/relationships.py +0 -0
  146. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/runner.py +0 -0
  147. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/schema.py +0 -0
  148. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/schema_manager.py +0 -0
  149. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/tables.py +0 -0
  150. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/schema_builder/views.py +0 -0
  151. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  152. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/__init__.py +0 -0
  153. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/attachments.py +0 -0
  154. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/crypto.py +0 -0
  155. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/items.py +0 -0
  156. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/model.py +0 -0
  157. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/org_service.py +0 -0
  158. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/service.py +0 -0
  159. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/__init__.py +0 -0
  160. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/coalesce.py +0 -0
  161. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/dag.py +0 -0
  162. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/errors.py +0 -0
  163. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/fallback.py +0 -0
  164. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/flush.py +0 -0
  165. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/managed.py +0 -0
  166. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/op.py +0 -0
  167. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/reads.py +0 -0
  168. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/session.py +0 -0
  169. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/session/telemetry.py +0 -0
  170. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  171. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/sql_executor/__init__.py +0 -0
  172. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/sql_executor/executor.py +0 -0
  173. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/sql_executor/queries.py +0 -0
  174. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/sql_executor/registry.py +0 -0
  175. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/sql_executor/types.py +0 -0
  176. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/sql_executor/utils.py +0 -0
  177. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/state.py +0 -0
  178. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/utils/__init__.py +0 -0
  179. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/utils/sql_utils.py +0 -0
  180. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/matrx_orm/utils/type_converters.py +0 -0
  181. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/scripts/git-branches.sh +0 -0
  182. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/scripts/publish.sh +0 -0
  183. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/scripts/release.sh +0 -0
  184. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/MODULE_README.md +0 -0
  185. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/__init__.py +0 -0
  186. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/conftest.py +0 -0
  187. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/MODULE_README.md +0 -0
  188. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/__init__.py +0 -0
  189. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_admin_db_columns.py +0 -0
  190. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_admin_search_negation.py +0 -0
  191. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_admin_search_sentinels.py +0 -0
  192. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_advisory_lock.py +0 -0
  193. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_agent_message.py +0 -0
  194. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_api_auth.py +0 -0
  195. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_api_config.py +0 -0
  196. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_api_handlers.py +0 -0
  197. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_api_protocol.py +0 -0
  198. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_array_agg_order_by.py +0 -0
  199. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_associations.py +0 -0
  200. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_bulk_hydration_offload.py +0 -0
  201. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_bulk_update_by_pk.py +0 -0
  202. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  203. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_cache_debug.py +0 -0
  204. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_call_function.py +0 -0
  205. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_case_when.py +0 -0
  206. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_composite_pk_filter.py +0 -0
  207. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_composite_pk_write_paths.py +0 -0
  208. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_config.py +0 -0
  209. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_conflict_target.py +0 -0
  210. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_conflict_writes.py +0 -0
  211. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_connection_codecs.py +0 -0
  212. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_connection_poison_guard.py +0 -0
  213. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_connection_primitives.py +0 -0
  214. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_count_composite_distinct.py +0 -0
  215. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_db_function_field.py +0 -0
  216. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_ddl_generator.py +0 -0
  217. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_derived_table.py +0 -0
  218. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_dirty_tracking.py +0 -0
  219. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_dynamic_crud.py +0 -0
  220. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_exceptions.py +0 -0
  221. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  222. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_expression_primitives.py +0 -0
  223. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_fields.py +0 -0
  224. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  225. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_from_alias.py +0 -0
  226. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_fts.py +0 -0
  227. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_func_expression_args.py +0 -0
  228. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_graph_walk.py +0 -0
  229. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_having_annotate_alias.py +0 -0
  230. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  231. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_introspect_rls_policies.py +0 -0
  232. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_join_and_aggregate.py +0 -0
  233. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_latest_rows.py +0 -0
  234. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_listen_notify.py +0 -0
  235. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_managed_write_guard.py +0 -0
  236. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_matrx_entity.py +0 -0
  237. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_migration_diff_types.py +0 -0
  238. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_migration_loader.py +0 -0
  239. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_model_instance.py +0 -0
  240. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_model_meta.py +0 -0
  241. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_order_by_expression.py +0 -0
  242. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_parameter_name_hydration.py +0 -0
  243. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_patch_jsonb_path.py +0 -0
  244. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_pool_holder_census.py +0 -0
  245. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_pool_loop_recreation.py +0 -0
  246. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_pool_maintenance.py +0 -0
  247. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_postgrest_filters.py +0 -0
  248. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_pydantic_bridge.py +0 -0
  249. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_query_builder.py +0 -0
  250. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_query_error_detail.py +0 -0
  251. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_query_executor_sql.py +0 -0
  252. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_query_timeout.py +0 -0
  253. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  254. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_queue_claim.py +0 -0
  255. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_registry.py +0 -0
  256. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_registry_multi_database.py +0 -0
  257. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_registry_multi_schema.py +0 -0
  258. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_relational_backfill.py +0 -0
  259. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_relations.py +0 -0
  260. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_resilience.py +0 -0
  261. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_rls_session_scope.py +0 -0
  262. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_schema_exists.py +0 -0
  263. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_session_advisory_lock.py +0 -0
  264. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_state_cache.py +0 -0
  265. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_subquery_filter_raw.py +0 -0
  266. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_subquery_in_filter.py +0 -0
  267. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_supabase_auth.py +0 -0
  268. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_supabase_config.py +0 -0
  269. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_tls.py +0 -0
  270. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_typed_join_projection.py +0 -0
  271. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_typed_predicates.py +0 -0
  272. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_update_case_expression.py +0 -0
  273. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_update_subquery.py +0 -0
  274. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_update_with_rebase.py +0 -0
  275. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_upsert_default_leak.py +0 -0
  276. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_upsert_with_conflict.py +0 -0
  277. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_values_jsonb_decode.py +0 -0
  278. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level1/test_write_retry_semantics.py +0 -0
  279. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/MODULE_README.md +0 -0
  280. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/__init__.py +0 -0
  281. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/conftest.py +0 -0
  282. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_bulk_ops.py +0 -0
  283. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_cache_integration.py +0 -0
  284. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_crud.py +0 -0
  285. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_foreign_keys.py +0 -0
  286. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_m2m.py +0 -0
  287. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_manager.py +0 -0
  288. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_migrations_live.py +0 -0
  289. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_query_execution.py +0 -0
  290. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/level2/test_schema_diff.py +0 -0
  291. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project/.env.example +0 -0
  292. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project/README.md +0 -0
  293. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project/__init__.py +0 -0
  294. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project/generate.py +0 -0
  295. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project/generated/.gitkeep +0 -0
  296. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project/matrx_orm.yaml +0 -0
  297. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project/test_schema_generation.py +0 -0
  298. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project_desktop/.env.example +0 -0
  299. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project_desktop/README.md +0 -0
  300. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project_desktop/__init__.py +0 -0
  301. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project_desktop/client_example.py +0 -0
  302. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project_desktop/client_example.ts +0 -0
  303. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  304. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/sample_project_desktop/server.py +0 -0
  305. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/entity_tests.py +0 -0
  306. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_base_generation.py +0 -0
  307. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  308. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_composite_unique_generation.py +0 -0
  309. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_dto_identity_generation.py +0 -0
  310. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_entity_capability_generation.py +0 -0
  311. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_generate_schema.py +0 -0
  312. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_junction_analysis.py +0 -0
  313. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_multi_schema_output.py +0 -0
  314. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_sql_expression_defaults.py +0 -0
  315. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_table_relationships_query.py +0 -0
  316. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema/test_view_generation.py +0 -0
  317. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  318. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  319. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/__init__.py +0 -0
  320. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/conftest.py +0 -0
  321. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_cache_no_downgrade.py +0 -0
  322. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_capture_poison_proof.py +0 -0
  323. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_coalesce.py +0 -0
  324. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_dag.py +0 -0
  325. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_disk_spill_fallback.py +0 -0
  326. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_flush_individual_fallback.py +0 -0
  327. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_governed_write.py +0 -0
  328. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_managed.py +0 -0
  329. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_op_enqueue_site.py +0 -0
  330. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_reads.py +0 -0
  331. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/session/test_session.py +0 -0
  332. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/test_database_name_alias.py +0 -0
  333. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/test_db_functions.py +0 -0
  334. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/test_model_cls_refactor.py +0 -0
  335. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  336. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/test_one_database.py +0 -0
  337. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/test_secrets_battery.py +0 -0
  338. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/test_sql_param_casts.py +0 -0
  339. {matrx_orm-3.1.73 → matrx_orm-3.1.75}/tests/test_sync_twins_guard.py +0 -0
@@ -248,6 +248,7 @@ requirements_filtered.txt
248
248
 
249
249
  # Claude Code local worktrees and per-user settings
250
250
  .claude/worktrees/
251
+ .worktrees/
251
252
  .claude/settings.local.json
252
253
 
253
254
  # Append-only snapshots from matrx_utils.update_history (unbounded; do not commit)
@@ -283,3 +284,4 @@ db/evidence/
283
284
 
284
285
  # Pooler contamination evidence — machine-local forensic capture, not source
285
286
  db/evidence/
287
+ db/mirror/.env.mirror
@@ -23,7 +23,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
23
23
  - 🚨 **ONE CONNECTION, REQUIRED** (`matrx_orm.platform_db`): `register_platform_db(name, ...)` + `platform_connection_url()` — the ONLY sanctioned way any Matrx service or package turns env vars into a pool. One set of variables (`SUPABASE_MATRIX_*`), owned here; TLS is a deployment input (`SUPABASE_MATRIX_SSL`), never hardcoded; **RAISES** if it isn't given a database — no degraded mode. Every package's `bootstrap_db()` is one call to it. A package may absolutely run on its OWN Postgres — that is a change of VALUES, never a new variable name; what it may never have is a *second candidate* or a fallback chain. `register_database_from_env` is the low-level primitive underneath — **application and package code must not call it directly** (guard: `scripts/check_one_database.py`, blocking in release).
24
24
  - **Core model layer**: `Model`, `BaseManager`, `BaseDTO`, `ModelView`, `model_registry`, plus 50+ field types (`CharField`, `IntegerField`, `ForeignKey`, `ManyToManyField`, `JSONField`, …).
25
25
  - **`MatrxEntity`** (`matrx_orm/entity.py`) — **the platform entity contract as an ORM primitive.** A table registered in `platform.entity_types` generates as a `MatrxEntity` (everything else stays a plain `Model`) and gets the behavior instead of every consumer hand-wiring it: `await page.versions()` (not a query on `history.row_versions`), `await page.restore(3)` (not a `version_restore` RPC + a cache bust — `restore()` busts the cache itself), `Note.alive()` / `.dead()` / `.soft_delete()` / `.undelete()`, and `Page.capabilities()`. Capability flags (`_entity_token`, `_is_versioned`, `_has_soft_delete`, `_is_org_scoped`, `_rls_variant`) are **stamped by the generator from the LIVE DATABASE** (does the table really carry the `_version_capture` trigger? a `deleted_at` column?) — never from `entity_types`' own flags, which are wrong on 61 of its 242 rows. **The database is truth; a flag is a claim.** Using a capability an entity does not declare RAISES (`EntityCapabilityError`) and names the fix — it never returns silently-wrong data, and `is_deleted` refuses to answer from a column a partial fetch never loaded. Drift guard: `scripts/check_entity_drift.py` (loud, non-blocking, in `release.sh`).
26
- - **Optimistic concurrency (`expected_version=`)** — opt-in compare-and-swap on the canonical `version` int column: `update_where` / `update_item` / instance `update()` take `expected_version=N` (`WHERE version = N`, `SET version = N + 1`; 0 rows → `DoesNotExist` if gone, `OptimisticLockError` with `current_version` + `current_row` if changed); `bulk_update(..., check_version=True)` guards per row; `MatrxEntity.update_with_rebase` auto-rebases past DISJOINT concurrent writes using `history.row_versions` snapshots. **Never automatic** — an unguarded write stays last-write-wins (the `_touch_row` trigger bumps `version` on every update, so auto-guarding would raise spurious conflicts). Contract: `common-docs/systems/optimistic-concurrency/FEATURE.md`.
26
+ - **Optimistic concurrency (`expected_version=`)** — opt-in compare-and-swap on the canonical `version` int column: `update_where` / `update_item` / instance `update()` take `expected_version=N` (`WHERE version = N`, `SET version = N + 1`; 0 rows → `DoesNotExist` if gone, `OptimisticLockError` if changed — carrying the **decision package**: `current_version`, `current_row`, `contested_fields` (current values of ONLY the fields this write attempted), and `changed_by`/`changed_at` from `history.row_versions` where versioned); `bulk_update(..., check_version=True)` guards per row; `MatrxEntity.update_with_rebase` auto-rebases past DISJOINT concurrent writes using `history.row_versions` snapshots. **Never automatic** — an unguarded write stays last-write-wins (the `_touch_row` trigger bumps `version` on every update, so auto-guarding would raise spurious conflicts). Contract: `common-docs/systems/optimistic-concurrency/FEATURE.md`.
27
27
  - **Change tracking** — every instance records which columns were **loaded** (a SELECTed row, or the caller's kwargs) and which were **changed**. `save()` writes only those, so `.only("id","email")` + `save()` can never NULL the columns it never fetched (it used to — silent data loss). Mutable containers (jsonb/arrays) are always rewritten when loaded, because `obj.metadata["k"]=1` cannot be seen by `__setattr__`; **reassigning** (`obj.metadata = {...}`) is tracked precisely and is the sanctioned way. `Model._hydrate()` is the ONE row→model entry point.
28
28
  - **Upsert writes only what the caller supplied.** `upsert` / `bulk_upsert` / `upsert_with_conflict` materialize every field's Python default so the **INSERT** half is complete — but `ON CONFLICT DO UPDATE SET` covers **only the columns present in the caller's `data`** (an explicit `update_fields` still wins). Letting defaults reach the UPDATE half silently RESET every defaulted column on each re-upsert of an existing row: it once reset scraped-and-analyzed `research.rs_source` rows to `scrape_status='pending', server_attempts=0` (a watchdog then finalized them `'failed'`) and undid users' manual `is_included=false`. A caller-supplied `None` is excluded too — it is dropped from the INSERT, and `EXCLUDED.<col>` on an omitted column resolves to that column's **DEFAULT**. An upsert of only conflict keys means "ensure this row exists" and reconciles to a no-op self-assignment, never a raise (a bare `DO NOTHING` would break `RETURNING *`). Pinned by `tests/level1/test_upsert_default_leak.py`.
29
29
  - **Read-only models** (`_read_only = True`): a model that can be queried but NEVER written through the ORM — every write path raises `ReadOnlyModelError` (enforced at `QueryExecutor.insert/bulk_insert/upsert/update/delete`, so it covers all of `create`/`save`/`update`/`delete`/`update_where`/`delete_where`/`upsert`/`bulk_*`). Used for DB-managed tables the app must not mutate. The canonical **`auth.users`** model is baked into the schema generator (`get_string_user_model`) as the full, curated Supabase auth schema (safe columns only — no password/token fields), `_read_only`, bound per-database. Read a user's profile/metadata there; mutate users only through Supabase Auth (GoTrue).
@@ -98,7 +98,7 @@ If the admin router ever needs auth beyond token-based, inject a `resolve_user`
98
98
 
99
99
  - Full type hints — the ORM's contract with callers is very type-heavy and IDE discoverability matters.
100
100
  - No docstrings except on public API (Model, QueryBuilder, field classes, migration entry points). One line.
101
- - Hot paths (`QueryBuilder.execute`, adapter fetches, dict→dataclass transforms) must stay allocation-light. Favor slotted dataclasses / `__slots__`.
101
+ - Hot paths (`QueryBuilder.execute`, adapter fetches, dict→dataclass transforms) must stay allocation-light. Favor mandated dataclasses / `__slots__`.
102
102
  - Explicit exception handling — never swallow `asyncpg` errors.
103
103
 
104
104
  ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matrx-orm
3
- Version: 3.1.73
3
+ Version: 3.1.75
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.15
28
+ Requires-Dist: matrx-utils>=2.0.16
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
@@ -203,6 +203,12 @@ from .core.pool_watch import (
203
203
  connection_holder_census,
204
204
  register_pool_saturation_sink,
205
205
  )
206
+ from .core.pooler_role import (
207
+ EmptyReadContaminationEvent,
208
+ PoolerRoleContamination,
209
+ SessionRoleChangeRefused,
210
+ register_contamination_sink,
211
+ )
206
212
  from .core.pydantic_bridge import (
207
213
  validate_input as validate_model_input,
208
214
  )
@@ -573,6 +579,11 @@ __all__ = [
573
579
  "PoolSaturationEvent",
574
580
  "connection_holder_census",
575
581
  "register_pool_saturation_sink",
582
+ # ── Pooler role contamination ──────────────────────────────────────────────
583
+ "EmptyReadContaminationEvent",
584
+ "PoolerRoleContamination",
585
+ "SessionRoleChangeRefused",
586
+ "register_contamination_sink",
576
587
  # ── Signals ────────────────────────────────────────────────────────────────
577
588
  "Signal",
578
589
  "pre_create",
@@ -26,6 +26,7 @@ __all__ = [
26
26
  "softdelete_unique_conflicts",
27
27
  "qualified_relation_names",
28
28
  "trigger_function_usage",
29
+ "trigger_column_coverage",
29
30
  "column_privileges",
30
31
  "entity_capability_state",
31
32
  ]
@@ -487,6 +488,51 @@ async def trigger_function_usage(
487
488
  )
488
489
 
489
490
 
491
+ async def trigger_column_coverage(
492
+ database: str,
493
+ *,
494
+ function_name: str,
495
+ column_names: list[str],
496
+ ) -> list[dict[str, Any]]:
497
+ """Return relations carrying any named column or the named trigger function."""
498
+ return await _query(
499
+ database,
500
+ """
501
+ WITH column_relations AS (
502
+ SELECT DISTINCT c.oid,
503
+ n.nspname AS schema,
504
+ c.relname AS table_name
505
+ FROM pg_attribute a
506
+ JOIN pg_class c ON c.oid = a.attrelid
507
+ JOIN pg_namespace n ON n.oid = c.relnamespace
508
+ WHERE a.attname = ANY($2::text[])
509
+ AND a.attnum > 0
510
+ AND NOT a.attisdropped
511
+ AND c.relkind = 'r'
512
+ ), trigger_relations AS (
513
+ SELECT DISTINCT t.tgrelid AS oid,
514
+ n.nspname AS schema,
515
+ c.relname AS table_name
516
+ FROM pg_trigger t
517
+ JOIN pg_class c ON c.oid = t.tgrelid
518
+ JOIN pg_namespace n ON n.oid = c.relnamespace
519
+ JOIN pg_proc p ON p.oid = t.tgfoid
520
+ WHERE NOT t.tgisinternal
521
+ AND p.proname = $1
522
+ )
523
+ SELECT COALESCE(cols.schema, triggers.schema) AS schema,
524
+ COALESCE(cols.table_name, triggers.table_name) AS table_name,
525
+ (cols.oid IS NOT NULL) AS has_columns,
526
+ (triggers.oid IS NOT NULL) AS has_trigger
527
+ FROM column_relations cols
528
+ FULL OUTER JOIN trigger_relations triggers ON cols.oid = triggers.oid
529
+ ORDER BY 1, 2
530
+ """,
531
+ function_name,
532
+ column_names,
533
+ )
534
+
535
+
490
536
  async def foreign_keys_to_relation(
491
537
  database: str,
492
538
  *,
@@ -89,6 +89,10 @@ Verified against code: 2026-08-15.
89
89
 
90
90
  ## Change log
91
91
 
92
+ - 2026-08-16: Kept the destructive transaction-pool contamination proof out of
93
+ the installable runtime package and isolated it under package tests; the
94
+ operator wrapper is SQL-free and runtime prose no longer resembles an
95
+ executable non-local role mutation.
92
96
  - 2026-08-15: Pinned transaction-local RLS role cleanup across success,
93
97
  exception, and early-return paths.
94
98
  - 2026-08-15: Added first-timeout pool holder census, host alarm seam, tracked raw-pool helper, and bounded cancellation-safe release.
@@ -18,6 +18,11 @@ from matrx_orm.core.config import (
18
18
  resolve_pool_mode,
19
19
  )
20
20
  from matrx_orm.error_handling import handle_orm_operation
21
+ from matrx_orm.core.pooler_role import (
22
+ VERIFY_EMPTY_READS,
23
+ assert_no_session_role_change,
24
+ reprove_empty_read,
25
+ )
21
26
  from matrx_orm.core.tls import resolve_asyncpg_ssl
22
27
  from matrx_orm.exceptions import (
23
28
  AdapterError,
@@ -327,6 +332,25 @@ class AsyncDatabaseManager:
327
332
  _keepalive_tasks: dict[str, asyncio.Task] = {} # per-config pool warmer
328
333
  _growth_locks: dict[str, asyncio.Lock] = {}
329
334
 
335
+ # Config names already resolved as "does an empty read need proving here?".
336
+ # Only a transaction-mode pool can be handed a server connection another
337
+ # client left wearing a role, so a direct Postgres pays nothing.
338
+ _empty_read_verification: dict[str, bool] = {}
339
+
340
+ @classmethod
341
+ def _verifies_empty_reads(cls, config_name: str) -> bool:
342
+ if not VERIFY_EMPTY_READS:
343
+ return False
344
+ cached = cls._empty_read_verification.get(config_name)
345
+ if cached is not None:
346
+ return cached
347
+ try:
348
+ verifies = resolve_pool_mode(get_database_config(config_name)) == "transaction"
349
+ except Exception:
350
+ verifies = False
351
+ cls._empty_read_verification[config_name] = verifies
352
+ return verifies
353
+
330
354
  @classmethod
331
355
  async def _replace_zero_connection_pool(cls, config_name: str, failed_pool: Any) -> bool:
332
356
  """Atomically discard a pool that has lost every physical connection."""
@@ -1025,6 +1049,11 @@ class AsyncDatabaseManager:
1025
1049
  ``.timeout()`` seam for known-long reads. ``None`` keeps the pool
1026
1050
  default.
1027
1051
  """
1052
+ # Layer 1 of the pooler-role defence: SQL that would leave a borrowed role
1053
+ # on a shared server connection never reaches Postgres. See
1054
+ # matrx_orm/core/pooler_role.py for what that contamination does to reads.
1055
+ assert_no_session_role_change(query)
1056
+
1028
1057
  # Reuse the active transaction connection when available
1029
1058
  try:
1030
1059
  from matrx_orm.core.transaction import get_active_connection
@@ -1124,6 +1153,21 @@ class AsyncDatabaseManager:
1124
1153
  read_attempt + 1,
1125
1154
  query[:120],
1126
1155
  )
1156
+ if not results and read_only and cls._verifies_empty_reads(config_name):
1157
+ # Layer 2: an empty result from a pooled connection is
1158
+ # unproven until a session that can state its own role
1159
+ # says so. A borrowed `authenticated` role returns ZERO
1160
+ # ROWS on an RLS table instead of raising.
1161
+ true_rows = await reprove_empty_read(
1162
+ conn,
1163
+ config_name=config_name,
1164
+ query=query,
1165
+ args=args,
1166
+ statement_timeout=statement_timeout,
1167
+ normalize=_normalize_record,
1168
+ )
1169
+ if true_rows is not None:
1170
+ return true_rows
1127
1171
  return [_normalize_record(r) for r in results]
1128
1172
  except asyncpg.exceptions.PostgresSyntaxError as e:
1129
1173
  raise QueryError(
@@ -0,0 +1,290 @@
1
+ """Pooler role contamination — the ORM's two layers against a SILENT empty read.
2
+
3
+ WHY THIS EXISTS (reproduced 2026-08-15, FOUND_DEFECTS "An ORM read returned ZERO
4
+ ROWS against a table holding 4,060"):
5
+
6
+ On a transaction-mode pooler (Supavisor / PgBouncer) routing is *per transaction*,
7
+ so consecutive statements from ONE client connection execute on DIFFERENT server
8
+ connections. A server connection that any client on the tenant left with a
9
+ non-LOCAL ``SET ROLE`` comes back out of the shared pool still wearing that role.
10
+ Our statement then runs as ``authenticated`` instead of ``postgres``.
11
+
12
+ **On an RLS table that GRANTs SELECT to ``authenticated``, that does not raise —
13
+ it returns ZERO ROWS.** Measured live against the platform pooler: a bare
14
+ ``SET ROLE authenticated`` issued on 8 connections made 22 of 40 subsequent ORM
15
+ reads of ``seo.backlink_observation`` (4,060 rows) come back empty, with no
16
+ exception, while an identity probe interleaved on the same pool reported
17
+ ``current_user=authenticated, session_user=postgres`` on 21 of 40 samples.
18
+
19
+ Silent emptiness is indistinguishable from "there is no data" for every sweep,
20
+ reconciler, digest and monitor in the platform: a broken read reports a quiet week
21
+ forever and every scheduled run stays green. ``db/pooler_session.py`` already says
22
+ it for the migration tools — **a privilege error must never be recorded as
23
+ absence** — and this module extends that to every ORM read, because a per-caller
24
+ defence does not scale.
25
+
26
+ TWO INDEPENDENT LAYERS, each sufficient alone, each screaming when it fires:
27
+
28
+ 1. **Prevention — the ORM refuses to emit a session-scoped role change.**
29
+ ``assert_no_session_role_change`` rejects a bare ``SET ROLE`` /
30
+ ``SET SESSION AUTHORIZATION`` / ``set_config('role', …, false)`` on every query
31
+ that goes through the ORM. This closes the one source class the 2026-08-14
32
+ investigation could never catch statically: SQL authored at RUNTIME — the
33
+ admin ``sql`` tool, ``db_admin``, registered ``SQLQuery`` objects, agent
34
+ scratch scripts. ``scripts/check_pooler_role_scope.py`` guards committed code;
35
+ this guards everything else. The sanctioned ways to change role remain
36
+ ``matrx_orm.rls_session`` and ``db/pooler_session.py::impersonate``.
37
+
38
+ 2. **Reconciliation — an empty read is re-proved on a session that can prove its
39
+ role.** A zero-row result from a read-only query on a transaction-pool config
40
+ is re-run inside an explicit transaction (pinned to ONE server connection)
41
+ that starts with ``RESET ROLE``. If the rows are really there, the caller gets
42
+ the TRUE rows and the lie is reported through every registered sink. If the
43
+ session cannot be made clean, it RAISES rather than handing back an absence.
44
+
45
+ Layer 2 follows the repo's guard doctrine: a guard that CAN reconcile MUST
46
+ reconcile — we hold the correct answer, so killing the caller's request would be
47
+ strictly worse than returning it loudly. It costs nothing on a non-empty read and
48
+ nothing at all on a direct (session-mode) Postgres connection, where cross-client
49
+ contamination cannot happen.
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import logging
55
+ import re
56
+ from collections.abc import Callable
57
+ from datetime import UTC, datetime
58
+ from typing import Any
59
+
60
+ from pydantic import BaseModel, ConfigDict
61
+
62
+ from matrx_utils import vcprint
63
+
64
+ logger = logging.getLogger("matrx_orm.pooler_role")
65
+
66
+ # Turning this off re-arms a silent-data-loss class platform-wide. It exists so a
67
+ # deployment on a genuinely unshared Postgres can skip the work, and so the cost
68
+ # is a code change (reviewable) rather than an env var (invisible).
69
+ VERIFY_EMPTY_READS = True
70
+
71
+ # How many times a re-proof may try to obtain a session running as our own login
72
+ # role before it gives up and raises. A contaminated server connection is repaired
73
+ # in-transaction, so one retry is almost always enough.
74
+ REPROVE_ATTEMPTS = 3
75
+
76
+ # A role change that is NOT scoped to a transaction. ``SET LOCAL ROLE`` and
77
+ # ``set_config('role', …, true)`` are safe and deliberately not matched.
78
+ _NON_LOCAL_SET_ROLE = re.compile(r"(?im)(?:^|;)\s*SET\s+(?!LOCAL\b)ROLE\b")
79
+ _NON_LOCAL_SET_AUTHORIZATION = re.compile(
80
+ r"(?im)(?:^|;)\s*SET\s+(?!LOCAL\b)SESSION\s+AUTHORIZATION\b"
81
+ )
82
+ _NON_LOCAL_SET_CONFIG_ROLE = re.compile(
83
+ r"(?is)set_config\s*\(\s*['\"]role['\"]\s*,.*?,\s*(?:false|'false'|\"false\")\s*\)"
84
+ )
85
+
86
+ # Cheap pre-filter so the overwhelming majority of queries pay a substring scan
87
+ # rather than three regexes. Deliberately loose — a false hint costs microseconds,
88
+ # a missed one costs the platform a silent empty read.
89
+ _ROLE_HINTS = ("role", "authorization", "set_config")
90
+
91
+
92
+ class SessionRoleChangeRefused(RuntimeError):
93
+ """A query tried to change the Postgres role for the whole session."""
94
+
95
+
96
+ class PoolerRoleContamination(RuntimeError):
97
+ """A statement could not be pinned to a session running as our own login role."""
98
+
99
+
100
+ class EmptyReadContaminationEvent(BaseModel):
101
+ """A read returned zero rows because the session was wearing a borrowed role."""
102
+
103
+ model_config = ConfigDict(extra="forbid", frozen=True)
104
+
105
+ config_name: str
106
+ query: str
107
+ contaminated_role: str
108
+ login_role: str
109
+ true_row_count: int
110
+ occurred_at: datetime
111
+
112
+
113
+ ContaminationSink = Callable[[EmptyReadContaminationEvent], None]
114
+
115
+ _sinks: list[ContaminationSink] = []
116
+
117
+
118
+ def register_contamination_sink(sink: ContaminationSink) -> None:
119
+ """Register a durable observer for proven silent-empty reads."""
120
+ if sink not in _sinks:
121
+ _sinks.append(sink)
122
+
123
+
124
+ def reset_contamination_sinks() -> None:
125
+ """Clear process state for isolated tests."""
126
+ _sinks.clear()
127
+
128
+
129
+ def assert_no_session_role_change(query: str) -> None:
130
+ """Refuse SQL that would leave a borrowed role on a pooled server connection.
131
+
132
+ Layer 1. Raises ``SessionRoleChangeRefused`` and names the sanctioned path;
133
+ it never rewrites the caller's SQL, because guessing which role a caller meant
134
+ is exactly the ambiguity a guard must not resolve on its own.
135
+ """
136
+ if not query:
137
+ return
138
+ lowered = query.lower()
139
+ if not any(hint in lowered for hint in _ROLE_HINTS):
140
+ return
141
+ if (
142
+ _NON_LOCAL_SET_ROLE.search(query)
143
+ or _NON_LOCAL_SET_AUTHORIZATION.search(query)
144
+ or _NON_LOCAL_SET_CONFIG_ROLE.search(query)
145
+ ):
146
+ vcprint(
147
+ "REFUSED: a session-scoped role change reached the ORM.\n"
148
+ f" query: {query[:300]}\n"
149
+ " On a transaction pooler this role rides the SHARED server connection "
150
+ "back into the pool, and the next client — any service on this tenant — "
151
+ "silently runs as that role. On an RLS table it does not error: it "
152
+ "returns ZERO ROWS, and absence gets recorded as fact.\n"
153
+ " Use `async with rls_session(claims, role=...)` (ORM) or "
154
+ "`impersonate(conn, role)` (db/pooler_session.py). Both use SET LOCAL "
155
+ "ROLE inside a transaction and cannot leak.",
156
+ color="red",
157
+ style="bold",
158
+ )
159
+ raise SessionRoleChangeRefused(
160
+ "A session-scoped role change (SET ROLE / SET SESSION AUTHORIZATION / "
161
+ "a non-local set_config role change) may never be executed through the ORM — it "
162
+ "contaminates the shared pooler and turns other services' reads into "
163
+ "silent empty results. Use rls_session(...) / impersonate(...), which "
164
+ "issue SET LOCAL ROLE inside a transaction."
165
+ )
166
+
167
+
168
+ async def emit_contamination(event: EmptyReadContaminationEvent) -> None:
169
+ """Scream about a proven silent-empty read, durably record it, fan out to sinks."""
170
+ vcprint(
171
+ "POOLER ROLE CONTAMINATION — an ORM read returned ZERO ROWS that were "
172
+ "really there.\n"
173
+ f" config: {event.config_name}\n"
174
+ f" session ran as: {event.contaminated_role} (login role "
175
+ f"{event.login_role})\n"
176
+ f" true row count on a clean session: {event.true_row_count}\n"
177
+ f" query: {event.query[:300]}\n"
178
+ " The caller has been given the TRUE rows. Something on this Postgres "
179
+ "tenant issued a non-LOCAL SET ROLE; every other client is exposed until "
180
+ "those server connections cycle.",
181
+ color="red",
182
+ style="bold",
183
+ )
184
+ logger.error(
185
+ "[pooler-role] silent empty read repaired on %s (ran as %s, %d true rows): %s",
186
+ event.config_name,
187
+ event.contaminated_role,
188
+ event.true_row_count,
189
+ event.query[:200],
190
+ )
191
+ # Durable by default — a host should not have to wire anything for the
192
+ # platform's loudest data-integrity alarm to survive the process.
193
+ try:
194
+ from matrx_orm.session.fallback import record_error
195
+
196
+ await record_error(
197
+ PoolerRoleContamination(
198
+ f"read returned 0 rows as '{event.contaminated_role}'; "
199
+ f"{event.true_row_count} rows were really there"
200
+ ),
201
+ kind="pooler_role_contamination",
202
+ source_app="matrx-orm",
203
+ route=f"orm:{event.config_name}",
204
+ payload={
205
+ "config_name": event.config_name,
206
+ "query": event.query[:2000],
207
+ "contaminated_role": event.contaminated_role,
208
+ "login_role": event.login_role,
209
+ "true_row_count": event.true_row_count,
210
+ },
211
+ )
212
+ except Exception:
213
+ logger.exception("[pooler-role] could not durably record the contamination event")
214
+
215
+ for sink in list(_sinks):
216
+ try:
217
+ sink(event)
218
+ except Exception:
219
+ logger.exception("[pooler-role] contamination sink raised")
220
+
221
+
222
+ async def reprove_empty_read(
223
+ conn: Any,
224
+ *,
225
+ config_name: str,
226
+ query: str,
227
+ args: tuple[Any, ...],
228
+ statement_timeout: float | None,
229
+ normalize: Callable[[Any], dict[str, Any]],
230
+ ) -> list[dict[str, Any]] | None:
231
+ """Re-run an empty read on a session proven to be running as US.
232
+
233
+ Layer 2. Returns the TRUE rows when the empty result was a lie, ``None`` when
234
+ the read was genuinely empty. Raises ``PoolerRoleContamination`` when a clean
235
+ session cannot be obtained at all — an unprovable absence is never a result.
236
+
237
+ ``conn`` must be a connection not currently inside a transaction; the explicit
238
+ transaction opened here is what pins every statement below to ONE server
239
+ connection, which is the only thing that makes ``RESET ROLE`` meaningful.
240
+ """
241
+ last_role: tuple[str, str] | None = None
242
+ for _ in range(REPROVE_ATTEMPTS):
243
+ async with conn.transaction():
244
+ await conn.execute("RESET ROLE")
245
+ identity = await conn.fetchrow("SELECT current_user, session_user")
246
+ current = identity["current_user"]
247
+ login = identity["session_user"]
248
+ if current != login:
249
+ last_role = (current, login)
250
+ continue
251
+ rows = await conn.fetch(query, *args, timeout=statement_timeout)
252
+ if not rows:
253
+ return None
254
+ true_rows = [normalize(r) for r in rows]
255
+
256
+ await emit_contamination(
257
+ EmptyReadContaminationEvent(
258
+ config_name=config_name,
259
+ query=query,
260
+ # The first read ran on a session we could not observe directly;
261
+ # what we can state is the role it was NOT running as.
262
+ contaminated_role="<not the login role>" if last_role is None else last_role[0],
263
+ login_role=login,
264
+ true_row_count=len(true_rows),
265
+ occurred_at=datetime.now(UTC),
266
+ )
267
+ )
268
+ return true_rows
269
+
270
+ current, login = last_role if last_role is not None else ("<unknown>", "<unknown>")
271
+ raise PoolerRoleContamination(
272
+ f"an empty read could not be proved: every attempt to pin a clean session "
273
+ f"landed as '{current}' (login role '{login}'). Anything this session could "
274
+ f"not see is a PRIVILEGE failure, not absence. Query: {query[:200]}"
275
+ )
276
+
277
+
278
+ __all__ = [
279
+ "ContaminationSink",
280
+ "EmptyReadContaminationEvent",
281
+ "PoolerRoleContamination",
282
+ "REPROVE_ATTEMPTS",
283
+ "SessionRoleChangeRefused",
284
+ "VERIFY_EMPTY_READS",
285
+ "assert_no_session_role_change",
286
+ "emit_contamination",
287
+ "register_contamination_sink",
288
+ "reprove_empty_read",
289
+ "reset_contamination_sinks",
290
+ ]
@@ -5,7 +5,10 @@ import ssl
5
5
  import traceback as _tb
6
6
  from collections.abc import Mapping
7
7
  from dataclasses import dataclass
8
+ from datetime import date, datetime, time
9
+ from decimal import Decimal
8
10
  from typing import Any
11
+ from uuid import UUID
9
12
 
10
13
  from matrx_utils import vcprint
11
14
 
@@ -1899,11 +1902,49 @@ class UnknownDatabaseError(ORMException):
1899
1902
  return f"{_RED}{body}{_RESET}"
1900
1903
 
1901
1904
 
1905
+ def jsonable(value: object) -> object:
1906
+ """Coerce a DB value into something JSON-serializable, losslessly where we can.
1907
+
1908
+ Error payloads cross the wire (the platform 409 `version_conflict` body is
1909
+ typed `JsonValue`), so a datetime / UUID / Decimal in a conflict's contested
1910
+ fields must not be what turns a conflict into a 500. Unknown types degrade
1911
+ to `str()` — a display value is always better than a crash inside an error
1912
+ handler.
1913
+ """
1914
+ if value is None or isinstance(value, bool | int | float | str):
1915
+ return value
1916
+ if isinstance(value, datetime | date | time):
1917
+ return value.isoformat()
1918
+ if isinstance(value, Decimal):
1919
+ return float(value)
1920
+ if isinstance(value, UUID):
1921
+ return str(value)
1922
+ if isinstance(value, Mapping):
1923
+ return {str(k): jsonable(v) for k, v in value.items()}
1924
+ if isinstance(value, list | tuple | set | frozenset):
1925
+ return [jsonable(v) for v in value]
1926
+ return str(value)
1927
+
1928
+
1902
1929
  class OptimisticLockError(ORMException):
1903
1930
  """Raised when an optimistic-lock version conflict is detected.
1904
1931
 
1905
- This means another process has updated the row since it was last fetched.
1906
- Re-fetch the record and retry the operation.
1932
+ THE LOSER GETS A DECISION, NOT AN ERROR (platform ruling 2026-08-10, item 3).
1933
+ A caller that lost a compare-and-swap already holds "proposed" (its own
1934
+ payload); this exception carries everything else it needs to RESOLVE rather
1935
+ than blindly retry:
1936
+
1937
+ ``expected_version`` — the revision the caller wrote against
1938
+ ``current_version`` — the revision that actually won
1939
+ ``contested_fields`` — {field: current value} for ONLY the fields this
1940
+ write attempted (never the whole row)
1941
+ ``changed_by`` / ``changed_at`` — who moved the row and when, from
1942
+ ``history.row_versions``; None on an unversioned
1943
+ entity or when the lookup found nothing
1944
+ ``current_row`` — the live instance, for a merge UI without a re-read
1945
+
1946
+ Resolution is just another write: keep-mine / keep-theirs / merged, resubmitted
1947
+ against ``current_version``. No queue, no resolution endpoint.
1907
1948
  """
1908
1949
 
1909
1950
  def __init__(
@@ -1914,13 +1955,29 @@ class OptimisticLockError(ORMException):
1914
1955
  message: str | None = None,
1915
1956
  current_version: int | None = None,
1916
1957
  current_row=None,
1958
+ contested_fields: Mapping[str, object] | None = None,
1959
+ changed_by: object = None,
1960
+ changed_at: object = None,
1917
1961
  ) -> None:
1918
1962
  msg = message or (
1919
1963
  f"Optimistic lock conflict on {getattr(model, '__name__', model)} "
1920
1964
  f"(pk={pk}, expected version={expected_version}"
1921
1965
  + (f", current version={current_version}" if current_version is not None else "")
1966
+ + (f", contested fields={sorted(contested_fields)}" if contested_fields else "")
1967
+ + (f", changed by {changed_by}" if changed_by else "")
1922
1968
  + "). The record was modified by another process — re-fetch and retry."
1923
1969
  )
1970
+ # Stored already-jsonable: this is a decision package for a human or a
1971
+ # wire payload, not a data-access path (that is `current_row`).
1972
+ self.contested_fields: dict[str, object] | None = (
1973
+ {str(k): jsonable(v) for k, v in contested_fields.items()}
1974
+ if contested_fields is not None
1975
+ else None
1976
+ )
1977
+ self.changed_by: str | None = str(changed_by) if changed_by is not None else None
1978
+ self.changed_at: str | None = (
1979
+ jsonable(changed_at) if changed_at is not None else None # type: ignore[assignment]
1980
+ )
1924
1981
  super().__init__(
1925
1982
  message=msg,
1926
1983
  model=model,
@@ -1928,6 +1985,9 @@ class OptimisticLockError(ORMException):
1928
1985
  "pk": pk,
1929
1986
  "expected_version": expected_version,
1930
1987
  "current_version": current_version,
1988
+ "contested_fields": self.contested_fields,
1989
+ "changed_by": self.changed_by,
1990
+ "changed_at": self.changed_at,
1931
1991
  },
1932
1992
  )
1933
1993
  self.expected_version = expected_version