matrx-orm 3.1.36__tar.gz → 3.1.38__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 (317) hide show
  1. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/CLAUDE.md +25 -0
  2. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/associations.py +34 -2
  4. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/update.py +8 -2
  5. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/query/builder.py +46 -4
  6. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/state.py +53 -0
  7. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/pyproject.toml +1 -1
  8. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_associations.py +39 -0
  9. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_cache_no_downgrade.py +39 -0
  10. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/.gitignore +0 -0
  11. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/AGENTS.md +0 -0
  12. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/BASE_CLASS_METHODS.md +0 -0
  13. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/MODEL_API.md +0 -0
  14. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/README.md +0 -0
  15. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/database/orm/extended/managers/ai_model_base.py +0 -0
  16. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/docs/migrations.md +0 -0
  17. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/MODULE_README.md +0 -0
  18. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/__init__.py +0 -0
  19. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/adapters/MODULE_README.md +0 -0
  20. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/adapters/__init__.py +0 -0
  21. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/adapters/async_postgresql.py +0 -0
  22. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/adapters/base_adapter.py +0 -0
  23. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/adapters/postgresql.py +0 -0
  24. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  25. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/adapters/supabase_adapter.py +0 -0
  26. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/admin/__init__.py +0 -0
  27. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/admin/router.py +0 -0
  28. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/api/__init__.py +0 -0
  29. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/api/auth.py +0 -0
  30. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/api/config.py +0 -0
  31. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/api/handlers.py +0 -0
  32. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/api/protocol.py +0 -0
  33. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/api/server.py +0 -0
  34. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/cache_debug.py +0 -0
  35. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/catalog.py +0 -0
  36. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/client/MODULE_README.md +0 -0
  37. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/client/__init__.py +0 -0
  38. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/client/postgres_connection.py +0 -0
  39. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/client/postgrest.py +0 -0
  40. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/client/supabase_auth.py +0 -0
  41. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/client/supabase_config.py +0 -0
  42. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/client/supabase_manager.py +0 -0
  43. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  44. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/MODULE_README.md +0 -0
  45. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  46. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/TASKS.md +0 -0
  47. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/__init__.py +0 -0
  48. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/async_db_manager.py +0 -0
  49. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/base.py +0 -0
  50. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/config.py +0 -0
  51. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/connections.py +0 -0
  52. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/db_function.py +0 -0
  53. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/diagnostics.py +0 -0
  54. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/expressions.py +0 -0
  55. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/extended.py +0 -0
  56. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/fields.py +0 -0
  57. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/introspection.py +0 -0
  58. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/listen.py +0 -0
  59. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/loop_filters.py +0 -0
  60. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/model_dto.py +0 -0
  61. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/model_view.py +0 -0
  62. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/paginator.py +0 -0
  63. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/pool_maintenance.py +0 -0
  64. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/pydantic_bridge.py +0 -0
  65. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/registry.py +0 -0
  66. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/relations.py +0 -0
  67. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/resilience.py +0 -0
  68. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/rls_session.py +0 -0
  69. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/signals.py +0 -0
  70. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/tls.py +0 -0
  71. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/transaction.py +0 -0
  72. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/types.py +0 -0
  73. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/core/write_queue.py +0 -0
  74. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/entity.py +0 -0
  75. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/error_handling.py +0 -0
  76. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/exceptions.py +0 -0
  77. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/extended/__init__.py +0 -0
  78. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/extended/app_error_handler.py +0 -0
  79. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/identity_rule.py +0 -0
  80. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/middleware/__init__.py +0 -0
  81. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/middleware/base.py +0 -0
  82. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/MODULE_README.md +0 -0
  83. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/__init__.py +0 -0
  84. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/cli.py +0 -0
  85. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/ddl.py +0 -0
  86. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/diff.py +0 -0
  87. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/executor.py +0 -0
  88. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/integration.py +0 -0
  89. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/loader.py +0 -0
  90. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/operations.py +0 -0
  91. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/state.py +0 -0
  92. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/migrations/table_filter.py +0 -0
  93. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/MODULE_README.md +0 -0
  94. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/__init__.py +0 -0
  95. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/bulk_update_values.py +0 -0
  96. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/conflict.py +0 -0
  97. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/conflict_writes.py +0 -0
  98. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/create.py +0 -0
  99. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/db_functions.py +0 -0
  100. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/delete.py +0 -0
  101. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/dynamic_admin.py +0 -0
  102. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/dynamic_crud.py +0 -0
  103. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/graph_walk.py +0 -0
  104. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/latest_rows.py +0 -0
  105. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/queue_claim.py +0 -0
  106. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/read.py +0 -0
  107. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/relational_backfill.py +0 -0
  108. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/operations/staging_load.py +0 -0
  109. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/py.typed +0 -0
  110. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/python_sql/MODULE_README.md +0 -0
  111. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/python_sql/__init__.py +0 -0
  112. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/python_sql/db_objects.py +0 -0
  113. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  114. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  115. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/query/MODULE_README.md +0 -0
  116. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/query/__init__.py +0 -0
  117. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/query/executor.py +0 -0
  118. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  119. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/__init__.py +0 -0
  120. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/code_handler.py +0 -0
  121. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  122. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/columns.py +0 -0
  123. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/common.py +0 -0
  124. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/diff_preview.py +0 -0
  125. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  126. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/generator.py +0 -0
  127. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  128. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  129. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  130. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  131. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/package_wiring.py +0 -0
  132. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/relationships.py +0 -0
  133. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/runner.py +0 -0
  134. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/schema.py +0 -0
  135. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/schema_manager.py +0 -0
  136. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/tables.py +0 -0
  137. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/schema_builder/views.py +0 -0
  138. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  139. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/secrets_battery/__init__.py +0 -0
  140. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/secrets_battery/crypto.py +0 -0
  141. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/secrets_battery/items.py +0 -0
  142. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/secrets_battery/model.py +0 -0
  143. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/secrets_battery/org_service.py +0 -0
  144. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/secrets_battery/service.py +0 -0
  145. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/README.md +0 -0
  146. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/__init__.py +0 -0
  147. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/coalesce.py +0 -0
  148. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/dag.py +0 -0
  149. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/errors.py +0 -0
  150. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/fallback.py +0 -0
  151. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/flush.py +0 -0
  152. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/lifecycle.py +0 -0
  153. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/managed.py +0 -0
  154. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/op.py +0 -0
  155. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/reads.py +0 -0
  156. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/session.py +0 -0
  157. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/session/telemetry.py +0 -0
  158. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  159. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/sql_executor/__init__.py +0 -0
  160. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/sql_executor/executor.py +0 -0
  161. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/sql_executor/queries.py +0 -0
  162. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/sql_executor/registry.py +0 -0
  163. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/sql_executor/types.py +0 -0
  164. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/sql_executor/utils.py +0 -0
  165. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/utils/__init__.py +0 -0
  166. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/utils/sql_utils.py +0 -0
  167. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/matrx_orm/utils/type_converters.py +0 -0
  168. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/scripts/git-branches.sh +0 -0
  169. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/scripts/publish.sh +0 -0
  170. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/scripts/release.sh +0 -0
  171. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/MODULE_README.md +0 -0
  172. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/__init__.py +0 -0
  173. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/conftest.py +0 -0
  174. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/MODULE_README.md +0 -0
  175. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/__init__.py +0 -0
  176. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_admin_db_columns.py +0 -0
  177. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_admin_search_negation.py +0 -0
  178. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_admin_search_sentinels.py +0 -0
  179. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_advisory_lock.py +0 -0
  180. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_agent_message.py +0 -0
  181. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_api_auth.py +0 -0
  182. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_api_config.py +0 -0
  183. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_api_handlers.py +0 -0
  184. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_api_protocol.py +0 -0
  185. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_array_agg_order_by.py +0 -0
  186. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_bulk_update_by_pk.py +0 -0
  187. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  188. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_cache_debug.py +0 -0
  189. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_call_function.py +0 -0
  190. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_case_when.py +0 -0
  191. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_catalog.py +0 -0
  192. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_composite_pk_filter.py +0 -0
  193. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_composite_pk_write_paths.py +0 -0
  194. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_config.py +0 -0
  195. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_conflict_target.py +0 -0
  196. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_conflict_writes.py +0 -0
  197. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_connection_codecs.py +0 -0
  198. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_connection_poison_guard.py +0 -0
  199. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_connection_primitives.py +0 -0
  200. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_count_composite_distinct.py +0 -0
  201. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_db_function_field.py +0 -0
  202. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_ddl_generator.py +0 -0
  203. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_derived_table.py +0 -0
  204. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_dirty_tracking.py +0 -0
  205. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_dynamic_crud.py +0 -0
  206. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_exceptions.py +0 -0
  207. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_expression_primitives.py +0 -0
  208. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_fields.py +0 -0
  209. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  210. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_from_alias.py +0 -0
  211. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_fts.py +0 -0
  212. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_func_expression_args.py +0 -0
  213. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_graph_walk.py +0 -0
  214. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_having_annotate_alias.py +0 -0
  215. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  216. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_introspect_rls_policies.py +0 -0
  217. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_join_and_aggregate.py +0 -0
  218. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_latest_rows.py +0 -0
  219. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_listen_notify.py +0 -0
  220. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_managed_write_guard.py +0 -0
  221. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_matrx_entity.py +0 -0
  222. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_migration_diff_types.py +0 -0
  223. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_migration_loader.py +0 -0
  224. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_model_instance.py +0 -0
  225. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_model_meta.py +0 -0
  226. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_order_by_expression.py +0 -0
  227. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_patch_jsonb_path.py +0 -0
  228. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_pool_loop_recreation.py +0 -0
  229. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_pool_maintenance.py +0 -0
  230. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_postgrest_filters.py +0 -0
  231. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_pydantic_bridge.py +0 -0
  232. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_query_builder.py +0 -0
  233. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_query_error_detail.py +0 -0
  234. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_query_executor_sql.py +0 -0
  235. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_query_timeout.py +0 -0
  236. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  237. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_queue_claim.py +0 -0
  238. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_registry.py +0 -0
  239. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_registry_multi_database.py +0 -0
  240. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_registry_multi_schema.py +0 -0
  241. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_relational_backfill.py +0 -0
  242. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_relations.py +0 -0
  243. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_resilience.py +0 -0
  244. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_schema_exists.py +0 -0
  245. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_session_advisory_lock.py +0 -0
  246. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_state_cache.py +0 -0
  247. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_subquery_filter_raw.py +0 -0
  248. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_subquery_in_filter.py +0 -0
  249. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_supabase_auth.py +0 -0
  250. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_supabase_config.py +0 -0
  251. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_tls.py +0 -0
  252. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_typed_join_projection.py +0 -0
  253. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_typed_predicates.py +0 -0
  254. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_update_case_expression.py +0 -0
  255. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_update_subquery.py +0 -0
  256. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_upsert_default_leak.py +0 -0
  257. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_upsert_with_conflict.py +0 -0
  258. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_values_jsonb_decode.py +0 -0
  259. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_watched_lifecycle.py +0 -0
  260. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level1/test_write_retry_semantics.py +0 -0
  261. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/MODULE_README.md +0 -0
  262. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/__init__.py +0 -0
  263. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/conftest.py +0 -0
  264. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_bulk_ops.py +0 -0
  265. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_cache_integration.py +0 -0
  266. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_crud.py +0 -0
  267. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_foreign_keys.py +0 -0
  268. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_m2m.py +0 -0
  269. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_manager.py +0 -0
  270. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_migrations_live.py +0 -0
  271. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_query_execution.py +0 -0
  272. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/level2/test_schema_diff.py +0 -0
  273. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project/.env.example +0 -0
  274. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project/README.md +0 -0
  275. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project/__init__.py +0 -0
  276. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project/generate.py +0 -0
  277. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project/generated/.gitkeep +0 -0
  278. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project/matrx_orm.yaml +0 -0
  279. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project/test_schema_generation.py +0 -0
  280. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project_desktop/.env.example +0 -0
  281. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project_desktop/README.md +0 -0
  282. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project_desktop/__init__.py +0 -0
  283. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project_desktop/client_example.py +0 -0
  284. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project_desktop/client_example.ts +0 -0
  285. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  286. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/sample_project_desktop/server.py +0 -0
  287. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/entity_tests.py +0 -0
  288. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_base_generation.py +0 -0
  289. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  290. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_composite_unique_generation.py +0 -0
  291. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_dto_identity_generation.py +0 -0
  292. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_entity_capability_generation.py +0 -0
  293. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_generate_schema.py +0 -0
  294. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_junction_analysis.py +0 -0
  295. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_multi_schema_output.py +0 -0
  296. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_sql_expression_defaults.py +0 -0
  297. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema/test_view_generation.py +0 -0
  298. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  299. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/__init__.py +0 -0
  300. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/conftest.py +0 -0
  301. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_capture_poison_proof.py +0 -0
  302. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_coalesce.py +0 -0
  303. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_dag.py +0 -0
  304. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_disk_spill_fallback.py +0 -0
  305. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_flush_individual_fallback.py +0 -0
  306. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_governed_write.py +0 -0
  307. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_managed.py +0 -0
  308. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_op_enqueue_site.py +0 -0
  309. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_reads.py +0 -0
  310. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/session/test_session.py +0 -0
  311. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/test_database_name_alias.py +0 -0
  312. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/test_db_functions.py +0 -0
  313. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/test_model_cls_refactor.py +0 -0
  314. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  315. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/test_secrets_battery.py +0 -0
  316. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/test_sql_param_casts.py +0 -0
  317. {matrx_orm-3.1.36 → matrx_orm-3.1.38}/tests/test_sync_twins_guard.py +0 -0
