matrx-orm 3.1.39__tar.gz → 3.1.41__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 (318) hide show
  1. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/CLAUDE.md +3 -0
  2. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/base.py +6 -6
  4. matrx_orm-3.1.41/matrx_orm/query/FEATURE.md +17 -0
  5. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/query/builder.py +9 -4
  6. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/FEATURE.md +24 -1
  7. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/__init__.py +2 -0
  8. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/items.py +56 -0
  9. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/org_service.py +38 -3
  10. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/pyproject.toml +1 -1
  11. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_query_builder.py +18 -0
  12. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_secrets_battery.py +193 -0
  13. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/.gitignore +0 -0
  14. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/AGENTS.md +0 -0
  15. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/BASE_CLASS_METHODS.md +0 -0
  16. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/MODEL_API.md +0 -0
  17. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/README.md +0 -0
  18. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/database/orm/extended/managers/ai_model_base.py +0 -0
  19. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/docs/migrations.md +0 -0
  20. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/MODULE_README.md +0 -0
  21. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/__init__.py +0 -0
  22. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/MODULE_README.md +0 -0
  23. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/__init__.py +0 -0
  24. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/async_postgresql.py +0 -0
  25. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/base_adapter.py +0 -0
  26. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/postgresql.py +0 -0
  27. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  28. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/adapters/supabase_adapter.py +0 -0
  29. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/admin/__init__.py +0 -0
  30. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/admin/router.py +0 -0
  31. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/__init__.py +0 -0
  32. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/auth.py +0 -0
  33. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/config.py +0 -0
  34. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/handlers.py +0 -0
  35. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/protocol.py +0 -0
  36. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/api/server.py +0 -0
  37. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/associations.py +0 -0
  38. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/cache_debug.py +0 -0
  39. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/catalog.py +0 -0
  40. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/MODULE_README.md +0 -0
  41. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/__init__.py +0 -0
  42. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/postgres_connection.py +0 -0
  43. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/postgrest.py +0 -0
  44. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/supabase_auth.py +0 -0
  45. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/supabase_config.py +0 -0
  46. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/client/supabase_manager.py +0 -0
  47. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  48. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/MODULE_README.md +0 -0
  49. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  50. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/TASKS.md +0 -0
  51. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/__init__.py +0 -0
  52. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/async_db_manager.py +0 -0
  53. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/config.py +0 -0
  54. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/connections.py +0 -0
  55. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/db_function.py +0 -0
  56. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/diagnostics.py +0 -0
  57. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/expressions.py +0 -0
  58. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/extended.py +0 -0
  59. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/fields.py +0 -0
  60. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/introspection.py +0 -0
  61. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/listen.py +0 -0
  62. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/loop_filters.py +0 -0
  63. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/model_dto.py +0 -0
  64. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/model_view.py +0 -0
  65. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/paginator.py +0 -0
  66. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/pool_maintenance.py +0 -0
  67. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/pydantic_bridge.py +0 -0
  68. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/registry.py +0 -0
  69. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/relations.py +0 -0
  70. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/resilience.py +0 -0
  71. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/rls_session.py +0 -0
  72. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/signals.py +0 -0
  73. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/tls.py +0 -0
  74. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/transaction.py +0 -0
  75. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/types.py +0 -0
  76. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/core/write_queue.py +0 -0
  77. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/entity.py +0 -0
  78. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/error_handling.py +0 -0
  79. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/exceptions.py +0 -0
  80. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/extended/__init__.py +0 -0
  81. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/extended/app_error_handler.py +0 -0
  82. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/identity_rule.py +0 -0
  83. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/middleware/__init__.py +0 -0
  84. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/middleware/base.py +0 -0
  85. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/MODULE_README.md +0 -0
  86. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/__init__.py +0 -0
  87. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/cli.py +0 -0
  88. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/ddl.py +0 -0
  89. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/diff.py +0 -0
  90. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/executor.py +0 -0
  91. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/integration.py +0 -0
  92. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/loader.py +0 -0
  93. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/operations.py +0 -0
  94. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/state.py +0 -0
  95. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/migrations/table_filter.py +0 -0
  96. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/MODULE_README.md +0 -0
  97. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/__init__.py +0 -0
  98. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/bulk_update_values.py +0 -0
  99. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/conflict.py +0 -0
  100. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/conflict_writes.py +0 -0
  101. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/create.py +0 -0
  102. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/db_functions.py +0 -0
  103. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/delete.py +0 -0
  104. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/dynamic_admin.py +0 -0
  105. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/dynamic_crud.py +0 -0
  106. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/graph_walk.py +0 -0
  107. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/latest_rows.py +0 -0
  108. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/queue_claim.py +0 -0
  109. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/read.py +0 -0
  110. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/relational_backfill.py +0 -0
  111. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/staging_load.py +0 -0
  112. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/operations/update.py +0 -0
  113. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/py.typed +0 -0
  114. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/MODULE_README.md +0 -0
  115. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/__init__.py +0 -0
  116. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/db_objects.py +0 -0
  117. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  118. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  119. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/query/MODULE_README.md +0 -0
  120. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/query/__init__.py +0 -0
  121. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/query/executor.py +0 -0
  122. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  123. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/__init__.py +0 -0
  124. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/code_handler.py +0 -0
  125. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  126. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/columns.py +0 -0
  127. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/common.py +0 -0
  128. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/diff_preview.py +0 -0
  129. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  130. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/generator.py +0 -0
  131. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  132. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  133. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  134. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  135. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/package_wiring.py +0 -0
  136. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/relationships.py +0 -0
  137. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/runner.py +0 -0
  138. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/schema.py +0 -0
  139. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/schema_manager.py +0 -0
  140. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/tables.py +0 -0
  141. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/schema_builder/views.py +0 -0
  142. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/crypto.py +0 -0
  143. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/model.py +0 -0
  144. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/secrets_battery/service.py +0 -0
  145. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/README.md +0 -0
  146. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/__init__.py +0 -0
  147. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/coalesce.py +0 -0
  148. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/dag.py +0 -0
  149. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/errors.py +0 -0
  150. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/fallback.py +0 -0
  151. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/flush.py +0 -0
  152. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/lifecycle.py +0 -0
  153. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/managed.py +0 -0
  154. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/op.py +0 -0
  155. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/reads.py +0 -0
  156. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/session.py +0 -0
  157. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/session/telemetry.py +0 -0
  158. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  159. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/__init__.py +0 -0
  160. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/executor.py +0 -0
  161. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/queries.py +0 -0
  162. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/registry.py +0 -0
  163. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/types.py +0 -0
  164. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/sql_executor/utils.py +0 -0
  165. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/state.py +0 -0
  166. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/utils/__init__.py +0 -0
  167. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/utils/sql_utils.py +0 -0
  168. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/matrx_orm/utils/type_converters.py +0 -0
  169. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/scripts/git-branches.sh +0 -0
  170. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/scripts/publish.sh +0 -0
  171. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/scripts/release.sh +0 -0
  172. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/MODULE_README.md +0 -0
  173. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/__init__.py +0 -0
  174. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/conftest.py +0 -0
  175. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/MODULE_README.md +0 -0
  176. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/__init__.py +0 -0
  177. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_admin_db_columns.py +0 -0
  178. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_admin_search_negation.py +0 -0
  179. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_admin_search_sentinels.py +0 -0
  180. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_advisory_lock.py +0 -0
  181. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_agent_message.py +0 -0
  182. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_api_auth.py +0 -0
  183. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_api_config.py +0 -0
  184. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_api_handlers.py +0 -0
  185. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_api_protocol.py +0 -0
  186. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_array_agg_order_by.py +0 -0
  187. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_associations.py +0 -0
  188. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_bulk_update_by_pk.py +0 -0
  189. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  190. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_cache_debug.py +0 -0
  191. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_call_function.py +0 -0
  192. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_case_when.py +0 -0
  193. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_catalog.py +0 -0
  194. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_composite_pk_filter.py +0 -0
  195. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_composite_pk_write_paths.py +0 -0
  196. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_config.py +0 -0
  197. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_conflict_target.py +0 -0
  198. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_conflict_writes.py +0 -0
  199. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_connection_codecs.py +0 -0
  200. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_connection_poison_guard.py +0 -0
  201. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_connection_primitives.py +0 -0
  202. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_count_composite_distinct.py +0 -0
  203. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_db_function_field.py +0 -0
  204. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_ddl_generator.py +0 -0
  205. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_derived_table.py +0 -0
  206. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_dirty_tracking.py +0 -0
  207. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_dynamic_crud.py +0 -0
  208. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_exceptions.py +0 -0
  209. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_expression_primitives.py +0 -0
  210. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_fields.py +0 -0
  211. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  212. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_from_alias.py +0 -0
  213. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_fts.py +0 -0
  214. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_func_expression_args.py +0 -0
  215. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_graph_walk.py +0 -0
  216. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_having_annotate_alias.py +0 -0
  217. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  218. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_introspect_rls_policies.py +0 -0
  219. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_join_and_aggregate.py +0 -0
  220. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_latest_rows.py +0 -0
  221. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_listen_notify.py +0 -0
  222. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_managed_write_guard.py +0 -0
  223. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_matrx_entity.py +0 -0
  224. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_migration_diff_types.py +0 -0
  225. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_migration_loader.py +0 -0
  226. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_model_instance.py +0 -0
  227. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_model_meta.py +0 -0
  228. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_order_by_expression.py +0 -0
  229. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_patch_jsonb_path.py +0 -0
  230. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_pool_loop_recreation.py +0 -0
  231. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_pool_maintenance.py +0 -0
  232. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_postgrest_filters.py +0 -0
  233. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_pydantic_bridge.py +0 -0
  234. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_query_error_detail.py +0 -0
  235. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_query_executor_sql.py +0 -0
  236. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_query_timeout.py +0 -0
  237. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  238. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_queue_claim.py +0 -0
  239. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_registry.py +0 -0
  240. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_registry_multi_database.py +0 -0
  241. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_registry_multi_schema.py +0 -0
  242. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_relational_backfill.py +0 -0
  243. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_relations.py +0 -0
  244. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_resilience.py +0 -0
  245. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_schema_exists.py +0 -0
  246. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_session_advisory_lock.py +0 -0
  247. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_state_cache.py +0 -0
  248. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_subquery_filter_raw.py +0 -0
  249. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_subquery_in_filter.py +0 -0
  250. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_supabase_auth.py +0 -0
  251. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_supabase_config.py +0 -0
  252. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_tls.py +0 -0
  253. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_typed_join_projection.py +0 -0
  254. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_typed_predicates.py +0 -0
  255. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_update_case_expression.py +0 -0
  256. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_update_subquery.py +0 -0
  257. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_upsert_default_leak.py +0 -0
  258. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_upsert_with_conflict.py +0 -0
  259. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_values_jsonb_decode.py +0 -0
  260. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_watched_lifecycle.py +0 -0
  261. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level1/test_write_retry_semantics.py +0 -0
  262. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/MODULE_README.md +0 -0
  263. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/__init__.py +0 -0
  264. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/conftest.py +0 -0
  265. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_bulk_ops.py +0 -0
  266. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_cache_integration.py +0 -0
  267. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_crud.py +0 -0
  268. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_foreign_keys.py +0 -0
  269. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_m2m.py +0 -0
  270. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_manager.py +0 -0
  271. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_migrations_live.py +0 -0
  272. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_query_execution.py +0 -0
  273. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/level2/test_schema_diff.py +0 -0
  274. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/.env.example +0 -0
  275. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/README.md +0 -0
  276. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/__init__.py +0 -0
  277. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/generate.py +0 -0
  278. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/generated/.gitkeep +0 -0
  279. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/matrx_orm.yaml +0 -0
  280. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project/test_schema_generation.py +0 -0
  281. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/.env.example +0 -0
  282. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/README.md +0 -0
  283. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/__init__.py +0 -0
  284. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/client_example.py +0 -0
  285. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/client_example.ts +0 -0
  286. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  287. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/sample_project_desktop/server.py +0 -0
  288. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/entity_tests.py +0 -0
  289. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_base_generation.py +0 -0
  290. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  291. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_composite_unique_generation.py +0 -0
  292. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_dto_identity_generation.py +0 -0
  293. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_entity_capability_generation.py +0 -0
  294. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_generate_schema.py +0 -0
  295. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_junction_analysis.py +0 -0
  296. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_multi_schema_output.py +0 -0
  297. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_sql_expression_defaults.py +0 -0
  298. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema/test_view_generation.py +0 -0
  299. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  300. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/__init__.py +0 -0
  301. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/conftest.py +0 -0
  302. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_cache_no_downgrade.py +0 -0
  303. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_capture_poison_proof.py +0 -0
  304. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_coalesce.py +0 -0
  305. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_dag.py +0 -0
  306. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_disk_spill_fallback.py +0 -0
  307. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_flush_individual_fallback.py +0 -0
  308. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_governed_write.py +0 -0
  309. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_managed.py +0 -0
  310. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_op_enqueue_site.py +0 -0
  311. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_reads.py +0 -0
  312. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/session/test_session.py +0 -0
  313. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_database_name_alias.py +0 -0
  314. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_db_functions.py +0 -0
  315. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_model_cls_refactor.py +0 -0
  316. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  317. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_sql_param_casts.py +0 -0
  318. {matrx_orm-3.1.39 → matrx_orm-3.1.41}/tests/test_sync_twins_guard.py +0 -0
