matrx-orm 3.1.34__tar.gz → 3.1.36__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.34 → matrx_orm-3.1.36}/CLAUDE.md +1 -0
  2. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/async_db_manager.py +31 -4
  4. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/base.py +11 -5
  5. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/README.md +18 -0
  6. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/flush.py +11 -2
  7. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/state.py +84 -1
  8. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/pyproject.toml +1 -1
  9. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_bulk_upsert_increment_set_fields.py +2 -2
  10. matrx_orm-3.1.36/tests/level1/test_pool_loop_recreation.py +57 -0
  11. matrx_orm-3.1.36/tests/session/test_cache_no_downgrade.py +97 -0
  12. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/.gitignore +0 -0
  13. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/AGENTS.md +0 -0
  14. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/BASE_CLASS_METHODS.md +0 -0
  15. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/MODEL_API.md +0 -0
  16. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/README.md +0 -0
  17. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/database/orm/extended/managers/ai_model_base.py +0 -0
  18. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/docs/migrations.md +0 -0
  19. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/MODULE_README.md +0 -0
  20. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/__init__.py +0 -0
  21. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/adapters/MODULE_README.md +0 -0
  22. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/adapters/__init__.py +0 -0
  23. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/adapters/async_postgresql.py +0 -0
  24. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/adapters/base_adapter.py +0 -0
  25. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/adapters/postgresql.py +0 -0
  26. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  27. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/adapters/supabase_adapter.py +0 -0
  28. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/admin/__init__.py +0 -0
  29. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/admin/router.py +0 -0
  30. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/api/__init__.py +0 -0
  31. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/api/auth.py +0 -0
  32. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/api/config.py +0 -0
  33. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/api/handlers.py +0 -0
  34. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/api/protocol.py +0 -0
  35. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/api/server.py +0 -0
  36. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/associations.py +0 -0
  37. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/cache_debug.py +0 -0
  38. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/catalog.py +0 -0
  39. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/client/MODULE_README.md +0 -0
  40. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/client/__init__.py +0 -0
  41. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/client/postgres_connection.py +0 -0
  42. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/client/postgrest.py +0 -0
  43. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/client/supabase_auth.py +0 -0
  44. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/client/supabase_config.py +0 -0
  45. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/client/supabase_manager.py +0 -0
  46. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  47. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/MODULE_README.md +0 -0
  48. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  49. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/TASKS.md +0 -0
  50. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/__init__.py +0 -0
  51. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/config.py +0 -0
  52. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/connections.py +0 -0
  53. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/db_function.py +0 -0
  54. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/diagnostics.py +0 -0
  55. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/expressions.py +0 -0
  56. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/extended.py +0 -0
  57. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/fields.py +0 -0
  58. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/introspection.py +0 -0
  59. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/listen.py +0 -0
  60. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/loop_filters.py +0 -0
  61. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/model_dto.py +0 -0
  62. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/model_view.py +0 -0
  63. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/paginator.py +0 -0
  64. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/pool_maintenance.py +0 -0
  65. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/pydantic_bridge.py +0 -0
  66. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/registry.py +0 -0
  67. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/relations.py +0 -0
  68. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/resilience.py +0 -0
  69. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/rls_session.py +0 -0
  70. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/signals.py +0 -0
  71. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/tls.py +0 -0
  72. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/transaction.py +0 -0
  73. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/types.py +0 -0
  74. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/core/write_queue.py +0 -0
  75. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/entity.py +0 -0
  76. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/error_handling.py +0 -0
  77. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/exceptions.py +0 -0
  78. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/extended/__init__.py +0 -0
  79. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/extended/app_error_handler.py +0 -0
  80. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/identity_rule.py +0 -0
  81. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/middleware/__init__.py +0 -0
  82. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/middleware/base.py +0 -0
  83. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/MODULE_README.md +0 -0
  84. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/__init__.py +0 -0
  85. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/cli.py +0 -0
  86. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/ddl.py +0 -0
  87. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/diff.py +0 -0
  88. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/executor.py +0 -0
  89. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/integration.py +0 -0
  90. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/loader.py +0 -0
  91. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/operations.py +0 -0
  92. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/state.py +0 -0
  93. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/migrations/table_filter.py +0 -0
  94. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/MODULE_README.md +0 -0
  95. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/__init__.py +0 -0
  96. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/bulk_update_values.py +0 -0
  97. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/conflict.py +0 -0
  98. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/conflict_writes.py +0 -0
  99. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/create.py +0 -0
  100. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/db_functions.py +0 -0
  101. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/delete.py +0 -0
  102. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/dynamic_admin.py +0 -0
  103. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/dynamic_crud.py +0 -0
  104. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/graph_walk.py +0 -0
  105. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/latest_rows.py +0 -0
  106. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/queue_claim.py +0 -0
  107. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/read.py +0 -0
  108. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/relational_backfill.py +0 -0
  109. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/staging_load.py +0 -0
  110. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/operations/update.py +0 -0
  111. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/py.typed +0 -0
  112. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/python_sql/MODULE_README.md +0 -0
  113. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/python_sql/__init__.py +0 -0
  114. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/python_sql/db_objects.py +0 -0
  115. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  116. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  117. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/query/MODULE_README.md +0 -0
  118. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/query/__init__.py +0 -0
  119. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/query/builder.py +0 -0
  120. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/query/executor.py +0 -0
  121. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  122. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/__init__.py +0 -0
  123. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/code_handler.py +0 -0
  124. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  125. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/columns.py +0 -0
  126. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/common.py +0 -0
  127. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/diff_preview.py +0 -0
  128. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  129. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/generator.py +0 -0
  130. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  131. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  132. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  133. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  134. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/package_wiring.py +0 -0
  135. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/relationships.py +0 -0
  136. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/runner.py +0 -0
  137. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/schema.py +0 -0
  138. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/schema_manager.py +0 -0
  139. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/tables.py +0 -0
  140. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/schema_builder/views.py +0 -0
  141. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  142. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/__init__.py +0 -0
  143. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/crypto.py +0 -0
  144. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/items.py +0 -0
  145. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/model.py +0 -0
  146. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/org_service.py +0 -0
  147. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/service.py +0 -0
  148. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/__init__.py +0 -0
  149. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/coalesce.py +0 -0
  150. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/dag.py +0 -0
  151. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/errors.py +0 -0
  152. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/fallback.py +0 -0
  153. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/lifecycle.py +0 -0
  154. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/managed.py +0 -0
  155. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/op.py +0 -0
  156. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/reads.py +0 -0
  157. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/session.py +0 -0
  158. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/session/telemetry.py +0 -0
  159. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  160. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/sql_executor/__init__.py +0 -0
  161. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/sql_executor/executor.py +0 -0
  162. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/sql_executor/queries.py +0 -0
  163. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/sql_executor/registry.py +0 -0
  164. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/sql_executor/types.py +0 -0
  165. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/sql_executor/utils.py +0 -0
  166. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/utils/__init__.py +0 -0
  167. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/utils/sql_utils.py +0 -0
  168. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/matrx_orm/utils/type_converters.py +0 -0
  169. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/scripts/git-branches.sh +0 -0
  170. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/scripts/publish.sh +0 -0
  171. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/scripts/release.sh +0 -0
  172. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/MODULE_README.md +0 -0
  173. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/__init__.py +0 -0
  174. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/conftest.py +0 -0
  175. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/MODULE_README.md +0 -0
  176. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/__init__.py +0 -0
  177. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_admin_db_columns.py +0 -0
  178. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_admin_search_negation.py +0 -0
  179. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_admin_search_sentinels.py +0 -0
  180. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_advisory_lock.py +0 -0
  181. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_agent_message.py +0 -0
  182. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_api_auth.py +0 -0
  183. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_api_config.py +0 -0
  184. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_api_handlers.py +0 -0
  185. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_api_protocol.py +0 -0
  186. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_array_agg_order_by.py +0 -0
  187. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_associations.py +0 -0
  188. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_bulk_update_by_pk.py +0 -0
  189. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_cache_debug.py +0 -0
  190. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_call_function.py +0 -0
  191. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_case_when.py +0 -0
  192. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_catalog.py +0 -0
  193. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_composite_pk_filter.py +0 -0
  194. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_composite_pk_write_paths.py +0 -0
  195. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_config.py +0 -0
  196. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_conflict_target.py +0 -0
  197. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_conflict_writes.py +0 -0
  198. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_connection_codecs.py +0 -0
  199. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_connection_poison_guard.py +0 -0
  200. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_connection_primitives.py +0 -0
  201. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_count_composite_distinct.py +0 -0
  202. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_db_function_field.py +0 -0
  203. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_ddl_generator.py +0 -0
  204. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_derived_table.py +0 -0
  205. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_dirty_tracking.py +0 -0
  206. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_dynamic_crud.py +0 -0
  207. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_exceptions.py +0 -0
  208. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_expression_primitives.py +0 -0
  209. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_fields.py +0 -0
  210. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  211. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_from_alias.py +0 -0
  212. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_fts.py +0 -0
  213. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_func_expression_args.py +0 -0
  214. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_graph_walk.py +0 -0
  215. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_having_annotate_alias.py +0 -0
  216. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  217. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_introspect_rls_policies.py +0 -0
  218. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_join_and_aggregate.py +0 -0
  219. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_latest_rows.py +0 -0
  220. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_listen_notify.py +0 -0
  221. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_managed_write_guard.py +0 -0
  222. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_matrx_entity.py +0 -0
  223. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_migration_diff_types.py +0 -0
  224. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_migration_loader.py +0 -0
  225. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_model_instance.py +0 -0
  226. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_model_meta.py +0 -0
  227. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_order_by_expression.py +0 -0
  228. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_patch_jsonb_path.py +0 -0
  229. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_pool_maintenance.py +0 -0
  230. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_postgrest_filters.py +0 -0
  231. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_pydantic_bridge.py +0 -0
  232. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_query_builder.py +0 -0
  233. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_query_error_detail.py +0 -0
  234. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_query_executor_sql.py +0 -0
  235. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_query_timeout.py +0 -0
  236. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  237. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_queue_claim.py +0 -0
  238. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_registry.py +0 -0
  239. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_registry_multi_database.py +0 -0
  240. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_registry_multi_schema.py +0 -0
  241. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_relational_backfill.py +0 -0
  242. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_relations.py +0 -0
  243. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_resilience.py +0 -0
  244. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_schema_exists.py +0 -0
  245. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_session_advisory_lock.py +0 -0
  246. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_state_cache.py +0 -0
  247. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_subquery_filter_raw.py +0 -0
  248. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_subquery_in_filter.py +0 -0
  249. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_supabase_auth.py +0 -0
  250. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_supabase_config.py +0 -0
  251. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_tls.py +0 -0
  252. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_typed_join_projection.py +0 -0
  253. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_typed_predicates.py +0 -0
  254. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_update_case_expression.py +0 -0
  255. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_update_subquery.py +0 -0
  256. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_upsert_default_leak.py +0 -0
  257. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_upsert_with_conflict.py +0 -0
  258. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_values_jsonb_decode.py +0 -0
  259. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_watched_lifecycle.py +0 -0
  260. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level1/test_write_retry_semantics.py +0 -0
  261. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/MODULE_README.md +0 -0
  262. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/__init__.py +0 -0
  263. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/conftest.py +0 -0
  264. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_bulk_ops.py +0 -0
  265. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_cache_integration.py +0 -0
  266. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_crud.py +0 -0
  267. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_foreign_keys.py +0 -0
  268. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_m2m.py +0 -0
  269. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_manager.py +0 -0
  270. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_migrations_live.py +0 -0
  271. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_query_execution.py +0 -0
  272. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/level2/test_schema_diff.py +0 -0
  273. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project/.env.example +0 -0
  274. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project/README.md +0 -0
  275. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project/__init__.py +0 -0
  276. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project/generate.py +0 -0
  277. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project/generated/.gitkeep +0 -0
  278. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project/matrx_orm.yaml +0 -0
  279. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project/test_schema_generation.py +0 -0
  280. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project_desktop/.env.example +0 -0
  281. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project_desktop/README.md +0 -0
  282. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project_desktop/__init__.py +0 -0
  283. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project_desktop/client_example.py +0 -0
  284. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project_desktop/client_example.ts +0 -0
  285. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  286. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/sample_project_desktop/server.py +0 -0
  287. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/entity_tests.py +0 -0
  288. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_base_generation.py +0 -0
  289. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  290. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_composite_unique_generation.py +0 -0
  291. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_dto_identity_generation.py +0 -0
  292. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_entity_capability_generation.py +0 -0
  293. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_generate_schema.py +0 -0
  294. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_junction_analysis.py +0 -0
  295. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_multi_schema_output.py +0 -0
  296. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_sql_expression_defaults.py +0 -0
  297. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema/test_view_generation.py +0 -0
  298. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  299. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/__init__.py +0 -0
  300. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/conftest.py +0 -0
  301. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_capture_poison_proof.py +0 -0
  302. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_coalesce.py +0 -0
  303. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_dag.py +0 -0
  304. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_disk_spill_fallback.py +0 -0
  305. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_flush_individual_fallback.py +0 -0
  306. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_governed_write.py +0 -0
  307. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_managed.py +0 -0
  308. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_op_enqueue_site.py +0 -0
  309. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_reads.py +0 -0
  310. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/session/test_session.py +0 -0
  311. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/test_database_name_alias.py +0 -0
  312. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/test_db_functions.py +0 -0
  313. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/test_model_cls_refactor.py +0 -0
  314. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  315. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/test_secrets_battery.py +0 -0
  316. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/test_sql_param_casts.py +0 -0
  317. {matrx_orm-3.1.34 → matrx_orm-3.1.36}/tests/test_sync_twins_guard.py +0 -0
