matrx-orm 3.1.20__tar.gz → 3.1.22__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 (289) hide show
  1. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/PKG-INFO +12 -1
  2. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/README.md +11 -0
  3. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/admin/router.py +72 -8
  4. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/config.py +1 -1
  5. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/columns.py +10 -0
  6. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/common.py +10 -0
  7. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/generator.py +1 -1
  8. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/schema.py +6 -0
  9. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/views.py +35 -8
  10. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/pyproject.toml +1 -1
  11. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_admin_db_columns.py +79 -0
  12. matrx_orm-3.1.22/tests/schema/test_view_generation.py +72 -0
  13. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/.gitignore +0 -0
  14. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/AGENTS.md +0 -0
  15. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/BASE_CLASS_METHODS.md +0 -0
  16. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/CLAUDE.md +0 -0
  17. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/MODEL_API.md +0 -0
  18. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/database/orm/extended/managers/ai_model_base.py +0 -0
  19. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/docs/migrations.md +0 -0
  20. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/MODULE_README.md +0 -0
  21. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/__init__.py +0 -0
  22. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/MODULE_README.md +0 -0
  23. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/__init__.py +0 -0
  24. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/async_postgresql.py +0 -0
  25. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/base_adapter.py +0 -0
  26. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/postgresql.py +0 -0
  27. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  28. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/supabase_adapter.py +0 -0
  29. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/admin/__init__.py +0 -0
  30. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/__init__.py +0 -0
  31. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/auth.py +0 -0
  32. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/config.py +0 -0
  33. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/handlers.py +0 -0
  34. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/protocol.py +0 -0
  35. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/server.py +0 -0
  36. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/associations.py +0 -0
  37. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/cache_debug.py +0 -0
  38. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/MODULE_README.md +0 -0
  39. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/__init__.py +0 -0
  40. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/postgres_connection.py +0 -0
  41. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/postgrest.py +0 -0
  42. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/supabase_auth.py +0 -0
  43. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/supabase_config.py +0 -0
  44. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/supabase_manager.py +0 -0
  45. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  46. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/MODULE_README.md +0 -0
  47. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  48. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/TASKS.md +0 -0
  49. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/__init__.py +0 -0
  50. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/async_db_manager.py +0 -0
  51. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/base.py +0 -0
  52. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/db_function.py +0 -0
  53. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/diagnostics.py +0 -0
  54. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/expressions.py +0 -0
  55. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/extended.py +0 -0
  56. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/fields.py +0 -0
  57. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/introspection.py +0 -0
  58. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/listen.py +0 -0
  59. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/loop_filters.py +0 -0
  60. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/model_dto.py +0 -0
  61. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/model_view.py +0 -0
  62. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/paginator.py +0 -0
  63. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/pool_maintenance.py +0 -0
  64. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/pydantic_bridge.py +0 -0
  65. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/registry.py +0 -0
  66. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/relations.py +0 -0
  67. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/resilience.py +0 -0
  68. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/rls_session.py +0 -0
  69. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/signals.py +0 -0
  70. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/tls.py +0 -0
  71. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/transaction.py +0 -0
  72. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/types.py +0 -0
  73. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/write_queue.py +0 -0
  74. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/entity.py +0 -0
  75. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/error_handling.py +0 -0
  76. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/exceptions.py +0 -0
  77. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/extended/__init__.py +0 -0
  78. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/extended/app_error_handler.py +0 -0
  79. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/identity_rule.py +0 -0
  80. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/middleware/__init__.py +0 -0
  81. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/middleware/base.py +0 -0
  82. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/MODULE_README.md +0 -0
  83. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/__init__.py +0 -0
  84. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/cli.py +0 -0
  85. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/ddl.py +0 -0
  86. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/diff.py +0 -0
  87. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/executor.py +0 -0
  88. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/integration.py +0 -0
  89. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/loader.py +0 -0
  90. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/operations.py +0 -0
  91. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/state.py +0 -0
  92. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/table_filter.py +0 -0
  93. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/MODULE_README.md +0 -0
  94. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/__init__.py +0 -0
  95. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/bulk_update_values.py +0 -0
  96. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/conflict.py +0 -0
  97. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/conflict_writes.py +0 -0
  98. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/create.py +0 -0
  99. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/delete.py +0 -0
  100. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/dynamic_admin.py +0 -0
  101. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/queue_claim.py +0 -0
  102. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/read.py +0 -0
  103. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/staging_load.py +0 -0
  104. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/update.py +0 -0
  105. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/py.typed +0 -0
  106. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/MODULE_README.md +0 -0
  107. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/__init__.py +0 -0
  108. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/db_objects.py +0 -0
  109. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  110. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  111. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/query/MODULE_README.md +0 -0
  112. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/query/__init__.py +0 -0
  113. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/query/builder.py +0 -0
  114. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/query/executor.py +0 -0
  115. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  116. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/__init__.py +0 -0
  117. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/code_handler.py +0 -0
  118. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  119. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/diff_preview.py +0 -0
  120. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  121. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  122. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  123. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  124. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  125. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/package_wiring.py +0 -0
  126. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/relationships.py +0 -0
  127. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/runner.py +0 -0
  128. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/schema_manager.py +0 -0
  129. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/tables.py +0 -0
  130. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/README.md +0 -0
  131. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/__init__.py +0 -0
  132. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/coalesce.py +0 -0
  133. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/dag.py +0 -0
  134. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/errors.py +0 -0
  135. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/fallback.py +0 -0
  136. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/flush.py +0 -0
  137. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/lifecycle.py +0 -0
  138. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/managed.py +0 -0
  139. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/op.py +0 -0
  140. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/reads.py +0 -0
  141. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/session.py +0 -0
  142. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/telemetry.py +0 -0
  143. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  144. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/__init__.py +0 -0
  145. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/executor.py +0 -0
  146. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/queries.py +0 -0
  147. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/registry.py +0 -0
  148. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/types.py +0 -0
  149. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/utils.py +0 -0
  150. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/state.py +0 -0
  151. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/utils/__init__.py +0 -0
  152. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/utils/sql_utils.py +0 -0
  153. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/utils/type_converters.py +0 -0
  154. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/scripts/git-branches.sh +0 -0
  155. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/scripts/publish.sh +0 -0
  156. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/scripts/release.sh +0 -0
  157. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/MODULE_README.md +0 -0
  158. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/__init__.py +0 -0
  159. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/conftest.py +0 -0
  160. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/MODULE_README.md +0 -0
  161. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/__init__.py +0 -0
  162. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_admin_search_negation.py +0 -0
  163. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_admin_search_sentinels.py +0 -0
  164. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_advisory_lock.py +0 -0
  165. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_agent_message.py +0 -0
  166. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_api_auth.py +0 -0
  167. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_api_config.py +0 -0
  168. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_api_handlers.py +0 -0
  169. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_api_protocol.py +0 -0
  170. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_array_agg_order_by.py +0 -0
  171. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_associations.py +0 -0
  172. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_bulk_update_by_pk.py +0 -0
  173. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  174. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_cache_debug.py +0 -0
  175. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_call_function.py +0 -0
  176. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_case_when.py +0 -0
  177. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_composite_pk_filter.py +0 -0
  178. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_composite_pk_write_paths.py +0 -0
  179. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_config.py +0 -0
  180. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_conflict_target.py +0 -0
  181. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_conflict_writes.py +0 -0
  182. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_connection_codecs.py +0 -0
  183. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_connection_poison_guard.py +0 -0
  184. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_count_composite_distinct.py +0 -0
  185. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_db_function_field.py +0 -0
  186. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_ddl_generator.py +0 -0
  187. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_derived_table.py +0 -0
  188. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_dirty_tracking.py +0 -0
  189. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_exceptions.py +0 -0
  190. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_execute_admin_sql.py +0 -0
  191. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_expression_primitives.py +0 -0
  192. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_fields.py +0 -0
  193. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  194. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_filter_raw.py +0 -0
  195. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_from_alias.py +0 -0
  196. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_fts.py +0 -0
  197. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_func_expression_args.py +0 -0
  198. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_having_annotate_alias.py +0 -0
  199. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  200. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_introspect_rls_policies.py +0 -0
  201. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_join_and_aggregate.py +0 -0
  202. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_listen_notify.py +0 -0
  203. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_managed_write_guard.py +0 -0
  204. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_matrx_entity.py +0 -0
  205. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_migration_diff_types.py +0 -0
  206. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_migration_loader.py +0 -0
  207. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_model_instance.py +0 -0
  208. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_model_meta.py +0 -0
  209. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_order_by_expression.py +0 -0
  210. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_patch_jsonb_path.py +0 -0
  211. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_pool_maintenance.py +0 -0
  212. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_postgrest_filters.py +0 -0
  213. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_query_builder.py +0 -0
  214. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_query_error_detail.py +0 -0
  215. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_query_executor_sql.py +0 -0
  216. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_query_timeout.py +0 -0
  217. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  218. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_queue_claim.py +0 -0
  219. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_registry.py +0 -0
  220. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_registry_multi_database.py +0 -0
  221. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_registry_multi_schema.py +0 -0
  222. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_relations.py +0 -0
  223. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_resilience.py +0 -0
  224. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_schema_exists.py +0 -0
  225. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_session_advisory_lock.py +0 -0
  226. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_state_cache.py +0 -0
  227. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_subquery_filter_raw.py +0 -0
  228. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_subquery_in_filter.py +0 -0
  229. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_supabase_auth.py +0 -0
  230. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_supabase_config.py +0 -0
  231. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_tls.py +0 -0
  232. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_update_case_expression.py +0 -0
  233. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_update_subquery.py +0 -0
  234. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_upsert_with_conflict.py +0 -0
  235. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_values_jsonb_decode.py +0 -0
  236. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_watched_lifecycle.py +0 -0
  237. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_write_retry_semantics.py +0 -0
  238. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/MODULE_README.md +0 -0
  239. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/__init__.py +0 -0
  240. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/conftest.py +0 -0
  241. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_bulk_ops.py +0 -0
  242. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_cache_integration.py +0 -0
  243. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_crud.py +0 -0
  244. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_foreign_keys.py +0 -0
  245. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_m2m.py +0 -0
  246. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_manager.py +0 -0
  247. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_migrations_live.py +0 -0
  248. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_query_execution.py +0 -0
  249. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_schema_diff.py +0 -0
  250. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/.env.example +0 -0
  251. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/README.md +0 -0
  252. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/__init__.py +0 -0
  253. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/generate.py +0 -0
  254. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/generated/.gitkeep +0 -0
  255. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/matrx_orm.yaml +0 -0
  256. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/test_schema_generation.py +0 -0
  257. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/.env.example +0 -0
  258. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/README.md +0 -0
  259. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/__init__.py +0 -0
  260. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/client_example.py +0 -0
  261. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/client_example.ts +0 -0
  262. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  263. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/server.py +0 -0
  264. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/entity_tests.py +0 -0
  265. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_base_generation.py +0 -0
  266. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  267. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_composite_unique_generation.py +0 -0
  268. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_dto_identity_generation.py +0 -0
  269. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_entity_capability_generation.py +0 -0
  270. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_generate_schema.py +0 -0
  271. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_multi_schema_output.py +0 -0
  272. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_sql_expression_defaults.py +0 -0
  273. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/__init__.py +0 -0
  274. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/conftest.py +0 -0
  275. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_capture_poison_proof.py +0 -0
  276. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_coalesce.py +0 -0
  277. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_dag.py +0 -0
  278. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_disk_spill_fallback.py +0 -0
  279. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_flush_individual_fallback.py +0 -0
  280. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_governed_write.py +0 -0
  281. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_managed.py +0 -0
  282. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_op_enqueue_site.py +0 -0
  283. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_reads.py +0 -0
  284. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_session.py +0 -0
  285. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_database_name_alias.py +0 -0
  286. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_model_cls_refactor.py +0 -0
  287. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  288. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_sql_param_casts.py +0 -0
  289. {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_sync_twins_guard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.20
3
+ Version: 3.1.22
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
@@ -113,6 +113,17 @@ The migration system tracks which branch a migration originated on and refuses t
113
113
 
114
114
  ### Mount the admin router
115
115
 
116
+ Rows with composite primary keys expose an opaque `__matrx_row_id` in list
117
+ responses and a matching virtual primary-key column descriptor. Pass that value
118
+ unchanged to row-detail, update, delete, and cache-eviction routes; the router
119
+ decodes it into the complete composite key. This also gives generated read-only
120
+ views collision-safe row navigation.
121
+
122
+ Generated views default to `id` only when they project it. Every other view must
123
+ declare bounded, unique columns in
124
+ `generate[].output.view_primary_keys`; generation fails instead of guessing a
125
+ first column or embedding an unbounded projected row in an identifier.
126
+
116
127
  ```python
117
128
  from fastapi import FastAPI
118
129
  from matrx_orm import admin_router
@@ -76,6 +76,17 @@ The migration system tracks which branch a migration originated on and refuses t
76
76
 
77
77
  ### Mount the admin router
78
78
 
79
+ Rows with composite primary keys expose an opaque `__matrx_row_id` in list
80
+ responses and a matching virtual primary-key column descriptor. Pass that value
81
+ unchanged to row-detail, update, delete, and cache-eviction routes; the router
82
+ decodes it into the complete composite key. This also gives generated read-only
83
+ views collision-safe row navigation.
84
+
85
+ Generated views default to `id` only when they project it. Every other view must
86
+ declare bounded, unique columns in
87
+ `generate[].output.view_primary_keys`; generation fails instead of guessing a
88
+ first column or embedding an unbounded projected row in an identifier.
89
+
79
90
  ```python
80
91
  from fastapi import FastAPI
81
92
  from matrx_orm import admin_router
@@ -24,6 +24,9 @@ Effective routes (relative to the mount prefix):
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
+ import base64
28
+ import binascii
29
+ import json
27
30
  import logging
28
31
  import math
29
32
  import re
@@ -44,6 +47,8 @@ log = logging.getLogger(__name__)
44
47
 
45
48
  # Must stay in sync with dashboard `ADMIN_LIST_MAX_PAGE_SIZE` (orm-proxy.ts).
46
49
  ADMIN_LIST_MAX_PAGE_SIZE = 2000
50
+ COMPOSITE_ROW_ID_FIELD = "__matrx_row_id"
51
+ _COMPOSITE_ROW_ID_PREFIX = "cmp1_"
47
52
 
48
53
 
49
54
  # ---------------------------------------------------------------------------
@@ -116,9 +121,59 @@ def _serialize(instance: Any) -> dict[str, Any]:
116
121
  if key in complete:
117
122
  continue
118
123
  complete[key] = serialize_value(value) if callable(serialize_value) else value
124
+ primary_keys = list(getattr(meta, "primary_keys", ()) or ())
125
+ if len(primary_keys) > 1:
126
+ values = [complete.get(primary_key) for primary_key in primary_keys]
127
+ payload = json.dumps(
128
+ values,
129
+ separators=(",", ":"),
130
+ sort_keys=True,
131
+ default=str,
132
+ ).encode("utf-8")
133
+ encoded = base64.urlsafe_b64encode(payload).decode("ascii").rstrip("=")
134
+ complete[COMPOSITE_ROW_ID_FIELD] = f"{_COMPOSITE_ROW_ID_PREFIX}{encoded}"
119
135
  return complete
120
136
 
121
137
 
138
+ def _decode_row_id(model: type, row_id: str) -> str | dict[str, Any]:
139
+ primary_keys = list(model._meta.primary_keys or ())
140
+ if len(primary_keys) <= 1:
141
+ return row_id
142
+ if not row_id.startswith(_COMPOSITE_ROW_ID_PREFIX):
143
+ raise ValueError(
144
+ f"{model.__name__} has a composite primary key; use the opaque row identity "
145
+ f"{COMPOSITE_ROW_ID_FIELD} returned by the list endpoint"
146
+ )
147
+ encoded = row_id.removeprefix(_COMPOSITE_ROW_ID_PREFIX)
148
+ try:
149
+ payload = base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4))
150
+ values = json.loads(payload)
151
+ except (binascii.Error, ValueError, json.JSONDecodeError) as exc:
152
+ raise ValueError("Invalid composite row id") from exc
153
+ if not isinstance(values, list) or len(values) != len(primary_keys):
154
+ raise ValueError(
155
+ f"Invalid composite row id: expected {len(primary_keys)} key values"
156
+ )
157
+ return dict(zip(primary_keys, values))
158
+
159
+
160
+ def _composite_row_id_descriptor() -> dict[str, Any]:
161
+ return {
162
+ "column_name": COMPOSITE_ROW_ID_FIELD,
163
+ "name": COMPOSITE_ROW_ID_FIELD,
164
+ "field_type": "TextField",
165
+ "data_type": "text",
166
+ "is_nullable": False,
167
+ "is_primary_key": True,
168
+ "is_unique": True,
169
+ "default": None,
170
+ "has_default": False,
171
+ "is_fk": False,
172
+ "is_array": False,
173
+ "description": "Opaque identity for a row with a composite primary key.",
174
+ }
175
+
176
+
122
177
  def _safe_scalar(value: Any) -> str | int | float | bool | None:
