matrx-orm 3.1.242__tar.gz → 3.1.244__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 (449) hide show
  1. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/CHANGELOG.md +14 -0
  2. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/__init__.py +6 -0
  4. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/postgrest_client_adapter.py +9 -0
  5. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/FEATURE.md +15 -8
  6. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/async_db_manager.py +59 -5
  7. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/expressions.py +76 -0
  8. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/fields.py +37 -0
  9. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/pooler_role.py +173 -28
  10. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/FEATURE.md +18 -0
  11. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/db_functions.py +11 -1
  12. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/update.py +36 -3
  13. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/executor.py +4 -1
  14. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/fallback.py +7 -3
  15. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/lifecycle.py +4 -2
  16. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/pyproject.toml +1 -1
  17. matrx_orm-3.1.244/tests/level1/test_guarded_read_never_reruns_empty.py +142 -0
  18. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_read_connection_retry.py +24 -0
  19. matrx_orm-3.1.244/tests/test_instance_update_runs_expressions.py +237 -0
  20. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/.gitignore +0 -0
  21. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/AGENTS.md +0 -0
  22. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/BASE_CLASS_METHODS.md +0 -0
  23. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/CLAUDE.md +0 -0
  24. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/MODEL_API.md +0 -0
  25. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/README.md +0 -0
  26. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/database/orm/extended/managers/ai_model_base.py +0 -0
  27. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/docs/migrations.md +0 -0
  28. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/FEATURE.md +0 -0
  29. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/MODULE_README.md +0 -0
  30. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/MODULE_README.md +0 -0
  31. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/__init__.py +0 -0
  32. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/async_postgresql.py +0 -0
  33. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/base_adapter.py +0 -0
  34. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/supabase_adapter.py +0 -0
  35. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/admin/FEATURE.md +0 -0
  36. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/admin/__init__.py +0 -0
  37. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/admin/router.py +0 -0
  38. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/__init__.py +0 -0
  39. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/auth.py +0 -0
  40. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/config.py +0 -0
  41. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/handlers.py +0 -0
  42. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/protocol.py +0 -0
  43. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/server.py +0 -0
  44. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/associations.py +0 -0
  45. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/cache_debug.py +0 -0
  46. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/catalog.py +0 -0
  47. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/catalog_sql.py +0 -0
  48. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/MODULE_README.md +0 -0
  49. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/__init__.py +0 -0
  50. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/postgres_connection.py +0 -0
  51. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/postgrest.py +0 -0
  52. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/supabase_auth.py +0 -0
  53. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/supabase_config.py +0 -0
  54. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/supabase_manager.py +0 -0
  55. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  56. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/MODULE_README.md +0 -0
  57. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  58. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/TASKS.md +0 -0
  59. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/__init__.py +0 -0
  60. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/admin_lane.py +0 -0
  61. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/base.py +0 -0
  62. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/config.py +0 -0
  63. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/connections.py +0 -0
  64. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/db_function.py +0 -0
  65. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/diagnostics.py +0 -0
  66. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/extended.py +0 -0
  67. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/foreign_session_state.py +0 -0
  68. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/introspection.py +0 -0
  69. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/listen.py +0 -0
  70. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/loop_filters.py +0 -0
  71. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/model_dto.py +0 -0
  72. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/model_view.py +0 -0
  73. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/one_database.py +0 -0
  74. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/paginator.py +0 -0
  75. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/pool_maintenance.py +0 -0
  76. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/pool_watch.py +0 -0
  77. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/pydantic_bridge.py +0 -0
  78. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/read_write.py +0 -0
  79. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/registry.py +0 -0
  80. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/relations.py +0 -0
  81. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/resilience.py +0 -0
  82. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/rls_session.py +0 -0
  83. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/session_context.py +0 -0
  84. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/signals.py +0 -0
  85. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/statement_ceiling.py +0 -0
  86. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/tls.py +0 -0
  87. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/transaction.py +0 -0
  88. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/types.py +0 -0
  89. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/write_queue.py +0 -0
  90. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/write_retry.py +0 -0
  91. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/entity.py +0 -0
  92. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/error_handling.py +0 -0
  93. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/exceptions.py +0 -0
  94. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/extended/__init__.py +0 -0
  95. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/extended/app_error_handler.py +0 -0
  96. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/identity_rule.py +0 -0
  97. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/local_sqlite.py +0 -0
  98. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/middleware/__init__.py +0 -0
  99. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/middleware/base.py +0 -0
  100. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/MODULE_README.md +0 -0
  101. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/__init__.py +0 -0
  102. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/cli.py +0 -0
  103. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/ddl.py +0 -0
  104. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/diff.py +0 -0
  105. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/executor.py +0 -0
  106. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/integration.py +0 -0
  107. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/loader.py +0 -0
  108. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/operations.py +0 -0
  109. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/state.py +0 -0
  110. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/table_filter.py +0 -0
  111. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/no_org_assignment.py +0 -0
  112. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/MODULE_README.md +0 -0
  113. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/__init__.py +0 -0
  114. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/bulk_update_values.py +0 -0
  115. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/catalog_select.py +0 -0
  116. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/conflict.py +0 -0
  117. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/conflict_writes.py +0 -0
  118. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/create.py +0 -0
  119. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/delete.py +0 -0
  120. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/dynamic_admin.py +0 -0
  121. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/dynamic_crud.py +0 -0
  122. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/graph_walk.py +0 -0
  123. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/latest_rows.py +0 -0
  124. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/queue_claim.py +0 -0
  125. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/read.py +0 -0
  126. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/relational_backfill.py +0 -0
  127. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/staging_load.py +0 -0
  128. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/platform_access.py +0 -0
  129. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/platform_db.py +0 -0
  130. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/py.typed +0 -0
  131. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/pytest_lock_guard.py +0 -0
  132. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/pytest_plugin.py +0 -0
  133. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/MODULE_README.md +0 -0
  134. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/__init__.py +0 -0
  135. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/db_objects.py +0 -0
  136. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  137. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  138. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/FEATURE.md +0 -0
  139. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/MODULE_README.md +0 -0
  140. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/__init__.py +0 -0
  141. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/builder.py +0 -0
  142. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/resources/__init__.py +0 -0
  143. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/resources/additional-assigners-2026-09-12.json +0 -0
  144. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/resources/org_assignment_trigger_baseline.json +0 -0
  145. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/FEATURE.md +0 -0
  146. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  147. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/__init__.py +0 -0
  148. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/code_handler.py +0 -0
  149. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  150. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/columns.py +0 -0
  151. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/common.py +0 -0
  152. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/diff_preview.py +0 -0
  153. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  154. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/excluded_targets.py +0 -0
  155. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/generator.py +0 -0
  156. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  157. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  158. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  159. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  160. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/package_wiring.py +0 -0
  161. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/relationships.py +0 -0
  162. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/runner.py +0 -0
  163. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/schema.py +0 -0
  164. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/schema_manager.py +0 -0
  165. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/tables.py +0 -0
  166. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/views.py +0 -0
  167. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  168. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/__init__.py +0 -0
  169. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/attachments.py +0 -0
  170. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/backup_restore.py +0 -0
  171. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/crypto.py +0 -0
  172. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/host_authorization.py +0 -0
  173. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/items.py +0 -0
  174. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/model.py +0 -0
  175. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/mutation_receipts.py +0 -0
  176. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_enable.py +0 -0
  177. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_lifecycle.py +0 -0
  178. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_passkeys.py +0 -0
  179. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_source.py +0 -0
  180. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/org_service.py +0 -0
  181. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/org_vault_gate.py +0 -0
  182. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/service.py +0 -0
  183. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/vault_history.py +0 -0
  184. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/README.md +0 -0
  185. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/__init__.py +0 -0
  186. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/coalesce.py +0 -0
  187. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/dag.py +0 -0
  188. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/errors.py +0 -0
  189. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/flush.py +0 -0
  190. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/managed.py +0 -0
  191. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/op.py +0 -0
  192. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/reads.py +0 -0
  193. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/session.py +0 -0
  194. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/telemetry.py +0 -0
  195. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  196. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/__init__.py +0 -0
  197. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/executor.py +0 -0
  198. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/queries.py +0 -0
  199. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/registry.py +0 -0
  200. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/types.py +0 -0
  201. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/utils.py +0 -0
  202. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/state.py +0 -0
  203. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/utils/__init__.py +0 -0
  204. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/utils/sql_utils.py +0 -0
  205. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/utils/type_converters.py +0 -0
  206. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/write_actor.py +0 -0
  207. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/scripts/git-branches.sh +0 -0
  208. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/scripts/publish.sh +0 -0
  209. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/scripts/release.sh +0 -0
  210. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/MODULE_README.md +0 -0
  211. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/__init__.py +0 -0
  212. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/conftest.py +0 -0
  213. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/MODULE_README.md +0 -0
  214. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/__init__.py +0 -0
  215. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_adapter_registry.py +0 -0
  216. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_admin_db_columns.py +0 -0
  217. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_admin_response_rendering.py +0 -0
  218. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_admin_search_negation.py +0 -0
  219. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_admin_search_sentinels.py +0 -0
  220. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_advisory_lock.py +0 -0
  221. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_agent_message.py +0 -0
  222. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_api_auth.py +0 -0
  223. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_api_config.py +0 -0
  224. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_api_handlers.py +0 -0
  225. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_api_protocol.py +0 -0
  226. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_array_agg_order_by.py +0 -0
  227. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_association_trigger_actor_naming.py +0 -0
  228. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_associations.py +0 -0
  229. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_associations_wiring.py +0 -0
  230. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_borrowed_role_never_reaches_server_calls.py +0 -0
  231. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_bulk_hydration_offload.py +0 -0
  232. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_bulk_update_by_pk.py +0 -0
  233. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  234. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_cache_debug.py +0 -0
  235. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_call_function.py +0 -0
  236. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_case_when.py +0 -0
  237. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_catalog.py +0 -0
  238. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_column_scoped_writes.py +0 -0
  239. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_committed_row_guard.py +0 -0
  240. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_composite_pk_filter.py +0 -0
  241. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_composite_pk_write_paths.py +0 -0
  242. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_config.py +0 -0
  243. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_conflict_target.py +0 -0
  244. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_conflict_writes.py +0 -0
  245. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_connection_codecs.py +0 -0
  246. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_connection_poison_guard.py +0 -0
  247. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_connection_primitives.py +0 -0
  248. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_count_composite_distinct.py +0 -0
  249. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_count_limit_param_leak.py +0 -0
  250. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_db_function_field.py +0 -0
  251. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_ddl_generator.py +0 -0
  252. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_deadlock_retry.py +0 -0
  253. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_derived_table.py +0 -0
  254. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_dirty_tracking.py +0 -0
  255. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_dynamic_crud.py +0 -0
  256. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_exceptions.py +0 -0
  257. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  258. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_expression_primitives.py +0 -0
  259. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_fetch_on_init_holds_whole_table.py +0 -0
  260. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_fields.py +0 -0
  261. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  262. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_fk_cycle_resolution.py +0 -0
  263. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_from_alias.py +0 -0
  264. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_fts.py +0 -0
  265. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_func_expression_args.py +0 -0
  266. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_graph_walk.py +0 -0
  267. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_having_annotate_alias.py +0 -0
  268. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  269. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_introspect_rls_policies.py +0 -0
  270. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_join_and_aggregate.py +0 -0
  271. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_latest_rows.py +0 -0
  272. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_listen_notify.py +0 -0
  273. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_local_sqlite.py +0 -0
  274. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_lock_contention_retry.py +0 -0
  275. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_managed_write_guard.py +0 -0
  276. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_matrx_entity.py +0 -0
  277. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_migration_diff_types.py +0 -0
  278. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_migration_loader.py +0 -0
  279. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_model_instance.py +0 -0
  280. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_model_meta.py +0 -0
  281. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_optimistic_concurrency.py +0 -0
  282. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_order_by_expression.py +0 -0
  283. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_org_assignment_inventory.py +0 -0
  284. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_orm_banners_are_client_safe.py +0 -0
  285. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_parameter_name_hydration.py +0 -0
  286. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_patch_jsonb_path.py +0 -0
  287. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_percentile_cont.py +0 -0
  288. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_persisted_count.py +0 -0
  289. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_person_scoped_session_bypasses_shared_cache.py +0 -0
  290. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pool_create_never_wedges.py +0 -0
  291. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pool_holder_census.py +0 -0
  292. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pool_loop_recreation.py +0 -0
  293. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pool_maintenance.py +0 -0
  294. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_postgrest_filters.py +0 -0
  295. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pydantic_bridge.py +0 -0
  296. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_query_builder.py +0 -0
  297. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_query_error_detail.py +0 -0
  298. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_query_executor_sql.py +0 -0
  299. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_query_timeout.py +0 -0
  300. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  301. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_queue_claim.py +0 -0
  302. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_read_write_declared.py +0 -0
  303. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_registry.py +0 -0
  304. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_registry_multi_database.py +0 -0
  305. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_registry_multi_schema.py +0 -0
  306. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_relational_backfill.py +0 -0
  307. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_relations.py +0 -0
  308. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_resilience.py +0 -0
  309. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_rls_session_scope.py +0 -0
  310. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_schema_exists.py +0 -0
  311. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_session_advisory_lock.py +0 -0
  312. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_standalone_write_session_context.py +0 -0
  313. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_state_cache.py +0 -0
  314. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_subquery_filter_raw.py +0 -0
  315. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_subquery_in_filter.py +0 -0
  316. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_subquery_nested_expressions.py +0 -0
  317. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_supabase_auth.py +0 -0
  318. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_supabase_config.py +0 -0
  319. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_sync_pool_declares_read_write.py +0 -0
  320. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_tls.py +0 -0
  321. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_transaction_database_identity.py +0 -0
  322. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_transaction_gucs_one_round_trip.py +0 -0
  323. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_transaction_pool_release_sends_nothing.py +0 -0
  324. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_typed_join_projection.py +0 -0
  325. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_typed_predicates.py +0 -0
  326. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_update_case_expression.py +0 -0
  327. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_update_subquery.py +0 -0
  328. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_update_with_rebase.py +0 -0
  329. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_upsert_default_leak.py +0 -0
  330. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_upsert_with_conflict.py +0 -0
  331. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_values_jsonb_decode.py +0 -0
  332. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_watched_lifecycle.py +0 -0
  333. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_write_queue_deadline.py +0 -0
  334. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_write_retry_semantics.py +0 -0
  335. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/MODULE_README.md +0 -0
  336. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/__init__.py +0 -0
  337. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/conftest.py +0 -0
  338. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_bulk_ops.py +0 -0
  339. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_cache_integration.py +0 -0
  340. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_crud.py +0 -0
  341. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_foreign_keys.py +0 -0
  342. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_m2m.py +0 -0
  343. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_manager.py +0 -0
  344. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_migrations_live.py +0 -0
  345. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_query_execution.py +0 -0
  346. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_schema_diff.py +0 -0
  347. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/verify_committed_row_guard_pg17.py +0 -0
  348. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/live_pooler_role.py +0 -0
  349. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/native_source_cases.py +0 -0
  350. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/native_source_oracle.rs +0 -0
  351. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/.env.example +0 -0
  352. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/README.md +0 -0
  353. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/__init__.py +0 -0
  354. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/generate.py +0 -0
  355. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/generated/.gitkeep +0 -0
  356. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/matrx_orm.yaml +0 -0
  357. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/test_schema_generation.py +0 -0
  358. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/.env.example +0 -0
  359. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/README.md +0 -0
  360. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/__init__.py +0 -0
  361. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_example.py +0 -0
  362. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_example.ts +0 -0
  363. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  364. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/server.py +0 -0
  365. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/entity_tests.py +0 -0
  366. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_base_generation.py +0 -0
  367. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  368. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_composite_unique_generation.py +0 -0
  369. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_dto_identity_generation.py +0 -0
  370. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_entity_capability_generation.py +0 -0
  371. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_enum_ordering.py +0 -0
  372. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_excluded_fk_targets.py +0 -0
  373. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_generate_schema.py +0 -0
  374. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_junction_analysis.py +0 -0
  375. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_multi_schema_output.py +0 -0
  376. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_schema_analysis.py +0 -0
  377. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_sql_expression_defaults.py +0 -0
  378. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_table_relationships_query.py +0 -0
  379. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_view_generation.py +0 -0
  380. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_array_default_parsing.py +0 -0
  381. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_enum_array_column.py +0 -0
  382. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  383. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  384. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_never_filter_columns.py +0 -0
  385. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/__init__.py +0 -0
  386. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/conftest.py +0 -0
  387. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_a_failed_capture_still_names_the_failure.py +0 -0
  388. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_a_grouped_update_writes_only_each_rows_own_fields.py +0 -0
  389. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_cache_no_downgrade.py +0 -0
  390. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_capture_poison_proof.py +0 -0
  391. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_coalesce.py +0 -0
  392. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_dag.py +0 -0
  393. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_disk_spill_fallback.py +0 -0
  394. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_flush_individual_fallback.py +0 -0
  395. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_governed_write.py +0 -0
  396. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_managed.py +0 -0
  397. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_op_enqueue_site.py +0 -0
  398. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_pending_insert_honors_operator_filters.py +0 -0
  399. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_reads.py +0 -0
  400. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_record_error_bounded_insert.py +0 -0
  401. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_record_error_storm.py +0 -0
  402. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_session.py +0 -0
  403. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_spill_guard_plugin.py +0 -0
  404. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_tests_never_write_live_system_error.py +0 -0
  405. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_backend_handed_over_mid_transaction_never_takes_our_writes.py +0 -0
  406. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_bulk_cache_never_stalls_the_loop.py +0 -0
  407. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_keyword_column_refuses_generation.py +0 -0
  408. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_list_of_uuids_binds_as_uuid_array.py +0 -0
  409. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_read_only_backend_never_refuses_our_writes.py +0 -0
  410. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_sealed_field_shows_no_character_of_its_value.py +0 -0
  411. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_array_arg_element_type.py +0 -0
  412. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_attachment_mutation_serialization.py +0 -0
  413. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_backup_alias_conflict.py +0 -0
  414. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_backup_history_restore.py +0 -0
  415. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_backup_restore_live.py +0 -0
  416. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_call_function_as.py +0 -0
  417. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_call_function_none_mode.py +0 -0
  418. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_composite_fk_is_not_a_column_fk.py +0 -0
  419. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_database_name_alias.py +0 -0
  420. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_db_functions.py +0 -0
  421. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_dsn_password_masking.py +0 -0
  422. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_dynamic_select_unregistered.py +0 -0
  423. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_first_asks_the_database_for_one_row.py +0 -0
  424. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_foreign_session_state.py +0 -0
  425. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_immutable_insert_or_exact_verify.py +0 -0
  426. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_keyed_digest.py +0 -0
  427. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_model_cls_refactor.py +0 -0
  428. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_enable.py +0 -0
  429. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_lifecycle.py +0 -0
  430. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_lifecycle_fences.py +0 -0
  431. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_passkey_receipts.py +0 -0
  432. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_source.py +0 -0
  433. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  434. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_one_database.py +0 -0
  435. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_order_by_desc_nulls_last.py +0 -0
  436. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_org_vault_gate.py +0 -0
  437. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_package_wiring_views.py +0 -0
  438. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_pooler_role_guard.py +0 -0
  439. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_secret_audit_names_its_organization.py +0 -0
  440. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_secrets_battery.py +0 -0
  441. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_secrets_minting_keys.py +0 -0
  442. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_shared_lock_guard.py +0 -0
  443. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_sql_param_casts.py +0 -0
  444. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_state_cache_sweeps_stale.py +0 -0
  445. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_statement_ceiling_warning.py +0 -0
  446. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_sync_twins_guard.py +0 -0
  447. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_update_result_truthiness.py +0 -0
  448. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_user_visible_lookup.py +0 -0
  449. {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_vault_aggregate_recovery_stateful.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - **Fix: an instance `update()` / `save()` runs a SQL expression instead of storing it.**
6
+ `row.update(metadata=JsonbMerge("metadata", {...}))` sent the expression through
7
+ `JSONField.get_db_prep_value`, and `json.dumps` wrote its attribute dict over the
8
+ column (1,328 live rows lost their metadata, 2026-10-22); a text column stored
9
+ `str(expression)`. `update_instance` and `bulk_update` now hand every expression to
10
+ the executor's SET compiler exactly as `update_where` does, and the instance ends
11
+ holding the computed value (re-read when RETURNING did not carry it). `F("col")` is
12
+ now a valid SET value. Every serialising path refuses an expression loudly with the
13
+ new `ExpressionAsDataError` (a `TypeError`): all `Field.get_db_prep_value`
14
+ implementations (wrapped per subclass), the json encoders (nested expressions), json
15
+ function arguments, and PostgREST adapter writes. New export `is_sql_expression`.
16
+ Guard: `tests/test_instance_update_runs_expressions.py` (9 live round trips; all 9
17
+ red on the prior code).
18
+
5
19
  - **Feature: entity doors in the associations engine.** A registry token may resolve to a
6
20
  *door* (`__entity_door__ = True`, `async load_by_ids(ids, *, actor_id) -> {id: row}`) instead of
7
21
  a Model, for nouns whose rows live behind the host's read doors (a record store). The engine
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matrx-orm
3
- Version: 3.1.242
3
+ Version: 3.1.244
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
@@ -129,7 +129,10 @@ from .core.expressions import (
129
129
  Like,
130
130
  Regex,
131
131
  JsonbConcat,
132
+ ExpressionAsDataError,
132
133
  JsonbMerge,
134
+ JsonbRemoveKeys,
135
+ is_sql_expression,
133
136
  JsonbObjectAgg,
134
137
  Lag,
135
138
  LastValue,
@@ -645,7 +648,10 @@ __all__ = [
645
648
  "ArrayIndex",
646
649
  "JsonText",
647
650
  "JsonbConcat",
651
+ "ExpressionAsDataError",
648
652
  "JsonbMerge",
653
+ "JsonbRemoveKeys",
654
+ "is_sql_expression",
649
655
  "JsonbObjectAgg",
650
656
  "Filter",
651
657
  "Case",
@@ -225,6 +225,15 @@ class PostgRESTClientAdapter(BaseAdapter):
225
225
  filters = self._translate_filters(query_dict.get("filters", {}))
226
226
  data: dict | list = query_dict.get("data", {})
227
227
 
228
+ # PostgREST takes JSON rows, not SQL: an expression object here would be
229
+ # serialised as data. Refuse before anything is sent.
230
+ from matrx_orm.core.expressions import ExpressionAsDataError, is_sql_expression
231
+
232
+ for row in data if isinstance(data, list) else [data]:
233
+ for column, value in (row or {}).items():
234
+ if is_sql_expression(value):
235
+ raise ExpressionAsDataError(column, value)
236
+
228
237
  logger.debug("[PostgRESTClientAdapter] %s %s", operation.upper(), table)
229
238
 
230
239
  try:
@@ -261,14 +261,21 @@ pinned server session's identity before and after `RESET SESSION AUTHORIZATION`
261
261
  borrowed role. This also repairs both `SET ROLE` and `SET SESSION AUTHORIZATION`;
262
262
  `RESET ROLE` alone does not repair the latter.
263
263
 
264
- **A clean, genuinely empty read is proved in ONE statement.** Before the pinned
265
- re-proof, `prove_empty_in_one_statement` runs `current_user`, `session_user`, the
266
- backend's login role (`pg_stat_activity.usename`, which `SET SESSION AUTHORIZATION`
267
- does not change) and `EXISTS (<query>)` as one statement — one server connection,
268
- one role. All three identities equal and no row: proven empty, one round trip
269
- instead of seven. Any other outcome (borrowed role, session-authorization swap,
270
- unreadable login, rows that appeared, a query that cannot embed, any error) runs
271
- the full repairing re-proof unchanged. Pinned by `tests/test_pooler_role_guard.py`.
264
+ **A read proves its own role; an empty result is never re-run** (2026-10-21). On a
265
+ transaction pool every single SELECT/WITH read is sent as `guarded_read_sql(query,
266
+ login)`: `SELECT * FROM (<query>) _matrx_guarded_read WHERE CASE WHEN current_user =
267
+ <login> AND session_user = <login> THEN true ELSE ('matrx_orm_borrowed_role:' ||
268
+ current_user)::int > 0 END`. The guard has no column references, so Postgres runs it
269
+ as a one-time filter before the scan (verified live with EXPLAIN; order and duplicate
270
+ column names preserved). A clean answer, empty or not, is proven by that one
271
+ statement; a borrowed role RAISES (22P02 naming the role), and only then does
272
+ `repair_guarded_read` emit the contamination alarm and re-read on a pinned, reset
273
+ session. `<login>` is learned once per pool by `learn_login_role` (pinned transaction,
274
+ both resets, `session_user`). Before this, every zero-row read was re-run as an
275
+ `EXISTS` proof: 3.5M extra statements in 5.7 h on the live database. A query that
276
+ cannot be wrapped (multi-statement, SHOW, VALUES) keeps the old path
277
+ (`prove_empty_in_one_statement`, then the pinned re-proof). Pinned by
278
+ `tests/level1/test_guarded_read_never_reruns_empty.py` and `tests/test_pooler_role_guard.py`.
272
279
 
273
280
  **Round trips are the launch cost on a remote pooler.** Every top-level BEGIN sets
274
281
  its ceilings, the host's declared session context and the opener's own settings
@@ -23,6 +23,10 @@ from matrx_orm.core.foreign_session_state import run_clearing_foreign_restrict
23
23
  from matrx_orm.core.pooler_role import (
24
24
  VERIFY_EMPTY_READS,
25
25
  assert_no_session_role_change,
26
+ borrowed_role_from_error,
27
+ guarded_read_sql,
28
+ learn_login_role,
29
+ repair_guarded_read,
26
30
  reprove_empty_read,
27
31
  )
28
32
  from matrx_orm.core.statement_ceiling import resolve_driver_timeout
@@ -454,6 +458,9 @@ class AsyncDatabaseManager:
454
458
  # Only a transaction-mode pool can be handed a server connection another
455
459
  # client left wearing a role, so a direct Postgres pays nothing.
456
460
  _empty_read_verification: dict[str, bool] = {}
461
+ # Our own login role per transaction-pool config, learned ONCE on a pinned,
462
+ # reset session; every guarded read compares the backend's identity to it.
463
+ _login_roles: dict[str, str] = {}
457
464
 
458
465
  @classmethod
459
466
  def _verifies_empty_reads(cls, config_name: str) -> bool:
@@ -1464,11 +1471,56 @@ class AsyncDatabaseManager:
1464
1471
  in_transaction=False,
1465
1472
  )
1466
1473
  try:
1474
+ # Layer 2: on a transaction pool the read carries its own
1475
+ # role proof (pooler_role.guarded_read_sql). A clean result —
1476
+ # empty or not — is proven by this one statement; the guard
1477
+ # RAISES on a borrowed role, and only then is the read repaired.
1478
+ guarded_sql: str | None = None
1479
+ login_role: str | None = None
1480
+ if (
1481
+ verify_empty
1482
+ and read_only
1483
+ and cls._verifies_empty_reads(config_name)
1484
+ ):
1485
+ login_role = cls._login_roles.get(config_name)
1486
+ if login_role is None:
1487
+ login_role = await learn_login_role(conn)
1488
+ cls._login_roles[config_name] = login_role
1489
+ guarded_sql = guarded_read_sql(query, login_role)
1490
+ repaired: list[list[dict[str, Any]]] = []
1491
+
1492
+ async def _fetch() -> list[Any]:
1493
+ if guarded_sql is None:
1494
+ return await conn.fetch(query, *args, timeout=driver_timeout)
1495
+ try:
1496
+ return await conn.fetch(
1497
+ guarded_sql, *args, timeout=driver_timeout
1498
+ )
1499
+ except asyncpg.exceptions.InvalidTextRepresentationError as exc:
1500
+ borrowed = borrowed_role_from_error(exc)
1501
+ if borrowed is None:
1502
+ raise
1503
+ repaired.append(
1504
+ await repair_guarded_read(
1505
+ conn,
1506
+ config_name=config_name,
1507
+ query=query,
1508
+ args=args,
1509
+ statement_timeout=driver_timeout,
1510
+ normalize=_normalize_record,
1511
+ borrowed_role=borrowed,
1512
+ login_role=login_role or "<unknown>",
1513
+ )
1514
+ )
1515
+ return []
1516
+
1467
1517
  results = await run_clearing_foreign_restrict(
1468
1518
  conn,
1469
- lambda: conn.fetch(query, *args, timeout=driver_timeout),
1519
+ _fetch,
1470
1520
  what=f"execute_query {query[:80]!r}",
1471
1521
  )
1522
+ if repaired:
1523
+ return repaired[0]
1472
1524
  if read_attempt > 0:
1473
1525
  logger.warning(
1474
1526
  "[read-retry] SELECT HEALED on attempt %d after a "
@@ -1478,14 +1530,16 @@ class AsyncDatabaseManager:
1478
1530
  )
1479
1531
  if (
1480
1532
  not results
1533
+ and guarded_sql is None
1481
1534
  and verify_empty
1482
1535
  and read_only
1483
1536
  and cls._verifies_empty_reads(config_name)
1484
1537
  ):
1485
- # Layer 2: an empty result from a pooled connection is
1486
- # unproven until a session that can state its own role
1487
- # says so. A borrowed `authenticated` role returns ZERO
1488
- # ROWS on an RLS table instead of raising.
1538
+ # A read that could not carry its own guard (not one
1539
+ # SELECT/WITH statement): an empty result from a pooled
1540
+ # connection is unproven until a session that can state
1541
+ # its own role says so. A borrowed `authenticated` role
1542
+ # returns ZERO ROWS on an RLS table instead of raising.
1489
1543
  true_rows = await reprove_empty_read(
1490
1544
  conn,
1491
1545
  config_name=config_name,
@@ -1575,6 +1575,34 @@ class JsonbMerge(Func):
1575
1575
  return f"JsonbMerge({self.args[0]!r}, {self.value!r})"
1576
1576
 
1577
1577
 
1578
+ class JsonbRemoveKeys(Func):
1579
+ """``COALESCE(field, '{}'::jsonb) - $keys::text[]`` — atomically drop top-level
1580
+ keys from a JSONB column (the removal sibling of :class:`JsonbMerge`), for use as
1581
+ an ``.update()`` / ``update_where()`` SET value. Every other key is untouched;
1582
+ a key that is absent is a no-op.
1583
+
1584
+ Usage::
1585
+
1586
+ await SiteKeywordValue.update_where(
1587
+ {"id__in": ids}, metadata=JsonbRemoveKeys("metadata", ["serp_cluster"])
1588
+ )
1589
+ """
1590
+
1591
+ function = "JSONB_REMOVE_KEYS"
1592
+
1593
+ def __init__(self, field: str, keys: list[str] | tuple[str, ...]) -> None:
1594
+ super().__init__(field)
1595
+ self.keys = [str(k) for k in keys]
1596
+
1597
+ def as_sql(self, params: list[Any]) -> str:
1598
+ inner = _render_func_arg(self.args[0], params)
1599
+ params.append(self.keys)
1600
+ return f"COALESCE({inner}, '{{}}'::jsonb) - ${len(params)}::text[]"
1601
+
1602
+ def __repr__(self) -> str:
1603
+ return f"JsonbRemoveKeys({self.args[0]!r}, {self.keys!r})"
1604
+
1605
+
1578
1606
  class JsonbConcat(Func):
1579
1607
  """``a || b`` — jsonb concat of two RENDERABLE expressions (columns,
1580
1608
  ``F()``/``Excluded()`` references, or nested ``Func``s), top-level keys
@@ -2566,3 +2594,51 @@ class TsMatch:
2566
2594
  f"TsMatch({self.column!r}, config={self.config!r}, "
2567
2595
  f"column_is_tsvector={self.column_is_tsvector})"
2568
2596
  )
2597
+
2598
+
2599
+ # ---------------------------------------------------------------------------
2600
+ # Expression-as-data guard
2601
+ # ---------------------------------------------------------------------------
2602
+
2603
+ # Expression types that carry no ``as_sql`` of their own (they are rendered by
2604
+ # their consumers) — listed so the predicate below still recognises them.
2605
+ _EXPRESSION_TYPES_WITHOUT_AS_SQL: tuple[type, ...] = (F, Q, OuterRef)
2606
+
2607
+
2608
+ def is_sql_expression(value: Any) -> bool:
2609
+ """True when *value* is a SQL expression object, never a storable value.
2610
+
2611
+ Every class in this module is SQL: either it renders itself through
2612
+ ``as_sql`` (``Func`` and all its subclasses — ``JsonbMerge``, ``Now``,
2613
+ ``Coalesce``…, ``Expression``/``BinaryExpr`` increments, ``Case``,
2614
+ ``Subquery``, ``Value``, ``Excluded``, predicates, ``Window``…) or it is
2615
+ one of the few renderer-side references (``F``, ``Q``, ``OuterRef``).
2616
+ ``as_sql`` is looked up on the CLASS, so a test double whose instance
2617
+ answers every attribute (``MagicMock``) is not mistaken for SQL.
2618
+ """
2619
+ if value is None or isinstance(value, type):
2620
+ return False
2621
+ if isinstance(value, _EXPRESSION_TYPES_WITHOUT_AS_SQL):
2622
+ return True
2623
+ return any("as_sql" in vars(klass) for klass in type(value).__mro__)
2624
+
2625
+
2626
+ class ExpressionAsDataError(TypeError):
2627
+ """A SQL expression object reached a path that would store it as data.
2628
+
2629
+ Raised instead of serialising the object (a jsonb column used to receive
2630
+ ``{"args": [...], "value": ...}`` — the 2026-10-22 metadata loss on 1,328
2631
+ rows). Expressions are compiled to SQL by ``Model.update_where`` /
2632
+ ``QueryBuilder.update`` and by instance ``update()`` / ``save()``; every
2633
+ other write path (INSERT, upsert data, filters) refuses them here.
2634
+ """
2635
+
2636
+ def __init__(self, field_name: str | None, value: Any) -> None:
2637
+ self.field_name = field_name
2638
+ self.value = value
2639
+ super().__init__(
2640
+ f"{type(value).__name__} is a SQL expression, not a value — refusing to "
2641
+ f"serialise {value!r} into field {field_name!r}. Expressions run as SQL only "
2642
+ f"in an UPDATE SET: use Model.update_where(filters, {field_name}=...) or "
2643
+ f"instance.update({field_name}=...)."
2644
+ )
@@ -44,7 +44,35 @@ def _strip_nul_bytes(value: Any) -> Any:
44
44
  return value
45
45
 
46
46
 
47
+ def _refuse_expression_as_data(prep: Any) -> Any:
48
+ """Wrap a ``get_db_prep_value`` so a SQL expression object can never be
49
+ serialised as a stored value (``json.dumps`` / ``str()`` of a ``JsonbMerge``
50
+ wrote its attribute dict into live jsonb columns). Every Field subclass is
51
+ wrapped at class creation, so a new field type cannot forget the check."""
52
+ import functools
53
+
54
+ if getattr(prep, "_refuses_expressions", False):
55
+ return prep
56
+
57
+ @functools.wraps(prep)
58
+ def guarded(self: Any, value: Any, *args: Any, **kwargs: Any) -> Any:
59
+ from matrx_orm.core.expressions import ExpressionAsDataError, is_sql_expression
60
+
61
+ if is_sql_expression(value):
62
+ raise ExpressionAsDataError(getattr(self, "name", None), value)
63
+ return prep(self, value, *args, **kwargs)
64
+
65
+ guarded._refuses_expressions = True # type: ignore[attr-defined]
66
+ return guarded
67
+
68
+
47
69
  class Field:
70
+ def __init_subclass__(cls, **kwargs: Any) -> None:
71
+ super().__init_subclass__(**kwargs)
72
+ own = cls.__dict__.get("get_db_prep_value")
73
+ if own is not None:
74
+ cls.get_db_prep_value = _refuse_expression_as_data(own) # type: ignore[method-assign]
75
+
48
76
  if TYPE_CHECKING:
49
77
 
50
78
  @overload
@@ -147,6 +175,8 @@ class Field:
147
175
  return None
148
176
  return value
149
177
 
178
+ get_db_prep_value = _refuse_expression_as_data(get_db_prep_value)
179
+
150
180
  def desc(self):
151
181
  """Mark this field for descending order."""
152
182
  self._order_direction = "DESC"
@@ -1166,6 +1196,13 @@ class JSONBField(Field, Generic[_JT]):
1166
1196
  import uuid as _uuid
1167
1197
  from decimal import Decimal as _Decimal
1168
1198
 
1199
+ from matrx_orm.core.expressions import ExpressionAsDataError, is_sql_expression
1200
+
1201
+ # A SQL expression nested inside a json value is never data — degrading
1202
+ # it to its attribute dict is exactly how JsonbMerge objects were stored.
1203
+ if is_sql_expression(obj):
1204
+ raise ExpressionAsDataError(None, obj)
1205
+
1169
1206
  # Pydantic v2 — preferred for the JSON-mode dump that handles nested
1170
1207
  # exotic types correctly.
1171
1208
  model_dump = getattr(obj, "model_dump", None)
@@ -35,9 +35,17 @@ TWO INDEPENDENT LAYERS, each sufficient alone, each screaming when it fires:
35
35
  this guards everything else. The sanctioned ways to change role remain
36
36
  ``matrx_orm.rls_session`` and ``db/pooler_session.py::impersonate``.
37
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).
38
+ 2. **Proof in the read itself, repair only when it fires.** On a transaction-pool
39
+ config every read-only query is sent wrapped by ``guarded_read_sql``: the
40
+ statement carries a one-time filter that RAISES when the backend's
41
+ ``current_user``/``session_user`` is not our login role (learned once per pool
42
+ by ``learn_login_role``). One statement runs on one server connection, so a
43
+ clean answer — rows or no rows — is proven by the same round trip that read
44
+ it; nothing is ever re-run because a result was empty (2026-10-21: the old
45
+ per-empty-result re-proof was 3.5M extra statements in 5.7 h). Only when the
46
+ guard fires, or a query cannot be wrapped (multi-statement), does the
47
+ repairing re-read below run: re-run inside an explicit transaction (pinned to
48
+ ONE server connection).
41
49
  The pinned session's identity is captured before and after resetting session
42
50
  authorization and role. The caller gets any rows that appeared, but a
43
51
  contamination alarm is emitted only when the role change was directly
@@ -46,9 +54,9 @@ TWO INDEPENDENT LAYERS, each sufficient alone, each screaming when it fires:
46
54
 
47
55
  Layer 2 follows the repo's guard doctrine: a guard that CAN reconcile MUST
48
56
  reconcile — we hold the correct answer, so killing the caller's request would be
49
- strictly worse than returning it loudly. It costs nothing on a non-empty read and
50
- nothing at all on a direct (session-mode) Postgres connection, where cross-client
51
- contamination cannot happen.
57
+ strictly worse than returning it loudly. A clean read pays one constant
58
+ one-time filter; a direct (session-mode) Postgres connection pays nothing at all,
59
+ because cross-client contamination cannot happen there.
52
60
  """
53
61
 
54
62
  from __future__ import annotations
@@ -223,6 +231,115 @@ async def emit_contamination(event: EmptyReadContaminationEvent) -> None:
223
231
  logger.exception("[pooler-role] contamination sink raised")
224
232
 
225
233
 
234
+ # The text the guard's one-time filter raises with. ``('<sentinel>' || role)::int``
235
+ # fails with 22P02 and names the borrowed role; nothing else in a read produces it.
236
+ BORROWED_ROLE_SENTINEL = "matrx_orm_borrowed_role:"
237
+ _SENTINEL_IN_ERROR = re.compile(re.escape(BORROWED_ROLE_SENTINEL) + r"([^\"']*)")
238
+
239
+
240
+ def _sql_literal(value: str) -> str:
241
+ return "'" + value.replace("'", "''") + "'"
242
+
243
+
244
+ def guarded_read_sql(query: str, login_role: str | None) -> str | None:
245
+ """``query`` wrapped so the read proves its own role, or ``None`` if it cannot be.
246
+
247
+ ``SELECT * FROM (<query>) _q WHERE <guard>``: the guard has no column
248
+ references, so Postgres plans it as a ONE-TIME FILTER evaluated once before
249
+ the scan (measured live 2026-10-21 — ``Result / One-Time Filter`` over the
250
+ original plan, order and duplicate column names preserved). On our login
251
+ role it is ``true``; on any other ``current_user`` or ``session_user`` it
252
+ casts ``BORROWED_ROLE_SENTINEL || current_user`` to int and RAISES, so a
253
+ borrowed role can never come back as an empty or partial result. Comparing
254
+ both identities against the login role learned for this pool also catches a
255
+ ``SET SESSION AUTHORIZATION`` swap, where the two agree on the wrong name.
256
+
257
+ Returns ``None`` (caller falls back to the repairing re-proof on empty) for
258
+ a query that is not a single SELECT/WITH statement or when the login role is
259
+ not yet known.
260
+ """
261
+ if not login_role:
262
+ return None
263
+ body = query.strip().rstrip(";").strip()
264
+ if not body or ";" in body:
265
+ return None
266
+ head = body.lstrip("( \n\t")[:6].lower()
267
+ if not (head.startswith("select") or head.startswith("with")):
268
+ # A leading comment, VALUES, TABLE, SHOW, EXPLAIN: not provably embeddable.
269
+ return None
270
+ role = _sql_literal(login_role)
271
+ # Newlines around the body: a trailing ``-- comment`` must not swallow the
272
+ # closing parenthesis.
273
+ return (
274
+ "SELECT * FROM (\n"
275
+ f"{body}\n"
276
+ ") _matrx_guarded_read WHERE CASE WHEN current_user = "
277
+ f"{role} AND session_user = {role} THEN true "
278
+ f"ELSE ({_sql_literal(BORROWED_ROLE_SENTINEL)} || current_user)::int > 0 END"
279
+ )
280
+
281
+
282
+ def borrowed_role_from_error(exc: BaseException) -> str | None:
283
+ """The role named by a fired read guard, or ``None`` for any other error."""
284
+ match = _SENTINEL_IN_ERROR.search(str(exc))
285
+ if match is None:
286
+ return None
287
+ return match.group(1) or "<unknown>"
288
+
289
+
290
+ async def learn_login_role(conn: Any) -> str:
291
+ """Our own login role, read on a pinned, reset session (once per pool).
292
+
293
+ The explicit transaction pins every statement to one server connection, and
294
+ both resets put a borrowed role back before ``session_user`` is read, so the
295
+ answer is the role the backend authenticated as — never a borrowed one.
296
+ """
297
+ async with conn.transaction():
298
+ await conn.execute("RESET SESSION AUTHORIZATION")
299
+ await conn.execute("RESET ROLE")
300
+ row = await conn.fetchrow("SELECT session_user AS login_, current_user AS current_")
301
+ login = row["login_"]
302
+ if not login or row["current_"] != login:
303
+ raise PoolerRoleContamination(
304
+ f"could not learn the login role: a reset session still runs as "
305
+ f"'{row['current_']}' (session user '{login}')"
306
+ )
307
+ return str(login)
308
+
309
+
310
+ async def repair_guarded_read(
311
+ conn: Any,
312
+ *,
313
+ config_name: str,
314
+ query: str,
315
+ args: tuple[Any, ...],
316
+ statement_timeout: float | None,
317
+ normalize: Callable[[Any], dict[str, Any]],
318
+ borrowed_role: str,
319
+ login_role: str,
320
+ ) -> list[dict[str, Any]]:
321
+ """The read guard fired: scream, then re-read on a pinned, repaired session.
322
+
323
+ The guard's raise IS direct observation of a borrowed role on the backend
324
+ that served the read, so the contamination alarm is emitted unconditionally.
325
+ The caller gets the true rows (``[]`` when the clean read is empty).
326
+ """
327
+ rows, _observed = await _pinned_reread(
328
+ conn, query=query, args=args, statement_timeout=statement_timeout, normalize=normalize
329
+ )
330
+ await emit_contamination(
331
+ EmptyReadContaminationEvent(
332
+ config_name=config_name,
333
+ query=query,
334
+ contaminated_role=borrowed_role,
335
+ login_role=login_role,
336
+ true_row_count=len(rows or []),
337
+ occurred_at=datetime.now(UTC),
338
+ )
339
+ )
340
+ return rows or []
341
+
342
+
226
343
  def _single_statement_proof_sql(query: str) -> str | None:
227
344
  """The one-statement absence proof for ``query``, or ``None`` if it cannot embed.
228
345
 
@@ -306,6 +423,44 @@ async def reprove_empty_read(
306
423
  ):
307
424
  return None
308
425
 
426
+ rows, observed = await _pinned_reread(
427
+ conn, query=query, args=args, statement_timeout=statement_timeout, normalize=normalize
428
+ )
429
+ if rows is None:
430
+ return None
431
+ # Rows may legitimately appear between two READ COMMITTED statements.
432
+ # guest_executions is the canonical example: concurrent first requests
433
+ # for one fingerprint race a lookup against creation. Empty -> non-empty
434
+ # is reconciliation, not proof of pool contamination. Only the captured
435
+ # before/after identity change can put this class in the repair queue.
436
+ if observed is not None:
437
+ await emit_contamination(
438
+ EmptyReadContaminationEvent(
439
+ config_name=config_name,
440
+ query=query,
441
+ contaminated_role=observed[0],
442
+ login_role=observed[1],
443
+ true_row_count=len(rows),
444
+ occurred_at=datetime.now(UTC),
445
+ )
446
+ )
447
+ return rows
448
+
449
+
450
+ async def _pinned_reread(
451
+ conn: Any,
452
+ *,
453
+ query: str,
454
+ args: tuple[Any, ...],
455
+ statement_timeout: float | None,
456
+ normalize: Callable[[Any], dict[str, Any]],
457
+ ) -> tuple[list[dict[str, Any]] | None, tuple[str, str] | None]:
458
+ """Re-read on ONE server connection after resetting both identities.
459
+
460
+ Returns ``(rows or None when empty, (borrowed, login) when the reset was seen
461
+ to change the identity else None)``. Raises ``PoolerRoleContamination`` when
462
+ no attempt lands on a clean session.
463
+ """
309
464
  last_role: tuple[str, str] | None = None
310
465
  for _ in range(REPROVE_ATTEMPTS):
311
466
  async with conn.transaction():
@@ -325,29 +480,14 @@ async def reprove_empty_read(
325
480
  last_role = (current, login)
326
481
  continue
327
482
  rows = await conn.fetch(query, *args, timeout=statement_timeout)
328
- if not rows:
329
- return None
330
- true_rows = [normalize(r) for r in rows]
331
-
332
- observed_contamination = (before_current, before_session) != (current, login)
333
-
334
- # Rows may legitimately appear between two READ COMMITTED statements.
335
- # guest_executions is the canonical example: concurrent first requests
336
- # for one fingerprint race a lookup against creation. Empty -> non-empty
337
- # is reconciliation, not proof of pool contamination. Only the captured
338
- # before/after identity change can put this class in the repair queue.
339
- if observed_contamination:
340
- await emit_contamination(
341
- EmptyReadContaminationEvent(
342
- config_name=config_name,
343
- query=query,
344
- contaminated_role=before_current,
345
- login_role=login,
346
- true_row_count=len(true_rows),
347
- occurred_at=datetime.now(UTC),
348
- )
483
+ observed = (
484
+ (before_current, login)
485
+ if (before_current, before_session) != (current, login)
486
+ else None
349
487
  )
350
- return true_rows
488
+ if not rows:
489
+ return None, observed
490
+ return [normalize(r) for r in rows], observed
351
491
 
352
492
  current, login = last_role if last_role is not None else ("<unknown>", "<unknown>")
353
493
  raise PoolerRoleContamination(
@@ -358,6 +498,7 @@ async def reprove_empty_read(
358
498
 
359
499
 
360
500
  __all__ = [
501
+ "BORROWED_ROLE_SENTINEL",
361
502
  "ContaminationSink",
362
503
  "EmptyReadContaminationEvent",
363
504
  "PoolerRoleContamination",
@@ -365,8 +506,12 @@ __all__ = [
365
506
  "SessionRoleChangeRefused",
366
507
  "VERIFY_EMPTY_READS",
367
508
  "assert_no_session_role_change",
509
+ "borrowed_role_from_error",
368
510
  "emit_contamination",
511
+ "guarded_read_sql",
512
+ "learn_login_role",
369
513
  "register_contamination_sink",
514
+ "repair_guarded_read",
370
515
  "reprove_empty_read",
371
516
  "reset_contamination_sinks",
372
517
  ]
@@ -34,6 +34,21 @@ READ depends on the database, not on the deploy. `catalog_select`:
34
34
  Guard: `tests/test_dynamic_select_unregistered.py` (failing before the fallback
35
35
  existed), plus `tests/level1/test_dynamic_crud.py` for the write-side refusals.
36
36
 
37
+ ## SQL expressions are compiled, never stored
38
+
39
+ An expression object (`JsonbMerge`, `F(...) + 1`, `Coalesce`, `Now()`, `Case`,
40
+ `Subquery`, `Value`, `Excluded`, any `Func` or class with `as_sql`, plus `F`/`Q`/
41
+ `OuterRef`) is SQL, never data. `update_where`, `QueryBuilder.update`, instance
42
+ `update()`/`save()` (`update_instance`) and `bulk_update` all hand it to the
43
+ executor's SET compiler; the instance ends holding the database-computed value.
44
+ Every other path that would serialise one — INSERT data, upsert rows, a json
45
+ value nesting one, a json function argument, a PostgREST write — raises
46
+ `ExpressionAsDataError` before anything is written (`Field.get_db_prep_value` is
47
+ wrapped for every Field subclass, and the json encoders refuse nested ones).
48
+ Predicate: `matrx_orm.is_sql_expression`. Guard:
49
+ `tests/test_instance_update_runs_expressions.py` (live round trip on a disposable
50
+ loopback Postgres, `ISOLATED_PG17_DSN`; red on the prior code).
51
+
37
52
  ## Event-loop safety
38
53
 
39
54
  - **Bulk row coercion never runs on the event loop.** `bulk_insert_ignore` and
@@ -51,6 +66,9 @@ Verified against `conflict_writes.py` and
51
66
 
52
67
  ## Change Log
53
68
 
69
+ - 2026-10-22 — Instance `update()`/`save()`/`bulk_update` compile SQL expressions;
70
+ every serialising path refuses them (`ExpressionAsDataError`).
71
+
54
72
  - 2026-09-26 — Added the typed `update_where` guard to `upsert_with_conflict`.
55
73
 
56
74
  - 2026-09-11 — Added the live-catalog read fallback so an unregistered view or a
@@ -34,6 +34,16 @@ if TYPE_CHECKING: # pragma: no cover
34
34
  _FUNCTION_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$")
35
35
 
36
36
 
37
+ def _json_arg_default(obj: Any) -> str:
38
+ """``default=str`` for json-bound function args — except a SQL expression,
39
+ which must never be stringified into data."""
40
+ from matrx_orm.core.expressions import ExpressionAsDataError, is_sql_expression
41
+
42
+ if is_sql_expression(obj):
43
+ raise ExpressionAsDataError(None, obj)
44
+ return str(obj)
45
+
46
+
37
47
  def _render_call(function_name: str, args: tuple[Any, ...]) -> tuple[str, list[Any]]:
38
48
  if not _FUNCTION_NAME_RE.match(function_name):
39
49
  raise ValueError(f"call_db_function: invalid function name {function_name!r}")
@@ -47,7 +57,7 @@ def _render_call(function_name: str, args: tuple[Any, ...]) -> tuple[str, list[A
47
57
  values.append(arg)
48
58
  elif isinstance(arg, dict | list):
49
59
  placeholders.append(f"${index}::jsonb")
50
- values.append(json.dumps(arg, ensure_ascii=False, default=str))
60
+ values.append(json.dumps(arg, ensure_ascii=False, default=_json_arg_default))
51
61
  else:
52
62
  placeholders.append(f"${index}")
53
63
  values.append(arg)