@@ -116,6 +116,31 @@ the target and those explicitly named cross-schema FK dependencies.
116
116
 
117
117
  ---
118
118
 
119
+ ## 🚨 THE IDENTITY CACHE MUST NEVER LIE (read before touching any write path)
120
+
121
+ `StateManager` keeps a process-local per-model identity cache (default
122
+ SHORT_TERM = **10 minutes**). Cached reads are the default (`use_cache=True`), so
123
+ anything wrong in that cache is served as truth for ten minutes. Three rules, all
124
+ enforced in code:
125
+
126
+ 1. **No downgrades.** A cache entry may only be replaced by a record at least as
127
+ informed. Instances built from a column SUBSET must call
128
+ `matrx_orm.state.mark_partial(...)` — `ModelState.cache` evicts them instead
129
+ of storing them. An undeclared downgrade evicts AND screams.
130
+ 2. **Writes refresh or evict — never leave a pre-write copy.** `QueryBuilder.update`
131
+ refreshes from its own `RETURNING *` rows; `QueryBuilder.delete` evicts the
132
+ affected PKs (or clears the model when the filter isn't PK-shaped).
133
+ 3. **Identity reads are authoritative.** Any read that decides ownership, org
134
+ attribution, or whether a request may run at all passes `use_cache=False`
135
+ (`load_by_id(id, use_cache=False)`). Fast is not a reason to guess.
136
+
137
+ This is not theoretical hygiene. On 2026-07-25 rule 1 was missing: `session/flush.py`
138
+ builds a `{pk + payload}` stub per queued UPDATE and `bulk_update` cached it, so a
139
+ chat conversation read back with `organization_id = None` right after its own
140
+ turn-1 title/`message_count` UPDATE flushed — and aidream stopped the user's next
141
+ turn on a value Postgres had held correctly the entire time. Guards:
142
+ `tests/session/test_cache_no_downgrade.py`.
143
+
119
144
  ## Known issues
120
145
 
121
146
  - **No CLAUDE.md existed before this commit** — that was the main gap.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.36
3
+ Version: 3.1.38
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
@@ -196,6 +196,15 @@ class Associations:
196
196
  self._org = org_field
197
197
  self._role = role_field
198
198
  self._role_available = True # refined by _validate_field_bindings when introspectable
199
+ # Optional edge-document columns (platform.associations carries them;
200
+ # a minimal host edge table may not). Passing the kwarg against a
201
+ # table without the column raises loudly at call time.
202
+ self._optional_cols: dict[str, bool] = {
203
+ "payload_kind": True,
204
+ "payload": True,
205
+ "label": True,
206
+ "position": True,
207
+ }
199
208
  self._validate_field_bindings()
200
209
 
201
210
  def _validate_field_bindings(self) -> None:
@@ -230,6 +239,8 @@ class Associations:
230
239
  # role is OPTIONAL on the edge table — engines over role-less edge
231
240
  # tables keep working; passing a role against one raises at call time.
232
241
  self._role_available = self._role in fields
242
+ for col in self._optional_cols:
243
+ self._optional_cols[col] = col in fields
233
244
 
234
245
  # ---- writes ----------------------------------------------------------
235
246
 
@@ -257,6 +268,10 @@ class Associations:
257
268
  metadata: dict[str, Any] | None = None,
258
269
  created_by: str | None = None,
259
270
  verify: bool = True,
271
+ payload_kind: str | None = None,
272
+ payload: dict[str, Any] | None = None,
273
+ label: str | None = None,
274
+ position: int | None = None,
260
275
  ) -> Any:
