matrx-orm 3.1.6__tar.gz → 3.1.7__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 (275) hide show
  1. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/BASE_CLASS_METHODS.md +16 -0
  2. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/PKG-INFO +1 -1
  3. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/query/builder.py +159 -111
  4. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/code_handler.py +3 -3
  5. matrx_orm-3.1.7/matrx_orm/schema_builder/codegen_writer.py +114 -0
  6. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/pyproject.toml +1 -1
  7. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_filter_raw.py +4 -1
  8. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_join_and_aggregate.py +4 -1
  9. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_query_builder.py +15 -2
  10. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_query_timeout.py +5 -2
  11. matrx_orm-3.1.7/tests/level1/test_querybuilder_clone_semantics.py +238 -0
  12. matrx_orm-3.1.6/tests/level1/test_querybuilder_mutation_semantics.py +0 -114
  13. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/.gitignore +0 -0
  14. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/CLAUDE.md +0 -0
  15. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/MODEL_API.md +0 -0
  16. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/README.md +0 -0
  17. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/database/orm/extended/managers/ai_model_base.py +0 -0
  18. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/docs/migrations.md +0 -0
  19. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/MODULE_README.md +0 -0
  20. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/__init__.py +0 -0
  21. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/adapters/MODULE_README.md +0 -0
  22. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/adapters/__init__.py +0 -0
  23. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/adapters/async_postgresql.py +0 -0
  24. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/adapters/base_adapter.py +0 -0
  25. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/adapters/postgresql.py +0 -0
  26. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  27. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/adapters/supabase_adapter.py +0 -0
  28. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/admin/__init__.py +0 -0
  29. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/admin/router.py +0 -0
  30. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/api/__init__.py +0 -0
  31. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/api/auth.py +0 -0
  32. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/api/config.py +0 -0
  33. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/api/handlers.py +0 -0
  34. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/api/protocol.py +0 -0
  35. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/api/server.py +0 -0
  36. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/associations.py +0 -0
  37. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/cache_debug.py +0 -0
  38. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/client/MODULE_README.md +0 -0
  39. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/client/__init__.py +0 -0
  40. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/client/postgres_connection.py +0 -0
  41. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/client/postgrest.py +0 -0
  42. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/client/supabase_auth.py +0 -0
  43. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/client/supabase_config.py +0 -0
  44. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/client/supabase_manager.py +0 -0
  45. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  46. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/MODULE_README.md +0 -0
  47. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  48. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/TASKS.md +0 -0
  49. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/__init__.py +0 -0
  50. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/async_db_manager.py +0 -0
  51. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/base.py +0 -0
  52. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/config.py +0 -0
  53. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/db_function.py +0 -0
  54. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/diagnostics.py +0 -0
  55. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/expressions.py +0 -0
  56. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/extended.py +0 -0
  57. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/fields.py +0 -0
  58. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/introspection.py +0 -0
  59. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/listen.py +0 -0
  60. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/loop_filters.py +0 -0
  61. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/model_dto.py +0 -0
  62. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/model_view.py +0 -0
  63. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/paginator.py +0 -0
  64. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/pydantic_bridge.py +0 -0
  65. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/registry.py +0 -0
  66. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/relations.py +0 -0
  67. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/resilience.py +0 -0
  68. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/rls_session.py +0 -0
  69. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/signals.py +0 -0
  70. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/transaction.py +0 -0
  71. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/types.py +0 -0
  72. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/core/write_queue.py +0 -0
  73. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/entity.py +0 -0
  74. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/error_handling.py +0 -0
  75. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/exceptions.py +0 -0
  76. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/extended/__init__.py +0 -0
  77. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/extended/app_error_handler.py +0 -0
  78. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/middleware/__init__.py +0 -0
  79. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/middleware/base.py +0 -0
  80. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/MODULE_README.md +0 -0
  81. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/__init__.py +0 -0
  82. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/cli.py +0 -0
  83. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/ddl.py +0 -0
  84. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/diff.py +0 -0
  85. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/executor.py +0 -0
  86. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/integration.py +0 -0
  87. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/loader.py +0 -0
  88. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/operations.py +0 -0
  89. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/state.py +0 -0
  90. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/migrations/table_filter.py +0 -0
  91. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/MODULE_README.md +0 -0
  92. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/__init__.py +0 -0
  93. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/bulk_update_values.py +0 -0
  94. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/conflict.py +0 -0
  95. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/conflict_writes.py +0 -0
  96. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/create.py +0 -0
  97. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/delete.py +0 -0
  98. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/dynamic_admin.py +0 -0
  99. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/queue_claim.py +0 -0
  100. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/read.py +0 -0
  101. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/staging_load.py +0 -0
  102. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/operations/update.py +0 -0
  103. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/py.typed +0 -0
  104. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/python_sql/MODULE_README.md +0 -0
  105. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/python_sql/__init__.py +0 -0
  106. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/python_sql/db_objects.py +0 -0
  107. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  108. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  109. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/query/MODULE_README.md +0 -0
  110. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/query/__init__.py +0 -0
  111. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/query/executor.py +0 -0
  112. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  113. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/__init__.py +0 -0
  114. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/columns.py +0 -0
  115. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/common.py +0 -0
  116. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/diff_preview.py +0 -0
  117. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  118. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/generator.py +0 -0
  119. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  120. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  121. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  122. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  123. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/package_wiring.py +0 -0
  124. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/relationships.py +0 -0
  125. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/runner.py +0 -0
  126. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/schema.py +0 -0
  127. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/schema_manager.py +0 -0
  128. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/tables.py +0 -0
  129. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/schema_builder/views.py +0 -0
  130. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/README.md +0 -0
  131. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/__init__.py +0 -0
  132. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/coalesce.py +0 -0
  133. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/dag.py +0 -0
  134. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/errors.py +0 -0
  135. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/fallback.py +0 -0
  136. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/flush.py +0 -0
  137. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/lifecycle.py +0 -0
  138. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/managed.py +0 -0
  139. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/op.py +0 -0
  140. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/reads.py +0 -0
  141. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/session.py +0 -0
  142. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/session/telemetry.py +0 -0
  143. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  144. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/sql_executor/__init__.py +0 -0
  145. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/sql_executor/executor.py +0 -0
  146. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/sql_executor/queries.py +0 -0
  147. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/sql_executor/registry.py +0 -0
  148. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/sql_executor/types.py +0 -0
  149. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/sql_executor/utils.py +0 -0
  150. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/state.py +0 -0
  151. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/utils/__init__.py +0 -0
  152. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/utils/sql_utils.py +0 -0
  153. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/matrx_orm/utils/type_converters.py +0 -0
  154. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/scripts/git-branches.sh +0 -0
  155. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/scripts/publish.sh +0 -0
  156. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/scripts/release.sh +0 -0
  157. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/MODULE_README.md +0 -0
  158. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/__init__.py +0 -0
  159. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/conftest.py +0 -0
  160. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/MODULE_README.md +0 -0
  161. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/__init__.py +0 -0
  162. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_admin_db_columns.py +0 -0
  163. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_admin_search_sentinels.py +0 -0
  164. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_advisory_lock.py +0 -0
  165. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_agent_message.py +0 -0
  166. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_api_auth.py +0 -0
  167. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_api_config.py +0 -0
  168. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_api_handlers.py +0 -0
  169. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_api_protocol.py +0 -0
  170. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_array_agg_order_by.py +0 -0
  171. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_associations.py +0 -0
  172. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_bulk_update_by_pk.py +0 -0
  173. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  174. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_cache_debug.py +0 -0
  175. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_call_function.py +0 -0
  176. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_case_when.py +0 -0
  177. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_composite_pk_filter.py +0 -0
  178. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_composite_pk_write_paths.py +0 -0
  179. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_config.py +0 -0
  180. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_conflict_target.py +0 -0
  181. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_conflict_writes.py +0 -0
  182. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_connection_codecs.py +0 -0
  183. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_connection_poison_guard.py +0 -0
  184. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_count_composite_distinct.py +0 -0
  185. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_db_function_field.py +0 -0
  186. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_ddl_generator.py +0 -0
  187. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_dirty_tracking.py +0 -0
  188. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_exceptions.py +0 -0
  189. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_execute_admin_sql.py +0 -0
  190. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_expression_primitives.py +0 -0
  191. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_fields.py +0 -0
  192. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  193. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_from_alias.py +0 -0
  194. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_fts.py +0 -0
  195. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_func_expression_args.py +0 -0
  196. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_having_annotate_alias.py +0 -0
  197. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  198. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_introspect_rls_policies.py +0 -0
  199. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_listen_notify.py +0 -0
  200. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_managed_write_guard.py +0 -0
  201. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_matrx_entity.py +0 -0
  202. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_migration_diff_types.py +0 -0
  203. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_migration_loader.py +0 -0
  204. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_model_instance.py +0 -0
  205. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_model_meta.py +0 -0
  206. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_order_by_expression.py +0 -0
  207. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_patch_jsonb_path.py +0 -0
  208. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_postgrest_filters.py +0 -0
  209. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_query_error_detail.py +0 -0
  210. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_query_executor_sql.py +0 -0
  211. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_queue_claim.py +0 -0
  212. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_registry.py +0 -0
  213. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_registry_multi_database.py +0 -0
  214. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_registry_multi_schema.py +0 -0
  215. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_relations.py +0 -0
  216. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_resilience.py +0 -0
  217. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_schema_exists.py +0 -0
  218. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_session_advisory_lock.py +0 -0
  219. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_state_cache.py +0 -0
  220. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_subquery_filter_raw.py +0 -0
  221. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_subquery_in_filter.py +0 -0
  222. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_supabase_auth.py +0 -0
  223. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_supabase_config.py +0 -0
  224. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_update_case_expression.py +0 -0
  225. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_update_subquery.py +0 -0
  226. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_upsert_with_conflict.py +0 -0
  227. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_values_jsonb_decode.py +0 -0
  228. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_watched_lifecycle.py +0 -0
  229. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level1/test_write_retry_semantics.py +0 -0
  230. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/MODULE_README.md +0 -0
  231. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/__init__.py +0 -0
  232. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/conftest.py +0 -0
  233. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_bulk_ops.py +0 -0
  234. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_cache_integration.py +0 -0
  235. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_crud.py +0 -0
  236. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_foreign_keys.py +0 -0
  237. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_m2m.py +0 -0
  238. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_manager.py +0 -0
  239. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_migrations_live.py +0 -0
  240. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_query_execution.py +0 -0
  241. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/level2/test_schema_diff.py +0 -0
  242. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project/.env.example +0 -0
  243. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project/README.md +0 -0
  244. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project/__init__.py +0 -0
  245. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project/generate.py +0 -0
  246. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project/generated/.gitkeep +0 -0
  247. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project/matrx_orm.yaml +0 -0
  248. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project/test_schema_generation.py +0 -0
  249. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project_desktop/.env.example +0 -0
  250. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project_desktop/README.md +0 -0
  251. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project_desktop/__init__.py +0 -0
  252. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project_desktop/client_example.py +0 -0
  253. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project_desktop/client_example.ts +0 -0
  254. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  255. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/sample_project_desktop/server.py +0 -0
  256. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/schema/entity_tests.py +0 -0
  257. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/schema/test_base_generation.py +0 -0
  258. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  259. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/schema/test_generate_schema.py +0 -0
  260. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/schema/test_multi_schema_output.py +0 -0
  261. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/__init__.py +0 -0
  262. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/conftest.py +0 -0
  263. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_capture_poison_proof.py +0 -0
  264. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_coalesce.py +0 -0
  265. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_dag.py +0 -0
  266. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_disk_spill_fallback.py +0 -0
  267. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_flush_individual_fallback.py +0 -0
  268. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_governed_write.py +0 -0
  269. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_managed.py +0 -0
  270. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_op_enqueue_site.py +0 -0
  271. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_reads.py +0 -0
  272. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/session/test_session.py +0 -0
  273. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/test_database_name_alias.py +0 -0
  274. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/tests/test_model_cls_refactor.py +0 -0
  275. {matrx_orm-3.1.6 → matrx_orm-3.1.7}/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.7
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
@@ -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,9 +1046,10 @@ 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
@@ -1047,8 +1094,9 @@ class QueryBuilder(Generic[ModelT]):
1047
1094
  if isinstance(k, slice):