123
178
  """Return value only if it is a JSON-safe scalar; drop everything else."""
124
179
  if value is None or callable(value):
@@ -307,8 +362,17 @@ async def _admin_column_descriptors(model: type) -> list[dict[str, Any]]:
307
362
  db_rows = await _fetch_db_columns(model)
308
363
  orm_fields = model._meta.fields
309
364
  if not db_rows:
310
- return [_field_descriptor(name, field) for name, field in orm_fields.items()]
311
- return [_db_row_to_descriptor(row, orm_fields.get(str(row["column_name"]))) for row in db_rows]
365
+ columns = [_field_descriptor(name, field) for name, field in orm_fields.items()]
366
+ else:
367
+ columns = [
368
+ _db_row_to_descriptor(row, orm_fields.get(str(row["column_name"])))
369
+ for row in db_rows
370
+ ]
371
+ if len(model._meta.primary_keys or ()) > 1:
372
+ # Put the opaque composite identity first so clients that select the
373
+ # first primary-key descriptor cannot accidentally use one component.
374
+ columns.insert(0, _composite_row_id_descriptor())
375
+ return columns
312
376
 
313
377
 
314
378
  # Regex to extract values from CHECK constraint definitions like:
@@ -666,7 +730,7 @@ async def get_row(
666
730
  log.info("get_row: table=%s id=%s", table_name, row_id)
667
731
 
668
732
  try:
669
- instance = await model.get_by_id(row_id, use_cache=False)
733
+ instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
670
734
  return JSONResponse(content=_serialize(instance))
671
735
  except Exception as exc:
672
736
  log.error("get_row: %s/%s failed: %s", table_name, row_id, exc)
@@ -704,7 +768,7 @@ async def update_row(
704
768
  log.info("update_row: table=%s id=%s fields=%s", table_name, row_id, list(body.keys()))
705
769
 
706
770
  try:
707
- instance = await model.get_by_id(row_id, use_cache=False)
771
+ instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
708
772
  await instance.update(**body)
709
773
  log.info("update_row: %s/%s updated", table_name, row_id)
710
774
  return JSONResponse(content=_serialize(instance))
@@ -723,7 +787,7 @@ async def delete_row(
723
787
  log.info("delete_row: table=%s id=%s", table_name, row_id)
724
788
 
725
789
  try:
726
- instance = await model.get_by_id(row_id, use_cache=False)
790
+ instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
727
791
  await instance.delete()
728
792
  log.info("delete_row: %s/%s deleted", table_name, row_id)
729
793
  return JSONResponse(status_code=200, content={"deleted": True, "id": row_id})
@@ -760,7 +824,7 @@ async def bulk_delete_rows(
760
824
  for raw_id in ids:
761
825
  row_id = str(raw_id)
762
826
  try:
763
- instance = await model.get_by_id(row_id, use_cache=False)
827
+ instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
764
828
  await instance.delete()
765
829
  deleted.append(row_id)
766
830
  except Exception as exc:
@@ -819,7 +883,7 @@ async def bulk_update_rows(
819
883
  for raw_id in ids:
820
884
  row_id = str(raw_id)
821
885
  try:
822
- instance = await model.get_by_id(row_id, use_cache=False)
886
+ instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
823
887
  await instance.update(**data)
824
888
  updated.append(row_id)
825
889
  except Exception as exc:
@@ -892,7 +956,7 @@ async def evict_row_cache(
892
956
  log.info("evict_row_cache: %s/%s", table_name, row_id)
893
957
 
894
958
  try:
895
- instance = await model.get_by_id(row_id, use_cache=False)
959
+ instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
896
960
  await StateManager.remove(model, instance)
897
961
  return JSONResponse(content={"evicted": True, "table_name": table_name, "id": row_id})
898
962
  except Exception as exc:
@@ -34,7 +34,7 @@ class DatabaseProjectConfig:
34
34
  field_overrides: dict[str, Any] = field(default_factory=dict)
35
35
 
36
36
  # Extra PostgreSQL schemas to introspect and include in schema builder output.
37
- # For Supabase projects this typically includes ["auth", "storage"].
37
+ # Hosted projects may add private platform schemas as needed.
38
38
  # These schemas live in the same database but outside the default "public" schema.
39
39
  additional_schemas: list = field(default_factory=list)
40
40
 
@@ -588,6 +588,16 @@ class Column:
588
588
  elif self.full_type in [
589
589
  "tsvector",
590
590
  "tsquery",
591
+ "regclass",
592
+ "regproc",
593
+ "regprocedure",
594
+ "regoper",
595
+ "regoperator",
596
+ "regtype",
597
+ "regrole",
598
+ "regnamespace",
599
+ "regconfig",
600
+ "regdictionary",
591
601
  "geometry",
592
602
  "geography",
593
603
  "box",
@@ -78,6 +78,10 @@ class GenerationOutputOverrides:
78
78
  exclude_views names of individual views to skip even when
79
79
  include_views is True (e.g. a view with a column shape
80
80
  the codegen can't represent). Default: none excluded.
81
+ view_primary_keys mapping of view name to bounded, unique projected
82
+ columns used as the read-only model identity. Views
83
+ projecting ``id`` default to ``[id]``; every other
84
+ generated view must declare this explicitly.
81
85
 
82
86
  All fields default to current behavior; an absent yaml block is a no-op.
83
87
  """
@@ -90,6 +94,7 @@ class GenerationOutputOverrides:
90
94
  emit_managers: bool = True
91
95
  include_views: bool = True
92
96
  exclude_views: list[str] | None = None
97
+ view_primary_keys: dict[str, list[str]] | None = None
93
98
 
94
99
  @classmethod
95
100
  def from_dict(cls, d: dict | None) -> GenerationOutputOverrides:
@@ -103,6 +108,11 @@ class GenerationOutputOverrides:
103
108
  emit_managers=bool(d.get("emit_managers", True)),
104
109
  include_views=bool(d.get("include_views", True)),
105
110
  exclude_views=list(d.get("exclude_views") or []) or None,
111
+ view_primary_keys={
112
+ str(view_name): [str(column) for column in columns]
113
+ for view_name, columns in (d.get("view_primary_keys") or {}).items()
114
+ }
115
+ or None,
106
116
  )
107
117
 
108
118
 
@@ -239,4 +239,4 @@ class {name_camel}Manager(BaseManager):
239
239
 
240
240
  async def _initialize_runtime_data(self, item: {name_camel}) -> None:
241
241
  pass
242
- """
242
+ """
@@ -65,6 +65,7 @@ class Schema:
65
65
  self._emit_managers = self.output_overrides.emit_managers
66
66
  self._include_views = self.output_overrides.include_views
67
67
  self._exclude_views = set(self.output_overrides.exclude_views or [])
68
+ self._view_primary_keys = self.output_overrides.view_primary_keys or {}
68
69
  self.tables = {}
69
70
  self.views = {}
70
71
  self.model_export_names: list[str] = []
@@ -1054,7 +1055,12 @@ class Schema:
1054
1055
  try:
1055
1056
  view_src, view_model_name, view_field_types = view.to_python_model(
1056
1057
  model_database_name=self._model_database_name,
1058
+ primary_keys=self._view_primary_keys.get(view_name),
1057
1059
  )
1060
+ except ValueError:
1061
+ # Missing/invalid view identity is a generation contract
1062
+ # failure, never a relation to silently omit.
1063
+ raise
1058
1064
  except Exception as e: # noqa: BLE001 — one bad view must not kill the run
1059
1065
  vcprint(
1060
1066
  f"[MATRX ORM] Skipping view '{self.name}.{view_name}' — "
@@ -90,9 +90,14 @@ class View:
90
90
  except (IndexError, ValueError):
91
91
  return "TextField()"
92
92
 
93
- try:
94
- field_class = self.utils.to_python_models_field(base_type)
95
- except ValueError:
93
+ # pg_catalog identifier/reference types and enum-typed view columns are
94
+ # read as strings. Views are read-only, so generating writable EnumField
95
+ # metadata would add complexity without preserving any extra behavior.
96
+ if base_type == "name" or col.get("enum_labels"):
97
+ return "TextField()"
98
+
99
+ field_class = self.utils.sql_to_python_models_field.get(base_type)
100
+ if field_class is None:
96
101
  vcprint(
97
102
  f"[MATRX ORM] View {self.schema}.{self.name}: column "
98
103
  f"'{col.get('name')}' has unmapped type {base_type!r} "
@@ -107,13 +112,16 @@ class View:
107
112
  self,
108
113
  *,
109
114
  model_database_name: str | None = None,
115
+ primary_keys: list[str] | None = None,
110
116
  ) -> tuple[str, str, set[str]]:
111
117
  """Return (class_source, model_class_name, field_type_names) for a
112
118
  read-only Model bound to this view.
113
119
 
114
- Views have no Postgres PK; Models need row identity, so we synthesize
115
- ``_primary_keys`` from ``id`` when present, else the first column (cache /
116
- ``load_by_id`` only). No ForeignKey — views carry no FK metadata.
120
+ Views have no Postgres PK, so their identity must come from a bounded,
121
+ unique projection. ``id`` is the safe default when projected; otherwise
122
+ the generation config must explicitly name the grouping/identity columns.
123
+ This prevents first-column collisions and avoids embedding arbitrary row
124
+ payloads in dashboard URLs. No ForeignKey — views carry no FK metadata.
117
125
  """
118
126
  columns = self.column_data or []
119
127
  py_fields: list[str] = []
@@ -140,14 +148,33 @@ class View:
140
148
  if not column_names:
141
149
  raise ValueError(f"View {self.schema}.{self.name} has no columns to model.")
142
150
 
143
- pk = "id" if "id" in column_names else column_names[0]
151
+ resolved_primary_keys = list(primary_keys or (["id"] if "id" in column_names else []))
152
+ if not resolved_primary_keys:
153
+ raise ValueError(
154
+ f"View {self.schema}.{self.name} has no projected id; declare "
155
+ "output.view_primary_keys for a bounded unique identity."
156
+ )
157
+ if len(set(resolved_primary_keys)) != len(resolved_primary_keys):
158
+ raise ValueError(
159
+ f"View {self.schema}.{self.name} declares duplicate identity columns: "
160
+ f"{resolved_primary_keys}"
161
+ )
162
+ missing_primary_keys = [
163
+ column for column in resolved_primary_keys if column not in column_names
164
+ ]
165
+ if missing_primary_keys:
166
+ raise ValueError(
167
+ f"View {self.schema}.{self.name} identity columns are not projected: "
168
+ f"{missing_primary_keys}"
169
+ )
144
170
 
145
171
  from matrx_orm.schema_builder.common import safe_model_class_name
146
172
 
147
173
  model_class_name = safe_model_class_name(self.name, self.schema, self.utils.to_pascal_case)
148
174
  db_name = model_database_name or self.database
149
175
 
150
- py_fields.append(f'_primary_keys = ["{pk}"]')
176
+ py_fields.append("_read_only = True")
177
+ py_fields.append(f"_primary_keys = {resolved_primary_keys!r}")
151
178
  py_fields.append(f'_table_name = "{self.name}"')
152
179
  py_fields.append(f'_db_schema = "{self.schema}"')
153
180
  py_fields.append(f'_database = "{db_name}"')
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.20"
3
+ version = "3.1.22"
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" }
@@ -5,8 +5,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
5
5
 
6
6
  import pytest
7
7
  from matrx_orm.admin.router import (
8
+ COMPOSITE_ROW_ID_FIELD,
8
9
  _admin_column_descriptors,
9
10
  _db_row_to_descriptor,
11
+ _decode_row_id,
10
12
  _serialize,
11
13
  )
12
14
 
@@ -25,6 +27,69 @@ def test_serialize_includes_extra_data() -> None:
25
27
  assert out["role"] == "authenticated"
26
28
 
27
29
 
30
+ def test_composite_row_identity_round_trips() -> None:
31
+ meta = SimpleNamespace(
32
+ fields={"site_id": object(), "page_id": object()},
33
+ primary_keys=["site_id", "page_id"],
34
+ )
35
+ instance = SimpleNamespace(
36
+ to_dict=lambda: {"site_id": "site-1", "page_id": "page/2"},
37
+ _meta=meta,
38
+ _extra_data={},
39
+ )
40
+ model = SimpleNamespace(__name__="CostByPage", _meta=meta)
41
+
42
+ out = _serialize(instance)
43
+
44
+ assert out[COMPOSITE_ROW_ID_FIELD].startswith("cmp1_")
45
+ assert _decode_row_id(model, out[COMPOSITE_ROW_ID_FIELD]) == {
46
+ "site_id": "site-1",
47
+ "page_id": "page/2",
48
+ }
49
+
50
+
51
+ def test_composite_row_identity_rejects_scalar() -> None:
52
+ model = SimpleNamespace(
53
+ __name__="CostByPage",
54
+ _meta=SimpleNamespace(primary_keys=["site_id", "page_id"]),
55
+ )
56
+
57
+ with pytest.raises(ValueError, match="opaque row identity"):
58
+ _decode_row_id(model, "site-1")
59
+
60
+
61
+ def test_composite_row_identity_contains_only_declared_key_values() -> None:
62
+ metadata = {"private": "x" * 10_000}
63
+ meta = SimpleNamespace(
64
+ fields={"site_id": object(), "page_id": object(), "metadata": object()},
65
+ primary_keys=["site_id", "page_id"],
66
+ )
67
+ instance = SimpleNamespace(
68
+ to_dict=lambda: {"site_id": "site-1", "page_id": "page-2", "metadata": metadata},
69
+ _meta=meta,
70
+ _extra_data={},
71
+ )
72
+
73
+ encoded = _serialize(instance)[COMPOSITE_ROW_ID_FIELD]
74
+
75
+ assert len(encoded) < 100
76
+ assert _decode_row_id(SimpleNamespace(__name__="View", _meta=meta), encoded) == {
77
+ "site_id": "site-1",
78
+ "page_id": "page-2",
79
+ }
80
+
81
+
82
+ @pytest.mark.parametrize("row_id", ["cmp1_not-base64!", "cmp1_WzFd"])
83
+ def test_composite_row_identity_rejects_malformed_payload(row_id: str) -> None:
84
+ model = SimpleNamespace(
85
+ __name__="CostByPage",
86
+ _meta=SimpleNamespace(primary_keys=["site_id", "page_id"]),
87
+ )
88
+
89
+ with pytest.raises(ValueError, match="Invalid composite row id"):
90
+ _decode_row_id(model, row_id)
91
+
92
+
28
93
  def test_db_row_to_descriptor_without_orm_field() -> None:
29
94
  row = {
30
95
  "column_name": "last_sign_in_at",
@@ -84,3 +149,17 @@ async def test_admin_column_descriptors_prefers_live_db() -> None:
84
149
  cols = await _admin_column_descriptors(model)
85
150
  assert [c["column_name"] for c in cols] == ["id", "email", "created_at"]
86
151
  assert cols[2]["field_type"] == "DateTimeField"
152
+
153
+
154
+ @pytest.mark.asyncio
155
+ async def test_admin_columns_expose_composite_row_identity_first() -> None:
156
+ model = MagicMock()
157
+ model._meta.fields = {"site_id": MagicMock(), "page_id": MagicMock()}
158
+ model._meta.primary_keys = ["site_id", "page_id"]
159
+ model._meta.table_name = "v_cost_by_page"
160
+ model.raw_sql = AsyncMock(return_value=[])
161
+
162
+ columns = await _admin_column_descriptors(model)
163
+
164
+ assert columns[0]["column_name"] == COMPOSITE_ROW_ID_FIELD
165
+ assert columns[0]["is_primary_key"] is True
@@ -0,0 +1,72 @@
1
+ from matrx_orm.schema_builder.views import View
2
+
3
+
4
+ def _view(*, base_type: str = "text", multiple_columns: bool = False) -> View:
5
+ columns = [
6
+ {
7
+ "name": "id",
8
+ "base_type": base_type,
9
+ "full_type": base_type,
10
+ "nullable": False,
11
+ }
12
+ ]
13
+ if multiple_columns:
14
+ columns.append(
15
+ {
16
+ "name": "item_id",
17
+ "base_type": "uuid",
18
+ "full_type": "uuid",
19
+ "nullable": False,
20
+ }
21
+ )
22
+ return View(
23
+ oid=1,
24
+ name="current_items",
25
+ type_="view",
26
+ schema="example",
27
+ database="test_database",
28
+ owner="postgres",
29
+ size_bytes=0,
30
+ description=None,
31
+ view_definition="select id from example.items",
32
+ column_data=columns,
33
+ )
34
+
35
+
36
+ def test_generated_view_model_is_structurally_read_only() -> None:
37
+ source, _, _ = _view().to_python_model()
38
+
39
+ assert "_read_only = True" in source
40
+
41
+
42
+ def test_generated_view_uses_declared_composite_identity() -> None:
43
+ source, _, _ = _view(multiple_columns=True).to_python_model(
44
+ primary_keys=["id", "item_id"]
45
+ )
46
+
47
+ assert "_primary_keys = ['id', 'item_id']" in source
48
+
49
+
50
+ def test_generated_view_defaults_to_projected_id() -> None:
51
+ source, _, _ = _view(multiple_columns=True).to_python_model()
52
+
53
+ assert "_primary_keys = ['id']" in source
54
+
55
+
56
+ def test_generated_view_without_id_requires_declared_identity() -> None:
57
+ view = _view()
58
+ view.column_data[0]["name"] = "group_name"
59
+
60
+ try:
61
+ view.to_python_model()
62
+ except ValueError as exc:
63
+ assert "view_primary_keys" in str(exc)
64
+ else:
65
+ raise AssertionError("missing view identity must fail generation")
66
+
67
+
68
+ def test_catalog_name_view_column_maps_to_text_without_error() -> None:
69
+ source, _, field_types = _view(base_type="name").to_python_model()
70
+
71
+ assert "id = TextField(null=False)" in source
72
+ assert field_types == {"TextField"}
File without changes
File without changes
File without changes
File without changes