matrx-orm 3.1.3__tar.gz → 3.1.5__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 (272) hide show
  1. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/CLAUDE.md +1 -0
  2. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/async_db_manager.py +40 -0
  4. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/db_function.py +11 -7
  5. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/expressions.py +79 -6
  6. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/pyproject.toml +1 -1
  7. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_case_when.py +88 -8
  8. matrx_orm-3.1.5/tests/level1/test_connection_codecs.py +93 -0
  9. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/.gitignore +0 -0
  10. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/BASE_CLASS_METHODS.md +0 -0
  11. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/MODEL_API.md +0 -0
  12. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/README.md +0 -0
  13. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/database/orm/extended/managers/ai_model_base.py +0 -0
  14. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/docs/migrations.md +0 -0
  15. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/MODULE_README.md +0 -0
  16. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/__init__.py +0 -0
  17. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/adapters/MODULE_README.md +0 -0
  18. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/adapters/__init__.py +0 -0
  19. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/adapters/async_postgresql.py +0 -0
  20. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/adapters/base_adapter.py +0 -0
  21. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/adapters/postgresql.py +0 -0
  22. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  23. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/adapters/supabase_adapter.py +0 -0
  24. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/admin/__init__.py +0 -0
  25. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/admin/router.py +0 -0
  26. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/api/__init__.py +0 -0
  27. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/api/auth.py +0 -0
  28. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/api/config.py +0 -0
  29. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/api/handlers.py +0 -0
  30. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/api/protocol.py +0 -0
  31. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/api/server.py +0 -0
  32. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/associations.py +0 -0
  33. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/cache_debug.py +0 -0
  34. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/client/MODULE_README.md +0 -0
  35. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/client/__init__.py +0 -0
  36. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/client/postgres_connection.py +0 -0
  37. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/client/postgrest.py +0 -0
  38. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/client/supabase_auth.py +0 -0
  39. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/client/supabase_config.py +0 -0
  40. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/client/supabase_manager.py +0 -0
  41. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  42. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/MODULE_README.md +0 -0
  43. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  44. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/TASKS.md +0 -0
  45. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/__init__.py +0 -0
  46. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/base.py +0 -0
  47. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/config.py +0 -0
  48. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/diagnostics.py +0 -0
  49. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/extended.py +0 -0
  50. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/fields.py +0 -0
  51. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/introspection.py +0 -0
  52. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/listen.py +0 -0
  53. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/loop_filters.py +0 -0
  54. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/model_dto.py +0 -0
  55. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/model_view.py +0 -0
  56. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/paginator.py +0 -0
  57. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/pydantic_bridge.py +0 -0
  58. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/registry.py +0 -0
  59. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/relations.py +0 -0
  60. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/resilience.py +0 -0
  61. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/rls_session.py +0 -0
  62. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/signals.py +0 -0
  63. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/transaction.py +0 -0
  64. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/types.py +0 -0
  65. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/core/write_queue.py +0 -0
  66. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/entity.py +0 -0
  67. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/error_handling.py +0 -0
  68. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/exceptions.py +0 -0
  69. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/extended/__init__.py +0 -0
  70. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/extended/app_error_handler.py +0 -0
  71. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/middleware/__init__.py +0 -0
  72. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/middleware/base.py +0 -0
  73. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/MODULE_README.md +0 -0
  74. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/__init__.py +0 -0
  75. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/cli.py +0 -0
  76. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/ddl.py +0 -0
  77. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/diff.py +0 -0
  78. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/executor.py +0 -0
  79. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/integration.py +0 -0
  80. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/loader.py +0 -0
  81. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/operations.py +0 -0
  82. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/state.py +0 -0
  83. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/migrations/table_filter.py +0 -0
  84. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/MODULE_README.md +0 -0
  85. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/__init__.py +0 -0
  86. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/bulk_update_values.py +0 -0
  87. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/conflict.py +0 -0
  88. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/conflict_writes.py +0 -0
  89. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/create.py +0 -0
  90. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/delete.py +0 -0
  91. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/dynamic_admin.py +0 -0
  92. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/queue_claim.py +0 -0
  93. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/read.py +0 -0
  94. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/staging_load.py +0 -0
  95. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/operations/update.py +0 -0
  96. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/py.typed +0 -0
  97. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/python_sql/MODULE_README.md +0 -0
  98. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/python_sql/__init__.py +0 -0
  99. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/python_sql/db_objects.py +0 -0
  100. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  101. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  102. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/query/MODULE_README.md +0 -0
  103. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/query/__init__.py +0 -0
  104. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/query/builder.py +0 -0
  105. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/query/executor.py +0 -0
  106. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  107. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/__init__.py +0 -0
  108. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/code_handler.py +0 -0
  109. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/columns.py +0 -0
  110. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/common.py +0 -0
  111. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/diff_preview.py +0 -0
  112. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  113. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/generator.py +0 -0
  114. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  115. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  116. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  117. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  118. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/package_wiring.py +0 -0
  119. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/relationships.py +0 -0
  120. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/runner.py +0 -0
  121. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/schema.py +0 -0
  122. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/schema_manager.py +0 -0
  123. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/tables.py +0 -0
  124. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/schema_builder/views.py +0 -0
  125. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/README.md +0 -0
  126. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/__init__.py +0 -0
  127. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/coalesce.py +0 -0
  128. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/dag.py +0 -0
  129. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/errors.py +0 -0
  130. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/fallback.py +0 -0
  131. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/flush.py +0 -0
  132. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/lifecycle.py +0 -0
  133. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/managed.py +0 -0
  134. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/op.py +0 -0
  135. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/reads.py +0 -0
  136. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/session.py +0 -0
  137. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/session/telemetry.py +0 -0
  138. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  139. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/sql_executor/__init__.py +0 -0
  140. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/sql_executor/executor.py +0 -0
  141. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/sql_executor/queries.py +0 -0
  142. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/sql_executor/registry.py +0 -0
  143. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/sql_executor/types.py +0 -0
  144. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/sql_executor/utils.py +0 -0
  145. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/state.py +0 -0
  146. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/utils/__init__.py +0 -0
  147. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/utils/sql_utils.py +0 -0
  148. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/matrx_orm/utils/type_converters.py +0 -0
  149. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/scripts/git-branches.sh +0 -0
  150. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/scripts/publish.sh +0 -0
  151. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/scripts/release.sh +0 -0
  152. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/MODULE_README.md +0 -0
  153. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/__init__.py +0 -0
  154. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/conftest.py +0 -0
  155. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/MODULE_README.md +0 -0
  156. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/__init__.py +0 -0
  157. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_admin_db_columns.py +0 -0
  158. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_admin_search_sentinels.py +0 -0
  159. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_advisory_lock.py +0 -0
  160. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_agent_message.py +0 -0
  161. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_api_auth.py +0 -0
  162. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_api_config.py +0 -0
  163. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_api_handlers.py +0 -0
  164. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_api_protocol.py +0 -0
  165. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_array_agg_order_by.py +0 -0
  166. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_associations.py +0 -0
  167. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_bulk_update_by_pk.py +0 -0
  168. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  169. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_cache_debug.py +0 -0
  170. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_call_function.py +0 -0
  171. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_composite_pk_filter.py +0 -0
  172. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_config.py +0 -0
  173. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_conflict_target.py +0 -0
  174. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_conflict_writes.py +0 -0
  175. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_connection_poison_guard.py +0 -0
  176. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_count_composite_distinct.py +0 -0
  177. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_db_function_field.py +0 -0
  178. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_ddl_generator.py +0 -0
  179. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_dirty_tracking.py +0 -0
  180. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_exceptions.py +0 -0
  181. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_execute_admin_sql.py +0 -0
  182. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_expression_primitives.py +0 -0
  183. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_fields.py +0 -0
  184. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  185. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_filter_raw.py +0 -0
  186. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_from_alias.py +0 -0
  187. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_fts.py +0 -0
  188. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_func_expression_args.py +0 -0
  189. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_having_annotate_alias.py +0 -0
  190. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  191. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_introspect_rls_policies.py +0 -0
  192. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_join_and_aggregate.py +0 -0
  193. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_listen_notify.py +0 -0
  194. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_managed_write_guard.py +0 -0
  195. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_matrx_entity.py +0 -0
  196. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_migration_diff_types.py +0 -0
  197. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_migration_loader.py +0 -0
  198. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_model_instance.py +0 -0
  199. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_model_meta.py +0 -0
  200. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_order_by_expression.py +0 -0
  201. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_patch_jsonb_path.py +0 -0
  202. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_postgrest_filters.py +0 -0
  203. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_query_builder.py +0 -0
  204. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_query_error_detail.py +0 -0
  205. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_query_executor_sql.py +0 -0
  206. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_query_timeout.py +0 -0
  207. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_querybuilder_mutation_semantics.py +0 -0
  208. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_queue_claim.py +0 -0
  209. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_registry.py +0 -0
  210. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_registry_multi_database.py +0 -0
  211. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_registry_multi_schema.py +0 -0
  212. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_relations.py +0 -0
  213. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_resilience.py +0 -0
  214. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_schema_exists.py +0 -0
  215. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_session_advisory_lock.py +0 -0
  216. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_state_cache.py +0 -0
  217. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_subquery_filter_raw.py +0 -0
  218. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_subquery_in_filter.py +0 -0
  219. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_supabase_auth.py +0 -0
  220. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_supabase_config.py +0 -0
  221. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_update_case_expression.py +0 -0
  222. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_update_subquery.py +0 -0
  223. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_upsert_with_conflict.py +0 -0
  224. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_values_jsonb_decode.py +0 -0
  225. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_watched_lifecycle.py +0 -0
  226. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level1/test_write_retry_semantics.py +0 -0
  227. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/MODULE_README.md +0 -0
  228. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/__init__.py +0 -0
  229. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/conftest.py +0 -0
  230. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_bulk_ops.py +0 -0
  231. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_cache_integration.py +0 -0
  232. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_crud.py +0 -0
  233. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_foreign_keys.py +0 -0
  234. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_m2m.py +0 -0
  235. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_manager.py +0 -0
  236. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_migrations_live.py +0 -0
  237. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_query_execution.py +0 -0
  238. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/level2/test_schema_diff.py +0 -0
  239. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project/.env.example +0 -0
  240. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project/README.md +0 -0
  241. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project/__init__.py +0 -0
  242. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project/generate.py +0 -0
  243. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project/generated/.gitkeep +0 -0
  244. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project/matrx_orm.yaml +0 -0
  245. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project/test_schema_generation.py +0 -0
  246. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project_desktop/.env.example +0 -0
  247. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project_desktop/README.md +0 -0
  248. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project_desktop/__init__.py +0 -0
  249. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project_desktop/client_example.py +0 -0
  250. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project_desktop/client_example.ts +0 -0
  251. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  252. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/sample_project_desktop/server.py +0 -0
  253. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/schema/entity_tests.py +0 -0
  254. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/schema/test_base_generation.py +0 -0
  255. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  256. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/schema/test_generate_schema.py +0 -0
  257. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/schema/test_multi_schema_output.py +0 -0
  258. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/__init__.py +0 -0
  259. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/conftest.py +0 -0
  260. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_capture_poison_proof.py +0 -0
  261. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_coalesce.py +0 -0
  262. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_dag.py +0 -0
  263. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_disk_spill_fallback.py +0 -0
  264. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_flush_individual_fallback.py +0 -0
  265. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_governed_write.py +0 -0
  266. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_managed.py +0 -0
  267. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_op_enqueue_site.py +0 -0
  268. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_reads.py +0 -0
  269. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/session/test_session.py +0 -0
  270. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/test_database_name_alias.py +0 -0
  271. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/test_model_cls_refactor.py +0 -0
  272. {matrx_orm-3.1.3 → matrx_orm-3.1.5}/tests/test_sql_param_casts.py +0 -0