1048
1095
  start = k.start or 0
1049
1096
  stop = k.stop if k.stop is not None else 0
1097
+ new = self
1050
1098
  if stop > 0:
1051
- self.limit(stop - start)
1052
- self.offset(start)
1053
- return self
1099
+ new = new.limit(stop - start)
1100
+ new = new.offset(start)
1101
+ return new
1054
1102
  raise TypeError("Index access not supported, use slicing")
@@ -2,14 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
 
5
- from matrx_utils.file_handling.specific_handlers.code_handler import CodeHandler
5
+ from matrx_orm.schema_builder.codegen_writer import CodegenWriter
6
6
 
7
7
  from matrx_orm.schema_builder.common import OutputConfig
8
8
 
9
9
 
10
- class SchemaCodeHandler(CodeHandler):
10
+ class SchemaCodeHandler(CodegenWriter):
11
11
  """
12
- Thin subclass of CodeHandler that gates file writes based on OutputConfig.
12
+ Thin subclass of CodegenWriter that gates file writes based on OutputConfig.
13
13
 
14
14
  - generate_and_save_code_from_object: skipped when the file extension is
15
15
  .py (python=False) or .ts (typescript=False).
@@ -0,0 +1,114 @@
1
+ """ORM-internal codegen file writer.
2
+
3
+ Replaces the former dependency on the platform file system's ``CodeHandler``
4
+ (now in matrx-files) with the ~80 lines of behavior the schema builder
5
+ actually uses: assemble code sections, write generated files under
6
+ ``settings.BASE_DIR/temp/code_generator/…`` (or directly to their final path
7
+ when ``save_direct``), and dump JSON artifacts. Paths and semantics are
8
+ byte-compatible with the old CodeHandler so `python db/generate.py` output
9
+ lands exactly where it always has.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from matrx_utils import print_link, to_matrx_json, vcprint
20
+ from matrx_utils.conf import settings
21
+
22
+ _APP_NAME = "code_generator"
23
+ _CODE_ROOT = "code"
24
+
25
+
26
+ class CodegenWriter:
27
+ """Minimal file writer for schema-builder output. Not a FileManager."""
28
+
29
+ def __init__(self, save_direct: bool = False):
30
+ self.save_direct = save_direct
31
+ self.verbose = False
32
+ self._written: list[str] = []
33
+
34
+ @property
35
+ def temp_dir(self) -> Path:
36
+ return Path(settings.BASE_DIR) / "temp" / _APP_NAME
37
+
38
+ # -- primitives ---------------------------------------------------------
39
+
40
+ def _write(self, full_path: Path, content: str) -> None:
41
+ full_path = Path(os.path.normpath(str(full_path)))
42
+ full_path.parent.mkdir(parents=True, exist_ok=True)
43
+ full_path.write_text(content, encoding="utf-8")
44
+ self._written.append(str(full_path))
45
+
46
+ def save_code_file(self, file_path: str, content: str) -> None:
47
+ self._write(self.temp_dir / _CODE_ROOT / file_path, content)
48
+
49
+ def save_code_anywhere(self, path: str, content: str) -> None:
50
+ self._write(Path(path), content)
51
+
52
+ def write_to_json(self, path: str, data: Any, root: str = "temp", clean: bool = True) -> bool:
53
+ payload = to_matrx_json(data) if clean else json.dumps(data, indent=2, ensure_ascii=False)
54
+ if not isinstance(payload, str):
55
+ payload = json.dumps(payload, indent=2, ensure_ascii=False, default=str)
56
+ self._write(self.temp_dir / path, payload)
57
+ return True
58
+
59
+ # -- section assembly (byte-compatible with the old CodeHandler) --------
60
+
61
+ def generate_and_save_code(
62
+ self,
63
+ temp_path: str,
64
+ main_code: str,
65
+ file_location: str | None = None,
66
+ import_lines: list[str] | str | None = None,
67
+ additional_top_lines: list[str] | str | None = None,
68
+ additional_bottom_lines: list[str] | str | None = None,
69
+ additional_code: list[str] | str | None = None,
70
+ path: str | None = None,
71
+ ) -> None:
72
+ sections: list[str] = []
73
+ if file_location:
74
+ sections.append(file_location)
75
+ for section in [import_lines, additional_top_lines, [main_code], additional_code, additional_bottom_lines]:
76
+ if section:
77
+ if isinstance(section, str):
78
+ section = [section]
79
+ sections.extend(section)
80
+ sections.append("")
81
+ code_content = "\n".join(sections).strip()
82
+
83
+ if self.save_direct:
84
+ if path is None:
85
+ raise ValueError("CodegenWriter: save_direct=True requires an explicit path")
86
+ self.save_code_anywhere(path, code_content)
87
+ else:
88
+ self.save_code_file(temp_path, code_content)
89
+
90
+ def generate_and_save_code_from_object(
91
+ self, config_obj: dict, main_code: str, additional_code: list[str] | str | None = None
92
+ ) -> None:
93
+ temp_path = config_obj["temp_path"]
94
+ path = os.path.join(config_obj["root"], temp_path)
95
+ self.generate_and_save_code(
96
+ temp_path,
97
+ main_code,
98
+ config_obj.get("file_location"),
99
+ config_obj.get("import_lines"),
100
+ config_obj.get("additional_top_lines"),
101
+ config_obj.get("additional_bottom_lines"),
102
+ additional_code,
103
+ path,
104
+ )
105
+
106
+ # -- reporting -----------------------------------------------------------
107
+
108
+ def print_all_batched(self) -> None:
109
+ if not self._written:
110
+ return
111
+ vcprint(f"[schema builder] {len(self._written)} file(s) written:", color="green")
112
+ for p in self._written:
113
+ print_link(p)
114
+ self._written.clear()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.6"
3
+ version = "3.1.7"
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" }
@@ -55,7 +55,10 @@ class TestFilterRawBuilderState:
55
55
  def test_is_chainable(self):
56
56
  C = _make_chunk_model()
57
57
  qb = QueryBuilder(C)
58
- assert qb.filter_raw("x = $1", 1) is qb
58
+ derived = qb.filter_raw("x = $1", 1)
59
+ assert isinstance(derived, QueryBuilder)
60
+ assert derived is not qb, "filter_raw() clones on chain — it must not return `self`"
61
+ assert qb.raw_filters == [], "the original builder must be unchanged"
59
62
 
60
63
  def test_build_query_carries_raw_filters(self):
61
64
  C = _make_chunk_model()