matrx-orm 3.1.6__tar.gz → 3.1.8__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 (276) hide show
  1. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/BASE_CLASS_METHODS.md +16 -0
  2. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/__init__.py +2 -1
  4. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/base.py +65 -0
  5. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/db_function.py +25 -1
  6. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/query/builder.py +191 -111
  7. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/code_handler.py +3 -3
  8. matrx_orm-3.1.8/matrx_orm/schema_builder/codegen_writer.py +114 -0
  9. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/pyproject.toml +1 -1
  10. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_filter_raw.py +4 -1
  11. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_join_and_aggregate.py +4 -1
  12. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_query_builder.py +15 -2
  13. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_query_timeout.py +5 -2
  14. matrx_orm-3.1.8/tests/level1/test_querybuilder_clone_semantics.py +238 -0
  15. matrx_orm-3.1.8/tests/test_sync_twins_guard.py +52 -0
  16. matrx_orm-3.1.6/tests/level1/test_querybuilder_mutation_semantics.py +0 -114
  17. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/.gitignore +0 -0
  18. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/CLAUDE.md +0 -0
  19. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/MODEL_API.md +0 -0
  20. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/README.md +0 -0
  21. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/database/orm/extended/managers/ai_model_base.py +0 -0
  22. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/docs/migrations.md +0 -0
  23. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/MODULE_README.md +0 -0
  24. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/adapters/MODULE_README.md +0 -0
  25. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/adapters/__init__.py +0 -0
  26. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/adapters/async_postgresql.py +0 -0
  27. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/adapters/base_adapter.py +0 -0
  28. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/adapters/postgresql.py +0 -0
  29. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  30. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/adapters/supabase_adapter.py +0 -0
  31. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/admin/__init__.py +0 -0
  32. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/admin/router.py +0 -0
  33. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/api/__init__.py +0 -0
  34. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/api/auth.py +0 -0
  35. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/api/config.py +0 -0
  36. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/api/handlers.py +0 -0
  37. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/api/protocol.py +0 -0
  38. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/api/server.py +0 -0
  39. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/associations.py +0 -0
  40. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/cache_debug.py +0 -0
  41. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/client/MODULE_README.md +0 -0
  42. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/client/__init__.py +0 -0
  43. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/client/postgres_connection.py +0 -0
  44. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/client/postgrest.py +0 -0
  45. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/client/supabase_auth.py +0 -0
  46. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/client/supabase_config.py +0 -0
  47. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/client/supabase_manager.py +0 -0
  48. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  49. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/MODULE_README.md +0 -0
  50. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  51. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/TASKS.md +0 -0
  52. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/__init__.py +0 -0
  53. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/async_db_manager.py +0 -0
  54. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/config.py +0 -0
  55. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/diagnostics.py +0 -0
  56. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/expressions.py +0 -0
  57. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/extended.py +0 -0
  58. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/fields.py +0 -0
  59. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/introspection.py +0 -0
  60. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/listen.py +0 -0
  61. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/loop_filters.py +0 -0
  62. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/model_dto.py +0 -0
  63. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/model_view.py +0 -0
  64. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/paginator.py +0 -0
  65. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/pydantic_bridge.py +0 -0
  66. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/registry.py +0 -0
  67. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/relations.py +0 -0
  68. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/resilience.py +0 -0
  69. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/rls_session.py +0 -0
  70. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/signals.py +0 -0
  71. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/transaction.py +0 -0
  72. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/types.py +0 -0
  73. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/core/write_queue.py +0 -0
  74. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/entity.py +0 -0
  75. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/error_handling.py +0 -0
  76. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/exceptions.py +0 -0
  77. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/extended/__init__.py +0 -0
  78. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/extended/app_error_handler.py +0 -0
  79. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/middleware/__init__.py +0 -0
  80. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/middleware/base.py +0 -0
  81. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/MODULE_README.md +0 -0
  82. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/__init__.py +0 -0
  83. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/cli.py +0 -0
  84. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/ddl.py +0 -0
  85. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/diff.py +0 -0
  86. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/executor.py +0 -0
  87. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/integration.py +0 -0
  88. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/loader.py +0 -0
  89. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/operations.py +0 -0
  90. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/state.py +0 -0
  91. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/migrations/table_filter.py +0 -0
  92. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/MODULE_README.md +0 -0
  93. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/__init__.py +0 -0
  94. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/bulk_update_values.py +0 -0
  95. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/conflict.py +0 -0
  96. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/conflict_writes.py +0 -0
  97. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/create.py +0 -0
  98. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/delete.py +0 -0
  99. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/dynamic_admin.py +0 -0
  100. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/queue_claim.py +0 -0
  101. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/read.py +0 -0
  102. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/staging_load.py +0 -0
  103. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/operations/update.py +0 -0
  104. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/py.typed +0 -0
  105. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/python_sql/MODULE_README.md +0 -0
  106. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/python_sql/__init__.py +0 -0
  107. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/python_sql/db_objects.py +0 -0
  108. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  109. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  110. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/query/MODULE_README.md +0 -0
  111. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/query/__init__.py +0 -0
  112. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/query/executor.py +0 -0
  113. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  114. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/__init__.py +0 -0
  115. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/columns.py +0 -0
  116. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/common.py +0 -0
  117. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/diff_preview.py +0 -0
  118. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  119. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/generator.py +0 -0
  120. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  121. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  122. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  123. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  124. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/package_wiring.py +0 -0
  125. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/relationships.py +0 -0
  126. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/runner.py +0 -0
  127. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/schema.py +0 -0
  128. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/schema_manager.py +0 -0
  129. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/tables.py +0 -0
  130. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/schema_builder/views.py +0 -0
  131. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/README.md +0 -0
  132. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/__init__.py +0 -0
  133. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/coalesce.py +0 -0
  134. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/dag.py +0 -0
  135. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/errors.py +0 -0
  136. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/fallback.py +0 -0
  137. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/flush.py +0 -0
  138. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/lifecycle.py +0 -0
  139. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/managed.py +0 -0
  140. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/op.py +0 -0
  141. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/reads.py +0 -0
  142. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/session.py +0 -0
  143. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/session/telemetry.py +0 -0
  144. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  145. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/sql_executor/__init__.py +0 -0
  146. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/sql_executor/executor.py +0 -0
  147. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/sql_executor/queries.py +0 -0
  148. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/sql_executor/registry.py +0 -0
  149. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/sql_executor/types.py +0 -0
  150. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/sql_executor/utils.py +0 -0
  151. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/state.py +0 -0
  152. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/utils/__init__.py +0 -0
  153. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/utils/sql_utils.py +0 -0
  154. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/matrx_orm/utils/type_converters.py +0 -0
  155. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/scripts/git-branches.sh +0 -0
  156. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/scripts/publish.sh +0 -0
  157. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/scripts/release.sh +0 -0
  158. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/MODULE_README.md +0 -0
  159. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/__init__.py +0 -0
  160. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/conftest.py +0 -0
  161. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/MODULE_README.md +0 -0
  162. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/__init__.py +0 -0
  163. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_admin_db_columns.py +0 -0
  164. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_admin_search_sentinels.py +0 -0
  165. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_advisory_lock.py +0 -0
  166. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_agent_message.py +0 -0
  167. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_api_auth.py +0 -0
  168. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_api_config.py +0 -0
  169. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_api_handlers.py +0 -0
  170. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_api_protocol.py +0 -0
  171. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_array_agg_order_by.py +0 -0
  172. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_associations.py +0 -0
  173. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_bulk_update_by_pk.py +0 -0
  174. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  175. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_cache_debug.py +0 -0
  176. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_call_function.py +0 -0
  177. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_case_when.py +0 -0
  178. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_composite_pk_filter.py +0 -0
  179. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_composite_pk_write_paths.py +0 -0
  180. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_config.py +0 -0
  181. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_conflict_target.py +0 -0
  182. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_conflict_writes.py +0 -0
  183. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_connection_codecs.py +0 -0
  184. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_connection_poison_guard.py +0 -0
  185. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_count_composite_distinct.py +0 -0
  186. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_db_function_field.py +0 -0
  187. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_ddl_generator.py +0 -0
  188. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_dirty_tracking.py +0 -0
  189. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_exceptions.py +0 -0
  190. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_execute_admin_sql.py +0 -0
  191. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_expression_primitives.py +0 -0
  192. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_fields.py +0 -0
  193. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  194. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_from_alias.py +0 -0
  195. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_fts.py +0 -0
  196. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_func_expression_args.py +0 -0
  197. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_having_annotate_alias.py +0 -0
  198. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  199. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_introspect_rls_policies.py +0 -0
  200. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_listen_notify.py +0 -0
  201. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_managed_write_guard.py +0 -0
  202. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_matrx_entity.py +0 -0
  203. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_migration_diff_types.py +0 -0
  204. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_migration_loader.py +0 -0
  205. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_model_instance.py +0 -0
  206. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_model_meta.py +0 -0
  207. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_order_by_expression.py +0 -0
  208. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_patch_jsonb_path.py +0 -0
  209. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_postgrest_filters.py +0 -0
  210. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_query_error_detail.py +0 -0
  211. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_query_executor_sql.py +0 -0
  212. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_queue_claim.py +0 -0
  213. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_registry.py +0 -0
  214. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_registry_multi_database.py +0 -0
  215. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_registry_multi_schema.py +0 -0
  216. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_relations.py +0 -0
  217. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_resilience.py +0 -0
  218. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_schema_exists.py +0 -0
  219. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_session_advisory_lock.py +0 -0
  220. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_state_cache.py +0 -0
  221. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_subquery_filter_raw.py +0 -0
  222. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_subquery_in_filter.py +0 -0
  223. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_supabase_auth.py +0 -0
  224. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_supabase_config.py +0 -0
  225. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_update_case_expression.py +0 -0
  226. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_update_subquery.py +0 -0
  227. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_upsert_with_conflict.py +0 -0
  228. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_values_jsonb_decode.py +0 -0
  229. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_watched_lifecycle.py +0 -0
  230. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level1/test_write_retry_semantics.py +0 -0
  231. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/MODULE_README.md +0 -0
  232. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/__init__.py +0 -0
  233. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/conftest.py +0 -0
  234. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_bulk_ops.py +0 -0
  235. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_cache_integration.py +0 -0
  236. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_crud.py +0 -0
  237. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_foreign_keys.py +0 -0
  238. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_m2m.py +0 -0
  239. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_manager.py +0 -0
  240. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_migrations_live.py +0 -0
  241. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_query_execution.py +0 -0
  242. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/level2/test_schema_diff.py +0 -0
  243. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project/.env.example +0 -0
  244. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project/README.md +0 -0
  245. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project/__init__.py +0 -0
  246. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project/generate.py +0 -0
  247. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project/generated/.gitkeep +0 -0
  248. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project/matrx_orm.yaml +0 -0
  249. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project/test_schema_generation.py +0 -0
  250. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project_desktop/.env.example +0 -0
  251. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project_desktop/README.md +0 -0
  252. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project_desktop/__init__.py +0 -0
  253. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project_desktop/client_example.py +0 -0
  254. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project_desktop/client_example.ts +0 -0
  255. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  256. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/sample_project_desktop/server.py +0 -0
  257. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/schema/entity_tests.py +0 -0
  258. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/schema/test_base_generation.py +0 -0
  259. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  260. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/schema/test_generate_schema.py +0 -0
  261. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/schema/test_multi_schema_output.py +0 -0
  262. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/__init__.py +0 -0
  263. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/conftest.py +0 -0
  264. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_capture_poison_proof.py +0 -0
  265. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_coalesce.py +0 -0
  266. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_dag.py +0 -0
  267. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_disk_spill_fallback.py +0 -0
  268. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_flush_individual_fallback.py +0 -0
  269. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_governed_write.py +0 -0
  270. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_managed.py +0 -0
  271. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_op_enqueue_site.py +0 -0
  272. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_reads.py +0 -0
  273. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/session/test_session.py +0 -0
  274. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/test_database_name_alias.py +0 -0
  275. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/test_model_cls_refactor.py +0 -0
  276. {matrx_orm-3.1.6 → matrx_orm-3.1.8}/tests/test_sql_param_casts.py +0 -0