@@ -20,6 +20,7 @@ matrx-orm is a standalone async Postgres ORM (`Model`, `QueryBuilder`, migration
20
20
  - **`MatrxEntity`** (`matrx_orm/entity.py`) — **the platform entity contract as an ORM primitive.** A table registered in `platform.entity_types` generates as a `MatrxEntity` (everything else stays a plain `Model`) and gets the behavior instead of every consumer hand-wiring it: `await page.versions()` (not a query on `history.row_versions`), `await page.restore(3)` (not a `version_restore` RPC + a cache bust — `restore()` busts the cache itself), `Note.alive()` / `.dead()` / `.soft_delete()` / `.undelete()`, and `Page.capabilities()`. Capability flags (`_entity_token`, `_is_versioned`, `_has_soft_delete`, `_is_org_scoped`, `_rls_variant`) are **stamped by the generator from the LIVE DATABASE** (does the table really carry the `_version_capture` trigger? a `deleted_at` column?) — never from `entity_types`' own flags, which are wrong on 61 of its 242 rows. **The database is truth; a flag is a claim.** Using a capability an entity does not declare RAISES (`EntityCapabilityError`) and names the fix — it never returns silently-wrong data, and `is_deleted` refuses to answer from a column a partial fetch never loaded. Drift guard: `scripts/check_entity_drift.py` (loud, non-blocking, in `release.sh`).
21
21
  - **Change tracking** — every instance records which columns were **loaded** (a SELECTed row, or the caller's kwargs) and which were **changed**. `save()` writes only those, so `.only("id","email")` + `save()` can never NULL the columns it never fetched (it used to — silent data loss). Mutable containers (jsonb/arrays) are always rewritten when loaded, because `obj.metadata["k"]=1` cannot be seen by `__setattr__`; **reassigning** (`obj.metadata = {...}`) is tracked precisely and is the sanctioned way. `Model._hydrate()` is the ONE row→model entry point.
22
22
  - **Read-only models** (`_read_only = True`): a model that can be queried but NEVER written through the ORM — every write path raises `ReadOnlyModelError` (enforced at `QueryExecutor.insert/bulk_insert/upsert/update/delete`, so it covers all of `create`/`save`/`update`/`delete`/`update_where`/`delete_where`/`upsert`/`bulk_*`). Used for DB-managed tables the app must not mutate. The canonical **`auth.users`** model is baked into the schema generator (`get_string_user_model`) as the full, curated Supabase auth schema (safe columns only — no password/token fields), `_read_only`, bound per-database. Read a user's profile/metadata there; mutate users only through Supabase Auth (GoTrue).
23
+ - **Connection-level type codecs** (`async_db_manager._init_connection_default`): every pooled connection decodes **uuid → `str`**, **vector → `list[float]`**, and **json/jsonb → `dict`/`list`** at the driver boundary — the TYPE decides the decode, never the call path. So `call_function` scalar/rows, raw fetches, and field hydration all agree; a caller must **never `json.loads` a fetched json/jsonb value** (it's already decoded; the 2026-07-13 `build_agent_context` crash was a jsonb RPC fetched pre-codec as a raw string). Encoding: a pre-serialised JSON `str` passes through untouched; a native dict/list is dumped (`_encode_json`). Pinned by `tests/level1/test_connection_codecs.py`.
23
24
  - **Query layer**: `QueryBuilder`, expressions (`F`, `Q`), window functions, CTEs, subqueries.
24
25
  - **Migrations**: `MigrationDB`, `MigrationLoader`, `MigrationExecutor`, `makemigrations`, `migrate` — migrations declare explicit `dependencies` and apply in topological order (no multi-head/merge support yet).
25
26
  - **Admin router** (FastAPI): `admin_router` exposes read/write endpoints for any registered model.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.3
3
+ Version: 3.1.5
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
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import json
4
5
  import logging
5
6
  import os
6
7
  import traceback
@@ -143,6 +144,23 @@ def _decode_vector(value: str) -> list[float]:
143
144
  return [float(x) for x in inner.split(",")]
144
145
 
145
146
 
147
+ def _encode_json(value: Any) -> str:
148
+ """Encode a Python value bound to a ``json``/``jsonb`` parameter.
149
+
150
+ A ``str`` passes through untouched — every existing ORM write path
151
+ (``JSONField.get_db_prep_value``, ``JsonbMerge``, ``call_function``'s
152
+ ``_bind_args``) pre-serialises to a JSON string, and asyncpg's built-in
153
+ codec (which this replaces) also required ``str``. Anything else (dict,
154
+ list, int, ...) is serialised here with the same coercion hook the field
155
+ layer uses, so binding a native container directly now also works.
156
+ """
157
+ if isinstance(value, str):
158
+ return value
159
+ from matrx_orm.core.fields import JSONBField
160
+
161
+ return json.dumps(value, default=JSONBField._json_default)
162
+
163
+
146
164
  async def _init_connection_default(conn: asyncpg.Connection) -> None:
147
165
  """Register custom type codecs on each new connection.
148
166
 
@@ -166,6 +184,28 @@ async def _init_connection_default(conn: asyncpg.Connection) -> None:
166
184
  format="text",
167
185
  )
168
186
 
187
+ # json/jsonb → dict/list (decode) and str-or-native → json text (encode).
188
+ #
189
+ # Without this codec asyncpg hands json/jsonb values back as RAW JSON
190
+ # STRINGS. The field layer papered over that (``JSONField.to_python`` does
191
+ # ``json.loads``), but every fetch path that bypasses field hydration —
192
+ # ``call_function`` scalar/rows, ``field=`` dereference, sql_executor rows
193
+ # — returned a ``str`` where the caller expected a ``dict``. That is
194
+ # exactly the 2026-07-13 ``build_agent_context`` crash ("'str' object has
195
+ # no attribute 'get'"): a jsonb-returning RPC moved from PostgREST (which
196
+ # decodes) to ``call_function`` (which didn't). Decoding at the driver
197
+ # boundary kills the whole class: the DRIVER knows the value is jsonb, so
198
+ # the type — not the call path — decides the decode. ``to_python`` already
199
+ # passes non-str values through, so field hydration is unaffected.
200
+ for _json_type in ("json", "jsonb"):
201
+ await conn.set_type_codec(
202
+ _json_type,
203
+ encoder=_encode_json,
204
+ decoder=json.loads,
205
+ schema="pg_catalog",
206
+ format="text",
207
+ )
208
+
169
209
  # pgvector → list[float]
170
210
  #
171
211
  # The ``vector`` type lives in whichever schema the pgvector extension was
@@ -14,7 +14,11 @@ hand-write `SELECT schema.func(...)` itself — call this instead.
14
14
 
15
15
  Three return modes:
16
16
  * ``mode="scalar"`` (default) — the function returns one usable value
17
- (text, integer, boolean, ...); we ``fetchval`` it.
17
+ (text, integer, boolean, ...); we ``fetchval`` it. A ``json``/``jsonb``
18
+ return value arrives as a decoded ``dict``/``list`` (never a raw JSON
19
+ string) — the pool's connection-level codec
20
+ (``async_db_manager._init_connection_default``) decodes at the driver
21
+ boundary. Callers must NOT ``json.loads`` a result.
18
22
  * ``mode="none"`` — the function's return value is discarded (e.g. it
19
23
  returns a composite/row type the caller doesn't need decoded). We
20
24
  ``execute`` the statement instead of ``fetchval``, which never attempts
@@ -37,12 +41,12 @@ composite; the single field still comes back scalar, through ``fetchval``.
37
41
  alongside a row-set, is a caller bug, not a valid combination).
38
42
 
39
43
  A ``dict``/``list`` positional argument is JSON-serialised and its
40
- placeholder is cast explicitly (``$N::jsonb``) asyncpg has no jsonb
41
- encoder, so a bare dict would bind as an unsupported parameter type and
42
- raise. This mirrors the same trick ``JsonbMerge`` already uses for
43
- ORM-generated ``UPDATE`` statements (see
44
- ``matrx_orm.core.expressions.JsonbMerge``) — it is not a special case, it is
45
- how every jsonb-typed function parameter must be bound.
44
+ placeholder is cast explicitly (``$N::jsonb``). The pool's connection-level
45
+ json codec (``async_db_manager._encode_json``) passes the pre-serialised
46
+ string through untouched, so this never double-encodes. This mirrors the
47
+ same trick ``JsonbMerge`` already uses for ORM-generated ``UPDATE``
48
+ statements (see ``matrx_orm.core.expressions.JsonbMerge``) — it is not a
49
+ special case, it is how every jsonb-typed function parameter is bound.
46
50
 
47
51
  A genuine Postgres **array** parameter (``uuid[]``, ``text[]``, ...) is a
48
52
  different shape from jsonb — asyncpg natively encodes a Python ``list`` as a
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from datetime import date, datetime
4
+ from decimal import Decimal
3
5
  from typing import Any, TYPE_CHECKING
6
+ from uuid import UUID
4
7
 
5
8
  if TYPE_CHECKING:
6
9
  pass
@@ -533,6 +536,70 @@ def _render_func_arg(arg: Any, params: list[Any]) -> str:
533
536
  return f"${len(params)}"
534
537
 
535
538
 
539
+ # ---------------------------------------------------------------------------
540
+ # Type-anchoring for bound literals in a CASE branch
541
+ # ---------------------------------------------------------------------------
542
+ #
543
+ # Postgres infers a bare `$N`'s type from its surrounding context. A CASE whose
544
+ # THEN/ELSE branches are ALL bound params has NO anchor to infer from, so it
545
+ # falls back to `text` — and asyncpg then refuses to encode a Python int/bool/
546
+ # float into that text slot:
547
+ #
548
+ # invalid input for query argument $5: 0 (expected str, got int)
549
+ #
550
+ # Verified live (project txzxabzwovsujtloxrus):
551
+ # PREPARE p AS SELECT 1 FROM t ORDER BY CASE WHEN lower(name) = $1
552
+ # THEN $2 ELSE $3 END;
553
+ # -> parameter_types = {text,text,text} # $2/$3 silently become text
554
+ # ... THEN $2::bigint ELSE $3::bigint END
555
+ # -> parameter_types = {text,bigint,bigint} # anchored
556
+ #
557
+ # So a CASE-branch literal is bound WITH an explicit cast: the value stays
558
+ # parameterized (zero injection surface) and Postgres gets its type anchor.
559
+ # Anywhere else (`col = $N`, `func($N)`) the column/signature already supplies
560
+ # the anchor, which is why this is scoped to CASE rather than applied to every
561
+ # bound literal — a blanket `::text` would break `uuid_col = $N`.
562
+ #
563
+ # Order matters: bool before int (bool IS a subclass of int), datetime before
564
+ # date (datetime IS a subclass of date).
565
+ _PG_LITERAL_CASTS: tuple[tuple[type, str], ...] = (
566
+ (bool, "boolean"),
567
+ (int, "bigint"),
568
+ (float, "double precision"),
569
+ (Decimal, "numeric"),
570
+ (datetime, "timestamptz"),
571
+ (date, "date"),
572
+ (UUID, "uuid"),
573
+ )
574
+
575
+
576
+ def _pg_cast_for_literal(value: Any) -> str | None:
577
+ """The Postgres type to anchor a bound Python literal to, or None if unknown."""
578
+ for py_type, pg_type in _PG_LITERAL_CASTS:
579
+ if isinstance(value, py_type):
580
+ return pg_type
581
+ return None
582
+
583
+
584
+ def _render_case_result(arg: Any, params: list[Any]) -> str:
585
+ """Render a CASE THEN/ELSE branch, type-anchoring any bound literal.
586
+
587
+ Identical to :func:`_render_func_arg` for columns/expressions; the ONLY
588
+ difference is that a bound literal gets an explicit `::type` cast, because a
589
+ CASE branch is a context Postgres cannot infer a param's type from.
590
+ """
591
+ if arg is None:
592
+ # An unadorned NULL is unknown-typed too, but it unifies with whatever
593
+ # the sibling branch resolves to — no cast needed, and none is possible.
594
+ return "NULL"
595
+ if isinstance(arg, str) or hasattr(arg, "as_sql") or isinstance(arg, F):
596
+ # Bare strings are column names, per the Func-arg convention.
597
+ return _render_func_arg(arg, params)
598
+ params.append(arg)
599
+ cast = _pg_cast_for_literal(arg)
600
+ return f"${len(params)}::{cast}" if cast else f"${len(params)}"
601
+
602
+
536
603
  # ---------------------------------------------------------------------------
537
604
  # Case / When — conditional CASE WHEN ... THEN ... ELSE ... END expression
538
605
  # ---------------------------------------------------------------------------
@@ -552,8 +619,13 @@ class When:
552
619
 
553
620
  Usage::
554
621
 
555
- When("lower(e.name) = $1", phrase, then=0) # -> WHEN lower(e.name) = $1 THEN $2
622
+ When("lower(e.name) = $1", phrase, then=0) # -> WHEN lower(e.name) = $1 THEN $2::bigint
556
623
  When("ed.src_id = ANY($1::uuid[])", ids, then="ed.dst_id") # then = bare column, not bound
624
+
625
+ A bound ``then``/``default`` literal is rendered WITH an explicit ``::type``
626
+ cast — a CASE branch gives Postgres no type to infer from, so an unadorned
627
+ ``$N`` silently resolves to ``text`` and asyncpg then rejects a Python int
628
+ ("expected str, got int"). See :func:`_render_case_result`.
557
629
  """
558
630
 
559
631
  def __init__(self, condition: str, *condition_params: Any, then: Any) -> None:
@@ -569,7 +641,7 @@ class When:
569
641
  params.extend(self.condition_params)
570
642
  else:
571
643
  cond_sql = self.condition
572
- then_sql = _render_func_arg(self.then, params)
644
+ then_sql = _render_case_result(self.then, params)
573
645
  return f"WHEN {cond_sql} THEN {then_sql}"
574
646
 
575
647
  def __repr__(self) -> str:
@@ -615,7 +687,7 @@ class Case:
615
687
  for w in self.whens:
616
688
  parts.append(w.as_sql(params))
617
689
  if self.default is not None:
618
- parts.append(f"ELSE {_render_func_arg(self.default, params)}")
690
+ parts.append(f"ELSE {_render_case_result(self.default, params)}")
619
691
  parts.append("END")
620
692
  return " ".join(parts)
621
693
 
@@ -708,9 +780,10 @@ class JsonbMerge(Func):
708
780
  sibling, expressible directly on ``Model.update_where`` (no manager
709
781
  needed).
710
782
 
711
- ``value`` is JSON-serialised here (never left to the caller) so it binds
712
- as a plain string param cast to ``jsonb`` asyncpg has no jsonb codec
713
- registered, so a bare dict param would bind as text and fail the cast.
783
+ ``value`` is JSON-serialised here (never left to the caller) and binds
784
+ as a string param cast to ``jsonb``. The pool's connection-level json
785
+ codec (``async_db_manager._encode_json``) passes pre-serialised strings
786
+ through untouched, so this never double-encodes.
714
787
 
715
788
  Usage::
716
789
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.3"
3
+ version = "3.1.5"
4
4
  description = "Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -48,14 +48,14 @@ def test_case_rejects_non_when_args():
48
48
  def test_single_when_with_literal_then_and_default():
49
49
  params: list = []
50
50
  sql = Case(When("status = 'paid'", then=1), default=0).as_sql(params)
51
- assert sql == "CASE WHEN status = 'paid' THEN $1 ELSE $2 END"
51
+ assert sql == "CASE WHEN status = 'paid' THEN $1::bigint ELSE $2::bigint END"
52
52
  assert params == [1, 0]
53
53
 
54
54
 
55
55
  def test_when_condition_with_bound_param():
56
56
  params: list = []
57
57
  sql = Case(When("lower(e.name) = $1", "acme corp", then=0), default=1).as_sql(params)
58
- assert sql == "CASE WHEN lower(e.name) = $1 THEN $2 ELSE $3 END"
58
+ assert sql == "CASE WHEN lower(e.name) = $1 THEN $2::bigint ELSE $3::bigint END"
59
59
  assert params == ["acme corp", 0, 1]
60
60
 
61
61
 
@@ -75,7 +75,7 @@ def test_when_then_bare_column_name_not_bound():
75
75
  def test_no_default_omits_else_clause():
76
76
  params: list = []
77
77
  sql = Case(When("x = 1", then=0)).as_sql(params)
78
- assert sql == "CASE WHEN x = 1 THEN $1 END"
78
+ assert sql == "CASE WHEN x = 1 THEN $1::bigint END"
79
79
  assert "ELSE" not in sql
80
80
 
81
81
 
@@ -86,14 +86,14 @@ def test_multiple_when_branches():
86
86
  When("tier = 'silver'", then=2),
87
87
  default=1,
88
88
  ).as_sql(params)
89
- assert sql == "CASE WHEN tier = 'gold' THEN $1 WHEN tier = 'silver' THEN $2 ELSE $3 END"
89
+ assert sql == "CASE WHEN tier = 'gold' THEN $1::bigint WHEN tier = 'silver' THEN $2::bigint ELSE $3::bigint END"
90
90
  assert params == [3, 2, 1]
91
91
 
92
92
 
93
93
  def test_condition_params_shift_past_existing_params():
94
94
  params: list = ["existing"]
95
95
  sql = Case(When("name = $1", "bob", then=1), default=0).as_sql(params)
96
- assert sql == "CASE WHEN name = $2 THEN $3 ELSE $4 END"
96
+ assert sql == "CASE WHEN name = $2 THEN $3::bigint ELSE $4::bigint END"
97
97
  assert params == ["existing", "bob", 1, 0]
98
98
 
99
99
 
@@ -110,7 +110,7 @@ def test_nested_case():
110
110
  params: list = []
111
111
  inner = Case(When("b = 1", then=2), default=3)
112
112
  sql = Case(When("a = 1", then=inner), default=0).as_sql(params)
113
- assert sql == "CASE WHEN a = 1 THEN CASE WHEN b = 1 THEN $1 ELSE $2 END ELSE $3 END"
113
+ assert sql == "CASE WHEN a = 1 THEN CASE WHEN b = 1 THEN $1::bigint ELSE $2::bigint END ELSE $3::bigint END"
114
114
  assert params == [2, 3, 0]
115
115
 
116
116
 
@@ -167,7 +167,7 @@ def test_case_in_order_by():
167
167
  "-amount",
168
168
  )
169
169
  sql, params = QueryExecutor(qb._build_query())._build_query()
170
- assert "ORDER BY CASE WHEN status = $1 THEN $2 ELSE $3 END ASC, amount DESC" in sql
170
+ assert "ORDER BY CASE WHEN status = $1 THEN $2::bigint ELSE $3::bigint END ASC, amount DESC" in sql
171
171
  assert params == ["paid", 0, 1]
172
172
 
173
173
 
@@ -181,7 +181,7 @@ def test_case_in_order_by_with_nulls_last():
181
181
  )