@@ -32,6 +32,9 @@ Every table that becomes a Model needs row identity: a single-column primary key
32
32
  subqueries. SQL-shaped strings fail closed: plain strings name validated
33
33
  columns only; compose casts, JSON paths, functions, ordering, joins, partial
34
34
  conflict targets, and filtered aggregates with the exported typed objects.
35
+ Large reconciliation/export scans call `.all(use_cache=False)` so returned
36
+ models do not quadratically populate the process identity cache. Contract:
37
+ [`matrx_orm/query/FEATURE.md`](matrx_orm/query/FEATURE.md).
35
38
  - **Migrations**: `MigrationDB`, `MigrationLoader`, `MigrationExecutor`, `makemigrations`, `migrate` — migrations declare explicit `dependencies` and apply in topological order (no multi-head/merge support yet).
36
39
  - **Admin router** (FastAPI): `admin_router` exposes read/write endpoints for any registered model.
37
40
  - **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.39
3
+ Version: 3.1.41
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
@@ -1292,17 +1292,17 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
1292
1292
  return run_sync(cls.delete_where(**filters))
1293
1293
 
1294
1294
  @classmethod
1295
- async def all(cls) -> list[Self]:
1295
+ async def all(cls, *, use_cache: bool = True) -> list[Self]:
1296
+ """Fetch every row, optionally bypassing identity-cache population."""
1297
+
1296
1298
  try:
1297
- results: list[Self] = await QueryBuilder(model_cls=cls).all()
1298
- await StateManager.cache_bulk(model_class=cls, records=results)
1299
- return results
1299
+ return await QueryBuilder(model_cls=cls).all(use_cache=use_cache)
1300
1300
  except ORMException as e:
1301
1301
  e.enrich(model=cls, operation="all")
1302
1302
  raise
1303
1303
 
1304
1304
  @classmethod
1305
- def all_sync(cls) -> list[Self]:
1305
+ def all_sync(cls, *, use_cache: bool = True) -> list[Self]:
1306
1306
  """
1307
1307
  Synchronous wrapper for all().
1308
1308
  Use this in synchronous contexts to fetch all results without async/await.
@@ -1316,7 +1316,7 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
1316
1316
  if "no running event loop" not in str(e):
1317
1317
  raise
1318
1318
 
1319
- return run_sync(cls.all())
1319
+ return run_sync(cls.all(use_cache=use_cache))
1320
1320
 
1321
1321
  async def save(self, **kwargs: Any) -> Self:
1322
1322
  """Save the current state of the model instance."""
@@ -0,0 +1,17 @@
1
+ # Query execution
2
+
3
+ `QueryBuilder` is the typed terminal-read and write layer behind every model.
4
+ Application packages compose queries here; they never bypass it with SQL.
5
+
6
+ ## Large scans and the identity cache
7
+
8
+ `.all()` populates `StateManager` by default so ordinary repeated identity reads
9
+ remain fast. A large reconciliation, export, or historical scan must call
10
+ `.all(use_cache=False)`. This still returns fully hydrated models and still
11
+ merges pending session state, but it does not insert every result into the
12
+ process identity cache.
13
+
14
+ `Model.all(use_cache=False)` exposes the same behavior. The default remains
15
+ `True` for compatibility. The cache bypass controls only cache population; it
16
+ does not weaken database authorization, filters, hydration, or transaction
17
+ semantics.
@@ -778,7 +778,7 @@ class QueryBuilder(Generic[ModelT]):
778
778
  if len(pks) == 1 and not self.excludes and set(combined) <= {pks[0], f"{pks[0]}__in"}:
779
779
  values = combined.get(pks[0])
780
780
  in_values = combined.get(f"{pks[0]}__in")
781
- targets = list(in_values) if isinstance(in_values, (list, tuple, set)) else []
781
+ targets = list(in_values) if isinstance(in_values, list | tuple | set) else []
782
782
  if values is not None:
783
783
  targets.append(values)
784
784
  if targets and StateManager.is_registered(self.model):
@@ -802,9 +802,12 @@ class QueryBuilder(Generic[ModelT]):
802
802
  # Terminal async methods
803
803
  # ------------------------------------------------------------------
804
804
 
805
- async def all(self) -> list[ModelT]:
805
+ async def all(self, *, use_cache: bool = True) -> list[ModelT]:
806
806
  """Fetch all matching records.