@@ -1,5 +1,21 @@
1
1
  # ORM Manager Base Class Methods
2
2
 
3
+ > **`QueryBuilder` chaining is clone-on-chain (copy-on-write), like Django's
4
+ > `QuerySet`.** Every chain method (`.filter()`, `.exclude()`, `.order_by()`,
5
+ > `.join()`, `.annotate()`, `.filter_raw()`, ...) returns a NEW `QueryBuilder`
6
+ > and never mutates the one it was called on. It is safe to save a base
7
+ > queryset and branch off it multiple times — each branch is independent:
8
+ >
9
+ > ```python
10
+ > base = Model.filter(doc_id=x)
11
+ > raw_count = await base.exclude(raw_text="").count()
12
+ > clean_count = await base.exclude(cleaned_text="").count() # independent of raw_count
13
+ > ```
14
+ >
15
+ > The one thing to remember: always use the RETURN VALUE of a chain call. A
16
+ > bare `qb.filter(x=1)` statement (return discarded) is a no-op on `qb`
17
+ > itself — reassign (`qb = qb.filter(x=1)`) or chain directly.
18
+
3
19
  > **Model-direct aliases (single-table work).** For small, isolated, single-table
4
20
  > work you do NOT need the manager — the generated `Model` class itself exposes
5
21
  > the same vocabulary as classmethods: `load_by_id(id)`, `load_by_id_or_none(id)`,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.6