182
182
  sql, params = QueryExecutor(qb._build_query())._build_query()
183
183
  assert (
184
- "ORDER BY CASE WHEN lower(status) = $1 THEN $2 ELSE $3 END ASC, "
184
+ "ORDER BY CASE WHEN lower(status) = $1 THEN $2::bigint ELSE $3::bigint END ASC, "
185
185
  "amount DESC NULLS LAST" in sql
186
186
  )
187
187
  assert params == ["paid", 0, 1]
@@ -264,3 +264,83 @@ def test_join_without_on_params_still_works_unchanged():
264
264
  sql, params = QueryExecutor(qb._build_query())._build_query()
265
265
  assert "INNER JOIN rag.kg_entities AS e2 ON e2.id = ed.src_id" in sql
266
266
  assert params == []
267
+
268
+
269
+ # ── Type anchoring: the bug this cast exists to prevent ──────────────────────
270
+ #
271
+ # REGRESSION GUARD. Do NOT "simplify" the `::type` casts out of a CASE branch.
272
+ #
273
+ # Postgres infers a bare `$N`'s type from context. A CASE whose THEN/ELSE
274
+ # branches are ALL bound params has NO context to infer from, so PG resolves
275
+ # them to `text` — and asyncpg then refuses to encode a Python int:
276
+ #
277
+ # asyncpg.exceptions.DataError:
278
+ # invalid input for query argument $5: 0 (expected str, got int)
279
+ #
280
+ # Verified live (project txzxabzwovsujtloxrus):
281
+ # ORDER BY CASE WHEN lower(name) = $1 THEN $2 ELSE $3 END
282
+ # -> parameter_types = {text,text,text} # WRONG, and silent
283
+ # ORDER BY CASE WHEN lower(name) = $1 THEN $2::bigint ELSE $3::bigint END
284
+ # -> parameter_types = {text,bigint,bigint} # anchored
285
+ #
286
+ # This shipped broken: every matrx-rag `rag_search` call died in _entity_recall
287
+ # (2026-07-13). The tests above had PINNED the unanchored SQL, so the suite was
288
+ # green while the feature was 100% down.
289
+
290
+
291
+ def test_bound_case_literals_are_type_anchored_not_bare_params():
292
+ """A bound THEN/ELSE literal MUST carry an explicit cast — a bare `$N` in a
293
+ CASE branch is silently typed `text` by Postgres and blows up on an int."""
294
+ params: list = []
295
+ sql = Case(When("lower(e.name) = $1", "acme", then=0), default=1).as_sql(params)
296
+ # The int literals are bound (never inlined) AND anchored.
297
+ assert "THEN $2::bigint" in sql
298
+ assert "ELSE $3::bigint" in sql
299
+ assert params == ["acme", 0, 1]
300
+ # No branch may render as a bare, unanchored parameter.
301
+ assert "THEN $2 " not in sql and not sql.endswith("THEN $2")
302
+ assert "ELSE $3 " not in sql and "ELSE $3 END" not in sql
303
+
304
+
305
+ @pytest.mark.parametrize(
306
+ "value,expected_cast",
307
+ [
308
+ (0, "bigint"),
309
+ (42, "bigint"),
310
+ (True, "boolean"), # bool BEFORE int — bool is a subclass of int
311
+ (False, "boolean"),
312
+ (1.5, "double precision"),
313
+ ],
314
+ )
315
+ def test_case_literal_cast_per_python_type(value, expected_cast):
316
+ params: list = []
317
+ sql = Case(When("x = 1", then=value)).as_sql(params)
318
+ assert sql == f"CASE WHEN x = 1 THEN $1::{expected_cast} END"
319
+ assert params == [value]
320
+
321
+
322
+ def test_bool_then_is_boolean_not_bigint():
323
+ """Guards the isinstance ordering: `isinstance(True, int)` is True, so a
324
+ naive int-first map would render a bool as ::bigint."""
325
+ params: list = []
326
+ sql = Case(When("x = 1", then=True), default=False).as_sql(params)
327
+ assert "::boolean" in sql
328
+ assert "::bigint" not in sql
329
+
330
+
331
+ def test_string_then_is_still_a_bare_column_not_a_cast_param():
332
+ """The str-is-a-column-name convention is UNCHANGED by the anchoring —
333
+ a str `then` must not suddenly start binding as `$N::text`."""
334
+ params: list = []
335
+ sql = Case(When("a = 1", then="ed.dst_id"), default="ed.src_id").as_sql(params)
336
+ assert sql == "CASE WHEN a = 1 THEN ed.dst_id ELSE ed.src_id END"
337
+ assert params == []
338
+
339
+
340
+ def test_none_then_renders_null_not_an_anchorless_param():
341
+ """A bound NULL is unknown-typed too; render it inline so it unifies with
342
+ the sibling branch instead of becoming another text param."""
343
+ params: list = []
344
+ sql = Case(When("a = 1", then=None), default=5).as_sql(params)
345
+ assert sql == "CASE WHEN a = 1 THEN NULL ELSE $1::bigint END"
346
+ assert params == [5]
@@ -0,0 +1,93 @@
1
+ """Level 1: connection-level type codecs (``_init_connection_default``).
2
+
3
+ The pool registers codecs so driver-boundary decoding is uniform across EVERY
4
+ fetch path — model hydration, ``call_function``, sql_executor. The json/jsonb
5
+ codec exists because asyncpg's default returns jsonb as a RAW JSON STRING;
6
+ only the field layer decoded it, so any path that bypassed field hydration
7
+ (e.g. ``call_function(mode="scalar")`` on a jsonb-returning RPC) handed
8
+ callers a ``str`` where they expected a ``dict`` — the 2026-07-13
9
+ ``build_agent_context`` crash. These tests pin the registration and the
10
+ encoder's str-pass-through contract so that class of bug stays extinct.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from unittest.mock import AsyncMock
17
+
18
+ import pytest
19
+
20
+ from matrx_orm.core.async_db_manager import (
21
+ _encode_json,
22
+ _init_connection_default,
23
+ )
24
+
25
+
26
+ class _FakeConn:
27
+ def __init__(self) -> None:
28
+ self.set_type_codec = AsyncMock()
29
+ # The vector-schema lookup in _init_connection_default; None = pgvector
30
+ # not installed, the only legitimate skip.
31
+ self.fetchval = AsyncMock(return_value=None)
32
+
33
+
34
+ class TestJsonCodecRegistration:
35
+ @pytest.mark.asyncio
36
+ async def test_registers_json_and_jsonb_decoders(self) -> None:
37
+ conn = _FakeConn()
38
+ await _init_connection_default(conn) # type: ignore[arg-type]
39
+ registered = {
40
+ call.args[0]: call.kwargs for call in conn.set_type_codec.await_args_list
41
+ }
42
+ for type_name in ("json", "jsonb"):
43
+ assert type_name in registered, (
44
+ f"{type_name} codec not registered — jsonb values fetched outside "
45
+ f"field hydration (call_function scalar/rows) will come back as "
46
+ f"raw JSON strings again (the build_agent_context crash class)."
47
+ )
48
+ kwargs = registered[type_name]
49
+ assert kwargs["decoder"] is json.loads
50
+ assert kwargs["encoder"] is _encode_json
51
+ assert kwargs["schema"] == "pg_catalog"
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_uuid_codec_still_registered(self) -> None:
55
+ conn = _FakeConn()
56
+ await _init_connection_default(conn) # type: ignore[arg-type]
57
+ assert "uuid" in {c.args[0] for c in conn.set_type_codec.await_args_list}
58
+
59
+
60
+ class TestEncodeJson:
61
+ def test_str_passes_through_untouched(self) -> None:
62
+ # Every existing write path (JSONField.get_db_prep_value, JsonbMerge,
63
+ # call_function._bind_args) pre-serialises to str; double-encoding
64
+ # would corrupt every jsonb write in the platform.
65
+ assert _encode_json('{"a": 1}') == '{"a": 1}'
66
+ assert _encode_json("plain") == "plain"
67
+
68
+ def test_dict_and_list_serialised(self) -> None:
69
+ assert json.loads(_encode_json({"a": 1})) == {"a": 1}
70
+ assert json.loads(_encode_json([1, "b"])) == [1, "b"]
71
+
72
+ def test_non_native_values_degrade_via_field_hook(self) -> None:
73
+ # Same coercion hook the field layer uses (JSONBField._json_default):
74
+ # a weird value degrades instead of crashing the write.
75
+ class Weird:
76
+ __slots__ = ()
77
+
78
+ def __repr__(self) -> str:
79
+ return "<weird>"
80
+
81
+ # The exact degrade shape belongs to _json_default's own contract;
82
+ # what THIS codec guarantees is: never crash, always emit valid JSON.
83
+ out = _encode_json({"w": Weird()})
84
+ assert isinstance(json.loads(out), dict)
85
+
86
+
87
+ class TestDecodeContract:
88
+ def test_decoder_yields_native_containers(self) -> None:
89
+ # json.loads is the registered decoder — pin the shapes callers rely on.
90
+ assert json.loads('{"variables": {}}') == {"variables": {}}
91
+ assert json.loads("[1, 2]") == [1, 2]
92
+ assert json.loads('"scalar"') == "scalar"
93
+ assert json.loads("null") is None
File without changes
File without changes
File without changes
File without changes