807
807
 
808
+ Pass ``use_cache=False`` for large scans that must not populate the
809
+ process identity cache.
810
+
808
811
  Session-aware: if an active Session has pending writes for this
809
812
  Model, the result merges pending state with DB rows. Pending
810
813
  INSERTs matching the filter appear in the result; pending UPDATEs
@@ -814,10 +817,12 @@ class QueryBuilder(Generic[ModelT]):
814
817
  try:
815
818
  executor = self._get_executor()
816
819
  results = await executor.all()
817
- await StateManager.cache_bulk(self.model, results)
820
+ if use_cache:
821
+ await StateManager.cache_bulk(self.model, results)
818
822
  if debug:
819
823
  vcprint(
820
- f"[BUILDER] all() cached {len(results)} records for {self.model.__name__}",
824
+ f"[BUILDER] all() fetched {len(results)} records for "
825
+ f"{self.model.__name__} (cache={use_cache})",
821
826
  color="bright_gold",
822
827
  )
823
828
  return _merge_pending_into_models(
@@ -37,6 +37,25 @@ row whose owner columns don't match its item's owner.
37
37
  `editable=false` blocks human value changes (`FieldNotEditableError`); the owning
38
38
  service rotates via `system_rotation=True` / `rotate_credential_fields`.
39
39
 
40
+ **`inject_into_sandbox=true` REQUIRES an env alias — refused otherwise.** A
41
+ container environment is a NAME→value map, so a field with `key IS NULL` has
42
+ nowhere to land and bulk resolution drops it. Accepting the flag anyway meant a
43
+ user could switch "inject into sandbox" on and get nothing, forever, with no
44
+ error — the silent-failure class this guard ends. Three layers, each sufficient
45
+ alone, each loud:
46
+
47
+ 1. **Write refusal** — `create_credential_bundle` / `add_credential_field` /
48
+ `update_field_metadata` raise `SandboxInjectionWithoutEnvKeyError` (a
49
+ `ValueError`, so hosts answer 422 with the reason). `update_field_metadata`
50
+ judges the POST-patch state, so *clearing the alias of an already-injecting
51
+ field* is refused the same way. Setting `env_key` and the flag in ONE patch
52
+ is the supported fix and always passes.
53
+ 2. **Resolution scream** — `_keyed_for_sandbox` logs an ERROR naming the row ids
54
+ and skips them, so anything that got in around layer 1 is visible.
55
+ 3. **DB CHECK** `user_secrets_sandbox_injection_needs_key` (migration 0265) makes
56
+ the state unrepresentable; the column default moved `true` → `false` so the
57
+ database stops manufacturing violations on keyless inserts.
58
+
40
59
  Item surface: `create_credential_item` · `create_credential_bundle` (item + N
41
60
  fields, ONE transaction) · `get/list/update/delete_credential_item` (delete =
42
61
  soft, atomic across item + fields + grants) · `add_credential_field` ·
@@ -118,7 +137,10 @@ EXCLUDES `sealed` fields, so a browser-facing caller can never receive sealed
118
137
  plaintext through an env dump. Only trusted execution (service-token sandbox
119
138
  injection, in-process context hydration, provider adapters) passes `True`.
120
139
  Rows with `key IS NULL` (item fields without an env alias) never resolve into
121
- an environment in either mode.
140
+ an environment in either mode — silently when nothing promised otherwise (a
141
+ keyless item field is normal), but **LOUDLY when `for_sandbox=True`**: such a row
142
+ is flagged for injection and cannot deliver, which is a defect (see the
143
+ handling doctrine above).
122
144
 
123
145
  **Legacy key-based writes respect item discipline:** `create_user_secret(upsert=True)`,
124
146
  `update_user_secret`, and `delete_user_secret` REFUSE a target row that is
@@ -216,6 +238,7 @@ create, reveal, sealed denial, editable gate, rotate, item grants, soft delete).
216
238
 
217
239
  ## Change Log
218
240
 