@@ -26,6 +26,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
26
26
  - **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`.
27
27
  - **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).
28
28
  - **Connection-level type codecs** (`async_db_manager._init_connection_default`): every pooled connection decodes **uuid → `str`**, **vector → `list[float]`**, and **json/jsonb → `dict`/`list`** at the driver boundary — the TYPE decides the decode, never the call path. So `call_function` scalar/rows, raw fetches, and field hydration all agree; a caller must **never `json.loads` a fetched json/jsonb value** (it's already decoded; the 2026-07-13 `build_agent_context` crash was a jsonb RPC fetched pre-codec as a raw string). Encoding: a pre-serialised JSON `str` passes through untouched; a native dict/list is dumped (`_encode_json`). Pinned by `tests/level1/test_connection_codecs.py`.
29
+ - **Connection pools are event-loop-owned.** `AsyncDatabaseManager.get_pool()` recreates a pool when a caller arrives on a different loop. If the former owner loop is already closed, stale-pool termination and cleanup discard that pool without trying to schedule callbacks onto the dead loop; unrelated runtime errors still raise. Pinned by `tests/level1/test_pool_loop_recreation.py`.
29
30
  - **Query layer**: `QueryBuilder`, typed predicates/projections (`F`, `Q`,
30
31
  `Predicate`, `Projection`), window functions, QueryBuilder-backed CTEs, and
31
32
  subqueries. SQL-shaped strings fail closed: plain strings name validated
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.34
3
+ Version: 3.1.36
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
@@ -39,6 +39,18 @@ from matrx_orm.exceptions import (
39
39
  logger = logging.getLogger("matrx_orm.async_db_manager")
40
40
 
41
41
 
42
+ def _terminate_pool_after_loop_change(pool: Any, config_name: str) -> None:
43
+ try:
44
+ pool.terminate()
45
+ except RuntimeError as exc:
46
+ if "event loop is closed" not in str(exc).lower():
47
+ raise
48
+ logger.warning(
49
+ "[matrx_orm] Discarded stale pool for '%s' after its owner event loop closed",
50
+ config_name,
51
+ )
52
+
53
+
42
54
  # asyncpg SQLSTATE class 42 "undefined …" — the SQL named something the live DB
43
55
  # does not have. All siblings of SyntaxOrAccessError; PostgresSyntaxError is
44
56
  # caught separately (it's an ORM-bug shape, not a schema-drift shape). These map
@@ -323,15 +335,15 @@ class AsyncDatabaseManager:
323
335
 
324
336
  def _dispose_old_pool() -> None:
325
337
  old_task.cancel()
326
- old_pool.terminate()
338
+ _terminate_pool_after_loop_change(old_pool, config_name)
327
339
 
328
340
  owner_loop.call_soon_threadsafe(_dispose_old_pool)
329
341
  else:
330
- old_pool.terminate()
342
+ _terminate_pool_after_loop_change(old_pool, config_name)
331
343
  except RuntimeError:
332
- old_pool.terminate()
344
+ _terminate_pool_after_loop_change(old_pool, config_name)
333
345
  else:
334
- old_pool.terminate()
346
+ _terminate_pool_after_loop_change(old_pool, config_name)
335
347
 
336
348
  # Remove the old pool so we can create a new one
337
349
  del cls._pools[config_name]
@@ -1116,6 +1128,21 @@ class AsyncDatabaseManager:
1116
1128
  for config_name, pool in list(cls._pools.items()):
1117
1129
  try:
1118
1130
  await pool.close()
1131
+ except RuntimeError as e:
1132
+ if "event loop is closed" in str(e).lower():
1133
+ logger.warning(
1134
+ "[matrx_orm] Discarded stale pool for '%s' during cleanup "
1135
+ "after its owner event loop closed",
1136
+ config_name,
1137
+ )
1138
+ continue
1139
+ raise StateError(
1140
+ model=None,
1141
+ operation="cleanup_pools",
1142
+ reason=f"Failed to close pool for {config_name}",
1143
+ details={"config_name": config_name},
1144
+ original_error=e,
1145
+ ) from e
1119
1146
  except Exception as e:
1120
1147
  raise StateError(
1121
1148
  model=None,
@@ -1155,24 +1155,30 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
1155
1155
  return pks[0]
1156
1156
 
1157
1157
  @classmethod
1158
- async def load_by_id(cls, item_id: Any) -> Self:
1158
+ async def load_by_id(cls, item_id: Any, use_cache: bool = True) -> Self:
1159
1159
  """Load a row by its primary key. Raises DoesNotExist if absent.
1160
1160
 
1161
1161
  Alias for ``get(**{pk: item_id})`` — mirrors ``BaseManager.load_by_id``.
1162
1162
  Composite-PK models accept a dict ``{pk: val}`` or a PK-ordered
1163
1163
  tuple/list (a bare scalar raises — see ``_build_pk_filter``).
1164
1164
  For a None-returning lookup use :meth:`load_by_id_or_none`.
1165
+
1166
+ ``use_cache=False`` is the AUTHORITATIVE read: pass it whenever the row
1167
+ decides identity, ownership, or whether the request may run at all. It
1168
+ also refreshes the cache with what it read, so a bypass never leaves a
1169
+ stale entry behind.
1165
1170
  """
1166
- return await cls.get(**cls._build_pk_filter(item_id))
1171
+ return await cls.get(use_cache=use_cache, **cls._build_pk_filter(item_id))
1167
1172
 
1168
1173
  @classmethod
1169
- async def load_by_id_or_none(cls, item_id: Any) -> Self | None:
1174
+ async def load_by_id_or_none(cls, item_id: Any, use_cache: bool = True) -> Self | None:
1170
1175
  """Load a row by its primary key, or None if absent.
1171
1176
 
1172
1177
  Alias for ``get_or_none(**{pk: item_id})``. Composite-PK models accept
1173
- a dict / PK-ordered tuple, same as :meth:`load_by_id`.
1178
+ a dict / PK-ordered tuple, same as :meth:`load_by_id`. See
1179
+ :meth:`load_by_id` for when to pass ``use_cache=False``.
1174
1180
  """
1175
- return await cls.get_or_none(**cls._build_pk_filter(item_id))
1181
+ return await cls.get_or_none(use_cache=use_cache, **cls._build_pk_filter(item_id))
1176
1182
 
1177
1183
  @classmethod
1178
1184
  async def filter_items(cls, *args: Any, **kwargs: Any) -> list[Self]:
@@ -134,6 +134,24 @@ Complex predicates (operator suffixes, JSON paths, joins) only match
134
134
  DB rows — the pending state is assumed safe (the DB still applies the
135
135
  predicate on its side).
136
136
 
137
+ ## 🚨 Flush stubs are PARTIAL — they never enter the identity cache
138
+
139
+ `flush._execute_group` turns each queued UPDATE into a lightweight
140
+ `model_cls(**{pk, **payload})` instance so `bulk_update` can build per-row SQL.
141
+ That instance is **not a row snapshot**: every column outside the payload holds a
142
+ model default (usually `None`). Each one is therefore stamped by
143
+ `matrx_orm.state.mark_partial(...)`, and `ModelState.cache` **evicts** instead of
144
+ storing any partial — the next read refetches the whole row.
145
+
146
+ This is load-bearing, not hygiene. `bulk_update` write-through used to install
147
+ those stubs as the row's cache entry, so for the whole cache TTL (SHORT_TERM = 10
148
+ min) a freshly-updated row read back with NULLs in every untouched column. On
149
+ 2026-07-25 that made a saved conversation read back with no `organization_id`
150
+ seconds after its own turn-1 title/`message_count` UPDATE flushed, and aidream's
151
+ identity guard hard-stopped the next turn. Any new code path that builds a model
152
+ instance from a column subset MUST call `mark_partial` — an undeclared downgrade
153
+ is evicted anyway and SCREAMS (see `state._scream_downgrade`).
154
+
137
155
  ## Nested Sessions
138
156
 
139
157
  Sub-agents, parallel-execute branches, workflow super-steps — every
@@ -29,6 +29,7 @@ from collections.abc import Sequence
29
29
  from contextvars import ContextVar
30
30
 
31
31
  from matrx_orm.session.op import SessionOp
32
+ from matrx_orm.state import mark_partial
32
33
 
33
34
  logger = logging.getLogger("matrx_orm.session.flush")
34
35
 
@@ -122,7 +123,13 @@ async def _execute_group(
122
123
  for op in ops:
123
124
  kwargs = {pk_field: op.pk_value}
124
125
  kwargs.update(op.payload)
125
- instances.append(model_cls(**kwargs))
126
+ # These are stubs, NOT row snapshots: every column outside `kwargs`
127
+ # is a model default (None), not the row's value. Declare that, or
128
+ # bulk_update's write-through would install one as the row's cache
129
+ # entry and blank every other column for the TTL (this is the bug
130
+ # that made a saved conversation read back with no organization_id
131
+ # on its next turn). See matrx_orm.state.mark_partial.
132
+ instances.append(mark_partial(model_cls(**kwargs), kwargs))
126
133
 
127
134
  await model_cls.bulk_update(instances, fields=all_fields)
128
135
  return len(instances)
@@ -132,7 +139,9 @@ async def _execute_group(
132
139
  pks = getattr(meta, "primary_keys", None) if meta else None
133
140
  pk_field = pks[0] if pks else ops[0].pk_field
134
141
 
135
- instances = [model_cls(**{pk_field: op.pk_value}) for op in ops]
142
+ instances = [
143
+ mark_partial(model_cls(**{pk_field: op.pk_value}), (pk_field,)) for op in ops
144
+ ]
136
145
  await model_cls.bulk_delete(instances)
137
146
  return len(instances)
138
147
 
@@ -15,6 +15,48 @@ from matrx_orm.exceptions import (
15
15
  )
16
16
 
17
17
 
18
+ # Attribute a write-through instance sets to declare "I am a {pk + payload}
19
+ # stub, not a row snapshot". Set by matrx_orm.session.flush; honored by
20
+ # ModelState.cache (evict, never store) and by mark_partial() below.
21
+ PARTIAL_FIELDS_ATTR = "_matrx_partial_fields"
22
+
23
+ # One scream per (model, blanked-field-set) — a downgrade is a code defect, so
24
+ # it must be visible, but it must not drown a hot loop in identical banners.
25
+ _SCREAMED_DOWNGRADES: set[tuple[str, tuple[str, ...]]] = set()
26
+
27
+
28
+ def mark_partial(instance, fields):
29
+ """Declare ``instance`` a partial write-through stub of its row.
30
+
31
+ Every caller that builds a model instance from a PK plus a subset of
32
+ columns (session flush, targeted bulk_update) MUST call this. Undeclared
33
+ partials trip the no-downgrade scream in :meth:`ModelState.cache`.
34
+ """
35
+ setattr(instance, PARTIAL_FIELDS_ATTR, frozenset(fields))
36
+ return instance
37
+
38
+
39
+ def _scream_downgrade(model_class, blanked: list[str]) -> None:
40
+ signature = (model_class.__name__, tuple(sorted(blanked)))
41
+ if signature in _SCREAMED_DOWNGRADES:
42
+ return
43
+ _SCREAMED_DOWNGRADES.add(signature)
44
+ vcprint(
45
+ "\n"
46
+ "══════════════════════════════════════════════════════════════════════\n"
47
+ " MATRX ORM — CACHE DOWNGRADE BLOCKED (no-downgrade law)\n"
48
+ "══════════════════════════════════════════════════════════════════════\n"
49
+ f" model: {model_class.__name__}\n"
50
+ f" would blank: {', '.join(sorted(blanked))}\n"
51
+ " action: cache entry EVICTED (next read refetches the full row).\n"
52
+ " Nothing was lost, but a PARTIAL model instance reached a\n"
53
+ " write-through cache path without calling\n"
54
+ " matrx_orm.state.mark_partial() — fix the writer.\n"
55
+ "══════════════════════════════════════════════════════════════════════",
56
+ color="red",
57
+ )
58
+
59
+
18
60
  class CachePolicy(Enum):
19
61
  PERMANENT = "permanent"
20
62
  LONG_TERM = "long_term"
@@ -192,7 +234,26 @@ class ModelState:
192
234
  return True
193
235
 
194
236
  async def cache(self, record):
195
- """Cache a record with proper error handling."""
237
+ """Cache a record with proper error handling.
238
+
239
+ THE NO-DOWNGRADE LAW: a cache entry may only ever be REPLACED by a
240
+ record that is at least as informed as the one already there. A
241
+ write-through instance built from ``{pk, **payload}`` is NOT a row
242
+ snapshot — every column outside the payload holds a model default
243
+ (usually ``None``), not the row's real value. Storing one blanks the
244
+ row for every cached reader until the TTL expires.
245
+
246
+ That is exactly how a conversation with a perfectly good
247
+ ``organization_id`` in Postgres read back as org-less for 10 minutes
248
+ after its own turn-1 title/message_count UPDATE flushed
249
+ (``session/flush.py`` builds partial instances; ``bulk_update`` cached
250
+ them), which then hard-failed the next turn's identity restore.
251
+
252
+ So: partial records EVICT instead of caching (the next read refetches
253
+ the whole row), and an unflagged record that would blank a populated
254
+ field evicts + SCREAMS — a downgrade means a partial instance reached
255
+ a write-through path without declaring itself.
256
+ """
196
257
  try:
197
258
  if not record:
198
259
  raise ValidationError(model=self.model_class, reason="Cannot cache None record")
@@ -204,6 +265,20 @@ class ModelState:
204
265
  reason="Could not generate cache key for record",
205
266
  )
206
267
 
268
+ if getattr(record, PARTIAL_FIELDS_ATTR, None) is not None:
269
+ # Declared partial (session flush write-through). Designed
270
+ # path — evict silently so the next read hits the DB.
271
+ self.evict_by_id(cache_key)
272
+ return
273
+
274
+ existing = self._cache.get(cache_key)
275
+ if existing is not None and existing is not record:
276
+ blanked = self._downgraded_fields(existing, record)
277
+ if blanked:
278
+ self.evict_by_id(cache_key)
279
+ _scream_downgrade(self.model_class, blanked)
280
+ return
281
+
207
282
  self._cache[cache_key] = record
208
283
  self._cache_times[cache_key] = datetime.now()
209
284
 
@@ -222,6 +297,14 @@ class ModelState:
222
297
  original_error=e,
223
298
  ) from e
224
299
 
300
+ def _downgraded_fields(self, existing, incoming) -> list[str]:
301
+ """Column names the incoming record would blank on the cached one."""
302
+ blanked: list[str] = []
303
+ for name in getattr(self.model_class, "_fields", {}):
304
+ if getattr(existing, name, None) is not None and getattr(incoming, name, None) is None:
305
+ blanked.append(name)
306
+ return blanked
307
+
225
308
  async def remove(self, record):
226
309
  cache_key = self._get_record_cache_key(record)
227
310
  self._cache.pop(cache_key, None)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.34"
3
+ version = "3.1.36"
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" }
@@ -23,10 +23,10 @@ No DB needed — a stub ``connection``/execute_write captures the rendered SQL
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ from datetime import timedelta
26
27
  from typing import Any
27
28
 
28
29
  import pytest
29
- from matrx_orm import Now
30
30
  from matrx_orm.core.base import Model, ModelMeta
31
31
  from matrx_orm.core.expressions import Case, Compare, Excluded, F, Interval, Is, Now, When
32
32
  from matrx_orm.core.fields import DateTimeField, FloatField, UUIDField
@@ -138,7 +138,7 @@ async def test_window_roll_case_set_fields_shifts_params_past_values() -> None:
138
138
  assert f"({table}.cost_used_usd + $" in sql
139
139
  assert "updated_at = NOW()" in sql
140
140
  assert len(params) == 8
141
- assert params[4:6] == ("24 hours", "24 hours")
141
+ assert params[4:6] == (timedelta(hours=24), timedelta(hours=24))
142
142
  assert params[6:] == (2.5, 2.5)
143
143
 
144
144
 
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pytest
6
+
7
+ from matrx_orm.core.async_db_manager import (
8
+ AsyncDatabaseManager,
9
+ _terminate_pool_after_loop_change,
10
+ )
11
+
12
+
13
+ class _ClosedLoopPool:
14
+ def __init__(self) -> None:
15
+ self.terminate_calls = 0
16
+ self.close_calls = 0
17
+
18
+ def terminate(self) -> None:
19
+ self.terminate_calls += 1
20
+ raise RuntimeError("Event loop is closed")
21
+
22
+ async def close(self) -> None:
23
+ self.close_calls += 1
24
+ raise RuntimeError("Event loop is closed")
25
+
26
+
27
+ def test_stale_pool_termination_tolerates_closed_owner_loop() -> None:
28
+ pool = _ClosedLoopPool()
29
+
30
+ _terminate_pool_after_loop_change(pool, "main")
31
+
32
+ assert pool.terminate_calls == 1
33
+
34
+
35
+ def test_stale_pool_termination_preserves_other_runtime_errors() -> None:
36
+ class _BrokenPool:
37
+ def terminate(self) -> None:
38
+ raise RuntimeError("unexpected pool corruption")
39
+
40
+ with pytest.raises(RuntimeError, match="unexpected pool corruption"):
41
+ _terminate_pool_after_loop_change(_BrokenPool(), "main")
42
+
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_cleanup_discards_pool_owned_by_closed_loop(monkeypatch: Any) -> None:
46
+ pool = _ClosedLoopPool()
47
+ monkeypatch.setattr(AsyncDatabaseManager, "_pools", {"main": pool})
48
+ monkeypatch.setattr(AsyncDatabaseManager, "_pool_loops", {"main": 1})
49
+ monkeypatch.setattr(AsyncDatabaseManager, "_locks", {"main": object()})
50
+ monkeypatch.setattr(AsyncDatabaseManager, "_growth_locks", {"main": object()})
51
+ monkeypatch.setattr(AsyncDatabaseManager, "_keepalive_tasks", {})
52
+
53
+ await AsyncDatabaseManager.cleanup()
54
+
55
+ assert pool.close_calls == 1
56
+ assert AsyncDatabaseManager._pools == {}
57
+ assert AsyncDatabaseManager._pool_loops == {}
@@ -0,0 +1,97 @@
1
+ """THE NO-DOWNGRADE LAW — a cache entry may never lose information.
2
+
3
+ Regression guard for the 2026-07-25 incident: a conversation with a correct
4
+ ``organization_id`` in Postgres read back org-less for the whole 10-minute
5
+ cache TTL, because ``session/flush.py`` builds ``{pk + payload}`` stubs for
6
+ queued UPDATEs and ``operations/update.py::bulk_update`` wrote them straight
7
+ into the identity cache. The next turn's identity restore then STOPPED the
8
+ request on data that did not exist.
9
+
10
+ Two independent guarantees are locked in here:
11
+ 1. flush declares its stubs partial (``mark_partial``).
12
+ 2. ``ModelState.cache`` never stores a partial or downgrading record — it
13
+ evicts, so the next read refetches the whole row.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import pytest
19
+
20
+ from matrx_orm.state import PARTIAL_FIELDS_ATTR, CachePolicy, ModelState, mark_partial
21
+
22
+
23
+ class _Conversation:
24
+ """Minimal Model stand-in with the fields the incident turned on."""
25
+
26
+ _fields = {
27
+ "id": object(),
28
+ "organization_id": object(),
29
+ "title": object(),
30
+ "message_count": object(),
31
+ }
32
+
33
+ class _Meta:
34
+ primary_keys = ["id"]
35
+
36
+ _meta = _Meta()
37
+
38
+ def __init__(self, **kwargs):
39
+ for name in self._fields:
40
+ setattr(self, name, kwargs.get(name))
41
+
42
+ @classmethod
43
+ def get_database_name(cls):
44
+ return "test_db"
45
+
46
+
47
+ def _state() -> ModelState:
48
+ state = ModelState(_Conversation)
49
+ state.policy = CachePolicy.SHORT_TERM
50
+ return state
51
+
52
+
53
+ def _full_row() -> _Conversation:
54
+ return _Conversation(
55
+ id="c1", organization_id="org-1", title="Anthropic Model Sync", message_count=3
56
+ )
57
+
58
+
59
+ @pytest.mark.asyncio
60
+ async def test_flush_update_stub_is_declared_partial():
61
+ stub = mark_partial(_Conversation(id="c1", message_count=5), {"id", "message_count"})
62
+ assert getattr(stub, PARTIAL_FIELDS_ATTR) == frozenset({"id", "message_count"})
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_partial_stub_evicts_instead_of_blanking_the_row():
67
+ state = _state()
68
+ await state.cache(_full_row())
69
+ assert (await state.get(id="c1")).organization_id == "org-1"
70
+
71
+ # Exactly what a queued conversation UPDATE flush produces on turn 1.
72
+ await state.cache(mark_partial(_Conversation(id="c1", message_count=5), {"id", "message_count"}))
73
+
74
+ # The org-less stub must NOT be servable. Cache miss → caller refetches.
75
+ assert await state.get(id="c1") is None
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_undeclared_downgrade_is_refused_too():
80
+ state = _state()
81
+ await state.cache(_full_row())
82
+ await state.cache(_Conversation(id="c1", message_count=7)) # no mark_partial
83
+ assert await state.get(id="c1") is None
84
+
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_equally_informed_record_still_replaces():
88
+ state = _state()
89
+ await state.cache(_full_row())
90
+ fresh = _Conversation(
91
+ id="c1", organization_id="org-1", title="Renamed", message_count=9
92
+ )
93
+ await state.cache(fresh)
94
+ cached = await state.get(id="c1")
95
+ assert cached is fresh
96
+ assert cached.title == "Renamed"
97
+ assert cached.organization_id == "org-1"
File without changes
File without changes
File without changes
File without changes