matrx-orm 3.1.72__tar.gz → 3.1.74__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/.gitignore +3 -1
  2. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/__init__.py +11 -0
  4. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/catalog.py +46 -0
  5. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/FEATURE.md +4 -0
  6. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/async_db_manager.py +44 -0
  7. matrx_orm-3.1.74/matrx_orm/core/pooler_role.py +290 -0
  8. matrx_orm-3.1.74/matrx_orm/schema_builder/FEATURE.md +12 -0
  9. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/columns.py +106 -3
  10. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/helpers/base_generators.py +15 -14
  11. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/pyproject.toml +1 -1
  12. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_catalog.py +17 -0
  13. matrx_orm-3.1.74/tests/live_pooler_role.py +104 -0
  14. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_base_generation.py +33 -0
  15. matrx_orm-3.1.74/tests/schema_builder/test_array_default_parsing.py +56 -0
  16. matrx_orm-3.1.74/tests/test_pooler_role_guard.py +142 -0
  17. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/AGENTS.md +0 -0
  18. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/BASE_CLASS_METHODS.md +0 -0
  19. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/CLAUDE.md +0 -0
  20. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/MODEL_API.md +0 -0
  21. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/README.md +0 -0
  22. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/database/orm/extended/managers/ai_model_base.py +0 -0
  23. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/docs/migrations.md +0 -0
  24. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/MODULE_README.md +0 -0
  25. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/MODULE_README.md +0 -0
  26. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/__init__.py +0 -0
  27. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/async_postgresql.py +0 -0
  28. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/base_adapter.py +0 -0
  29. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/postgresql.py +0 -0
  30. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  31. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/supabase_adapter.py +0 -0
  32. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/admin/__init__.py +0 -0
  33. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/admin/router.py +0 -0
  34. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/__init__.py +0 -0
  35. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/auth.py +0 -0
  36. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/config.py +0 -0
  37. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/handlers.py +0 -0
  38. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/protocol.py +0 -0
  39. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/server.py +0 -0
  40. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/associations.py +0 -0
  41. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/cache_debug.py +0 -0
  42. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/catalog_sql.py +0 -0
  43. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/MODULE_README.md +0 -0
  44. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/__init__.py +0 -0
  45. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/postgres_connection.py +0 -0
  46. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/postgrest.py +0 -0
  47. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/supabase_auth.py +0 -0
  48. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/supabase_config.py +0 -0
  49. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/supabase_manager.py +0 -0
  50. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  51. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/MODULE_README.md +0 -0
  52. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  53. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/TASKS.md +0 -0
  54. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/__init__.py +0 -0
  55. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/base.py +0 -0
  56. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/config.py +0 -0
  57. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/connections.py +0 -0
  58. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/db_function.py +0 -0
  59. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/diagnostics.py +0 -0
  60. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/expressions.py +0 -0
  61. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/extended.py +0 -0
  62. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/fields.py +0 -0
  63. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/introspection.py +0 -0
  64. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/listen.py +0 -0
  65. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/loop_filters.py +0 -0
  66. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/model_dto.py +0 -0
  67. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/model_view.py +0 -0
  68. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/one_database.py +0 -0
  69. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/paginator.py +0 -0
  70. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/pool_maintenance.py +0 -0
  71. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/pool_watch.py +0 -0
  72. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/pydantic_bridge.py +0 -0
  73. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/registry.py +0 -0
  74. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/relations.py +0 -0
  75. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/resilience.py +0 -0
  76. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/rls_session.py +0 -0
  77. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/signals.py +0 -0
  78. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/tls.py +0 -0
  79. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/transaction.py +0 -0
  80. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/types.py +0 -0
  81. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/write_queue.py +0 -0
  82. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/entity.py +0 -0
  83. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/error_handling.py +0 -0
  84. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/exceptions.py +0 -0
  85. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/extended/__init__.py +0 -0
  86. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/extended/app_error_handler.py +0 -0
  87. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/identity_rule.py +0 -0
  88. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/middleware/__init__.py +0 -0
  89. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/middleware/base.py +0 -0
  90. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/MODULE_README.md +0 -0
  91. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/__init__.py +0 -0
  92. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/cli.py +0 -0
  93. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/ddl.py +0 -0
  94. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/diff.py +0 -0
  95. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/executor.py +0 -0
  96. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/integration.py +0 -0
  97. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/loader.py +0 -0
  98. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/operations.py +0 -0
  99. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/state.py +0 -0
  100. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/table_filter.py +0 -0
  101. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/MODULE_README.md +0 -0
  102. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/__init__.py +0 -0
  103. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/bulk_update_values.py +0 -0
  104. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/conflict.py +0 -0
  105. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/conflict_writes.py +0 -0
  106. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/create.py +0 -0
  107. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/db_functions.py +0 -0
  108. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/delete.py +0 -0
  109. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/dynamic_admin.py +0 -0
  110. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/dynamic_crud.py +0 -0
  111. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/graph_walk.py +0 -0
  112. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/latest_rows.py +0 -0
  113. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/queue_claim.py +0 -0
  114. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/read.py +0 -0
  115. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/relational_backfill.py +0 -0
  116. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/staging_load.py +0 -0
  117. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/update.py +0 -0
  118. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/platform_db.py +0 -0
  119. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/py.typed +0 -0
  120. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/MODULE_README.md +0 -0
  121. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/__init__.py +0 -0
  122. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/db_objects.py +0 -0
  123. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  124. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  125. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/FEATURE.md +0 -0
  126. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/MODULE_README.md +0 -0
  127. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/__init__.py +0 -0
  128. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/builder.py +0 -0
  129. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/executor.py +0 -0
  130. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  131. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/__init__.py +0 -0
  132. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/code_handler.py +0 -0
  133. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  134. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/common.py +0 -0
  135. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/diff_preview.py +0 -0
  136. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  137. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/generator.py +0 -0
  138. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  139. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  140. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  141. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/package_wiring.py +0 -0
  142. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/relationships.py +0 -0
  143. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/runner.py +0 -0
  144. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/schema.py +0 -0
  145. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/schema_manager.py +0 -0
  146. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/tables.py +0 -0
  147. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/views.py +0 -0
  148. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  149. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/__init__.py +0 -0
  150. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/attachments.py +0 -0
  151. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/crypto.py +0 -0
  152. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/items.py +0 -0
  153. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/model.py +0 -0
  154. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/org_service.py +0 -0
  155. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/service.py +0 -0
  156. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/README.md +0 -0
  157. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/__init__.py +0 -0
  158. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/coalesce.py +0 -0
  159. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/dag.py +0 -0
  160. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/errors.py +0 -0
  161. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/fallback.py +0 -0
  162. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/flush.py +0 -0
  163. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/lifecycle.py +0 -0
  164. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/managed.py +0 -0
  165. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/op.py +0 -0
  166. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/reads.py +0 -0
  167. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/session.py +0 -0
  168. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/telemetry.py +0 -0
  169. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  170. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/__init__.py +0 -0
  171. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/executor.py +0 -0
  172. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/queries.py +0 -0
  173. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/registry.py +0 -0
  174. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/types.py +0 -0
  175. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/utils.py +0 -0
  176. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/state.py +0 -0
  177. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/utils/__init__.py +0 -0
  178. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/utils/sql_utils.py +0 -0
  179. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/utils/type_converters.py +0 -0
  180. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/scripts/git-branches.sh +0 -0
  181. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/scripts/publish.sh +0 -0
  182. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/scripts/release.sh +0 -0
  183. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/MODULE_README.md +0 -0
  184. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/__init__.py +0 -0
  185. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/conftest.py +0 -0
  186. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/MODULE_README.md +0 -0
  187. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/__init__.py +0 -0
  188. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_admin_db_columns.py +0 -0
  189. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_admin_search_negation.py +0 -0
  190. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_admin_search_sentinels.py +0 -0
  191. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_advisory_lock.py +0 -0
  192. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_agent_message.py +0 -0
  193. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_api_auth.py +0 -0
  194. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_api_config.py +0 -0
  195. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_api_handlers.py +0 -0
  196. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_api_protocol.py +0 -0
  197. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_array_agg_order_by.py +0 -0
  198. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_associations.py +0 -0
  199. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_bulk_hydration_offload.py +0 -0
  200. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_bulk_update_by_pk.py +0 -0
  201. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  202. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_cache_debug.py +0 -0
  203. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_call_function.py +0 -0
  204. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_case_when.py +0 -0
  205. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_composite_pk_filter.py +0 -0
  206. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_composite_pk_write_paths.py +0 -0
  207. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_config.py +0 -0
  208. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_conflict_target.py +0 -0
  209. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_conflict_writes.py +0 -0
  210. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_connection_codecs.py +0 -0
  211. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_connection_poison_guard.py +0 -0
  212. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_connection_primitives.py +0 -0
  213. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_count_composite_distinct.py +0 -0
  214. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_db_function_field.py +0 -0
  215. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_ddl_generator.py +0 -0
  216. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_derived_table.py +0 -0
  217. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_dirty_tracking.py +0 -0
  218. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_dynamic_crud.py +0 -0
  219. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_exceptions.py +0 -0
  220. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  221. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_expression_primitives.py +0 -0
  222. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_fields.py +0 -0
  223. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  224. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_from_alias.py +0 -0
  225. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_fts.py +0 -0
  226. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_func_expression_args.py +0 -0
  227. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_graph_walk.py +0 -0
  228. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_having_annotate_alias.py +0 -0
  229. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  230. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_introspect_rls_policies.py +0 -0
  231. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_join_and_aggregate.py +0 -0
  232. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_latest_rows.py +0 -0
  233. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_listen_notify.py +0 -0
  234. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_managed_write_guard.py +0 -0
  235. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_matrx_entity.py +0 -0
  236. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_migration_diff_types.py +0 -0
  237. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_migration_loader.py +0 -0
  238. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_model_instance.py +0 -0
  239. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_model_meta.py +0 -0
  240. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_optimistic_concurrency.py +0 -0
  241. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_order_by_expression.py +0 -0
  242. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_parameter_name_hydration.py +0 -0
  243. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_patch_jsonb_path.py +0 -0
  244. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_pool_holder_census.py +0 -0
  245. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_pool_loop_recreation.py +0 -0
  246. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_pool_maintenance.py +0 -0
  247. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_postgrest_filters.py +0 -0
  248. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_pydantic_bridge.py +0 -0
  249. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_query_builder.py +0 -0
  250. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_query_error_detail.py +0 -0
  251. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_query_executor_sql.py +0 -0
  252. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_query_timeout.py +0 -0
  253. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  254. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_queue_claim.py +0 -0
  255. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_registry.py +0 -0
  256. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_registry_multi_database.py +0 -0
  257. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_registry_multi_schema.py +0 -0
  258. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_relational_backfill.py +0 -0
  259. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_relations.py +0 -0
  260. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_resilience.py +0 -0
  261. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_rls_session_scope.py +0 -0
  262. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_schema_exists.py +0 -0
  263. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_session_advisory_lock.py +0 -0
  264. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_state_cache.py +0 -0
  265. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_subquery_filter_raw.py +0 -0
  266. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_subquery_in_filter.py +0 -0
  267. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_supabase_auth.py +0 -0
  268. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_supabase_config.py +0 -0
  269. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_tls.py +0 -0
  270. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_typed_join_projection.py +0 -0
  271. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_typed_predicates.py +0 -0
  272. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_update_case_expression.py +0 -0
  273. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_update_subquery.py +0 -0
  274. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_update_with_rebase.py +0 -0
  275. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_upsert_default_leak.py +0 -0
  276. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_upsert_with_conflict.py +0 -0
  277. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_values_jsonb_decode.py +0 -0
  278. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_watched_lifecycle.py +0 -0
  279. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_write_retry_semantics.py +0 -0
  280. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/MODULE_README.md +0 -0
  281. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/__init__.py +0 -0
  282. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/conftest.py +0 -0
  283. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_bulk_ops.py +0 -0
  284. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_cache_integration.py +0 -0
  285. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_crud.py +0 -0
  286. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_foreign_keys.py +0 -0
  287. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_m2m.py +0 -0
  288. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_manager.py +0 -0
  289. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_migrations_live.py +0 -0
  290. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_query_execution.py +0 -0
  291. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_schema_diff.py +0 -0
  292. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/.env.example +0 -0
  293. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/README.md +0 -0
  294. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/__init__.py +0 -0
  295. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/generate.py +0 -0
  296. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/generated/.gitkeep +0 -0
  297. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/matrx_orm.yaml +0 -0
  298. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/test_schema_generation.py +0 -0
  299. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/.env.example +0 -0
  300. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/README.md +0 -0
  301. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/__init__.py +0 -0
  302. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/client_example.py +0 -0
  303. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/client_example.ts +0 -0
  304. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  305. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/server.py +0 -0
  306. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/entity_tests.py +0 -0
  307. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  308. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_composite_unique_generation.py +0 -0
  309. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_dto_identity_generation.py +0 -0
  310. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_entity_capability_generation.py +0 -0
  311. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_generate_schema.py +0 -0
  312. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_junction_analysis.py +0 -0
  313. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_multi_schema_output.py +0 -0
  314. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_sql_expression_defaults.py +0 -0
  315. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_table_relationships_query.py +0 -0
  316. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_view_generation.py +0 -0
  317. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  318. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  319. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/__init__.py +0 -0
  320. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/conftest.py +0 -0
  321. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_cache_no_downgrade.py +0 -0
  322. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_capture_poison_proof.py +0 -0
  323. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_coalesce.py +0 -0
  324. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_dag.py +0 -0
  325. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_disk_spill_fallback.py +0 -0
  326. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_flush_individual_fallback.py +0 -0
  327. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_governed_write.py +0 -0
  328. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_managed.py +0 -0
  329. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_op_enqueue_site.py +0 -0
  330. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_reads.py +0 -0
  331. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_session.py +0 -0
  332. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_database_name_alias.py +0 -0
  333. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_db_functions.py +0 -0
  334. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_model_cls_refactor.py +0 -0
  335. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  336. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_one_database.py +0 -0
  337. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_secrets_battery.py +0 -0
  338. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_sql_param_casts.py +0 -0
  339. {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_sync_twins_guard.py +0 -0
@@ -218,7 +218,7 @@ frontend/
218
218
  # Armani
219
219
  .history*
220
220
  .history/
221
- local_data/
221
+ /local_data/
222
222
  local_reports_data/
223
223
  webscraper/quick_scrapes/temp/
224
224
  automation_matrix/ai_apis/fireworks/_dev/*
@@ -248,6 +248,7 @@ requirements_filtered.txt
248
248
 
249
249
  # Claude Code local worktrees and per-user settings
250
250
  .claude/worktrees/
251
+ .worktrees/
251
252
  .claude/settings.local.json
252
253
 
253
254
  # Append-only snapshots from matrx_utils.update_history (unbounded; do not commit)
@@ -283,3 +284,4 @@ db/evidence/
283
284
 
284
285
  # Pooler contamination evidence — machine-local forensic capture, not source
285
286
  db/evidence/
287
+ db/mirror/.env.mirror
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matrx-orm
3
- Version: 3.1.72
3
+ Version: 3.1.74
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
@@ -203,6 +203,12 @@ from .core.pool_watch import (
203
203
  connection_holder_census,
204
204
  register_pool_saturation_sink,
205
205
  )
206
+ from .core.pooler_role import (
207
+ EmptyReadContaminationEvent,
208
+ PoolerRoleContamination,
209
+ SessionRoleChangeRefused,
210
+ register_contamination_sink,
211
+ )
206
212
  from .core.pydantic_bridge import (
207
213
  validate_input as validate_model_input,
208
214
  )
@@ -573,6 +579,11 @@ __all__ = [
573
579
  "PoolSaturationEvent",
574
580
  "connection_holder_census",
575
581
  "register_pool_saturation_sink",
582
+ # ── Pooler role contamination ──────────────────────────────────────────────
583
+ "EmptyReadContaminationEvent",
584
+ "PoolerRoleContamination",
585
+ "SessionRoleChangeRefused",
586
+ "register_contamination_sink",
576
587
  # ── Signals ────────────────────────────────────────────────────────────────
577
588
  "Signal",
578
589
  "pre_create",
@@ -26,6 +26,7 @@ __all__ = [
26
26
  "softdelete_unique_conflicts",
27
27
  "qualified_relation_names",
28
28
  "trigger_function_usage",
29
+ "trigger_column_coverage",
29
30
  "column_privileges",
30
31
  "entity_capability_state",
31
32
  ]
@@ -487,6 +488,51 @@ async def trigger_function_usage(
487
488
  )
488
489
 
489
490
 
491
+ async def trigger_column_coverage(
492
+ database: str,
493
+ *,
494
+ function_name: str,
495
+ column_names: list[str],
496
+ ) -> list[dict[str, Any]]:
497
+ """Return relations carrying any named column or the named trigger function."""
498
+ return await _query(
499
+ database,
500
+ """
501
+ WITH column_relations AS (
502
+ SELECT DISTINCT c.oid,
503
+ n.nspname AS schema,
504
+ c.relname AS table_name
505
+ FROM pg_attribute a
506
+ JOIN pg_class c ON c.oid = a.attrelid
507
+ JOIN pg_namespace n ON n.oid = c.relnamespace
508
+ WHERE a.attname = ANY($2::text[])
509
+ AND a.attnum > 0
510
+ AND NOT a.attisdropped
511
+ AND c.relkind = 'r'
512
+ ), trigger_relations AS (
513
+ SELECT DISTINCT t.tgrelid AS oid,
514
+ n.nspname AS schema,
515
+ c.relname AS table_name
516
+ FROM pg_trigger t
517
+ JOIN pg_class c ON c.oid = t.tgrelid
518
+ JOIN pg_namespace n ON n.oid = c.relnamespace
519
+ JOIN pg_proc p ON p.oid = t.tgfoid
520
+ WHERE NOT t.tgisinternal
521
+ AND p.proname = $1
522
+ )
523
+ SELECT COALESCE(cols.schema, triggers.schema) AS schema,
524
+ COALESCE(cols.table_name, triggers.table_name) AS table_name,
525
+ (cols.oid IS NOT NULL) AS has_columns,
526
+ (triggers.oid IS NOT NULL) AS has_trigger
527
+ FROM column_relations cols
528
+ FULL OUTER JOIN trigger_relations triggers ON cols.oid = triggers.oid
529
+ ORDER BY 1, 2
530
+ """,
531
+ function_name,
532
+ column_names,
533
+ )
534
+
535
+
490
536
  async def foreign_keys_to_relation(
491
537
  database: str,
492
538
  *,
@@ -89,6 +89,10 @@ Verified against code: 2026-08-15.
89
89
 
90
90
  ## Change log
91
91
 
92
+ - 2026-08-16: Kept the destructive transaction-pool contamination proof out of
93
+ the installable runtime package and isolated it under package tests; the
94
+ operator wrapper is SQL-free and runtime prose no longer resembles an
95
+ executable non-local role mutation.
92
96
  - 2026-08-15: Pinned transaction-local RLS role cleanup across success,
93
97
  exception, and early-return paths.
94
98
  - 2026-08-15: Added first-timeout pool holder census, host alarm seam, tracked raw-pool helper, and bounded cancellation-safe release.
@@ -18,6 +18,11 @@ from matrx_orm.core.config import (
18
18
  resolve_pool_mode,
19
19
  )
20
20
  from matrx_orm.error_handling import handle_orm_operation
21
+ from matrx_orm.core.pooler_role import (
22
+ VERIFY_EMPTY_READS,
23
+ assert_no_session_role_change,
24
+ reprove_empty_read,
25
+ )
21
26
  from matrx_orm.core.tls import resolve_asyncpg_ssl
22
27
  from matrx_orm.exceptions import (
23
28
  AdapterError,
@@ -327,6 +332,25 @@ class AsyncDatabaseManager:
327
332
  _keepalive_tasks: dict[str, asyncio.Task] = {} # per-config pool warmer
328
333
  _growth_locks: dict[str, asyncio.Lock] = {}
329
334
 
335
+ # Config names already resolved as "does an empty read need proving here?".
336
+ # Only a transaction-mode pool can be handed a server connection another
337
+ # client left wearing a role, so a direct Postgres pays nothing.
338
+ _empty_read_verification: dict[str, bool] = {}
339
+
340
+ @classmethod
341
+ def _verifies_empty_reads(cls, config_name: str) -> bool:
342
+ if not VERIFY_EMPTY_READS:
343
+ return False
344
+ cached = cls._empty_read_verification.get(config_name)
345
+ if cached is not None:
346
+ return cached
347
+ try:
348
+ verifies = resolve_pool_mode(get_database_config(config_name)) == "transaction"
349
+ except Exception:
350
+ verifies = False
351
+ cls._empty_read_verification[config_name] = verifies
352
+ return verifies
353
+
330
354
  @classmethod
331
355
  async def _replace_zero_connection_pool(cls, config_name: str, failed_pool: Any) -> bool:
332
356
  """Atomically discard a pool that has lost every physical connection."""
@@ -1025,6 +1049,11 @@ class AsyncDatabaseManager:
1025
1049
  ``.timeout()`` seam for known-long reads. ``None`` keeps the pool
1026
1050
  default.
1027
1051
  """
1052
+ # Layer 1 of the pooler-role defence: SQL that would leave a borrowed role
1053
+ # on a shared server connection never reaches Postgres. See
1054
+ # matrx_orm/core/pooler_role.py for what that contamination does to reads.
1055
+ assert_no_session_role_change(query)
1056
+
1028
1057
  # Reuse the active transaction connection when available
1029
1058
  try:
1030
1059
  from matrx_orm.core.transaction import get_active_connection
@@ -1124,6 +1153,21 @@ class AsyncDatabaseManager:
1124
1153
  read_attempt + 1,
1125
1154
  query[:120],
1126
1155
  )
1156
+ if not results and read_only and cls._verifies_empty_reads(config_name):
1157
+ # Layer 2: an empty result from a pooled connection is
1158
+ # unproven until a session that can state its own role
1159
+ # says so. A borrowed `authenticated` role returns ZERO
1160
+ # ROWS on an RLS table instead of raising.
1161
+ true_rows = await reprove_empty_read(
1162
+ conn,
1163
+ config_name=config_name,
1164
+ query=query,
1165
+ args=args,
1166
+ statement_timeout=statement_timeout,
1167
+ normalize=_normalize_record,
1168
+ )
1169
+ if true_rows is not None:
1170
+ return true_rows
1127
1171
  return [_normalize_record(r) for r in results]
1128
1172
  except asyncpg.exceptions.PostgresSyntaxError as e:
1129
1173
  raise QueryError(
@@ -0,0 +1,290 @@
1
+ """Pooler role contamination — the ORM's two layers against a SILENT empty read.
2
+
3
+ WHY THIS EXISTS (reproduced 2026-08-15, FOUND_DEFECTS "An ORM read returned ZERO
4
+ ROWS against a table holding 4,060"):
5
+
6
+ On a transaction-mode pooler (Supavisor / PgBouncer) routing is *per transaction*,
7
+ so consecutive statements from ONE client connection execute on DIFFERENT server
8
+ connections. A server connection that any client on the tenant left with a
9
+ non-LOCAL ``SET ROLE`` comes back out of the shared pool still wearing that role.
10
+ Our statement then runs as ``authenticated`` instead of ``postgres``.
11
+
12
+ **On an RLS table that GRANTs SELECT to ``authenticated``, that does not raise —
13
+ it returns ZERO ROWS.** Measured live against the platform pooler: a bare
14
+ ``SET ROLE authenticated`` issued on 8 connections made 22 of 40 subsequent ORM
15
+ reads of ``seo.backlink_observation`` (4,060 rows) come back empty, with no
16
+ exception, while an identity probe interleaved on the same pool reported
17
+ ``current_user=authenticated, session_user=postgres`` on 21 of 40 samples.
18
+
19
+ Silent emptiness is indistinguishable from "there is no data" for every sweep,
20
+ reconciler, digest and monitor in the platform: a broken read reports a quiet week
21
+ forever and every scheduled run stays green. ``db/pooler_session.py`` already says
22
+ it for the migration tools — **a privilege error must never be recorded as
23
+ absence** — and this module extends that to every ORM read, because a per-caller
24
+ defence does not scale.
25
+
26
+ TWO INDEPENDENT LAYERS, each sufficient alone, each screaming when it fires:
27
+
28
+ 1. **Prevention — the ORM refuses to emit a session-scoped role change.**
29
+ ``assert_no_session_role_change`` rejects a bare ``SET ROLE`` /
30
+ ``SET SESSION AUTHORIZATION`` / ``set_config('role', …, false)`` on every query
31
+ that goes through the ORM. This closes the one source class the 2026-08-14
32
+ investigation could never catch statically: SQL authored at RUNTIME — the
33
+ admin ``sql`` tool, ``db_admin``, registered ``SQLQuery`` objects, agent
34
+ scratch scripts. ``scripts/check_pooler_role_scope.py`` guards committed code;
35
+ this guards everything else. The sanctioned ways to change role remain
36
+ ``matrx_orm.rls_session`` and ``db/pooler_session.py::impersonate``.
37
+
38
+ 2. **Reconciliation — an empty read is re-proved on a session that can prove its
39
+ role.** A zero-row result from a read-only query on a transaction-pool config
40
+ is re-run inside an explicit transaction (pinned to ONE server connection)
41
+ that starts with ``RESET ROLE``. If the rows are really there, the caller gets
42
+ the TRUE rows and the lie is reported through every registered sink. If the
43
+ session cannot be made clean, it RAISES rather than handing back an absence.
44
+
45
+ Layer 2 follows the repo's guard doctrine: a guard that CAN reconcile MUST
46
+ reconcile — we hold the correct answer, so killing the caller's request would be
47
+ strictly worse than returning it loudly. It costs nothing on a non-empty read and
48
+ nothing at all on a direct (session-mode) Postgres connection, where cross-client
49
+ contamination cannot happen.
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import logging
55
+ import re
56
+ from collections.abc import Callable
57
+ from datetime import UTC, datetime
58
+ from typing import Any
59
+
60
+ from pydantic import BaseModel, ConfigDict
61
+
62
+ from matrx_utils import vcprint
63
+
64
+ logger = logging.getLogger("matrx_orm.pooler_role")
65
+
66
+ # Turning this off re-arms a silent-data-loss class platform-wide. It exists so a
67
+ # deployment on a genuinely unshared Postgres can skip the work, and so the cost
68
+ # is a code change (reviewable) rather than an env var (invisible).
69
+ VERIFY_EMPTY_READS = True
70
+
71
+ # How many times a re-proof may try to obtain a session running as our own login
72
+ # role before it gives up and raises. A contaminated server connection is repaired
73
+ # in-transaction, so one retry is almost always enough.
74
+ REPROVE_ATTEMPTS = 3
75
+
76
+ # A role change that is NOT scoped to a transaction. ``SET LOCAL ROLE`` and
77
+ # ``set_config('role', …, true)`` are safe and deliberately not matched.
78
+ _NON_LOCAL_SET_ROLE = re.compile(r"(?im)(?:^|;)\s*SET\s+(?!LOCAL\b)ROLE\b")
79
+ _NON_LOCAL_SET_AUTHORIZATION = re.compile(
80
+ r"(?im)(?:^|;)\s*SET\s+(?!LOCAL\b)SESSION\s+AUTHORIZATION\b"
81
+ )
82
+ _NON_LOCAL_SET_CONFIG_ROLE = re.compile(
83
+ r"(?is)set_config\s*\(\s*['\"]role['\"]\s*,.*?,\s*(?:false|'false'|\"false\")\s*\)"
84
+ )
85
+
86
+ # Cheap pre-filter so the overwhelming majority of queries pay a substring scan
87
+ # rather than three regexes. Deliberately loose — a false hint costs microseconds,
88
+ # a missed one costs the platform a silent empty read.
89
+ _ROLE_HINTS = ("role", "authorization", "set_config")
90
+
91
+
92
+ class SessionRoleChangeRefused(RuntimeError):
93
+ """A query tried to change the Postgres role for the whole session."""
94
+
95
+
96
+ class PoolerRoleContamination(RuntimeError):
97
+ """A statement could not be pinned to a session running as our own login role."""
98
+
99
+
100
+ class EmptyReadContaminationEvent(BaseModel):
101
+ """A read returned zero rows because the session was wearing a borrowed role."""
102
+
103
+ model_config = ConfigDict(extra="forbid", frozen=True)
104
+
105
+ config_name: str
106
+ query: str
107
+ contaminated_role: str
108
+ login_role: str
109
+ true_row_count: int
110
+ occurred_at: datetime
111
+
112
+
113
+ ContaminationSink = Callable[[EmptyReadContaminationEvent], None]
114
+
115
+ _sinks: list[ContaminationSink] = []
116
+
117
+
118
+ def register_contamination_sink(sink: ContaminationSink) -> None:
119
+ """Register a durable observer for proven silent-empty reads."""
120
+ if sink not in _sinks:
121
+ _sinks.append(sink)
122
+
123
+
124
+ def reset_contamination_sinks() -> None:
125
+ """Clear process state for isolated tests."""
126
+ _sinks.clear()
127
+
128
+
129
+ def assert_no_session_role_change(query: str) -> None:
130
+ """Refuse SQL that would leave a borrowed role on a pooled server connection.
131
+
132
+ Layer 1. Raises ``SessionRoleChangeRefused`` and names the sanctioned path;
133
+ it never rewrites the caller's SQL, because guessing which role a caller meant
134
+ is exactly the ambiguity a guard must not resolve on its own.
135
+ """
136
+ if not query:
137
+ return
138
+ lowered = query.lower()
139
+ if not any(hint in lowered for hint in _ROLE_HINTS):
140
+ return
141
+ if (
142
+ _NON_LOCAL_SET_ROLE.search(query)
143
+ or _NON_LOCAL_SET_AUTHORIZATION.search(query)
144
+ or _NON_LOCAL_SET_CONFIG_ROLE.search(query)
145
+ ):
146
+ vcprint(
147
+ "REFUSED: a session-scoped role change reached the ORM.\n"
148
+ f" query: {query[:300]}\n"
149
+ " On a transaction pooler this role rides the SHARED server connection "
150
+ "back into the pool, and the next client — any service on this tenant — "
151
+ "silently runs as that role. On an RLS table it does not error: it "
152
+ "returns ZERO ROWS, and absence gets recorded as fact.\n"
153
+ " Use `async with rls_session(claims, role=...)` (ORM) or "
154
+ "`impersonate(conn, role)` (db/pooler_session.py). Both use SET LOCAL "
155
+ "ROLE inside a transaction and cannot leak.",
156
+ color="red",
157
+ style="bold",
158
+ )
159
+ raise SessionRoleChangeRefused(
160
+ "A session-scoped role change (SET ROLE / SET SESSION AUTHORIZATION / "
161
+ "a non-local set_config role change) may never be executed through the ORM — it "
162
+ "contaminates the shared pooler and turns other services' reads into "
163
+ "silent empty results. Use rls_session(...) / impersonate(...), which "
164
+ "issue SET LOCAL ROLE inside a transaction."
165
+ )
166
+
167
+
168
+ async def emit_contamination(event: EmptyReadContaminationEvent) -> None:
169
+ """Scream about a proven silent-empty read, durably record it, fan out to sinks."""
170
+ vcprint(
171
+ "POOLER ROLE CONTAMINATION — an ORM read returned ZERO ROWS that were "
172
+ "really there.\n"
173
+ f" config: {event.config_name}\n"
174
+ f" session ran as: {event.contaminated_role} (login role "
175
+ f"{event.login_role})\n"
176
+ f" true row count on a clean session: {event.true_row_count}\n"
177
+ f" query: {event.query[:300]}\n"
178
+ " The caller has been given the TRUE rows. Something on this Postgres "
179
+ "tenant issued a non-LOCAL SET ROLE; every other client is exposed until "
180
+ "those server connections cycle.",
181
+ color="red",
182
+ style="bold",
183
+ )
184
+ logger.error(
185
+ "[pooler-role] silent empty read repaired on %s (ran as %s, %d true rows): %s",
186
+ event.config_name,
187
+ event.contaminated_role,
188
+ event.true_row_count,
189
+ event.query[:200],
190
+ )
191
+ # Durable by default — a host should not have to wire anything for the
192
+ # platform's loudest data-integrity alarm to survive the process.
193
+ try:
194
+ from matrx_orm.session.fallback import record_error
195
+
196
+ await record_error(
197
+ PoolerRoleContamination(
198
+ f"read returned 0 rows as '{event.contaminated_role}'; "
199
+ f"{event.true_row_count} rows were really there"
200
+ ),
201
+ kind="pooler_role_contamination",
202
+ source_app="matrx-orm",
203
+ route=f"orm:{event.config_name}",
204
+ payload={
205
+ "config_name": event.config_name,
206
+ "query": event.query[:2000],
207
+ "contaminated_role": event.contaminated_role,
208
+ "login_role": event.login_role,
209
+ "true_row_count": event.true_row_count,
210
+ },
211
+ )
212
+ except Exception:
213
+ logger.exception("[pooler-role] could not durably record the contamination event")
214
+
215
+ for sink in list(_sinks):
216
+ try:
217
+ sink(event)
218
+ except Exception:
219
+ logger.exception("[pooler-role] contamination sink raised")
220
+
221
+
222
+ async def reprove_empty_read(
223
+ conn: Any,
224
+ *,
225
+ config_name: str,
226
+ query: str,
227
+ args: tuple[Any, ...],
228
+ statement_timeout: float | None,
229
+ normalize: Callable[[Any], dict[str, Any]],
230
+ ) -> list[dict[str, Any]] | None:
231
+ """Re-run an empty read on a session proven to be running as US.
232
+
233
+ Layer 2. Returns the TRUE rows when the empty result was a lie, ``None`` when
234
+ the read was genuinely empty. Raises ``PoolerRoleContamination`` when a clean
235
+ session cannot be obtained at all — an unprovable absence is never a result.
236
+
237
+ ``conn`` must be a connection not currently inside a transaction; the explicit
238
+ transaction opened here is what pins every statement below to ONE server
239
+ connection, which is the only thing that makes ``RESET ROLE`` meaningful.
240
+ """
241
+ last_role: tuple[str, str] | None = None
242
+ for _ in range(REPROVE_ATTEMPTS):
243
+ async with conn.transaction():
244
+ await conn.execute("RESET ROLE")
245
+ identity = await conn.fetchrow("SELECT current_user, session_user")
246
+ current = identity["current_user"]
247
+ login = identity["session_user"]
248
+ if current != login:
249
+ last_role = (current, login)
250
+ continue
251
+ rows = await conn.fetch(query, *args, timeout=statement_timeout)
252
+ if not rows:
253
+ return None
254
+ true_rows = [normalize(r) for r in rows]
255
+
256
+ await emit_contamination(
257
+ EmptyReadContaminationEvent(
258
+ config_name=config_name,
259
+ query=query,
260
+ # The first read ran on a session we could not observe directly;
261
+ # what we can state is the role it was NOT running as.
262
+ contaminated_role="<not the login role>" if last_role is None else last_role[0],
263
+ login_role=login,
264
+ true_row_count=len(true_rows),
265
+ occurred_at=datetime.now(UTC),
266
+ )
267
+ )
268
+ return true_rows
269
+
270
+ current, login = last_role if last_role is not None else ("<unknown>", "<unknown>")
271
+ raise PoolerRoleContamination(
272
+ f"an empty read could not be proved: every attempt to pin a clean session "
273
+ f"landed as '{current}' (login role '{login}'). Anything this session could "
274
+ f"not see is a PRIVILEGE failure, not absence. Query: {query[:200]}"
275
+ )
276
+
277
+
278
+ __all__ = [
279
+ "ContaminationSink",
280
+ "EmptyReadContaminationEvent",
281
+ "PoolerRoleContamination",
282
+ "REPROVE_ATTEMPTS",
283
+ "SessionRoleChangeRefused",
284
+ "VERIFY_EMPTY_READS",
285
+ "assert_no_session_role_change",
286
+ "emit_contamination",
287
+ "register_contamination_sink",
288
+ "reprove_empty_read",
289
+ "reset_contamination_sinks",
290
+ ]
@@ -0,0 +1,12 @@
1
+ # Schema Builder
2
+
3
+ The schema builder generates importable models and manager wrappers from live database metadata.
4
+
5
+ ## Manager method names
6
+
7
+ - A generated manager must never contain duplicate method names; later definitions silently replace earlier behavior in Python.
8
+ - When a table's singular and plural tokens are identical (for example, `entity_grants`), item methods keep the table token and collection methods use the `all_` prefix: `load_entity_grants(...)` and `load_all_entity_grants(...)`.
9
+ - Relation collection wrappers follow the same rule, such as `get_all_entity_grants_with_iam_organizations()`.
10
+ - Generated code must pass Ruff without unused imports or redefined methods.
11
+
12
+ Guard: `packages/matrx-orm/tests/schema/test_base_generation.py`.
@@ -27,6 +27,86 @@ _SQL_DYNAMIC_DEFAULT_KEYWORDS = frozenset(
27
27
  )
28
28
 
29
29
 
30
+ def _parse_array_default(value: str) -> list[object]:
31
+ """Turn a non-empty PostgreSQL array default into a real Python list.
32
+
33
+ Handles the two spellings Postgres reports for the same thing:
34
+
35
+ ARRAY['react'::text, 'lucide-react'::text] → ['react', 'lucide-react']
36
+ '{chat_gpt,claude,gemini}'::text[] → ['chat_gpt', 'claude', 'gemini']
37
+
38
+ Returns ``[]`` for anything it cannot confidently parse — a nested array, a
39
+ quoted element containing a comma, a function call inside the literal. An
40
+ empty list is the same conservative answer the generator gave before, and it
41
+ is deliberately preferred over a half-parsed guess: a WRONG default is worse
42
+ than an absent one, because it looks authoritative.
43
+ """
44
+ text = value.strip()
45
+
46
+ if text.startswith("ARRAY["):
47
+ inner = text[len("ARRAY[") :]
48
+ close = inner.rfind("]")
49
+ if close == -1:
50
+ return []
51
+ elements = [e.strip() for e in inner[:close].split(",")]
52
+ # In ARRAY[...] every element is a SQL EXPRESSION, so a bare token is an
53
+ # identifier or a function call, not a string — refuse the whole default
54
+ # rather than stringify something the database would evaluate.
55
+ bare_is_a_string = False
56
+ else:
57
+ # '{a,b,c}'::text[] — take what is inside the FIRST quoted run, which is
58
+ # the array literal itself; everything after it is the cast.
59
+ open_brace = text.find("{")
60
+ close_brace = text.rfind("}")
61
+ if open_brace == -1 or close_brace <= open_brace:
62
+ return []
63
+ body = text[open_brace + 1 : close_brace]
64
+ if not body.strip():
65
+ return []
66
+ # A quoted element may legally contain a comma; splitting naively would
67
+ # invent elements. Refuse rather than guess.
68
+ if '"' in body:
69
+ return []
70
+ elements = [e.strip() for e in body.split(",")]
71
+ # Inside a curly ARRAY LITERAL an unquoted element IS its literal text —
72
+ # '{chat_gpt,claude}' means the two strings, not two identifiers. A
73
+ # numeric-typed array still parses its elements as numbers below.
74
+ bare_is_a_string = not _array_cast_is_numeric(text)
75
+
76
+ parsed: list[object] = []
77
+ for element in elements:
78
+ if not element:
79
+ return []
80
+ # Strip a per-element cast: 'react'::text → 'react'
81
+ item = re.sub(r"::[A-Za-z_][A-Za-z0-9_ .]*$", "", element).strip()
82
+ if item.startswith("'") and item.endswith("'") and len(item) >= 2:
83
+ parsed.append(item[1:-1].replace("''", "'"))
84
+ continue
85
+ try:
86
+ literal = ast.literal_eval(item)
87
+ except (ValueError, SyntaxError):
88
+ if bare_is_a_string:
89
+ parsed.append(item)
90
+ continue
91
+ return []
92
+ if not isinstance(literal, str | int | float | bool):
93
+ return []
94
+ parsed.append(literal)
95
+ return parsed
96
+
97
+
98
+ _NUMERIC_ARRAY_CAST_RE = re.compile(
99
+ r"::\s*(?:small|big)?int(?:eger)?\d*\s*\[\]$|"
100
+ r"::\s*(?:numeric|decimal|real|double precision|float\d*)\s*\[\]$",
101
+ re.IGNORECASE,
102
+ )
103
+
104
+
105
+ def _array_cast_is_numeric(value: str) -> bool:
106
+ """Whether ``'{...}'::<type>[]`` casts to a numeric element type."""
107
+ return bool(_NUMERIC_ARRAY_CAST_RE.search(value.strip()))
108
+
109
+
30
110
  def is_sql_expression_default(value: object) -> bool:
31
111
  """Return whether a PostgreSQL default must be evaluated by the database."""
32
112
  if not isinstance(value, str):
@@ -1068,9 +1148,32 @@ class Column:
1068
1148
  elif value.startswith("ARRAY[]::") and value.endswith("[]"):
1069
1149
  return {"blank": "[]", "generator": ""}
1070
1150
 
1071
- # Handle non-empty ARRAY[...] syntax: ARRAY['react'::text, 'lucide-react'::text, ...]
1072
- elif value.startswith("ARRAY["):
1073
- return {"blank": "[]", "generator": ""}
1151
+ # 🚨 NON-EMPTY array defaults, BOTH Postgres spellings. Two separate
1152
+ # bugs lived here and both were silent:
1153
+ #
1154
+ # * ``'{a,b}'::text[]`` (the curly-literal spelling) matched NO
1155
+ # branch at all and fell through to the scalar handlers, which
1156
+ # wrapped the whole thing in quotes and emitted
1157
+ # ``default=''{a,b}'::text[]'`` — a Python SYNTAX ERROR that
1158
+ # makes the generated models file unimportable for the entire
1159
+ # repo. It was hand-patched in the output once (2026-08-16,
1160
+ # seo.ai_visibility_panel.engines) and the very next
1161
+ # regeneration re-broke it, because the generator was never
1162
+ # fixed. Patching generated output is not a fix.
1163
+ # * ``ARRAY['a'::text, 'b'::text]`` DID match, and silently
1164
+ # returned ``[]`` — so a column whose database default is 13
1165
+ # values got a Python default of zero values, and a model
1166
+ # instantiated in code disagreed with every row the DB wrote.
1167
+ # No error, no log, just a wrong default.
1168
+ #
1169
+ # Both are the same question — "what list does this default mean?" —
1170
+ # so both go through one parser that answers it. Anything it cannot
1171
+ # confidently parse returns an EMPTY list rather than a guess, which
1172
+ # is the pre-existing (conservative) behaviour, never a syntax error.
1173
+ elif value.startswith("ARRAY[") or (
1174
+ value.startswith("'{") and "}'::" in value and value.endswith("[]")
1175
+ ):
1176
+ return {"blank": _parse_array_default(value), "generator": ""}
1074
1177
 
1075
1178
  # Handle simple numeric defaults (no quotes, no casting)
1076
1179
  # This includes integers, decimals, negative numbers, scientific notation