261
276
  """Idempotently create the ``source -> target`` edge in ``org_id``.
262
277
 
@@ -288,6 +303,17 @@ class Associations:
288
303
  defaults["metadata"] = metadata
289
304
  if created_by is not None:
290
305
  defaults["created_by"] = created_by
306
+ extras = {"payload_kind": payload_kind, "payload": payload, "label": label, "position": position}
307
+ for col, value in extras.items():
308
+ if value is None:
309
+ continue
310
+ if not self._optional_cols.get(col, True):
311
+ model_name = getattr(self._assoc, "__name__", str(self._assoc))
312
+ raise ValueError(
313
+ f"{col}={value!r} was passed but edge model {model_name} has no "
314
+ f"{col!r} column — add the column or drop the argument."
315
+ )
316
+ defaults[col] = value
291
317
  lookup = {
292
318
  self._st: source.type,
293
319
  self._si: source.id,
@@ -300,8 +326,14 @@ class Associations:
300
326
  elif role is not None:
301
327
  self._role_filter(role) # raises with the loud config message
302
328
  edge, created = await self._assoc.get_or_create(defaults=defaults, **lookup)
303
- if not created and metadata is not None and getattr(edge, "metadata", None) != metadata:
304
- edge = await edge.update(metadata=metadata)
329
+ if not created:
330
+ updates = {
331
+ col: value
332
+ for col, value in {"metadata": metadata, **extras}.items()
333
+ if value is not None and getattr(edge, col, None) != value
334
+ }
335
+ if updates:
336
+ edge = await edge.update(**updates)
305
337
  return edge
306
338
 
307
339
  async def dissociate(
@@ -64,8 +64,14 @@ async def bulk_update(model_cls: type[Model], objects: Sequence[Model], fields:
64
64
  result = await QueryBuilder(model_cls).filter(**pk_filter).update(**update_data)
65
65
  if result.rows_affected > 0:
66
66
  rows_affected += 1
67
-
68
- await StateManager.cache(model_cls, obj)
67
+ # Do NOT cache `obj` here. It is the CALLER's instance and is
68
+ # routinely a partial {pk + payload} stub (see
69
+ # session/flush.py), so writing it into the identity cache
70
+ # blanked every column the update didn't touch — the
71
+ # 2026-07-25 "conversation has no durable organization_id"
72
+ # bug. QueryBuilder.update already refreshed the cache from
73
+ # the statement's own RETURNING * rows, which is the
74
+ # authoritative post-write state.
69
75
 
70
76
  if debug and (i + 1) % 10 == 0:
71
77
  vcprint(f"Processed {i + 1}/{len(valid_objects)} updates...", color="cyan")
@@ -765,6 +765,30 @@ class QueryBuilder(Generic[ModelT]):
765
765
  combined[f"exclude__{k}"] = v
766
766
  return combined
767
767
 
768
+ def _invalidate_cache_for_filters(self) -> None:
769
+ """Evict the rows this statement affected from the identity cache.
770
+
771
+ Precise when the filter is a plain primary-key match (``pk=`` or
772
+ ``pk__in=``) — the common case by far. Anything else (multi-column
773
+ predicate, operator suffix, excludes) means the affected ids are unknown,
774
+ so the model's cache is cleared. Correctness over warmth.
775
+ """
776
+ pks = self.model._meta.primary_keys or ["id"]
777
+ combined = self._merge_filters_excludes()
778
+ if len(pks) == 1 and not self.excludes and set(combined) <= {pks[0], f"{pks[0]}__in"}:
779
+ values = combined.get(pks[0])
780
+ in_values = combined.get(f"{pks[0]}__in")
781
+ targets = list(in_values) if isinstance(in_values, (list, tuple, set)) else []
782
+ if values is not None:
783
+ targets.append(values)
784
+ if targets and StateManager.is_registered(self.model):
785
+ database = self.model.get_database_name()
786
+ state = StateManager._states[(database, self.model.__name__)]
787
+ for value in targets:
788
+ state.evict_by_id(value)
789
+ return
790
+ StateManager.clear_model(self.model)
791
+
768
792
  def _merge_having(self) -> dict[str, Any]:
769
793
  combined: dict[str, Any] = {}
770
794
  for h in self.having_filters:
@@ -862,11 +886,19 @@ class QueryBuilder(Generic[ModelT]):
862
886
  return None
863
887
 
864
888
  async def update(self, **kwargs: Any) -> UpdateResult:
865
- """Update matching records."""
889
+ """Update matching records.
890
+
891
+ The identity cache is refreshed from the statement's own
892
+ ``RETURNING *`` rows — an UPDATE here used to leave the PRE-write copy
893
+ cached and readable for the rest of the TTL (SHORT_TERM = 10 min), so a
894
+ row could be read back with values Postgres no longer held.
895
+ """
866
896
  try:
867
897
  if not kwargs:
868
898
  raise ValidationError(model=self.model, reason="No update data provided")
869
- return await self._get_executor().update(**kwargs)
899
+ result = await self._get_executor().update(**kwargs)
900
+ await StateManager.refresh_from_rows(self.model, result.updated_rows)
901
+ return result
870
902
  except ValidationError:
871
903
  raise
872
904
  except ORMException as e:
@@ -874,9 +906,19 @@ class QueryBuilder(Generic[ModelT]):
874
906
  raise
875
907
 
876
908
  async def delete(self) -> int:
877
- """Delete matching records."""
909
+ """Delete matching records.
910
+
911
+ Evicts the deleted rows from the identity cache — a filtered DELETE used
912
+ to leave them cached and readable for the rest of the TTL. When the
913
+ filter isn't a plain primary-key match the affected ids are unknown, so
914
+ the model's cache is cleared outright: losing warmth for one model is
915
+ free, serving a deleted row for 10 minutes is not.
916
+ """
878
917
  try:
879
- return await self._get_executor().delete()
918
+ deleted = await self._get_executor().delete()
919
+ if deleted:
920
+ self._invalidate_cache_for_filters()
921
+ return deleted
880
922
  except ORMException as e:
881
923
  e.enrich(model=self.model, operation="delete", filters=self._merge_filters_excludes())
882
924
  raise
@@ -555,6 +555,59 @@ class StateManager:
555
555
  original_error=e,
556
556
  ) from e
557
557
 
558
+ @classmethod
559
+ async def refresh_from_rows(cls, model_class, rows, *, max_rows: int = 200) -> None:
560
+ """Write authoritative post-write rows over whatever is cached.
561
+
562
+ For write paths that already have the true row in hand (``UPDATE …
563
+ RETURNING *``). Unlike :meth:`cache_bulk` this OVERWRITES an existing
564
+ entry — that is the point: a targeted UPDATE otherwise left the pre-write
565
+ copy cached and readable for the rest of the TTL.
566
+
567
+ Above ``max_rows`` the rows are evicted instead of cached, so a mass
568
+ UPDATE can't flood the cache with rows nobody asked for. Never raises —
569
+ a cache refresh must not fail the write that already committed.
570
+ """
571
+ if not rows or not cls.is_registered(model_class):
572
+ return
573
+ database = model_class.get_database_name()
574
+ state = cls._states[(database, model_class.__name__)]
575
+ pks = model_class._meta.primary_keys or ["id"]
576
+ evict_only = len(rows) > max_rows
577
+ for row in rows:
578
+ try:
579
+ if evict_only:
580
+ values = [row.get(pk) if isinstance(row, dict) else getattr(row, pk, None) for pk in pks]
581
+ if all(v is not None for v in values):
582
+ state.evict_by_id(*values)
583
+ continue
584
+ record = row if not isinstance(row, dict) else model_class.from_db_result(row)
585
+ await state.cache(record)
586
+ except Exception as exc: # noqa: BLE001 — never fail a committed write
587
+ vcprint(
588
+ f"[matrx_orm.state] refresh_from_rows skipped one "
589
+ f"{model_class.__name__} row ({type(exc).__name__}: {exc})",
590
+ color="yellow",
591
+ )
592
+
593
+ @classmethod
594
+ def clear_model(cls, model_class) -> int:
595
+ """Drop every cached record for a model. Returns the count dropped.
596
+
597
+ The correctness fallback for a write whose affected PKs are unknown
598
+ (a filtered DELETE). Losing cache warmth for one model is free; serving
599
+ a deleted row for 10 minutes is not.
600
+ """
601
+ if not cls.is_registered(model_class):
602
+ return 0
603
+ database = model_class.get_database_name()
604
+ state = cls._states[(database, model_class.__name__)]
605
+ dropped = len(state._cache)
606
+ state._cache.clear()
607
+ state._cache_times.clear()
608
+ state._subscriptions.clear()
609
+ return dropped
610
+
558
611
  @classmethod
559
612
  async def remove(cls, model_class, record):
560
613
  """Removes a specific record from the cache."""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.36"
3
+ version = "3.1.38"
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" }
@@ -262,6 +262,45 @@ async def test_load_gated_returns_system_vocabulary_cross_org():
262
262
  assert [t.id for t in targets] == ["k1"]
263
263
 
264
264
 
265
+ @pytest.mark.asyncio
266
+ async def test_associate_writes_and_replaces_payload_document():
267
+ a = _assoc()
268
+ _seed_n1_p1()
269
+ src, tgt = EntityRef("note", "n1"), EntityRef("project", "p1")
270
+ e1 = await a.associate(
271
+ src, tgt, org_id="org1", role="reviewed_by",
272
+ payload_kind="plan_review", payload={"review_date": "2026-07-25"},
273
+ label="Review", position=1,
274
+ )
275
+ assert e1.payload_kind == "plan_review" and e1.payload == {"review_date": "2026-07-25"}
276
+ assert e1.label == "Review" and e1.position == 1
277
+ # repeat call: passed values full-replace; omitted (None) stay untouched
278
+ e2 = await a.associate(
279
+ src, tgt, org_id="org1", role="reviewed_by",
280
+ payload={"review_date": "2026-07-26", "notes": "x"},
281
+ )
282
+ assert e2 is e1 and e2.payload == {"review_date": "2026-07-26", "notes": "x"}
283
+ assert e2.payload_kind == "plan_review" and e2.label == "Review"
284
+ assert len(AssociationRow._rows) == 1
285
+
286
+
287
+ @pytest.mark.asyncio
288
+ async def test_associate_payload_against_payloadless_table_raises():
289
+ from types import SimpleNamespace
290
+
291
+ cols = ("source_type", "source_id", "target_type", "target_id", "org_id", "role", "metadata")
292
+ model = type(
293
+ "_MiniEdge", (_FakeBase,), {"_meta": SimpleNamespace(fields={c: object() for c in cols})}
294
+ )
295
+ a = Associations(association_model=model, registry=_token_registry())
296
+ _seed_n1_p1()
297
+ with pytest.raises(ValueError, match="payload_kind"):
298
+ await a.associate(
299
+ EntityRef("note", "n1"), EntityRef("project", "p1"),
300
+ org_id="org1", payload_kind="plan_review",
301
+ )
302
+
303
+
265
304
  @pytest.mark.asyncio
266
305
  async def test_associate_verify_false_skips_endpoint_check():
267
306
  a = _assoc() # no endpoints seeded at all
@@ -95,3 +95,42 @@ async def test_equally_informed_record_still_replaces():
95
95
  assert cached is fresh
96
96
  assert cached.title == "Renamed"
97
97
  assert cached.organization_id == "org-1"
98
+
99
+
100
+ @pytest.mark.asyncio
101
+ async def test_refresh_from_rows_overwrites_a_stale_entry():
102
+ """A targeted UPDATE has the true post-write row in hand (RETURNING *), so the
103
+ cache is refreshed with it — cache_bulk's "only if absent" rule would have
104
+ left the PRE-write copy readable for the rest of the TTL."""
105
+ state = _state()
106
+ await state.cache(_full_row())
107
+
108
+ from matrx_orm.state import StateManager
109
+
110
+ StateManager._states[("test_db", "_Conversation")] = state
111
+ _Conversation.from_db_result = classmethod(lambda cls, row: cls(**row))
112
+ try:
113
+ await StateManager.refresh_from_rows(
114
+ _Conversation,
115
+ [{"id": "c1", "organization_id": "org-1", "title": "Renamed", "message_count": 4}],
116
+ )
117
+ cached = await state.get(id="c1")
118
+ assert cached.title == "Renamed"
119
+ assert cached.organization_id == "org-1"
120
+ finally:
121
+ StateManager._states.pop(("test_db", "_Conversation"), None)
122
+
123
+
124
+ @pytest.mark.asyncio
125
+ async def test_clear_model_drops_every_entry():
126
+ state = _state()
127
+ await state.cache(_full_row())
128
+
129
+ from matrx_orm.state import StateManager
130
+
131
+ StateManager._states[("test_db", "_Conversation")] = state
132
+ try:
133
+ assert StateManager.clear_model(_Conversation) == 1
134
+ assert await state.get(id="c1") is None
135
+ finally:
136
+ StateManager._states.pop(("test_db", "_Conversation"), None)
File without changes
File without changes
File without changes
File without changes