241
+ - 2026-07-26 — Closed the silent sandbox-injection gap: `SandboxInjectionWithoutEnvKeyError` at every field write path, a loud ERROR-and-skip in `for_sandbox` bulk resolution, and DB CHECK `user_secrets_sandbox_injection_needs_key` (+ column default `true`→`false`). Live at the time of the fix: 0 broken rows, but 61 keyless item fields were one toggle away from it.
219
242
  - 2026-07-23 — `update_field_metadata` primitive (metadata-only, version-preserving; `users.bump_user_secret_value_version` verified value-conditional live — no trigger change needed).
220
243
  - 2026-07-23 — Adversarial-review hardening: include_sealed-aware bulk env resolution (+ keyless-row exclusion), ItemManagedSecretError legacy-write guards + legacy audit rows, ItemGrantSpec per-grantee grants, reveal liveness filters, rotate liveness re-check + audits inside owning transactions.
221
244
  - 2026-07-23 — Phase 2: item layer (`items.py` — bundles, handling modes, stable resolution, item grants, transfer/fork), rotation primitives (`fernet_for_key`/`multi_fernet_for_keys`) + `scripts/rotate_credentials_key.py` (closes the no-rotation-script ticket), migration 0236 grants fixup (item-level grants possible).
@@ -48,6 +48,7 @@ from .items import (
48
48
  FieldNotEditableError,
49
49
  FieldSpec,
50
50
  ItemGrantSpec,
51
+ SandboxInjectionWithoutEnvKeyError,
51
52
  SealedFieldError,
52
53
  add_credential_field,
53
54
  add_item_grant,
@@ -150,6 +151,7 @@ __all__ = [
150
151
  "FieldNotEditableError",
151
152
  "FieldSpec",
152
153
  "ItemGrantSpec",
154
+ "SandboxInjectionWithoutEnvKeyError",
153
155
  "SealedFieldError",
154
156
  "URI_MATCH_MODES",
155
157
  "add_credential_field",
@@ -69,6 +69,18 @@ class FieldNotEditableError(PermissionError):
69
69
  (``rotate_credential_fields``), never by hand."""
70
70
 
71
71
 
72
+ class SandboxInjectionWithoutEnvKeyError(ValueError):
73
+ """``inject_into_sandbox=true`` on a field that has no environment-variable
74
+ alias (``key``). A container environment is a NAME→value map: with no name
75
+ the value has nowhere to land, so the flag is a promise the platform cannot
76
+ keep — the field would silently never appear in any sandbox.
77
+
78
+ Refused LOUDLY at every write path instead of accepted-and-dropped (hosts
79
+ map ``ValueError`` → 422). Set an env alias in the same request (the vault
80
+ field-metadata patch takes ``env_key`` and ``inject_into_sandbox``
81
+ together) or leave injection off."""
82
+
83
+
72
84
  # ── Shapes ────────────────────────────────────────────────────────────────
73
85
 
74
86
 
@@ -175,6 +187,26 @@ def _validate_handling(handling: str) -> None:
175
187
  raise ValueError(f"handling must be one of {HANDLING_MODES}; got {handling!r}")
176
188
 
177
189
 
190
+ def _validate_sandbox_injectable(
191
+ *,
192
+ field_key: str | None,
193
+ env_key: str | None,
194
+ inject_into_sandbox: bool,
195
+ ) -> None:
196
+ """LAYER 1 of the no-silent-injection guard: a field may only be flagged
197
+ for sandbox injection while it carries an env alias. See
198
+ :class:`SandboxInjectionWithoutEnvKeyError`. Layer 2 is the loud skip in
199
+ ``org_service`` bulk resolution; layer 3 is the DB CHECK
200
+ ``user_secrets_sandbox_injection_needs_key``."""
201
+ if inject_into_sandbox and not env_key:
202
+ raise SandboxInjectionWithoutEnvKeyError(
203
+ f"field {field_key or '<unnamed>'!r} cannot be injected into sandboxes without "
204
+ "an environment-variable alias (env_key) — a container env has no name to bind "
205
+ "the value to, so the flag would silently do nothing. Set env_key in the same "
206
+ "request, or leave inject_into_sandbox off."
207
+ )
208
+
209
+
178
210
  def _validate_uri_match_mode(mode: str) -> None:
179
211
  if mode not in URI_MATCH_MODES:
180
212
  raise ValueError(f"uri_match_mode must be one of {URI_MATCH_MODES}; got {mode!r}")
@@ -457,6 +489,11 @@ async def create_credential_bundle(
457
489
  for spec in fields:
458
490
  _validate_field_key(spec.field_key)
459
491
  _validate_handling(spec.handling)
492
+ _validate_sandbox_injectable(
493
+ field_key=spec.field_key,
494
+ env_key=spec.env_key,
495
+ inject_into_sandbox=spec.inject_into_sandbox,
496
+ )
460
497
  if not isinstance(spec.value, str):
461
498
  raise ValueError(f"field {spec.field_key!r}: value must be a string")
462
499
  if spec.field_key in seen:
@@ -667,6 +704,11 @@ async def add_credential_field(
667
704
  the item so field-level scoping stays consistent)."""
668
705
  _validate_field_key(spec.field_key)
669
706
  _validate_handling(spec.handling)
707
+ _validate_sandbox_injectable(
708
+ field_key=spec.field_key,
709
+ env_key=spec.env_key,
710
+ inject_into_sandbox=spec.inject_into_sandbox,
711
+ )
670
712
  if not isinstance(spec.value, str):
671
713
  raise ValueError("value must be a string")
672
714
  raw = await _require_item_raw(item_id)
@@ -837,6 +879,19 @@ async def update_field_metadata(
837
879
  "a one-way door; rotate/replace the field instead of unsealing it"
838
880
  )
839
881
 
882
+ # The POST-patch state is what matters: enabling injection on a keyless
883
+ # field AND clearing the alias of an already-injecting field are the
884
+ # same silent breakage from opposite directions. Both are refused.
885
+ _validate_sandbox_injectable(
886
+ field_key=raw.get("field_key") or raw.get("key"),
887
+ env_key=(raw.get("key") if env_key is _UNSET else env_key),
888
+ inject_into_sandbox=(
889
+ bool(raw.get("inject_into_sandbox"))
890
+ if inject_into_sandbox is None
891
+ else inject_into_sandbox
892
+ ),
893
+ )
894
+
840
895
  updates: dict[str, Any] = {"updated_by": actor_id, "updated_at": datetime.now(UTC)}
841
896
  if inject_into_sandbox is not None:
842
897
  updates["inject_into_sandbox"] = inject_into_sandbox
@@ -1457,6 +1512,7 @@ __all__ = [
1457
1512
  "FieldNotEditableError",
1458
1513
  "FieldSpec",
1459
1514
  "ItemGrantSpec",
1515
+ "SandboxInjectionWithoutEnvKeyError",
1460
1516
  "SealedFieldError",
1461
1517
  "add_credential_field",
1462
1518
  "create_credential_bundle",
@@ -458,6 +458,31 @@ async def delete_org_secret(secret_id: str, *, actor_id: str) -> None:
458
458
  )
459
459
 
460
460
 
461
+ def _keyed_for_sandbox(rows: list[dict[str, Any]], *, scope: str) -> list[dict[str, Any]]:
462
+ """LAYER 2 of the no-silent-injection guard (loud).
463
+
464
+ A row flagged ``inject_into_sandbox`` but carrying no env alias can never
465
+ land in a container environment — there is no name to bind it to. Every
466
+ write path REFUSES that combination
467
+ (:class:`~matrx_orm.secrets_battery.items.SandboxInjectionWithoutEnvKeyError`)
468
+ and a DB CHECK backs it, so anything arriving here got in around both:
469
+ direct SQL, an unguarded migration, or data that predates the guards.
470
+ Dropping it quietly is precisely the failure this exists to end."""
471
+ keyed = [r for r in rows if r.get("key")]
472
+ orphans = len(rows) - len(keyed)
473
+ if orphans:
474
+ logger.error(
475
+ "[secrets_battery] %d secret(s) in scope %s are flagged "
476
+ "inject_into_sandbox but have NO env alias (key IS NULL) — they can never reach "
477
+ "a sandbox and are being SKIPPED. This state is refused by every write path; "
478
+ "these rows bypassed it. ids=%s",
479
+ orphans,
480
+ scope,
481
+ sorted(str(r["id"]) for r in rows if not r.get("key")),
482
+ )
483
+ return keyed
484
+
485
+
461
486
  async def resolve_org_secrets(
462
487
  organization_id: str,
463
488
  *,
@@ -487,15 +512,21 @@ async def resolve_org_secrets(
487
512
  "organization_id": organization_id,
488
513
  "deleted_at__isnull": True,
489
514
  "is_active": True,
490
- "key__isnull": False,
491
515
  }
492
516
  if not include_sealed:
493
517
  filters["handling__ne"] = "sealed"
494
518
  if for_sandbox:
519
+ # Keyless rows are pulled deliberately here so the injection guard can
520
+ # SCREAM about them (a keyless field flagged for injection is a defect);
521
+ # everywhere else a keyless item field is normal and silently skipped.
495
522
  filters["inject_into_sandbox"] = True
523
+ else:
524
+ filters["key__isnull"] = False
496
525
  if keys:
497
526
  filters["key__in"] = keys
498
527
  rows = await UserSecret.filter(**filters).values("id", "key", "value_encrypted", "access_mode")
528
+ if for_sandbox:
529
+ rows = _keyed_for_sandbox(rows, scope=f"organization {organization_id}")
499
530
  if not rows:
500
531
  return {}
501
532
 
@@ -567,15 +598,19 @@ async def resolve_effective_secrets(
567
598
  "user_id": user_id,
568
599
  "deleted_at__isnull": True,
569
600
  "is_active": True,
570
- "key__isnull": False,
571
601
  }
572
602
  if not include_sealed:
573
603
  filters["handling__ne"] = "sealed"
574
604
  if for_sandbox:
605
+ # See resolve_org_secrets: keyless rows are pulled so the guard screams.
575
606
  filters["inject_into_sandbox"] = True
607
+ else:
608
+ filters["key__isnull"] = False
576
609
  if keys:
577
610
  filters["key__in"] = keys
578
- rows = await UserSecret.filter(**filters).values("key", "value_encrypted")
611
+ rows = await UserSecret.filter(**filters).values("id", "key", "value_encrypted")
612
+ if for_sandbox:
613
+ rows = _keyed_for_sandbox(rows, scope=f"user {user_id}")
579
614
  for row in rows:
580
615
  try:
581
616
  resolved[str(row["key"])] = decrypt_value(row["value_encrypted"])
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.39"
3
+ version = "3.1.41"
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" }
@@ -1,11 +1,15 @@
1
1
  """Level 1: QueryBuilder — filter, exclude, order_by, limit, offset, select, _build_query."""
2
2
 
3
+ from types import SimpleNamespace
4
+ from unittest.mock import AsyncMock
5
+
3
6
  import pytest
4
7
  from matrx_orm.core.base import Model, ModelMeta
5
8
  from matrx_orm.core.fields import CharField, IntegerField, UUIDField
6
9
  from matrx_orm.core.registry import ModelRegistry
7
10
  from matrx_orm.exceptions import DoesNotExist, PermissionDeniedError
8
11
  from matrx_orm.query.builder import QueryBuilder
12
+ from matrx_orm.state import StateManager
9
13
 
10
14
 
11
15
  @pytest.fixture(autouse=True)
@@ -85,6 +89,20 @@ class TestQueryBuilderChaining:
85
89
  qb = QueryBuilder(M).filter()
86
90
  assert qb.filters == []
87
91
 
92
+ @pytest.mark.asyncio
93
+ async def test_all_can_skip_identity_cache_for_large_scans(self, monkeypatch):
94
+ M = _make_model()
95
+ rows = [M(id="00000000-0000-0000-0000-000000000001", name="Alice")]
96
+ executor = SimpleNamespace(all=AsyncMock(return_value=rows))
97
+ cache_bulk = AsyncMock()
98
+ monkeypatch.setattr(QueryBuilder, "_get_executor", lambda _self: executor)
99
+ monkeypatch.setattr(StateManager, "cache_bulk", cache_bulk)
100
+
101
+ result = await QueryBuilder(M).all(use_cache=False)
102
+
103
+ assert result == rows
104
+ cache_bulk.assert_not_awaited()
105
+
88
106
 
89
107
  class TestBuildQuery:
90
108
  def test_build_query_basic(self):
@@ -246,6 +246,199 @@ async def test_resolve_effective_secrets_personal_branch_is_handling_aware(monke
246
246
  assert filters["key__isnull"] is False
247
247
 
248
248
 
249
+ # ── The no-silent-sandbox-injection guard ─────────────────────────────────
250
+ #
251
+ # A field flagged inject_into_sandbox with no env alias (key IS NULL) can never
252
+ # reach a container environment — the resolver has no name to bind it to. Before
253
+ # these guards the flag was accepted and the row silently dropped, so a user
254
+ # could switch "inject into sandbox" on and get nothing, forever, with no error.
255
+ # Layer 1 = write refusal (below), layer 2 = loud skip at resolution,
256
+ # layer 3 = DB CHECK user_secrets_sandbox_injection_needs_key.
257
+
258
+
259
+ def test_injection_without_env_key_is_refused():
260
+ from matrx_orm.secrets_battery import SandboxInjectionWithoutEnvKeyError
261
+ from matrx_orm.secrets_battery.items import _validate_sandbox_injectable
262
+
263
+ with pytest.raises(SandboxInjectionWithoutEnvKeyError, match="env"):
264
+ _validate_sandbox_injectable(field_key="api_key", env_key=None, inject_into_sandbox=True)
265
+ with pytest.raises(SandboxInjectionWithoutEnvKeyError):
266
+ _validate_sandbox_injectable(field_key="api_key", env_key="", inject_into_sandbox=True)
267
+
268
+
269
+ def test_injection_with_env_key_is_allowed():
270
+ from matrx_orm.secrets_battery.items import _validate_sandbox_injectable
271
+
272
+ _validate_sandbox_injectable(
273
+ field_key="api_key", env_key="ACME_API_KEY", inject_into_sandbox=True
274
+ )
275
+ # Keyless is perfectly normal as long as nothing promised injection.
276
+ _validate_sandbox_injectable(field_key="password", env_key=None, inject_into_sandbox=False)
277
+
278
+
279
+ def test_injection_error_is_a_value_error_so_hosts_answer_422():
280
+ """The vault router maps ValueError → 422 with the message; a user who
281
+ flips the switch must SEE why, not get a silent 200."""
282
+ from matrx_orm.secrets_battery import SandboxInjectionWithoutEnvKeyError
283
+
284
+ assert issubclass(SandboxInjectionWithoutEnvKeyError, ValueError)
285
+
286
+
287
+ @pytest.mark.asyncio
288
+ async def test_create_credential_bundle_refuses_injection_without_env_key():
289
+ from matrx_orm.secrets_battery import (
290
+ FieldSpec,
291
+ SandboxInjectionWithoutEnvKeyError,
292
+ create_credential_bundle,
293
+ )
294
+
295
+ # Validation runs before any DB work, so this needs no database.
296
+ with pytest.raises(SandboxInjectionWithoutEnvKeyError):
297
+ await create_credential_bundle(
298
+ user_id="u1",
299
+ display_name="Acme",
300
+ fields=[FieldSpec(field_key="api_key", value="v", inject_into_sandbox=True)],
301
+ )
302
+
303
+
304
+ def _noop_transaction(_name):
305
+ class _T:
306
+ async def __aenter__(self):
307
+ return self
308
+
309
+ async def __aexit__(self, *exc):
310
+ return False
311
+
312
+ return _T()
313
+
314
+
315
+ @pytest.mark.parametrize(
316
+ "stored,patch",
317
+ [
318
+ # The FE's exact flow: flip the switch on a keyless item field.
319
+ ({"key": None, "inject_into_sandbox": False}, {"inject_into_sandbox": True}),
320
+ # The same breakage from the other direction: clear the alias of a
321
+ # field that is already injecting.
322
+ ({"key": "ACME_KEY", "inject_into_sandbox": True}, {"env_key": None}),
323
+ ],
324
+ )
325
+ @pytest.mark.asyncio
326
+ async def test_update_field_metadata_refuses_a_no_op_injection(monkeypatch, stored, patch):
327
+ from matrx_orm.secrets_battery import SandboxInjectionWithoutEnvKeyError, items
328
+
329
+ row = {
330
+ "id": "f1",
331
+ "credential_item_id": "i1",
332
+ "field_key": "api_key",
333
+ "handling": "revealable",
334
+ "editable": True,
335
+ "value_hint": "…",
336
+ "value_version": 1,
337
+ "is_active": True,
338
+ "description": None,
339
+ "created_at": "",
340
+ "updated_at": "",
341
+ "user_id": "u1",
342
+ "organization_id": None,
343
+ **stored,
344
+ }
345
+ monkeypatch.setattr(items, "UserSecret", _fake_user_secret([row], []))
346
+ monkeypatch.setattr(items, "transaction", _noop_transaction)
347
+ monkeypatch.setattr(items, "record_secret_audit", _noop_audit)
348
+
349
+ with pytest.raises(SandboxInjectionWithoutEnvKeyError):
350
+ await items.update_field_metadata("f1", actor_id="u1", **patch)
351
+
352
+
353
+ @pytest.mark.asyncio
354
+ async def test_update_field_metadata_allows_alias_and_injection_together(monkeypatch):
355
+ """The refusal must never be a dead end: naming the alias and enabling
356
+ injection in ONE patch is the supported fix, so it has to pass."""
357
+ from matrx_orm.secrets_battery import items
358
+
359
+ row = {
360
+ "id": "f1",
361
+ "credential_item_id": "i1",
362
+ "field_key": "api_key",
363
+ "key": None,
364
+ "handling": "revealable",
365
+ "editable": True,
366
+ "inject_into_sandbox": False,
367
+ "value_hint": "…",
368
+ "value_version": 1,
369
+ "is_active": True,
370
+ "description": None,
371
+ "created_at": "",
372
+ "updated_at": "",
373
+ "user_id": "u1",
374
+ "organization_id": None,
375
+ }
376
+ class _FakeUserSecret:
377
+ @classmethod
378
+ def filter(cls, **kw):
379
+ # The env-alias uniqueness probe must come back empty.
380
+ return _FakeQB([] if kw.get("key") == "ACME_KEY" else [row])
381
+
382
+ @classmethod
383
+ async def update_where(cls, *a, **kw):
384
+ class _R:
385
+ rows_affected = 1
386
+
387
+ return _R()
388
+
389
+ monkeypatch.setattr(items, "UserSecret", _FakeUserSecret)
390
+ monkeypatch.setattr(items, "transaction", _noop_transaction)
391
+ monkeypatch.setattr(items, "record_secret_audit", _noop_audit)
392
+
393
+ out = await items.update_field_metadata(
394
+ "f1", actor_id="u1", env_key="ACME_KEY", inject_into_sandbox=True
395
+ )
396
+ assert out.field_key == "api_key"
397
+
398
+
399
+ @pytest.mark.asyncio
400
+ async def test_resolve_for_sandbox_screams_and_skips_keyless_rows(monkeypatch, caplog):
401
+ """Layer 2: a row that got around the write guards is SKIPPED LOUDLY."""
402
+ import logging
403
+
404
+ from matrx_orm.secrets_battery import org_service
405
+
406
+ rows = [
407
+ {"id": "orphan-1", "key": None, "value_encrypted": b"x"},
408
+ {"id": "keyed-1", "key": "GOOD_KEY", "value_encrypted": b"y"},
409
+ ]
410
+ captured: list[dict] = []
411
+ monkeypatch.setattr(org_service, "UserSecret", _fake_user_secret(rows, captured))
412
+ monkeypatch.setattr(org_service, "decrypt_value", lambda b: "plain")
413
+
414
+ with caplog.at_level(logging.ERROR, logger=org_service.logger.name):
415
+ out = await org_service.resolve_effective_secrets("u1", None, for_sandbox=True)
416
+
417
+ assert out == {"GOOD_KEY": "plain"}
418
+ # The keyless row is pulled (not filtered away) precisely so it can scream.
419
+ assert "key__isnull" not in captured[0]
420
+ assert captured[0]["inject_into_sandbox"] is True
421
+ assert any(
422
+ r.levelno >= logging.ERROR and "orphan-1" in r.getMessage() for r in caplog.records
423
+ ), "a keyless injectable row must produce a loud ERROR naming it"
424
+
425
+
426
+ @pytest.mark.asyncio
427
+ async def test_non_sandbox_resolution_still_drops_keyless_rows_silently(monkeypatch, caplog):
428
+ """Keyless item fields are NORMAL outside sandbox injection (61 live rows
429
+ at the time of writing) — they must not generate alarm noise."""
430
+ import logging
431
+
432
+ from matrx_orm.secrets_battery import org_service
433
+
434
+ captured: list[dict] = []
435
+ monkeypatch.setattr(org_service, "UserSecret", _fake_user_secret([], captured))
436
+ with caplog.at_level(logging.ERROR, logger=org_service.logger.name):
437
+ await org_service.resolve_effective_secrets("u1", None)
438
+ assert captured[0]["key__isnull"] is False
439
+ assert not [r for r in caplog.records if r.levelno >= logging.ERROR]
440
+
441
+
249
442
  # ── Sealed reveal refusal + liveness filters ──────────────────────────────
250
443
 
251
444
 
File without changes
File without changes
File without changes
File without changes