3
+ Version: 3.1.8
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
@@ -27,7 +27,7 @@ from .core.config import (
27
27
  resolve_database_name,
28
28
  resolve_pool_mode,
29
29
  )
30
- from .core.db_function import ArrayArg, call_function
30
+ from .core.db_function import ArrayArg, call_function, call_function_sync
31
31
  from .entity import EntityCapabilityError, MatrxEntity
32
32
  from .core.diagnostics import collect_pool_diagnostics, log_pool_diagnostics
33
33
  from .core.introspection import introspect_columns, introspect_rls_policies, schema_exists
@@ -318,6 +318,7 @@ __all__ = [
318
318
  "install_asyncpg_idle_close_filter",
319
319
  # ── DB-side function calls (RPC) ──────────────────────────────────────────
320
320
  "call_function",
321
+ "call_function_sync",
321
322
  "MatrxEntity",
322
323
  "EntityCapabilityError",
323
324
  "ArrayArg",
@@ -1263,6 +1263,71 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
1263
1263
 
1264
1264
  return run_sync(cls.filter(**kwargs).all())
1265
1265
 
1266
+ @classmethod
1267
+ def create_sync(cls, **kwargs: Any) -> Self:
1268
+ """Synchronous twin of create(). Sync contexts only."""
1269
+ try:
1270
+ asyncio.get_running_loop()
1271
+ raise RuntimeError(
1272
+ "Model.create_sync() called in an async context. Use await Model.create(...) instead."
1273
+ )
1274
+ except RuntimeError as e:
1275
+ if "no running event loop" not in str(e):
1276
+ raise
1277
+
1278
+ return run_sync(cls.create(**kwargs))
1279
+
1280
+ @classmethod
1281
+ def upsert_sync(
1282
+ cls,
1283
+ data: dict[str, Any],
1284
+ conflict_fields: list[str] | None = None,
1285
+ update_fields: list[str] | None = None,
1286
+ *,
1287
+ on_conflict: list[str] | ConflictTarget | None = None, # noqa: F821
1288
+ ) -> Self:
1289
+ """Synchronous twin of upsert(). Sync contexts only."""
1290
+ try:
1291
+ asyncio.get_running_loop()
1292
+ raise RuntimeError(
1293
+ "Model.upsert_sync() called in an async context. Use await Model.upsert(...) instead."
1294
+ )
1295
+ except RuntimeError as e:
1296
+ if "no running event loop" not in str(e):
1297
+ raise
1298
+
1299
+ return run_sync(
1300
+ cls.upsert(data, conflict_fields, update_fields, on_conflict=on_conflict)
1301
+ )
1302
+
1303
+ @classmethod
1304
+ def update_where_sync(cls, filters: dict[str, Any], **updates: Any) -> UpdateResult:
1305
+ """Synchronous twin of update_where(). Sync contexts only."""
1306
+ try:
1307
+ asyncio.get_running_loop()
1308
+ raise RuntimeError(
1309
+ "Model.update_where_sync() called in an async context. Use await Model.update_where(...) instead."
1310
+ )
1311
+ except RuntimeError as e:
1312
+ if "no running event loop" not in str(e):
1313
+ raise
1314
+
1315
+ return run_sync(cls.update_where(filters, **updates))
1316
+
1317
+ @classmethod
1318
+ def delete_where_sync(cls, **filters: Any) -> int:
1319
+ """Synchronous twin of delete_where(). Sync contexts only."""
1320
+ try:
1321
+ asyncio.get_running_loop()
1322
+ raise RuntimeError(
1323
+ "Model.delete_where_sync() called in an async context. Use await Model.delete_where(...) instead."
1324
+ )
1325
+ except RuntimeError as e:
1326
+ if "no running event loop" not in str(e):
1327
+ raise
1328
+
1329
+ return run_sync(cls.delete_where(**filters))
1330
+
1266
1331
  @classmethod
1267
1332
  async def all(cls) -> list[Self]:
1268
1333
  try:
@@ -70,7 +70,7 @@ from typing import Any, Literal
70
70
  from matrx_orm.core.async_db_manager import AsyncDatabaseManager
71
71
  from matrx_orm.core.transaction import get_active_connection
72
72
 
73
- __all__ = ["call_function", "ArrayArg"]
73
+ __all__ = ["call_function", "call_function_sync", "ArrayArg"]
74
74
 
75
75
  # Postgres unquoted-identifier shape. Deliberately strict — this is the only
76
76
  # thing standing between a caller-supplied name and the query string, so a
@@ -195,3 +195,27 @@ async def call_function(
195
195
  rows = await conn.fetch(sql, *bound_args)
196
196
  return [dict(r) for r in rows]
197
197
  return await conn.fetchval(sql, *bound_args)
198
+
199
+
200
+ def call_function_sync(
201
+ database: str,
202
+ schema: str,
203
+ function: str,
204
+ *args: Any,
205
+ mode: Literal["scalar", "none", "rows"] = "scalar",
206
+ field: str | None = None,
207
+ ) -> Any:
208
+ """Synchronous twin of :func:`call_function`. Sync contexts only."""
209
+ import asyncio
210
+
211
+ from matrx_orm.core.async_db_manager import run_sync
212
+
213
+ try:
214
+ asyncio.get_running_loop()
215
+ raise RuntimeError(
216
+ "call_function_sync() called in an async context. Use await call_function(...) instead."
217
+ )
218
+ except RuntimeError as e:
219
+ if "no running event loop" not in str(e):
220
+ raise
221
+ return run_sync(call_function(database, schema, function, *args, mode=mode, field=field))
@@ -127,46 +127,68 @@ class QueryBuilder(Generic[ModelT]):
127
127
  """Lazy, chainable query spec — built up via `.filter()`/`.exclude()`/etc,
128
128
  executed on `.all()`/`.count()`/`.exists()`/`.values()`/....
129
129
 
130
- ⚠️ **MUTATES IN PLACE, DOES NOT CLONE.** Every chain method
131
- (`.filter()`, `.exclude()`, `.join()`, `.annotate()`, `.filter_raw()`,
132
- `.order_by()`, `.limit()`, ...) appends to `self`'s internal lists/dicts
133
- and returns `self` — it is Django-`QuerySet`-SHAPED but NOT
134
- Django-`QuerySet`-SAFE: Django clones on every chain call so branching
135
- off a saved queryset is side-effect-free; this builder does not, so
136
- branching off a saved builder ACCUMULATES.
137
-
138
- Concretely, this is a bug, not a style nit::
139
-
140
- pages_qs = Model.filter(doc_id=x)
141
- a = await pages_qs.exclude(raw_text="").count()
142
- b = await pages_qs.exclude(cleaned_text="").count() # BUG: still has
143
- # the raw_text exclude from the line above `b` silently counts
144
- # "NOT raw_text AND NOT cleaned_text", not just "NOT cleaned_text".
145
-
146
- (This exact shape shipped in `aidream/services/rag/library_status.py`;
147
- see the fix + comment there for the corrected pattern.)
148
-
149
- **The safe patterns:**
150
- - Reassign every time: `qb = qb.filter_raw(...)` (fine — no branching
151
- off a shared base after the reassignment).
152
- - Build a FRESH queryset for each independent read:
153
- `await Model.filter(x=1).exclude(a="").count()` and
154
- `await Model.filter(x=1).exclude(b="").count()` as two separate
155
- expressions, never sharing a `pages_qs`-style variable across calls
156
- that each need a different filter set.
157
- - If you truly need to branch off one shared base, don't reuse the
158
- builder object itself reissue `Model.filter(...)` (or whatever
159
- built the base) for each branch.
160
-
161
- This is intentional current behavior, not an oversight to "just fix" —
162
- switching to clone-on-chain is a real, wide-blast-radius ORM change
163
- (every `matrx-*` package + `aidream/services/*` calls this) that needs
164
- its own audited migration, not a silent semantic flip. Tracked as a
165
- platform follow-up; see `tests/level1/test_querybuilder_mutation_semantics.py`
166
- for the pinned-current-behavior regression test that must be updated
167
- (not just deleted) the day this changes.
130
+ **Clone-on-chain (copy-on-write) Django-`QuerySet`-SAFE.** Every chain
131
+ method (`.filter()`, `.exclude()`, `.join()`, `.annotate()`,
132
+ `.filter_raw()`, `.order_by()`, `.limit()`, ...) returns a NEW
133
+ `QueryBuilder` that shares nothing mutable with `self` — the original
134
+ builder is never modified. This matches Django's `QuerySet`: branching
135
+ off a saved queryset is side-effect-free, and reusing a variable across
136
+ independent calls no longer accumulates state.
137
+
138
+ ::
139
+
140
+ base = Model.filter(doc_id=x)
141
+ raw_count = await base.exclude(raw_text="").count()
142
+ clean_count = await base.exclude(cleaned_text="").count()
143
+ # Independent `clean_count` does NOT carry the raw_text exclude.
144
+ # `base` itself is untouched by either branch.
145
+
146
+ (Before this, the two branches above bled into each other — the exact
147
+ shape that shipped a real count-corruption bug in
148
+ `aidream/services/rag/library_status.py`, worked around there by
149
+ building fresh querysets. That workaround is no longer required, though
150
+ still harmless.)
151
+
152
+ **Still true:** always use the RETURN VALUE of a chain call a bare
153
+ `qb.filter(x=1)` statement (return discarded, not reassigned) is now a
154
+ no-op on `qb` itself (the filter lives only on the discarded clone).
155
+ Reassign (`qb = qb.filter(x=1)`) or chain directly
156
+ (`Model.filter(x=1).exclude(y=2)`).
157
+
158
+ Terminal methods (`.all()`, `.first()`, `.get()`, `.count()`,
159
+ `.exists()`, `.values()`, `.update()`, `.delete()`, `.aggregate()`, ...)
160
+ execute the query and do NOT clone.
168
161
  """
169
162
 
163
+ def _clone(self) -> QueryBuilder[ModelT]:
164
+ """Return a new QueryBuilder sharing `self`'s scalar state but with
165
+ every mutable container (lists/dicts/sets this builder appends to)
166
+ copied — so mutating the clone's containers never touches `self`'s,
167
+ and vice versa. Elements inside those containers (filter dicts, Q
168
+ objects, join specs, ...) are treated as immutable value objects and
169
+ are NOT deep-copied — only the containers themselves are copied.
170
+ """
171
+ new = self.__class__.__new__(self.__class__)
172
+ new.__dict__.update(self.__dict__)
173
+ new.filters = list(self.filters)
174
+ new.excludes = list(self.excludes)
175
+ new.order_by_fields = list(self.order_by_fields)
176
+ new.select_fields = list(self.select_fields)
177
+ new.deferred_fields = set(self.deferred_fields)
178
+ new.only_fields = set(self.only_fields)
179
+ new.prefetch_fields = list(self.prefetch_fields)
180
+ new.joins = list(self.joins)
181
+ new.group_by_fields = list(self.group_by_fields)
182
+ new.having_filters = list(self.having_filters)
183
+ new.aggregations = list(self.aggregations)
184
+ new.distinct_fields = list(self.distinct_fields) if self.distinct_fields is not None else None
185
+ new.for_update_opts = dict(self.for_update_opts) if self.for_update_opts is not None else None
186
+ new.select_related_fields = list(self.select_related_fields)
187
+ new.ctes = list(self.ctes)
188
+ new.vector_order = dict(self.vector_order) if self.vector_order is not None else None
189
+ new.raw_filters = list(self.raw_filters)
190
+ return new
191
+
170
192
  model: type[ModelT]
171
193
  database: str
172
194
  filters: list[Any] # dict[str,Any] or Q objects
@@ -235,18 +257,17 @@ class QueryBuilder(Generic[ModelT]):
235
257
  .filter(Q(status="active") | Q(role="admin"))
236
258
  .filter(Q(a=1), b=2) # AND composition
237
259
 
238
- ⚠️ Mutates `self` and returns it does NOT clone (see the class
239
- docstring). Reusing a saved builder across independent branches
240
- accumulates filters onto every branch.
260
+ Clone-on-chain: returns a NEW builder; `self` is unchanged.
241
261
  """
242
262
  from ..core.expressions import Exists, Q, TsMatch
243
263
 
264
+ new = self._clone()
244
265
  for arg in args:
245
266
  # Positional predicate expressions the executor renders from the
246
267
  # filters list: Q, Exists(...) (correlated subquery), and
247
268
  # TsMatch(...) (full-text WHERE match).
248
269
  if isinstance(arg, (Q, Exists, TsMatch)):
249
- self.filters.append(arg)
270
+ new.filters.append(arg)
250
271
  else:
251
272
  raise TypeError(
252
273
  "Positional filter argument must be a Q, Exists, or TsMatch object, "
@@ -254,8 +275,8 @@ class QueryBuilder(Generic[ModelT]):
254
275
  )
255
276
  if kwargs:
256
277
  self._validate_filter_keys(kwargs)
257
- self.filters.append(kwargs)
258
- return self
278
+ new.filters.append(kwargs)
279
+ return new
259
280
 
260
281
  def _validate_filter_keys(self, kwargs: dict[str, Any]) -> None:
261
282
  """Reject filter kwargs whose field/operator don't exist on the model.
@@ -330,15 +351,15 @@ class QueryBuilder(Generic[ModelT]):
330
351
  def exclude(self, **kwargs: Any) -> QueryBuilder[ModelT]:
331
352
  """Apply SQL exclusion filters.
332
353
 
333
- ⚠️ Mutates `self` and returns it does NOT clone (see the class
334
- docstring). Calling `.exclude()` twice off the SAME saved builder
335
- variable ANDs both excludes together onto one query — it does not
336
- give you two independent counts. Build a fresh queryset per
337
- independent read instead.
354
+ Clone-on-chain: returns a NEW builder; `self` is unchanged. Calling
355
+ `.exclude()` twice off the SAME saved builder variable now gives two
356
+ INDEPENDENT branches (each excludes only its own kwargs) — it does
357
+ not AND both excludes together the way in-place mutation used to.
338
358
  """
359
+ new = self._clone()
339
360
  if kwargs:
340
- self.excludes.append(kwargs)
341
- return self
361
+ new.excludes.append(kwargs)
362
+ return new
342
363
 
343
364
  def filter_raw(self, sql: str, *params: Any) -> QueryBuilder[ModelT]:
344
365
  """Add a parametrized raw SQL fragment to the WHERE clause (AND-ed).
@@ -392,33 +413,40 @@ class QueryBuilder(Generic[ModelT]):
392
413
  f"but found placeholders {sorted(found) or 'none'} for {len(params)} param(s).\n"
393
414
  f" SQL: {sql!r}"
394
415
  )
395
- self.raw_filters.append((sql, list(params)))
396
- return self
416
+ new = self._clone()
417
+ new.raw_filters.append((sql, list(params)))
418
+ return new
397
419
 
398
420
  def order_by(self, *fields: str | Any) -> QueryBuilder[ModelT]:
399
- self.order_by_fields.extend(fields)
400
- return self
421
+ new = self._clone()
422
+ new.order_by_fields.extend(fields)
423
+ return new
401
424
 
402
425
  def limit(self, value: int) -> QueryBuilder[ModelT]:
403
- self.limit_val = value
404
- return self
426
+ new = self._clone()
427
+ new.limit_val = value
428
+ return new
405
429
 
406
430
  def offset(self, value: int) -> QueryBuilder[ModelT]:
407
- self.offset_val = value
408
- return self
431
+ new = self._clone()
432
+ new.offset_val = value
433
+ return new
409
434
 
410
435
  def select(self, *fields: str) -> QueryBuilder[ModelT]:
411
- self.select_fields.extend(fields)
412
- return self
436
+ new = self._clone()
437
+ new.select_fields.extend(fields)
438
+ return new
413
439
 
414
440
  def prefetch_related(self, *fields: str) -> QueryBuilder[ModelT]:
415
- self.prefetch_fields.extend(fields)
416
- return self
441
+ new = self._clone()
442
+ new.prefetch_fields.extend(fields)
443
+ return new
417
444
 
418
445
  def select_related(self, *fields: str) -> QueryBuilder[ModelT]:
419
446
  """Eager-load FK targets via JOIN in the same query (no N+1)."""
420
- self.select_related_fields.extend(fields)
421
- return self
447
+ new = self._clone()
448
+ new.select_related_fields.extend(fields)
449
+ return new
422
450
 
423
451
  def join(
424
452
  self,
@@ -482,7 +510,8 @@ class QueryBuilder(Generic[ModelT]):
482
510
  on_sql, on_params = partner.render()
483
511
  qb.join(KgEntities, on=f"e2.id = {on_sql}", on_params=on_params, alias="e2")
484
512
  """
485
- self.joins.append(
513
+ new = self._clone()
514
+ new.joins.append(
486
515
  {
487
516
  "model": model_cls,
488
517
  "on": on,
@@ -491,15 +520,17 @@ class QueryBuilder(Generic[ModelT]):
491
520
  "on_params": list(on_params) if on_params else [],
492
521
  }
493
522
  )
494
- return self
523
+ return new
495
524
 
496
525
  def group_by(self, *fields: str) -> QueryBuilder[ModelT]:
497
- self.group_by_fields.extend(fields)
498
- return self
526
+ new = self._clone()
527
+ new.group_by_fields.extend(fields)
528
+ return new
499
529
 
500
530
  def having(self, **kwargs: Any) -> QueryBuilder[ModelT]:
501
- self.having_filters.append(kwargs)
502
- return self
531
+ new = self._clone()
532
+ new.having_filters.append(kwargs)
533
+ return new
503
534
 
504
535
  def annotate(self, **kwargs: Any) -> QueryBuilder[ModelT]:
505
536
  """Add computed columns (aggregates or functions) to the SELECT.
@@ -508,9 +539,10 @@ class QueryBuilder(Generic[ModelT]):
508
539
 
509
540
  .annotate(total=Sum("amount"), name_lower=Lower("username"))
510
541
  """
542
+ new = self._clone()
511
543
  for key, value in kwargs.items():
512
- self.aggregations.append({"name": key, "function": value})
513
- return self
544
+ new.aggregations.append({"name": key, "function": value})
545
+ return new
514
546
 
515
547
  def distinct(self, *fields: str) -> QueryBuilder[ModelT]:
516
548
  """Add DISTINCT or DISTINCT ON to the query.
@@ -518,8 +550,9 @@ class QueryBuilder(Generic[ModelT]):
518
550
  No args → ``SELECT DISTINCT ...``
519
551
  With args → ``SELECT DISTINCT ON (f1, f2) ...`` (PostgreSQL-specific)
520
552
  """
521
- self.distinct_fields = list(fields)
522
- return self
553
+ new = self._clone()
554
+ new.distinct_fields = list(fields)
555
+ return new
523
556
 
524
557
  def select_for_update(
525
558
  self,
@@ -531,8 +564,9 @@ class QueryBuilder(Generic[ModelT]):
531
564
 
532
565
  Must be used inside a transaction.
533
566
  """
534
- self.for_update_opts = {"nowait": nowait, "skip_locked": skip_locked, "of": of or []}
535
- return self
567
+ new = self._clone()
568
+ new.for_update_opts = {"nowait": nowait, "skip_locked": skip_locked, "of": of or []}
569
+ return new
536
570
 
537
571
  def using(self, database: str) -> QueryBuilder[ModelT]:
538
572
  """Override the database for this query.
@@ -541,8 +575,9 @@ class QueryBuilder(Generic[ModelT]):
541
575
 
542
576
  await User.filter(is_active=True).using("replica").all()
543
577
  """
544
- self.database = database
545
- return self
578
+ new = self._clone()
579
+ new.database = database
580
+ return new
546
581
 
547
582
  def timeout(self, seconds: float) -> QueryBuilder[ModelT]:
548
583
  """Override the STATEMENT timeout for this query only (seconds).
@@ -566,8 +601,9 @@ class QueryBuilder(Generic[ModelT]):
566
601
  """
567
602
  if seconds is not None and seconds <= 0:
568
603
  raise ValueError(f"timeout() requires a positive number of seconds, got {seconds!r}")
569
- self.timeout_val = seconds
570
- return self
604
+ new = self._clone()
605
+ new.timeout_val = seconds
606
+ return new
571
607
 
572
608
  def from_alias(self, alias: str) -> QueryBuilder[ModelT]:
573
609
  """Alias the base (FROM) table — renders ``FROM <table> AS <alias>``.
@@ -589,8 +625,9 @@ class QueryBuilder(Generic[ModelT]):
589
625
  f"from_alias must be a plain ASCII identifier, got {alias!r} — it is "
590
626
  "trusted, code-supplied text (a short alias like 'c'), never user input"
591
627
  )
592
- self.from_alias_name = alias
593
- return self
628
+ new = self._clone()
629
+ new.from_alias_name = alias
630
+ return new
594
631
 
595
632
  def only(self, *fields: str) -> QueryBuilder[ModelT]:
596
633
  """Load only these columns. Unlisted columns on the model instance will be None.
@@ -599,9 +636,10 @@ class QueryBuilder(Generic[ModelT]):
599
636
 
600
637
  await User.filter(is_active=True).only("id", "email").all()
601
638
  """
602
- self.only_fields = set(fields)
603
- self.select_fields = list(fields)
604
- return self
639
+ new = self._clone()
640
+ new.only_fields = set(fields)
641
+ new.select_fields = list(fields)
642
+ return new
605
643
 
606
644
  def defer(self, *fields: str) -> QueryBuilder[ModelT]:
607
645
  """Exclude these columns from the SELECT. They will be None on the returned instances.
@@ -610,8 +648,9 @@ class QueryBuilder(Generic[ModelT]):
610
648
 
611
649
  await User.filter(is_active=True).defer("large_blob", "heavy_json").all()
612
650
  """
613
- self.deferred_fields = set(fields)
614
- return self
651
+ new = self._clone()
652
+ new.deferred_fields = set(fields)
653
+ return new
615
654
 
616
655
  def with_cte(self, *ctes: Any) -> QueryBuilder[ModelT]:
617
656
  """Prepend one or more CTEs (WITH clauses) to the query.
@@ -627,8 +666,9 @@ class QueryBuilder(Generic[ModelT]):
627
666
  tree = CTE("org_tree", raw_recursive_sql, recursive=True)
628
667
  rows = await Org.filter().with_cte(tree).raw_sql(\"SELECT * FROM org_tree\")
629
668
  """
630
- self.ctes.extend(ctes)
631
- return self
669
+ new = self._clone()
670
+ new.ctes.extend(ctes)
671
+ return new
632
672
 
633
673
  def nearest(
634
674
  self,
@@ -665,20 +705,22 @@ class QueryBuilder(Generic[ModelT]):
665
705
  for doc in results:
666
706
  print(doc._vector_distance, doc.title)
667
707
  """
668
- self.vector_order = {"column": column, "vector": vector, "metric": metric}
669
- self.vector_null_guard = null_guard
670
- return self
708
+ new = self._clone()
709
+ new.vector_order = {"column": column, "vector": vector, "metric": metric}
710
+ new.vector_null_guard = null_guard
711
+ return new
671
712
 
672
713
  def reverse(self) -> QueryBuilder[ModelT]:
673
714
  """Reverse each ordering term."""
715
+ new = self._clone()
674
716
  reversed_fields = []
675
- for term in self.order_by_fields:
717
+ for term in new.order_by_fields:
676
718
  if isinstance(term, str):
677
719
  reversed_fields.append(term[1:] if term.startswith("-") else f"-{term}")
678
720
  else:
679
721
  reversed_fields.append(term)
680
- self.order_by_fields = reversed_fields
681
- return self
722
+ new.order_by_fields = reversed_fields
723
+ return new
682
724
 
683
725
  # ------------------------------------------------------------------
684
726
  # Internal query dict construction
@@ -814,8 +856,7 @@ class QueryBuilder(Generic[ModelT]):
814
856
  """Order by primary key (all columns, for composite PKs) descending
815
857
  and fetch the first record."""
816
858
  pk_names = self.model._meta.primary_keys or ["id"]
817
- self.order_by(*(f"-{pk}" for pk in pk_names))
818
- return await self.first()
859
+ return await self.order_by(*(f"-{pk}" for pk in pk_names)).first()
819
860
 
820
861
  async def get(self) -> ModelT:
821
862
  """Retrieve exactly one matching record; raises if 0 or >1 found.
@@ -917,11 +958,14 @@ class QueryBuilder(Generic[ModelT]):
917
958
  total=Sum("amount"), avg=Avg("amount"), count=Count("id")
918
959
  )
919
960
  """
920
- if kwargs:
921
- for key, value in kwargs.items():
922
- self.aggregations.append({"name": key, "function": value})
961
+ # Terminal, but takes query-shaping args — build on a clone so `self`
962
+ # is never mutated (clone-on-chain contract: a reused base builder must
963
+ # stay pristine after a derived .aggregate() call).
964
+ target = self._clone() if kwargs else self
965
+ for key, value in kwargs.items():
966
+ target.aggregations.append({"name": key, "function": value})
923
967
  try:
924
- return await self._get_executor().aggregate()
968
+ return await target._get_executor().aggregate()
925
969
  except ORMException as e:
926
970
  e.enrich(
927
971
  model=self.model, operation="aggregate", filters=self._merge_filters_excludes()
@@ -929,9 +973,10 @@ class QueryBuilder(Generic[ModelT]):
929
973
  raise
930
974
 
931
975
  async def values(self, *fields: str) -> list[dict[str, Any]]:
976
+ target = self._clone() if fields else self
932
977
  if fields:
933
- self.select_fields = list(fields)
934
- return await self._get_executor().values()
978
+ target.select_fields = list(fields)
979
+ return await target._get_executor().values()
935
980
 
936
981
  def compile(self, *fields: str) -> tuple[str, list[Any]]:
937
982
  """Build the SELECT SQL + bound-parameter list WITHOUT executing.
@@ -950,9 +995,10 @@ class QueryBuilder(Generic[ModelT]):
950
995
  await conn.execute("SET LOCAL hnsw.iterative_scan = 'relaxed_order'")
951
996
  rows = await conn.fetch(sql, *params)
952
997
  """
998
+ target = self._clone() if fields else self
953
999
  if fields:
954
- self.select_fields = list(fields)
955
- return self._get_executor()._build_query()
1000
+ target.select_fields = list(fields)
1001
+ return target._get_executor()._build_query()
956
1002
 
957
1003
  async def fetch_via(
958
1004
  self,
@@ -1000,14 +1046,47 @@ class QueryBuilder(Generic[ModelT]):
1000
1046
  async def values_list(
1001
1047
  self, *fields: str, flat: bool = False
1002
1048
  ) -> list[tuple[Any, ...]] | list[Any]:
1049
+ target = self._clone() if fields else self
1003
1050
  if fields:
1004
- self.select_fields = list(fields)
1005
- return await self._get_executor().values_list(flat=flat)
1051
+ target.select_fields = list(fields)
1052
+ return await target._get_executor().values_list(flat=flat)
1006
1053
 
1007
1054
  # ------------------------------------------------------------------
1008
1055
  # Synchronous wrappers
1009
1056
  # ------------------------------------------------------------------
1010
1057
 
1058
+ def all_sync(self) -> list[ModelT]:
1059
+ """Synchronous execution of the built query. Sync contexts only."""
1060
+ import asyncio
1061
+
1062
+ from matrx_orm.core.async_db_manager import run_sync
1063
+
1064
+ try:
1065
+ asyncio.get_running_loop()
1066
+ raise RuntimeError(
1067
+ "QueryBuilder.all_sync() called in an async context. Use await .all() instead."
1068
+ )
1069
+ except RuntimeError as e:
1070
+ if "no running event loop" not in str(e):
1071
+ raise
1072
+ return run_sync(self.all())
1073
+
1074
+ def first_sync(self) -> ModelT | None:
1075
+ """Synchronous twin of first(). Sync contexts only."""
1076
+ import asyncio
1077
+
1078
+ from matrx_orm.core.async_db_manager import run_sync
1079
+
1080
+ try:
1081
+ asyncio.get_running_loop()
1082
+ raise RuntimeError(
1083
+ "QueryBuilder.first_sync() called in an async context. Use await .first() instead."
1084
+ )
1085
+ except RuntimeError as e:
1086
+ if "no running event loop" not in str(e):
1087
+ raise
1088
+ return run_sync(self.first())
1089
+
1011
1090
  def values_sync(self, *fields: str) -> list[dict[str, Any]]:
1012
1091
  """Synchronous wrapper for values()."""
1013
1092
  try:
@@ -1047,8 +1126,9 @@ class QueryBuilder(Generic[ModelT]):
1047
1126
  if isinstance(k, slice):
1048
1127
  start = k.start or 0
1049
1128
  stop = k.stop if k.stop is not None else 0
1129
+ new = self
1050
1130
  if stop > 0:
1051
- self.limit(stop - start)
1052
- self.offset(start)
1053
- return self
1131
+ new = new.limit(stop - start)
1132
+ new = new.offset(start)
1133
+ return new
1054
1134
  raise TypeError("Index access not supported, use slicing")