matrx-orm 3.1.77__tar.gz → 3.1.78__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 (344) hide show
  1. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/CLAUDE.md +3 -0
  2. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/PKG-INFO +4 -2
  3. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/README.md +2 -0
  4. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/__init__.py +7 -0
  5. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/admin/FEATURE.md +12 -1
  6. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/admin/router.py +55 -21
  7. matrx_orm-3.1.78/matrx_orm/local_sqlite.py +40 -0
  8. matrx_orm-3.1.78/matrx_orm/operations/FEATURE.md +23 -0
  9. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/conflict_writes.py +9 -2
  10. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/pyproject.toml +2 -2
  11. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_admin_db_columns.py +64 -2
  12. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_conflict_writes.py +74 -0
  13. matrx_orm-3.1.78/tests/level1/test_local_sqlite.py +58 -0
  14. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/.gitignore +0 -0
  15. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/AGENTS.md +0 -0
  16. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/BASE_CLASS_METHODS.md +0 -0
  17. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/MODEL_API.md +0 -0
  18. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/database/orm/extended/managers/ai_model_base.py +0 -0
  19. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/docs/migrations.md +0 -0
  20. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/MODULE_README.md +0 -0
  21. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/MODULE_README.md +0 -0
  22. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/__init__.py +0 -0
  23. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/async_postgresql.py +0 -0
  24. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/base_adapter.py +0 -0
  25. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/postgresql.py +0 -0
  26. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  27. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/supabase_adapter.py +0 -0
  28. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/admin/__init__.py +0 -0
  29. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/__init__.py +0 -0
  30. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/auth.py +0 -0
  31. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/config.py +0 -0
  32. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/handlers.py +0 -0
  33. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/protocol.py +0 -0
  34. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/server.py +0 -0
  35. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/associations.py +0 -0
  36. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/cache_debug.py +0 -0
  37. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/catalog.py +0 -0
  38. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/catalog_sql.py +0 -0
  39. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/MODULE_README.md +0 -0
  40. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/__init__.py +0 -0
  41. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/postgres_connection.py +0 -0
  42. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/postgrest.py +0 -0
  43. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/supabase_auth.py +0 -0
  44. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/supabase_config.py +0 -0
  45. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/supabase_manager.py +0 -0
  46. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  47. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/FEATURE.md +0 -0
  48. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/MODULE_README.md +0 -0
  49. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  50. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/TASKS.md +0 -0
  51. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/__init__.py +0 -0
  52. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/async_db_manager.py +0 -0
  53. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/base.py +0 -0
  54. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/config.py +0 -0
  55. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/connections.py +0 -0
  56. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/db_function.py +0 -0
  57. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/diagnostics.py +0 -0
  58. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/expressions.py +0 -0
  59. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/extended.py +0 -0
  60. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/fields.py +0 -0
  61. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/introspection.py +0 -0
  62. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/listen.py +0 -0
  63. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/loop_filters.py +0 -0
  64. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/model_dto.py +0 -0
  65. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/model_view.py +0 -0
  66. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/one_database.py +0 -0
  67. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/paginator.py +0 -0
  68. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/pool_maintenance.py +0 -0
  69. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/pool_watch.py +0 -0
  70. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/pooler_role.py +0 -0
  71. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/pydantic_bridge.py +0 -0
  72. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/registry.py +0 -0
  73. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/relations.py +0 -0
  74. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/resilience.py +0 -0
  75. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/rls_session.py +0 -0
  76. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/signals.py +0 -0
  77. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/tls.py +0 -0
  78. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/transaction.py +0 -0
  79. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/types.py +0 -0
  80. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/write_queue.py +0 -0
  81. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/entity.py +0 -0
  82. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/error_handling.py +0 -0
  83. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/exceptions.py +0 -0
  84. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/extended/__init__.py +0 -0
  85. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/extended/app_error_handler.py +0 -0
  86. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/identity_rule.py +0 -0
  87. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/middleware/__init__.py +0 -0
  88. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/middleware/base.py +0 -0
  89. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/MODULE_README.md +0 -0
  90. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/__init__.py +0 -0
  91. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/cli.py +0 -0
  92. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/ddl.py +0 -0
  93. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/diff.py +0 -0
  94. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/executor.py +0 -0
  95. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/integration.py +0 -0
  96. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/loader.py +0 -0
  97. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/operations.py +0 -0
  98. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/state.py +0 -0
  99. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/table_filter.py +0 -0
  100. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/MODULE_README.md +0 -0
  101. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/__init__.py +0 -0
  102. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/bulk_update_values.py +0 -0
  103. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/conflict.py +0 -0
  104. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/create.py +0 -0
  105. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/db_functions.py +0 -0
  106. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/delete.py +0 -0
  107. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/dynamic_admin.py +0 -0
  108. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/dynamic_crud.py +0 -0
  109. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/graph_walk.py +0 -0
  110. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/latest_rows.py +0 -0
  111. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/queue_claim.py +0 -0
  112. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/read.py +0 -0
  113. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/relational_backfill.py +0 -0
  114. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/staging_load.py +0 -0
  115. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/update.py +0 -0
  116. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/platform_db.py +0 -0
  117. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/py.typed +0 -0
  118. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/MODULE_README.md +0 -0
  119. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/__init__.py +0 -0
  120. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/db_objects.py +0 -0
  121. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  122. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  123. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/FEATURE.md +0 -0
  124. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/MODULE_README.md +0 -0
  125. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/__init__.py +0 -0
  126. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/builder.py +0 -0
  127. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/executor.py +0 -0
  128. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/FEATURE.md +0 -0
  129. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  130. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/__init__.py +0 -0
  131. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/code_handler.py +0 -0
  132. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  133. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/columns.py +0 -0
  134. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/common.py +0 -0
  135. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/diff_preview.py +0 -0
  136. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  137. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/generator.py +0 -0
  138. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  139. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  140. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  141. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  142. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/package_wiring.py +0 -0
  143. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/relationships.py +0 -0
  144. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/runner.py +0 -0
  145. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/schema.py +0 -0
  146. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/schema_manager.py +0 -0
  147. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/tables.py +0 -0
  148. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/views.py +0 -0
  149. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/FEATURE.md +0 -0
  150. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/__init__.py +0 -0
  151. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/attachments.py +0 -0
  152. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/crypto.py +0 -0
  153. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/items.py +0 -0
  154. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/model.py +0 -0
  155. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/org_service.py +0 -0
  156. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/service.py +0 -0
  157. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/README.md +0 -0
  158. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/__init__.py +0 -0
  159. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/coalesce.py +0 -0
  160. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/dag.py +0 -0
  161. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/errors.py +0 -0
  162. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/fallback.py +0 -0
  163. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/flush.py +0 -0
  164. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/lifecycle.py +0 -0
  165. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/managed.py +0 -0
  166. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/op.py +0 -0
  167. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/reads.py +0 -0
  168. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/session.py +0 -0
  169. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/telemetry.py +0 -0
  170. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  171. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/__init__.py +0 -0
  172. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/executor.py +0 -0
  173. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/queries.py +0 -0
  174. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/registry.py +0 -0
  175. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/types.py +0 -0
  176. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/utils.py +0 -0
  177. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/state.py +0 -0
  178. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/utils/__init__.py +0 -0
  179. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/utils/sql_utils.py +0 -0
  180. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/utils/type_converters.py +0 -0
  181. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/scripts/git-branches.sh +0 -0
  182. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/scripts/publish.sh +0 -0
  183. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/scripts/release.sh +0 -0
  184. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/MODULE_README.md +0 -0
  185. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/__init__.py +0 -0
  186. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/conftest.py +0 -0
  187. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/MODULE_README.md +0 -0
  188. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/__init__.py +0 -0
  189. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_admin_response_rendering.py +0 -0
  190. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_admin_search_negation.py +0 -0
  191. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_admin_search_sentinels.py +0 -0
  192. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_advisory_lock.py +0 -0
  193. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_agent_message.py +0 -0
  194. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_api_auth.py +0 -0
  195. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_api_config.py +0 -0
  196. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_api_handlers.py +0 -0
  197. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_api_protocol.py +0 -0
  198. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_array_agg_order_by.py +0 -0
  199. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_associations.py +0 -0
  200. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_bulk_hydration_offload.py +0 -0
  201. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_bulk_update_by_pk.py +0 -0
  202. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  203. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_cache_debug.py +0 -0
  204. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_call_function.py +0 -0
  205. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_case_when.py +0 -0
  206. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_catalog.py +0 -0
  207. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_composite_pk_filter.py +0 -0
  208. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_composite_pk_write_paths.py +0 -0
  209. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_config.py +0 -0
  210. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_conflict_target.py +0 -0
  211. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_connection_codecs.py +0 -0
  212. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_connection_poison_guard.py +0 -0
  213. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_connection_primitives.py +0 -0
  214. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_count_composite_distinct.py +0 -0
  215. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_db_function_field.py +0 -0
  216. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_ddl_generator.py +0 -0
  217. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_derived_table.py +0 -0
  218. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_dirty_tracking.py +0 -0
  219. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_dynamic_crud.py +0 -0
  220. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_exceptions.py +0 -0
  221. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  222. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_expression_primitives.py +0 -0
  223. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_fields.py +0 -0
  224. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  225. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_from_alias.py +0 -0
  226. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_fts.py +0 -0
  227. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_func_expression_args.py +0 -0
  228. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_graph_walk.py +0 -0
  229. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_having_annotate_alias.py +0 -0
  230. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  231. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_introspect_rls_policies.py +0 -0
  232. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_join_and_aggregate.py +0 -0
  233. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_latest_rows.py +0 -0
  234. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_listen_notify.py +0 -0
  235. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_managed_write_guard.py +0 -0
  236. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_matrx_entity.py +0 -0
  237. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_migration_diff_types.py +0 -0
  238. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_migration_loader.py +0 -0
  239. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_model_instance.py +0 -0
  240. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_model_meta.py +0 -0
  241. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_optimistic_concurrency.py +0 -0
  242. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_order_by_expression.py +0 -0
  243. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_parameter_name_hydration.py +0 -0
  244. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_patch_jsonb_path.py +0 -0
  245. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_pool_holder_census.py +0 -0
  246. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_pool_loop_recreation.py +0 -0
  247. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_pool_maintenance.py +0 -0
  248. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_postgrest_filters.py +0 -0
  249. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_pydantic_bridge.py +0 -0
  250. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_query_builder.py +0 -0
  251. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_query_error_detail.py +0 -0
  252. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_query_executor_sql.py +0 -0
  253. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_query_timeout.py +0 -0
  254. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  255. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_queue_claim.py +0 -0
  256. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_registry.py +0 -0
  257. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_registry_multi_database.py +0 -0
  258. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_registry_multi_schema.py +0 -0
  259. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_relational_backfill.py +0 -0
  260. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_relations.py +0 -0
  261. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_resilience.py +0 -0
  262. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_rls_session_scope.py +0 -0
  263. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_schema_exists.py +0 -0
  264. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_session_advisory_lock.py +0 -0
  265. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_state_cache.py +0 -0
  266. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_subquery_filter_raw.py +0 -0
  267. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_subquery_in_filter.py +0 -0
  268. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_supabase_auth.py +0 -0
  269. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_supabase_config.py +0 -0
  270. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_tls.py +0 -0
  271. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_typed_join_projection.py +0 -0
  272. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_typed_predicates.py +0 -0
  273. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_update_case_expression.py +0 -0
  274. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_update_subquery.py +0 -0
  275. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_update_with_rebase.py +0 -0
  276. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_upsert_default_leak.py +0 -0
  277. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_upsert_with_conflict.py +0 -0
  278. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_values_jsonb_decode.py +0 -0
  279. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_watched_lifecycle.py +0 -0
  280. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_write_retry_semantics.py +0 -0
  281. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/MODULE_README.md +0 -0
  282. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/__init__.py +0 -0
  283. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/conftest.py +0 -0
  284. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_bulk_ops.py +0 -0
  285. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_cache_integration.py +0 -0
  286. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_crud.py +0 -0
  287. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_foreign_keys.py +0 -0
  288. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_m2m.py +0 -0
  289. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_manager.py +0 -0
  290. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_migrations_live.py +0 -0
  291. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_query_execution.py +0 -0
  292. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_schema_diff.py +0 -0
  293. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/live_pooler_role.py +0 -0
  294. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/.env.example +0 -0
  295. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/README.md +0 -0
  296. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/__init__.py +0 -0
  297. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/generate.py +0 -0
  298. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/generated/.gitkeep +0 -0
  299. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/matrx_orm.yaml +0 -0
  300. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/test_schema_generation.py +0 -0
  301. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/.env.example +0 -0
  302. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/README.md +0 -0
  303. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/__init__.py +0 -0
  304. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_example.py +0 -0
  305. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_example.ts +0 -0
  306. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  307. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/server.py +0 -0
  308. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/entity_tests.py +0 -0
  309. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_base_generation.py +0 -0
  310. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  311. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_composite_unique_generation.py +0 -0
  312. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_dto_identity_generation.py +0 -0
  313. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_entity_capability_generation.py +0 -0
  314. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_generate_schema.py +0 -0
  315. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_junction_analysis.py +0 -0
  316. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_multi_schema_output.py +0 -0
  317. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_sql_expression_defaults.py +0 -0
  318. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_table_relationships_query.py +0 -0
  319. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_view_generation.py +0 -0
  320. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema_builder/test_array_default_parsing.py +0 -0
  321. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  322. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  323. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/__init__.py +0 -0
  324. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/conftest.py +0 -0
  325. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_cache_no_downgrade.py +0 -0
  326. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_capture_poison_proof.py +0 -0
  327. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_coalesce.py +0 -0
  328. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_dag.py +0 -0
  329. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_disk_spill_fallback.py +0 -0
  330. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_flush_individual_fallback.py +0 -0
  331. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_governed_write.py +0 -0
  332. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_managed.py +0 -0
  333. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_op_enqueue_site.py +0 -0
  334. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_reads.py +0 -0
  335. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_session.py +0 -0
  336. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_database_name_alias.py +0 -0
  337. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_db_functions.py +0 -0
  338. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_model_cls_refactor.py +0 -0
  339. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  340. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_one_database.py +0 -0
  341. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_pooler_role_guard.py +0 -0
  342. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_secrets_battery.py +0 -0
  343. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_sql_param_casts.py +0 -0
  344. {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_sync_twins_guard.py +0 -0
@@ -38,6 +38,9 @@ Every table that becomes a Model needs row identity: a single-column primary key
38
38
  Large reconciliation/export scans call `.all(use_cache=False)` so returned
39
39
  models do not quadratically populate the process identity cache. Contract:
40
40
  [`matrx_orm/query/FEATURE.md`](matrx_orm/query/FEATURE.md).
41
+ - **Conflict writes**: bulk insert-ignore and counter-upsert row coercion runs
42
+ off the event loop before statement chunking. Contract:
43
+ [`matrx_orm/operations/FEATURE.md`](matrx_orm/operations/FEATURE.md).
41
44
  - **Migrations**: `MigrationDB`, `MigrationLoader`, `MigrationExecutor`, `makemigrations`, `migrate` — migrations declare explicit `dependencies` and apply in topological order (no multi-head/merge support yet).
42
45
  - **Admin router** (FastAPI): `admin_router` exposes read/write endpoints for any registered model; bulk response rendering stays off the event loop. Contract: [`matrx_orm/admin/FEATURE.md`](matrx_orm/admin/FEATURE.md).
43
46
  - **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matrx-orm
3
- Version: 3.1.77
3
+ Version: 3.1.78
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
@@ -25,7 +25,7 @@ Requires-Python: >=3.13
25
25
  Requires-Dist: asyncpg>=0.31.0
26
26
  Requires-Dist: cryptography>=43.0
27
27
  Requires-Dist: gitpython>=3.1.40
28
- Requires-Dist: matrx-utils>=2.0.18
28
+ Requires-Dist: matrx-utils>=2.0.19
29
29
  Requires-Dist: psycopg-pool>=3.2.5
30
30
  Requires-Dist: psycopg[binary]>=3.2.5
31
31
  Requires-Dist: python-dotenv>=1.0
@@ -57,6 +57,8 @@ Python 3.13+ required. Needs a PostgreSQL server (or Supabase / any Postgres-com
57
57
  - **Admin router** (FastAPI): `admin_router` exposes a ready-to-mount set of read/write endpoints for every registered model.
58
58
  - **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
59
59
  - **Adapters**: `AsyncPostgreSQLAdapter`, `SupabaseAdapter`, `PostgRESTClientAdapter`.
60
+ - **Local artifact reads**: `read_local_sqlite_rows` provides a validated, read-only
61
+ projection boundary for SQLite files owned by external tools such as Chromium.
60
62
  - **Signals**: `pre_create`, `post_create`, `pre_save`, `post_save`, `pre_delete`, `post_delete`.
61
63
  - **Schema builder** (`matrx_orm.schema_builder`): code generation for Python + TypeScript type definitions from the live DB schema — useful for keeping a frontend's row types in sync.
62
64
 
@@ -18,6 +18,8 @@ Python 3.13+ required. Needs a PostgreSQL server (or Supabase / any Postgres-com
18
18
  - **Admin router** (FastAPI): `admin_router` exposes a ready-to-mount set of read/write endpoints for every registered model.
19
19
  - **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
20
20
  - **Adapters**: `AsyncPostgreSQLAdapter`, `SupabaseAdapter`, `PostgRESTClientAdapter`.
21
+ - **Local artifact reads**: `read_local_sqlite_rows` provides a validated, read-only
22
+ projection boundary for SQLite files owned by external tools such as Chromium.
21
23
  - **Signals**: `pre_create`, `post_create`, `pre_save`, `post_save`, `pre_delete`, `post_delete`.
22
24
  - **Schema builder** (`matrx_orm.schema_builder`): code generation for Python + TypeScript type definitions from the live DB schema — useful for keeping a frontend's row types in sync.
23
25
 
@@ -43,6 +43,7 @@ from .platform_db import (
43
43
  platform_connection_url,
44
44
  register_platform_db,
45
45
  )
46
+ from .local_sqlite import SQLiteValue, read_local_sqlite_rows
46
47
  from .core.diagnostics import collect_pool_diagnostics, log_pool_diagnostics
47
48
  from .core.introspection import introspect_columns, introspect_rls_policies, schema_exists
48
49
  from .core.one_database import OneDatabaseReport, PoolIdentity, audit_one_database
@@ -633,6 +634,12 @@ __all__ = [
633
634
  "AsyncPostgreSQLAdapter",
634
635
  "SupabaseAdapter",
635
636
  "PostgRESTClientAdapter",
637
+ "create_external_pool",
638
+ "get_registered_pool",
639
+ "open_listener_connection",
640
+ # ── Read-only local SQLite artifacts ──────────────────────────────────────
641
+ "SQLiteValue",
642
+ "read_local_sqlite_rows",
636
643
  # ── Cache ──────────────────────────────────────────────────────────────────
637
644
  "CachePolicy",
638
645
  "StateManager",
@@ -14,10 +14,21 @@ FastAPI admin surface.
14
14
  - **The page cap is 2,000 rows.** Keep `ADMIN_LIST_MAX_PAGE_SIZE` aligned with the
15
15
  dashboard constant in `orm-proxy.ts`.
16
16
 
17
+ ## Database identity
18
+
19
+ - **A table is `database.schema.table`.** `GET /tables` and cache stats expose
20
+ `database_name`; every columns, row, mutation, bulk, and cache endpoint accepts
21
+ `database=` and passes it to the multi-database model registry.
22
+ - **Ambiguity is a controlled conflict.** An omitted database that matches more
23
+ than one registered model returns HTTP 409 with the candidates. It never guesses
24
+ and never escapes as a request crash.
25
+
17
26
  Verified against `router.py` and
18
- `tests/level1/test_admin_response_rendering.py` on 2026-08-17.
27
+ `tests/level1/test_admin_db_columns.py` on 2026-08-17.
19
28
 
20
29
  ## Change Log
21
30
 
22
31
  - 2026-08-17 — Moved bulk row serialization and JSON encoding off the event
23
32
  loop after a 1.1-second production stall.
33
+ - 2026-08-17 — Carried database identity through every admin relation endpoint
34
+ and converted unqualified cross-database ambiguity into HTTP 409.
@@ -58,22 +58,42 @@ _COMPOSITE_ROW_ID_PREFIX = "cmp1_"
58
58
  # ---------------------------------------------------------------------------
59
59
 
60
60
 
61
- def _model_by_table(table_name: str, schema: str | None = None) -> type | None:
62
- return model_registry.get_model_by_table(schema or "public", table_name)
61
+ def _model_by_table(
62
+ table_name: str,
63
+ schema: str | None = None,
64
+ database: str | None = None,
65
+ ) -> type | None:
66
+ return model_registry.get_model_by_table(
67
+ schema or "public",
68
+ table_name,
69
+ database=database,
70
+ )
63
71
 
64
72
 
65
- def _require_model(table_name: str, schema: str | None = None) -> type:
66
- model = _model_by_table(table_name, schema)
73
+ def _require_model(
74
+ table_name: str,
75
+ schema: str | None = None,
76
+ database: str | None = None,
77
+ ) -> type:
78
+ try:
79
+ model = _model_by_table(table_name, schema, database)
80
+ except ValueError as exc:
81
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
67
82
  if model is None:
68
83
  effective_schema = schema or "public"
84
+ database_suffix = f" in database '{database}'" if database else ""
69
85
  log.warning(
70
- "Table '%s.%s' not found in model registry",
86
+ "Table '%s.%s' not found in model registry%s",
71
87
  effective_schema,
72
88
  table_name,
89
+ database_suffix,
73
90
  )
74
91
  raise HTTPException(
75
92
  status_code=404,
76
- detail=f"Table '{effective_schema}.{table_name}' not found in model registry",
93
+ detail=(
94
+ f"Table '{effective_schema}.{table_name}' not found in model registry"
95
+ f"{database_suffix}"
96
+ ),
77
97
  )
78
98
  return model
79
99
 
@@ -428,16 +448,19 @@ async def _fetch_check_choices(model: type) -> dict[str, list[str]]:
428
448
  async def list_tables() -> list[dict[str, object]]:
429
449
  models = model_registry.all_models()
430
450
  log.info("list_tables: %d models in registry", len(models))
431
- sample_model = next(iter(models.values()), None)
432
- db_counts = await _fetch_db_column_counts(sample_model) if sample_model else {}
451
+ counts_by_database: dict[str, dict[tuple[str, str], int]] = {}
433
452
  tables = []
434
453
  for model in models.values():
435
454
  meta = model._meta
436
455
  schema = _model_schema(model)
456
+ database = model.get_database_name()
457
+ if database not in counts_by_database:
458
+ counts_by_database[database] = await _fetch_db_column_counts(model)
437
459
  orm_count = len(meta.fields)
438
- db_count = db_counts.get((schema, meta.table_name))
460
+ db_count = counts_by_database[database].get((schema, meta.table_name))
439
461
  tables.append(
440
462
  {
463
+ "database_name": database,
441
464
  "table_name": meta.table_name,
442
465
  "table_schema": schema,
443
466
  "name": meta.table_name,
@@ -445,7 +468,7 @@ async def list_tables() -> list[dict[str, object]]:
445
468
  "column_count": db_count if db_count is not None else orm_count,
446
469
  }
447
470
  )
448
- tables.sort(key=lambda t: (t["table_schema"], t["table_name"]))
471
+ tables.sort(key=lambda t: (t["database_name"], t["table_schema"], t["table_name"]))
449
472
  return tables
450
473
 
451
474
 
@@ -453,8 +476,9 @@ async def list_tables() -> list[dict[str, object]]:
453
476
  async def get_columns(
454
477
  table_name: str,
455
478
  schema: str | None = Query(None, description="PostgreSQL schema name"),
479
+ database: str | None = Query(None, description="Registered database project name"),
456
480
  ) -> list[dict[str, object]]:
457
- model = _require_model(table_name, schema)
481
+ model = _require_model(table_name, schema, database)
458
482
  columns = await _admin_column_descriptors(model)
459
483
 
460
484
  # Enrich with CHECK constraint choices from the database
@@ -647,8 +671,9 @@ async def list_rows(
647
671
  sort_dir: str = Query("asc", pattern="^(asc|desc)$"),
648
672
  search: str | None = Query(None, description="Column filters as 'col1:val1,col2:val2'"),
649
673
  schema: str | None = Query(None, description="PostgreSQL schema name"),
674
+ database: str | None = Query(None, description="Registered database project name"),
650
675
  ) -> Response:
651
- model = _require_model(table_name, schema)
676
+ model = _require_model(table_name, schema, database)
652
677
  log.info(
653
678
  "list_rows: table=%s page=%d size=%d sort_by=%s sort_dir=%s search=%s",
654
679
  table_name,
@@ -707,8 +732,9 @@ async def get_row(
707
732
  table_name: str,
708
733
  row_id: str,
709
734
  schema: str | None = Query(None, description="PostgreSQL schema name"),
735
+ database: str | None = Query(None, description="Registered database project name"),
710
736
  ) -> JSONResponse:
711
- model = _require_model(table_name, schema)
737
+ model = _require_model(table_name, schema, database)
712
738
  log.info("get_row: table=%s id=%s", table_name, row_id)
713
739
 
714
740
  try:
@@ -724,8 +750,9 @@ async def create_row(
724
750
  table_name: str,
725
751
  request: Request,
726
752
  schema: str | None = Query(None, description="PostgreSQL schema name"),
753
+ database: str | None = Query(None, description="Registered database project name"),
727
754
  ) -> JSONResponse:
728
- model = _require_model(table_name, schema)
755
+ model = _require_model(table_name, schema, database)
729
756
  body: dict[str, Any] = await request.json()
730
757
  log.info("create_row: table=%s fields=%s", table_name, list(body.keys()))
731
758
 
@@ -744,8 +771,9 @@ async def update_row(
744
771
  row_id: str,
745
772
  request: Request,
746
773
  schema: str | None = Query(None, description="PostgreSQL schema name"),
774
+ database: str | None = Query(None, description="Registered database project name"),
747
775
  ) -> JSONResponse:
748
- model = _require_model(table_name, schema)
776
+ model = _require_model(table_name, schema, database)
749
777
  body: dict[str, Any] = await request.json()
750
778
  log.info("update_row: table=%s id=%s fields=%s", table_name, row_id, list(body.keys()))
751
779
 
@@ -764,8 +792,9 @@ async def delete_row(
764
792
  table_name: str,
765
793
  row_id: str,
766
794
  schema: str | None = Query(None, description="PostgreSQL schema name"),
795
+ database: str | None = Query(None, description="Registered database project name"),
767
796
  ) -> JSONResponse:
768
- model = _require_model(table_name, schema)
797
+ model = _require_model(table_name, schema, database)
769
798
  log.info("delete_row: table=%s id=%s", table_name, row_id)
770
799
 
771
800
  try:
@@ -783,10 +812,11 @@ async def bulk_delete_rows(
783
812
  table_name: str,
784
813
  request: Request,
785
814
  schema: str | None = Query(None, description="PostgreSQL schema name"),
815
+ database: str | None = Query(None, description="Registered database project name"),
786
816
  ) -> JSONResponse:
787
817
  """Delete many rows in one call. Never fails the whole batch on one bad id —
788
818
  returns a partial-result envelope so the caller can report what landed."""
789
- model = _require_model(table_name, schema)
819
+ model = _require_model(table_name, schema, database)
790
820
  body: dict[str, Any] = await request.json()
791
821
  ids = body.get("ids")
792
822
  if not isinstance(ids, list) or not ids:
@@ -830,11 +860,12 @@ async def bulk_update_rows(
830
860
  table_name: str,
831
861
  request: Request,
832
862
  schema: str | None = Query(None, description="PostgreSQL schema name"),
863
+ database: str | None = Query(None, description="Registered database project name"),
833
864
  ) -> JSONResponse:
834
865
  """Apply the same field changes to many rows in one call. Never fails the
835
866
  whole batch on one bad id — returns a partial-result envelope (same shape
836
867
  as bulk-delete) so the caller can report what landed."""
837
- model = _require_model(table_name, schema)
868
+ model = _require_model(table_name, schema, database)
838
869
  body: dict[str, Any] = await request.json()
839
870
  ids = body.get("ids")
840
871
  data = body.get("data")
@@ -901,12 +932,13 @@ async def cache_stats() -> list[dict[str, object]]:
901
932
  results.append(
902
933
  {
903
934
  "model_name": model.__name__,
935
+ "database_name": model.get_database_name(),
904
936
  "table_name": model._meta.table_name,
905
937
  "table_schema": _model_schema(model),
906
938
  "cached_count": count,
907
939
  }
908
940
  )
909
- results.sort(key=lambda r: (r["table_schema"], r["table_name"]))
941
+ results.sort(key=lambda r: (r["database_name"], r["table_schema"], r["table_name"]))
910
942
  return results
911
943
 
912
944
 
@@ -914,9 +946,10 @@ async def cache_stats() -> list[dict[str, object]]:
914
946
  async def clear_table_cache(
915
947
  table_name: str,
916
948
  schema: str | None = Query(None, description="PostgreSQL schema name"),
949
+ database: str | None = Query(None, description="Registered database project name"),
917
950
  ) -> JSONResponse:
918
951
  """Flush the entire in-memory cache for a table."""
919
- model = _require_model(table_name, schema)
952
+ model = _require_model(table_name, schema, database)
920
953
  log.info("clear_table_cache: %s", table_name)
921
954
 
922
955
  try:
@@ -932,9 +965,10 @@ async def evict_row_cache(
932
965
  table_name: str,
933
966
  row_id: str,
934
967
  schema: str | None = Query(None, description="PostgreSQL schema name"),
968
+ database: str | None = Query(None, description="Registered database project name"),
935
969
  ) -> JSONResponse:
936
970
  """Evict a single record from the in-memory cache."""
937
- model = _require_model(table_name, schema)
971
+ model = _require_model(table_name, schema, database)
938
972
  log.info("evict_row_cache: %s/%s", table_name, row_id)
939
973
 
940
974
  try:
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import sqlite3
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+
8
+ type SQLiteValue = str | int | float | bytes | None
9
+
10
+ _IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
11
+
12
+
13
+ def _identifier(value: str) -> str:
14
+ if not _IDENTIFIER.fullmatch(value):
15
+ raise ValueError(f"invalid SQLite identifier: {value!r}")
16
+ return f'"{value}"'
17
+
18
+
19
+ def read_local_sqlite_rows(
20
+ database: str | Path,
21
+ *,
22
+ table: str,
23
+ columns: Sequence[str],
24
+ order_by: Sequence[str] = (),
25
+ ) -> list[tuple[SQLiteValue, ...]]:
26
+ """Read a projected table from a local SQLite artifact in read-only mode."""
27
+ if not columns:
28
+ raise ValueError("columns must contain at least one identifier")
29
+
30
+ projection = ", ".join(_identifier(column) for column in columns)
31
+ statement = f"SELECT {projection} FROM {_identifier(table)}"
32
+ if order_by:
33
+ statement += " ORDER BY " + ", ".join(_identifier(column) for column in order_by)
34
+
35
+ path = Path(database).expanduser().resolve()
36
+ connection = sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)
37
+ try:
38
+ return [tuple(row) for row in connection.execute(statement)]
39
+ finally:
40
+ connection.close()
@@ -0,0 +1,23 @@
1
+ # Conflict-write operations
2
+
3
+ `conflict_writes.py` provides typed insert-ignore and conflict-update primitives
4
+ for append-only rows, partial unique indexes, and atomic counters.
5
+
6
+ ## Event-loop safety
7
+
8
+ - **Bulk row coercion never runs on the event loop.** `bulk_insert_ignore` and
9
+ `bulk_upsert_increment` pass model construction, defaults, and field DB-value
10
+ conversion through `_prep_bulk_rows`, which uses `asyncio.to_thread`.
11
+ - **Single-row writes stay inline.** `insert_ignore` and
12
+ `upsert_with_conflict` avoid thread-dispatch overhead for one row.
13
+ - **Statement chunking still enforces the bind-parameter ceiling.** Offloading
14
+ row preparation does not change `_MAX_BIND_PARAMS` or caller-connection
15
+ transaction semantics.
16
+
17
+ Verified against `conflict_writes.py` and
18
+ `tests/level1/test_conflict_writes.py` on 2026-08-17.
19
+
20
+ ## Change Log
21
+
22
+ - 2026-08-17 — Moved bulk model materialization off the event loop after a
23
+ scheduled search-performance import froze unrelated requests for 2.8 seconds.
@@ -33,6 +33,7 @@ primitive instead of hand-rolled f-strings.
33
33
 
34
34
  from __future__ import annotations
35
35
 
36
+ import asyncio
36
37
  import re
37
38
  from typing import TYPE_CHECKING, Any
38
39
 
@@ -115,6 +116,12 @@ def _prep_rows(model_cls: type[Model], objects_data: list[dict[str, Any]]) -> li
115
116
  return data_list
116
117
 
117
118
 
119
+ async def _prep_bulk_rows(
120
+ model_cls: type[Model], objects_data: list[dict[str, Any]]
121
+ ) -> list[dict[str, Any]]:
122
+ return await asyncio.to_thread(_prep_rows, model_cls, objects_data)
123
+
124
+
118
125
  async def _run(
119
126
  model_cls: type[Model],
120
127
  sql: str,
@@ -171,7 +178,7 @@ async def bulk_insert_ignore(
171
178
  if isinstance(returning, str) and not _IDENT_RE.match(returning):
172
179
  raise ValueError(f"returning column must be a plain identifier; got {returning!r}")
173
180
  returning_clause = "*" if returning is True else returning if returning else "1"
174
- data_list = _prep_rows(model_cls, objects_data)
181
+ data_list = await _prep_bulk_rows(model_cls, objects_data)
175
182
  target_clause = _render_conflict_target(on_conflict)
176
183
  table = model_cls._meta.qualified_table_name
177
184
  col_count = len({k for row in data_list for k in row})
@@ -362,7 +369,7 @@ async def bulk_upsert_increment(
362
369
  raise ValueError(f"set_fields column must be a plain identifier; got {col!r}")
363
370
  if col in increments:
364
371
  raise ValueError(f"{col!r} is in both increments and set_fields — pick one")
365
- data_list = _prep_rows(model_cls, objects_data)
372
+ data_list = await _prep_bulk_rows(model_cls, objects_data)
366
373
  target_clause = _render_conflict_target(on_conflict)
367
374
  table = model_cls._meta.qualified_table_name
368
375
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "matrx-orm"
3
- version = "3.1.77"
3
+ version = "3.1.78"
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" }
@@ -41,7 +41,7 @@ dependencies = [
41
41
  "gitpython>=3.1.40",
42
42
  "psycopg[binary]>=3.2.5",
43
43
  "psycopg-pool>=3.2.5",
44
- "matrx-utils>=2.0.18",
44
+ "matrx-utils>=2.0.19",
45
45
  "pyyaml>=6.0.3",
46
46
  "python-dotenv>=1.0",
47
47
  "cryptography>=43.0",
@@ -4,13 +4,16 @@ from types import SimpleNamespace
4
4
  from unittest.mock import AsyncMock, MagicMock, patch
5
5
 
6
6
  import pytest
7
+ from fastapi import HTTPException
7
8
  from matrx_orm.admin.router import (
8
9
  COMPOSITE_ROW_ID_FIELD,
9
10
  _admin_column_descriptors,
10
11
  _db_row_to_descriptor,
11
12
  _decode_row_id,
12
13
  _model_by_table,
14
+ _require_model,
13
15
  _serialize,
16
+ list_tables,
14
17
  )
15
18
 
16
19
 
@@ -35,7 +38,7 @@ def test_admin_model_lookup_defaults_to_exact_public_key() -> None:
35
38
  return_value=sentinel,
36
39
  ) as lookup:
37
40
  assert _model_by_table("definition") is sentinel
38
- lookup.assert_called_once_with("public", "definition")
41
+ lookup.assert_called_once_with("public", "definition", database=None)
39
42
 
40
43
 
41
44
  def test_admin_model_lookup_uses_requested_schema() -> None:
@@ -44,7 +47,66 @@ def test_admin_model_lookup_uses_requested_schema() -> None:
44
47
  return_value=None,
45
48
  ) as lookup:
46
49
  assert _model_by_table("definition", "agent") is None
47
- lookup.assert_called_once_with("agent", "definition")
50
+ lookup.assert_called_once_with("agent", "definition", database=None)
51
+
52
+
53
+ def test_admin_model_lookup_uses_requested_database() -> None:
54
+ sentinel = object()
55
+ with patch(
56
+ "matrx_orm.admin.router.model_registry.get_model_by_table",
57
+ return_value=sentinel,
58
+ ) as lookup:
59
+ assert _model_by_table("users", "auth", "main") is sentinel
60
+ lookup.assert_called_once_with("auth", "users", database="main")
61
+
62
+
63
+ def test_admin_model_lookup_returns_conflict_for_ambiguous_relation() -> None:
64
+ with (
65
+ patch(
66
+ "matrx_orm.admin.router.model_registry.get_model_by_table",
67
+ side_effect=ValueError("Table 'auth.users' is ambiguous across databases"),
68
+ ),
69
+ pytest.raises(HTTPException) as exc_info,
70
+ ):
71
+ _require_model("users", "auth")
72
+
73
+ assert exc_info.value.status_code == 409
74
+ assert "ambiguous" in str(exc_info.value.detail)
75
+
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_admin_table_inventory_exposes_database_identity() -> None:
79
+ model = SimpleNamespace(
80
+ __name__="Users",
81
+ _meta=SimpleNamespace(
82
+ table_name="users",
83
+ db_schema="auth",
84
+ fields={"id": object()},
85
+ ),
86
+ get_database_name=lambda: "supabase_automation_matrix",
87
+ )
88
+ with (
89
+ patch(
90
+ "matrx_orm.admin.router.model_registry.all_models",
91
+ return_value={("supabase_automation_matrix", "auth", "Users"): model},
92
+ ),
93
+ patch(
94
+ "matrx_orm.admin.router._fetch_db_column_counts",
95
+ new=AsyncMock(return_value={("auth", "users"): 24}),
96
+ ),
97
+ ):
98
+ tables = await list_tables()
99
+
100
+ assert tables == [
101
+ {
102
+ "database_name": "supabase_automation_matrix",
103
+ "table_name": "users",
104
+ "table_schema": "auth",
105
+ "name": "users",
106
+ "model_name": "Users",
107
+ "column_count": 24,
108
+ }
109
+ ]
48
110
 
49
111
 
50
112
  def test_composite_row_identity_round_trips() -> None:
@@ -13,6 +13,9 @@ caller-connection seam are all exercised without a registered database.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import asyncio
17
+ import threading
18
+ import time
16
19
  from typing import Any
17
20
 
18
21
  import pytest
@@ -27,6 +30,7 @@ from matrx_orm.operations.conflict_writes import (
27
30
  bulk_insert_ignore,
28
31
  bulk_upsert_increment,
29
32
  )
33
+ import matrx_orm.operations.conflict_writes as conflict_writes
30
34
 
31
35
 
32
36
  class _Meta:
@@ -143,6 +147,76 @@ async def test_insert_ignore_empty_is_noop_without_touching_connection() -> None
143
147
  assert conn.calls == []
144
148
 
145
149
 
150
+ @pytest.mark.asyncio
151
+ async def test_insert_ignore_prepares_bulk_rows_off_event_loop(
152
+ monkeypatch: pytest.MonkeyPatch,
153
+ ) -> None:
154
+ model = _make_model("t", {"id": TextField()})
155
+ conn = _CaptureConn()
156
+ loop_thread = threading.get_ident()
157
+ prep_thread: int | None = None
158
+ original_prep_rows = conflict_writes._prep_rows
159
+
160
+ def slow_prep_rows(
161
+ model_cls: type,
162
+ objects_data: list[dict[str, Any]],
163
+ ) -> list[dict[str, Any]]:
164
+ nonlocal prep_thread
165
+ prep_thread = threading.get_ident()
166
+ time.sleep(0.05)
167
+ return original_prep_rows(model_cls, objects_data)
168
+
169
+ monkeypatch.setattr(conflict_writes, "_prep_rows", slow_prep_rows)
170
+ write_task = asyncio.create_task(
171
+ bulk_insert_ignore(model, [{"id": "x"}], on_conflict=["id"], connection=conn)
172
+ )
173
+
174
+ await asyncio.sleep(0.01)
175
+
176
+ assert not write_task.done()
177
+ await write_task
178
+ assert prep_thread is not None
179
+ assert prep_thread != loop_thread
180
+
181
+
182
+ @pytest.mark.asyncio
183
+ async def test_increment_prepares_bulk_rows_off_event_loop(
184
+ monkeypatch: pytest.MonkeyPatch,
185
+ ) -> None:
186
+ model = _make_model("t", {"id": TextField(), "count": IntegerField()})
187
+ conn = _CaptureConn()
188
+ loop_thread = threading.get_ident()
189
+ prep_thread: int | None = None
190
+ original_prep_rows = conflict_writes._prep_rows
191
+
192
+ def slow_prep_rows(
193
+ model_cls: type,
194
+ objects_data: list[dict[str, Any]],
195
+ ) -> list[dict[str, Any]]:
196
+ nonlocal prep_thread
197
+ prep_thread = threading.get_ident()
198
+ time.sleep(0.05)
199
+ return original_prep_rows(model_cls, objects_data)
200
+
201
+ monkeypatch.setattr(conflict_writes, "_prep_rows", slow_prep_rows)
202
+ write_task = asyncio.create_task(
203
+ bulk_upsert_increment(
204
+ model,
205
+ [{"id": "x", "count": 1}],
206
+ on_conflict=["id"],
207
+ increments={"count": 1},
208
+ connection=conn,
209
+ )
210
+ )
211
+
212
+ await asyncio.sleep(0.01)
213
+
214
+ assert not write_task.done()
215
+ await write_task
216
+ assert prep_thread is not None
217
+ assert prep_thread != loop_thread
218
+
219
+
146
220
  @pytest.mark.asyncio
147
221
  async def test_insert_ignore_returning_column_gives_landed_ids_only() -> None:
148
222
  """The matrx-rag derived-chunk write shape: ON CONFLICT DO NOTHING, then
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from matrx_orm import read_local_sqlite_rows
9
+
10
+
11
+ def _fixture_database(path: Path) -> None:
12
+ connection = sqlite3.connect(path)
13
+ try:
14
+ connection.execute("CREATE TABLE events (id INTEGER PRIMARY KEY, label TEXT, payload BLOB)")
15
+ connection.executemany(
16
+ "INSERT INTO events (id, label, payload) VALUES (?, ?, ?)",
17
+ [(2, "later", b"two"), (1, "first", b"one")],
18
+ )
19
+ connection.commit()
20
+ finally:
21
+ connection.close()
22
+
23
+
24
+ def test_read_local_sqlite_rows_projects_and_orders(tmp_path: Path) -> None:
25
+ database = tmp_path / "artifact.db"
26
+ _fixture_database(database)
27
+
28
+ rows = read_local_sqlite_rows(
29
+ database,
30
+ table="events",
31
+ columns=("label", "payload"),
32
+ order_by=("id",),
33
+ )
34
+
35
+ assert rows == [("first", b"one"), ("later", b"two")]
36
+
37
+
38
+ def test_read_local_sqlite_rows_rejects_statement_shaped_table_name(tmp_path: Path) -> None:
39
+ database = tmp_path / "artifact.db"
40
+ _fixture_database(database)
41
+
42
+ with pytest.raises(ValueError, match="invalid SQLite identifier"):
43
+ read_local_sqlite_rows(
44
+ database,
45
+ table='events"; DELETE FROM events; --',
46
+ columns=("id",),
47
+ )
48
+
49
+
50
+ @pytest.mark.parametrize("columns", [(), ("label DESC",)])
51
+ def test_read_local_sqlite_rows_rejects_invalid_projection(
52
+ tmp_path: Path, columns: tuple[str, ...]
53
+ ) -> None:
54
+ database = tmp_path / "artifact.db"
55
+ _fixture_database(database)
56
+
57
+ with pytest.raises(ValueError):
58
+ read_local_sqlite_rows(database, table="events", columns=columns)
File without changes
File without changes