matrx-orm 3.1.134__tar.gz → 3.1.136__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.
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/CLAUDE.md +1 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/PKG-INFO +2 -2
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/__init__.py +8 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/__init__.py +7 -31
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/async_postgresql.py +7 -1
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/base_adapter.py +3 -1
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/postgrest_client_adapter.py +1 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/supabase_adapter.py +7 -1
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/FEATURE.md +4 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/MODULE_README.md +1 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/async_db_manager.py +30 -1
- matrx_orm-3.1.136/matrx_orm/core/session_context.py +94 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/transaction.py +17 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/loader.py +9 -8
- matrx_orm-3.1.136/matrx_orm/pytest_plugin.py +200 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/builder.py +9 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/executor.py +8 -1
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/common.py +16 -5
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/package_wiring.py +10 -8
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/runner.py +13 -3
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/README.md +36 -3
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/fallback.py +148 -26
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/session.py +40 -2
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/pyproject.toml +5 -2
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_query_timeout.py +39 -1
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_flush_individual_fallback.py +50 -0
- matrx_orm-3.1.136/tests/session/test_record_error_bounded_insert.py +89 -0
- matrx_orm-3.1.136/tests/session/test_record_error_storm.py +67 -0
- matrx_orm-3.1.136/tests/session/test_spill_guard_plugin.py +231 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/.gitignore +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/AGENTS.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/MODEL_API.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/docs/migrations.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/FEATURE.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/admin/FEATURE.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/pooler_role.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/local_sqlite.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/FEATURE.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/catalog_select.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/platform_access.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/host_authorization.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/scripts/release.sh +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/conftest.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_adapter_registry.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_admin_response_rendering.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_associations_wiring.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_local_sqlite.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_persisted_count.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_read_connection_retry.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_subquery_nested_expressions.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_write_queue_deadline.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_enum_ordering.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_schema_analysis.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_array_arg_element_type.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_dynamic_select_unregistered.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_keyed_digest.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_secrets_minting_keys.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.134 → matrx_orm-3.1.136}/tests/test_sync_twins_guard.py +0 -0
|
@@ -52,6 +52,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
|
|
|
52
52
|
- **Schema builder** (`matrx_orm.schema_builder`): code generation for Python + TypeScript type definitions from the DB schema. Postgres **VIEWs** are emitted as **read-only `Model` classes** into the same schema models file (on by default; per-entry `output.include_views: false` opts a schema out, `output.exclude_views: [...]` skips individual views). View models get no manager/DTO/base-class/auto_config (views are not writable), synthesize `_primary_keys` from an `id` column (else the first column, for cache-key/`load_by_id` only), and emit plain typed fields with **no `ForeignKey`** (views carry no FK metadata — nothing cross-schema to resolve). Never hand-write a view model again.
|
|
53
53
|
- **Secrets battery** (`matrx_orm.secrets_battery`): the ONE canonical user-credential vault — `users.user_secrets` CRUD + the platform's single Fernet primitive, keyed by `CREDENTIALS_ENCRYPTION_KEY`. Any package: `configure_secrets(db_config_name=...)` once, then `await require_user_secret(user_id, key)`. No package ever calls another service to resolve a credential. Contract: [`matrx_orm/secrets_battery/FEATURE.md`](matrx_orm/secrets_battery/FEATURE.md).
|
|
54
54
|
- **Polymorphic associations** (`matrx_orm.associations`): `EntityRegistry` (token→Model) + `Associations` — a registry-driven, **access-gated** edge engine over a host's polymorphic edge table. Every `source→target` hop is org-scoped *and* each resolved endpoint is re-authorized against the ACTOR (fail-closed, screams on a genuine leak), so a hop can't widen access past what the actor may reach. 🚨 **The gate is `access_check` (wire `matrx_orm.platform_access_check` → `iam.has_access_for`), NEVER an org-column comparison** — a person belongs to several orgs on purpose, so org-equality denies her own data across her own workspaces (2026-08-30 incident) while granting any co-member of a private row. The edge's `org_id` is a TENANCY stamp, not a permission. Org-equality survives only for UNATTENDED writes (no actor) as an integrity guard, and **system vocabulary is exempt entirely** (existence still required): a model generated with `_rls_variant="system"` is platform-shared dictionary data (seo.keyword/topic) every tenant legitimately links to. Hardcodes no table names — the host wires its own edge + token-registry models.
|
|
55
|
+
- **The host's declared session context** (`matrx_orm.core.session_context`): `configure_session_context(provider)` once at startup, and every **top-level** `transaction()` — including the one `rls_session` opens, before it drops to the restricted role — emits whatever the provider returns as transaction-local GUCs. This is how an application-level declaration reaches Postgres at all: a GUC set outside a transaction has no meaning, so the BEGIN is the only place it can land. aidream wires it to its provenance declaration (`app.actor_tier` / `app.actor_system`, read by `platform.actor_tier()`, the history capture and the tier rule). A nested entry is a savepoint inside a transaction that already carries them and emits nothing — and a write made **outside** any transaction carries no declaration at all, which is why a single-row write that must record its author still opens one. No provider configured ⇒ no GUCs (standalone behaviour unchanged); a provider that raises fails the transaction loudly rather than committing rows with a false provenance.
|
|
55
56
|
- **The platform access policy** (`matrx_orm.platform_access`): `platform_access_check(actor, token, id, level)` → `iam.has_access_for`. The ONE answer to "may this actor touch this row?", shared with RLS and the file-access gate. Fails closed. Never re-derive it from `organization_id` or `created_by` — both are strict subsets that deny legitimate access.
|
|
56
57
|
|
|
57
58
|
Public API is defined explicitly in `matrx_orm/__init__.py` (100+ exports). Treat changes there as semver-breaking.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.136
|
|
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.
|
|
28
|
+
Requires-Dist: matrx-utils>=2.0.36
|
|
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
|
|
@@ -240,6 +240,11 @@ from .core.registry import (
|
|
|
240
240
|
)
|
|
241
241
|
from .core.relations import ManyToManyField
|
|
242
242
|
from .core.rls_session import rls_session
|
|
243
|
+
from .core.session_context import (
|
|
244
|
+
SessionContextProvider,
|
|
245
|
+
configure_session_context,
|
|
246
|
+
session_context_gucs,
|
|
247
|
+
)
|
|
243
248
|
from .core.signals import (
|
|
244
249
|
Signal,
|
|
245
250
|
post_create,
|
|
@@ -632,6 +637,9 @@ __all__ = [
|
|
|
632
637
|
"transaction",
|
|
633
638
|
"acquire_connection",
|
|
634
639
|
"set_local_guc",
|
|
640
|
+
"SessionContextProvider",
|
|
641
|
+
"configure_session_context",
|
|
642
|
+
"session_context_gucs",
|
|
635
643
|
"advisory_xact_lock",
|
|
636
644
|
"try_advisory_xact_lock",
|
|
637
645
|
"advisory_lock",
|
|
@@ -30,8 +30,6 @@ Register any ``BaseAdapter`` subclass before the first query::
|
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
32
|
import logging
|
|
33
|
-
import importlib.util
|
|
34
|
-
import sys
|
|
35
33
|
|
|
36
34
|
from matrx_orm.adapters.base_adapter import BaseAdapter
|
|
37
35
|
from matrx_orm.adapters.async_postgresql import AsyncPostgreSQLAdapter
|
|
@@ -143,35 +141,13 @@ class AdapterRegistry:
|
|
|
143
141
|
raise ValueError(f"Invalid dotted adapter path: {dotted_path!r}")
|
|
144
142
|
|
|
145
143
|
# A custom adapter is an extension seam, not a closed list of built-in
|
|
146
|
-
# modules.
|
|
147
|
-
# third-party adapter
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# find_spec() imports the PARENT package, and a parent __init__ that
|
|
154
|
-
# re-exports this child imports it as a side effect. Re-read the
|
|
155
|
-
# cache: executing the spec now would install a SECOND module object
|
|
156
|
-
# over the real one, so the adapter class taken from it would be a
|
|
157
|
-
# different class than the one everyone else imported.
|
|
158
|
-
module = sys.modules.get(module_path)
|
|
159
|
-
if module is None:
|
|
160
|
-
if spec is None or spec.loader is None:
|
|
161
|
-
raise ModuleNotFoundError(f"No importable adapter module named {module_path!r}")
|
|
162
|
-
module = importlib.util.module_from_spec(spec)
|
|
163
|
-
sys.modules[module_path] = module
|
|
164
|
-
try:
|
|
165
|
-
spec.loader.exec_module(module)
|
|
166
|
-
except Exception:
|
|
167
|
-
if sys.modules.get(module_path) is module:
|
|
168
|
-
sys.modules.pop(module_path, None)
|
|
169
|
-
raise
|
|
170
|
-
parent_path, _, child_name = module_path.rpartition(".")
|
|
171
|
-
if parent_path:
|
|
172
|
-
parent = sys.modules.get(parent_path)
|
|
173
|
-
if parent is not None:
|
|
174
|
-
setattr(parent, child_name, module)
|
|
144
|
+
# modules. It loads through THE declared-module seam
|
|
145
|
+
# (matrx_utils.module_loading): an unloaded third-party adapter stays
|
|
146
|
+
# supported, a provider SDK name is refused, and the mandate/provider
|
|
147
|
+
# scan sees a fact instead of an opaque computed import.
|
|
148
|
+
from matrx_utils.module_loading import load_declared_module
|
|
149
|
+
|
|
150
|
+
module = load_declared_module(module_path)
|
|
175
151
|
klass = getattr(module, class_name)
|
|
176
152
|
instance = klass(config_name)
|
|
177
153
|
if not isinstance(instance, BaseAdapter):
|
|
@@ -41,11 +41,17 @@ class AsyncPostgreSQLAdapter(BaseAdapter):
|
|
|
41
41
|
*args: Any,
|
|
42
42
|
timeout: float = 10.0,
|
|
43
43
|
statement_timeout: float | None = None,
|
|
44
|
+
verify_empty: bool = True,
|
|
44
45
|
) -> list[dict[str, Any]]:
|
|
45
46
|
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
46
47
|
|
|
47
48
|
return await AsyncDatabaseManager.execute_query(
|
|
48
|
-
self._config_name,
|
|
49
|
+
self._config_name,
|
|
50
|
+
sql,
|
|
51
|
+
*args,
|
|
52
|
+
timeout=timeout,
|
|
53
|
+
statement_timeout=statement_timeout,
|
|
54
|
+
verify_empty=verify_empty,
|
|
49
55
|
)
|
|
50
56
|
|
|
51
57
|
async def execute_write(
|
|
@@ -33,12 +33,14 @@ class BaseAdapter(ABC):
|
|
|
33
33
|
*args: Any,
|
|
34
34
|
timeout: float = 10.0,
|
|
35
35
|
statement_timeout: float | None = None,
|
|
36
|
+
verify_empty: bool = True,
|
|
36
37
|
) -> list[dict[str, Any]]:
|
|
37
38
|
"""Execute a read (SELECT) query and return rows as dicts.
|
|
38
39
|
|
|
39
40
|
``timeout`` is the connection-acquire timeout; ``statement_timeout``
|
|
40
41
|
(when set) overrides the pool's ``command_timeout`` for this statement
|
|
41
|
-
only (QueryBuilder.timeout()).
|
|
42
|
+
only (QueryBuilder.timeout()). ``verify_empty=False`` is reserved for
|
|
43
|
+
replay-safe polling reads whose cursor does not advance on an empty page.
|
|
42
44
|
"""
|
|
43
45
|
|
|
44
46
|
@abstractmethod
|
|
@@ -103,11 +103,17 @@ class SupabaseAdapter(BaseAdapter):
|
|
|
103
103
|
*args: Any,
|
|
104
104
|
timeout: float = 10.0,
|
|
105
105
|
statement_timeout: float | None = None,
|
|
106
|
+
verify_empty: bool = True,
|
|
106
107
|
) -> list[dict[str, Any]]:
|
|
107
108
|
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
108
109
|
|
|
109
110
|
return await AsyncDatabaseManager.execute_query(
|
|
110
|
-
self._config_name,
|
|
111
|
+
self._config_name,
|
|
112
|
+
sql,
|
|
113
|
+
*args,
|
|
114
|
+
timeout=timeout,
|
|
115
|
+
statement_timeout=statement_timeout,
|
|
116
|
+
verify_empty=verify_empty,
|
|
111
117
|
)
|
|
112
118
|
|
|
113
119
|
async def execute_write(
|
|
@@ -221,6 +221,10 @@ Verified against the live database: 2026-08-21.
|
|
|
221
221
|
interface errors.
|
|
222
222
|
- 2026-08-22: Retried read-only mid-query connection loss once on a fresh
|
|
223
223
|
checkout while preserving no-retry write semantics.
|
|
224
|
+
- 2026-09-11: Replay-safe polling reads may explicitly skip empty-result
|
|
225
|
+
reproof; their unchanged cursor makes an empty-to-nonempty race recover on
|
|
226
|
+
the next poll, avoiding the transaction-pool reproof convoy without relaxing
|
|
227
|
+
the default integrity guard for ordinary reads.
|
|
224
228
|
- 2026-08-22: Preserved the deepest Postgres SQLSTATE on `AppError` as a
|
|
225
229
|
lightweight classification (without retaining traceback frames), so an
|
|
226
230
|
idempotent transactional owner can recover from a database role transition
|
|
@@ -617,6 +617,7 @@ Filepath: src/matrx_orm/core/async_db_manager.py [python]
|
|
|
617
617
|
|
|
618
618
|
class AsyncDatabaseManager:
|
|
619
619
|
def __new__(cls)
|
|
620
|
+
def pool_snapshot(cls, config_name: str) -> dict[str, int] | None # {size, idle, max, min} of an EXISTING pool; never creates, never awaits — for error paths
|
|
620
621
|
async def get_pool(cls, config_name)
|
|
621
622
|
async def get_connection(cls, config_name, timeout = 10.0)
|
|
622
623
|
async def execute_query(cls, config_name: str, query: str, *args: Any, timeout: float = 10.0) -> list[dict[str, Any]]
|
|
@@ -427,6 +427,29 @@ class AsyncDatabaseManager:
|
|
|
427
427
|
cls._instance = super().__new__(cls)
|
|
428
428
|
return cls._instance
|
|
429
429
|
|
|
430
|
+
@classmethod
|
|
431
|
+
def pool_snapshot(cls, config_name: str) -> dict[str, int] | None:
|
|
432
|
+
"""Read-only sizing of an EXISTING pool: ``{"size", "idle", "max", "min"}``.
|
|
433
|
+
|
|
434
|
+
Never creates a pool (unlike :meth:`get_pool`) and never awaits — safe
|
|
435
|
+
to call from an error path while a deadline is already blown. ``None``
|
|
436
|
+
when no pool exists for the (alias-resolved) name, so a caller can say
|
|
437
|
+
"no pool" instead of inventing zeros.
|
|
438
|
+
"""
|
|
439
|
+
name = resolve_database_name(config_name)
|
|
440
|
+
pool = cls._pools.get(name)
|
|
441
|
+
if pool is None:
|
|
442
|
+
return None
|
|
443
|
+
try:
|
|
444
|
+
return {
|
|
445
|
+
"size": int(pool.get_size()),
|
|
446
|
+
"idle": int(pool.get_idle_size()),
|
|
447
|
+
"max": int(pool.get_max_size()),
|
|
448
|
+
"min": int(pool.get_min_size()),
|
|
449
|
+
}
|
|
450
|
+
except Exception: # noqa: BLE001 — a half-closed pool answers nothing, not a lie
|
|
451
|
+
return None
|
|
452
|
+
|
|
430
453
|
@classmethod
|
|
431
454
|
async def get_pool(cls, config_name):
|
|
432
455
|
"""Get or create a connection pool for the specified database with error handling.
|
|
@@ -1153,6 +1176,7 @@ class AsyncDatabaseManager:
|
|
|
1153
1176
|
*args: Any,
|
|
1154
1177
|
timeout: float = 10.0,
|
|
1155
1178
|
statement_timeout: float | None = None,
|
|
1179
|
+
verify_empty: bool = True,
|
|
1156
1180
|
) -> list[dict[str, Any]]:
|
|
1157
1181
|
"""Execute a query and return results with error handling.
|
|
1158
1182
|
|
|
@@ -1274,7 +1298,12 @@ class AsyncDatabaseManager:
|
|
|
1274
1298
|
read_attempt + 1,
|
|
1275
1299
|
query[:120],
|
|
1276
1300
|
)
|
|
1277
|
-
if
|
|
1301
|
+
if (
|
|
1302
|
+
not results
|
|
1303
|
+
and verify_empty
|
|
1304
|
+
and read_only
|
|
1305
|
+
and cls._verifies_empty_reads(config_name)
|
|
1306
|
+
):
|
|
1278
1307
|
# Layer 2: an empty result from a pooled connection is
|
|
1279
1308
|
# unproven until a session that can state its own role
|
|
1280
1309
|
# says so. A borrowed `authenticated` role returns ZERO
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""The host's DECLARED session context — GUCs every transaction opens with.
|
|
2
|
+
|
|
3
|
+
Some facts about a block of work are known by the application and readable only
|
|
4
|
+
by the database: *which tier of actor* is authoring these writes, for instance.
|
|
5
|
+
Postgres learns them through transaction-local GUCs (``set_config(..., true)``),
|
|
6
|
+
and a GUC set outside a transaction has no meaning — so the ONE place that can
|
|
7
|
+
reliably apply them is the moment this package opens a transaction.
|
|
8
|
+
|
|
9
|
+
The host declares a provider once at startup::
|
|
10
|
+
|
|
11
|
+
from matrx_orm import configure_session_context
|
|
12
|
+
configure_session_context(lambda: {"app.actor_tier": "ai", "app.actor_system": "x"})
|
|
13
|
+
|
|
14
|
+
and from then on every TOP-LEVEL ``transaction()`` (which includes the one
|
|
15
|
+
``rls_session`` opens, before it drops to the restricted role) emits whatever
|
|
16
|
+
the provider returns at that instant. A savepoint (nested transaction) emits
|
|
17
|
+
nothing: it is already inside the transaction the GUCs were set on.
|
|
18
|
+
|
|
19
|
+
Why a provider and not a value: what is true changes per task — the provider is
|
|
20
|
+
normally a ContextVar read, so a block that declares "an AI is writing this"
|
|
21
|
+
affects exactly the work inside it and nothing running beside it.
|
|
22
|
+
|
|
23
|
+
The package NEVER imports the host: no provider configured ⇒ no GUCs, which is
|
|
24
|
+
exactly how matrx-orm behaves standalone. A provider that raises is a host bug
|
|
25
|
+
and it surfaces — the transaction fails loudly rather than silently committing
|
|
26
|
+
writes whose provenance is a lie.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
from collections.abc import Callable, Mapping
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
# Same shape matrx_orm.core.transaction.set_local_guc accepts.
|
|
36
|
+
_GUC_NAME_RE = re.compile(r"^[a-z_][a-z0-9_.]*$")
|
|
37
|
+
|
|
38
|
+
SessionContextProvider = Callable[[], Mapping[str, str] | None]
|
|
39
|
+
|
|
40
|
+
_provider: SessionContextProvider | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def configure_session_context(provider: SessionContextProvider | None) -> None:
|
|
44
|
+
"""Bind (or with ``None``, unbind) the host's session-context provider.
|
|
45
|
+
|
|
46
|
+
Call once at startup, alongside the other package ``configure_*`` calls.
|
|
47
|
+
``provider`` is invoked on every top-level transaction open and must return
|
|
48
|
+
a mapping of GUC name → value (or ``None`` / ``{}`` for "nothing to say").
|
|
49
|
+
"""
|
|
50
|
+
global _provider
|
|
51
|
+
if provider is not None and not callable(provider):
|
|
52
|
+
raise TypeError("configure_session_context(provider) requires a callable or None")
|
|
53
|
+
_provider = provider
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def session_context_gucs() -> dict[str, str]:
|
|
57
|
+
"""The GUCs to set on a transaction being opened right now.
|
|
58
|
+
|
|
59
|
+
Empty when no provider is configured. Raises when a configured provider
|
|
60
|
+
returns something unusable — a malformed declaration is a host defect, and
|
|
61
|
+
writing rows with a wrong (or absent) provenance stamp is worse than a loud
|
|
62
|
+
failure at BEGIN.
|
|
63
|
+
"""
|
|
64
|
+
provider = _provider
|
|
65
|
+
if provider is None:
|
|
66
|
+
return {}
|
|
67
|
+
declared = provider()
|
|
68
|
+
if not declared:
|
|
69
|
+
return {}
|
|
70
|
+
if not isinstance(declared, Mapping):
|
|
71
|
+
raise TypeError(
|
|
72
|
+
"the session-context provider must return a mapping of GUC name → value; "
|
|
73
|
+
f"got {type(declared).__name__}"
|
|
74
|
+
)
|
|
75
|
+
out: dict[str, str] = {}
|
|
76
|
+
for name, value in declared.items():
|
|
77
|
+
if not isinstance(name, str) or not _GUC_NAME_RE.match(name):
|
|
78
|
+
raise ValueError(f"invalid session-context GUC name: {name!r}")
|
|
79
|
+
out[name] = "" if value is None else str(value)
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def apply_session_context(connection: Any) -> None:
|
|
84
|
+
"""Emit the declared GUCs on ``connection`` (already inside a BEGIN)."""
|
|
85
|
+
for name, value in session_context_gucs().items():
|
|
86
|
+
await connection.execute("SELECT set_config($1, $2, true)", name, value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = [
|
|
90
|
+
"SessionContextProvider",
|
|
91
|
+
"apply_session_context",
|
|
92
|
+
"configure_session_context",
|
|
93
|
+
"session_context_gucs",
|
|
94
|
+
]
|
|
@@ -249,6 +249,23 @@ class TransactionContext:
|
|
|
249
249
|
) from e
|
|
250
250
|
self._conn_token = _active_connection.set(conn)
|
|
251
251
|
self._depth_token = _savepoint_depth.set(0)
|
|
252
|
+
# THE HOST'S DECLARED SESSION CONTEXT (provenance and friends). A
|
|
253
|
+
# transaction-local GUC only has meaning inside a transaction, so
|
|
254
|
+
# this — the moment one opens — is the only place a declaration made
|
|
255
|
+
# in application code can reach Postgres. Set while still the
|
|
256
|
+
# privileged role: `rls_session` opens through here and drops to the
|
|
257
|
+
# restricted role afterwards. Nested entries are savepoints inside a
|
|
258
|
+
# transaction that already carries them, so they emit nothing.
|
|
259
|
+
from matrx_orm.core.session_context import apply_session_context
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
await apply_session_context(conn)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
await self.__aexit__(type(e), e, e.__traceback__)
|
|
265
|
+
raise DatabaseError(
|
|
266
|
+
message=f"Failed to apply the declared session context: {e}",
|
|
267
|
+
details={"database": self.database},
|
|
268
|
+
) from e
|
|
252
269
|
return self
|
|
253
270
|
|
|
254
271
|
async def __aexit__(
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import importlib.util
|
|
6
5
|
import re
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from pathlib import Path
|
|
@@ -124,15 +123,17 @@ class MigrationLoader:
|
|
|
124
123
|
|
|
125
124
|
def _load_module(self, filepath: Path, name: str) -> Any:
|
|
126
125
|
"""Dynamically import a migration file as a module."""
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
from matrx_utils.module_loading import ProviderModuleRefused, load_module_from_path
|
|
127
|
+
|
|
128
|
+
# Only "no such file" / "refused" become MigrationError; an error the
|
|
129
|
+
# migration raises while executing propagates raw, exactly as before.
|
|
130
|
+
try:
|
|
131
|
+
return load_module_from_path(filepath, module_name=f"matrx_migrations.{name}")
|
|
132
|
+
except (FileNotFoundError, ProviderModuleRefused) as exc:
|
|
129
133
|
raise MigrationError(
|
|
130
134
|
migration=name,
|
|
131
|
-
original_error=f"Could not load migration file: {filepath}",
|
|
132
|
-
)
|
|
133
|
-
module = importlib.util.module_from_spec(spec)
|
|
134
|
-
spec.loader.exec_module(module)
|
|
135
|
-
return module
|
|
135
|
+
original_error=f"Could not load migration file: {filepath} ({exc})",
|
|
136
|
+
) from exc
|
|
136
137
|
|
|
137
138
|
def _validate_graph(self) -> None:
|
|
138
139
|
"""Ensure all declared dependencies exist."""
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Session-wide guard: a test that reaches the durable-write spill lane.
|
|
2
|
+
|
|
3
|
+
Registered as a ``pytest11`` entry point so pytest auto-loads it in every suite
|
|
4
|
+
**regardless of rootdir** — same mechanism, same reason, as
|
|
5
|
+
``matrx_connect.pytest_plugin`` and ``matrx_utils.pytest_plugin``.
|
|
6
|
+
|
|
7
|
+
WHY IT EXISTS. The write-durability chain is: Session flush → on failure
|
|
8
|
+
``record_failures`` → on failure ``_spill_ops_to_disk``. The spill directory is
|
|
9
|
+
resolved from ``MATRX_SPILL_DIR`` → a packaged default → the OS temp dir, and
|
|
10
|
+
that last rung is SHARED by every process on the machine — including the local
|
|
11
|
+
API server, whose auto-replay loop drains it every minute back into
|
|
12
|
+
``ops.system_write_failure`` and then re-applies the rows to the real tables.
|
|
13
|
+
|
|
14
|
+
Measured 2026-09-11: a matrx-ai unit test drove the orchestrator with a
|
|
15
|
+
fabricated user and conversation. Its collaborators were doubled, but the
|
|
16
|
+
coordinator queue helpers were not, and another suite in the same process had
|
|
17
|
+
already registered REAL database credentials and left a request lane open. The
|
|
18
|
+
flush hit production Postgres, the ``_stamp_org_default`` trigger refused the
|
|
19
|
+
row (the user does not exist), the failure capture itself failed (the same
|
|
20
|
+
user is a foreign key on the capture table), the rows were spilled to
|
|
21
|
+
``$TMPDIR/matrx-write-spill`` — and the developer's local server drained them
|
|
22
|
+
into the live failure table and tried to replay test fixtures against
|
|
23
|
+
production forty times, landing a ``system_error`` row per attempt.
|
|
24
|
+
|
|
25
|
+
THE CONTRACT. No automated test may spill. Under pytest this plugin:
|
|
26
|
+
|
|
27
|
+
1. points ``MATRX_SPILL_DIR`` at a fresh, session-private temp dir BEFORE any
|
|
28
|
+
test runs, so a spill can never reach a directory another process drains;
|
|
29
|
+
2. FAILS the session if anything was spilled there, naming every file — a
|
|
30
|
+
spill under pytest is proof that a test wrote through to a real database
|
|
31
|
+
and could not even record its failure. Fix the test's isolation (double the
|
|
32
|
+
write seam, or install ``aidream.testing.offline`` placeholders); never
|
|
33
|
+
delete the evidence to make the run green;
|
|
34
|
+
3. re-confines the cascade after EVERY test: a test that assigns
|
|
35
|
+
``MATRX_SPILL_DIR`` or ``fallback._spill_dir_cache`` directly (no
|
|
36
|
+
monkeypatch revert) would otherwise leave every later spill landing in its
|
|
37
|
+
directory, unseen. Such a test is named in the summary and its directory is
|
|
38
|
+
swept too (hole found by the adversarial review of the first version).
|
|
39
|
+
|
|
40
|
+
Suites that exercise the spill lane on purpose (``matrx-orm``'s
|
|
41
|
+
``test_disk_spill_fallback``) point the cascade at their own ``tmp_path`` via
|
|
42
|
+
``monkeypatch`` and reset the cache; those files never land in the session dir
|
|
43
|
+
and the revert happens before this plugin looks.
|
|
44
|
+
|
|
45
|
+
Opt out for one run with ``-p no:matrx_write_spill_guard``.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
import os
|
|
52
|
+
import tempfile
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
import pytest
|
|
56
|
+
|
|
57
|
+
_SESSION_SPILL_DIR: Path | None = None
|
|
58
|
+
_PREVIOUS_ENV: str | None = None
|
|
59
|
+
# Tests that repointed the spill cascade (env var or the module cache) and left
|
|
60
|
+
# it repointed. Each is re-confined at its teardown; its directory is still
|
|
61
|
+
# swept for spills at session end, so hijacking the cascade cannot hide one.
|
|
62
|
+
_HIJACKS: dict[str, Path] = {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def session_spill_dir() -> Path | None:
|
|
66
|
+
"""The directory this session's spills are confined to (None if not configured)."""
|
|
67
|
+
return _SESSION_SPILL_DIR
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def spilled_files() -> list[Path]:
|
|
71
|
+
"""Every op spilled to disk during this session, oldest first — in the
|
|
72
|
+
session dir and in any directory a test repointed the cascade to."""
|
|
73
|
+
if _SESSION_SPILL_DIR is None:
|
|
74
|
+
return []
|
|
75
|
+
seen: set[Path] = set()
|
|
76
|
+
files: list[Path] = []
|
|
77
|
+
for directory in (_SESSION_SPILL_DIR, *_HIJACKS.values()):
|
|
78
|
+
if directory in seen or not directory.exists():
|
|
79
|
+
continue
|
|
80
|
+
seen.add(directory)
|
|
81
|
+
files.extend(directory.glob("*.json"))
|
|
82
|
+
return sorted(files, key=lambda f: (f.stat().st_mtime, f.name))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _describe(path: Path) -> str:
|
|
86
|
+
try:
|
|
87
|
+
row = json.loads(path.read_text())
|
|
88
|
+
except Exception: # noqa: BLE001 — a half-written file is still evidence
|
|
89
|
+
return f"{path.name}: (unreadable)"
|
|
90
|
+
return (
|
|
91
|
+
f"{path.name}: {row.get('table_target')} {row.get('op_type')} "
|
|
92
|
+
f"request_id={row.get('request_id')} user_id={row.get('user_id')} "
|
|
93
|
+
f"error={str(row.get('error_text') or '')[:160]!r}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _reconfine(nodeid: str) -> None:
|
|
98
|
+
"""After a test: if it left the cascade pointing elsewhere, record where and
|
|
99
|
+
point it back at the session dir. A test that repoints via ``monkeypatch``
|
|
100
|
+
(auto-reverted before this runs) is never recorded; one that assigns
|
|
101
|
+
``os.environ`` or the module cache directly is."""
|
|
102
|
+
if _SESSION_SPILL_DIR is None:
|
|
103
|
+
return
|
|
104
|
+
try:
|
|
105
|
+
from matrx_orm.session import fallback
|
|
106
|
+
except Exception: # noqa: BLE001 — the ORM is not imported in every suite
|
|
107
|
+
return
|
|
108
|
+
cache: Path | None = fallback._spill_dir_cache
|
|
109
|
+
env = os.environ.get("MATRX_SPILL_DIR")
|
|
110
|
+
hijacked: Path | None = None
|
|
111
|
+
if cache is not None and cache != _SESSION_SPILL_DIR:
|
|
112
|
+
hijacked = cache
|
|
113
|
+
elif env != str(_SESSION_SPILL_DIR):
|
|
114
|
+
hijacked = Path(env) if env else None
|
|
115
|
+
if hijacked is not None:
|
|
116
|
+
_HIJACKS.setdefault(nodeid, hijacked)
|
|
117
|
+
if hijacked is not None or env != str(_SESSION_SPILL_DIR):
|
|
118
|
+
fallback._spill_dir_cache = None
|
|
119
|
+
os.environ["MATRX_SPILL_DIR"] = str(_SESSION_SPILL_DIR)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def pytest_configure(config: pytest.Config) -> None: # noqa: ARG001 — hook signature
|
|
123
|
+
global _SESSION_SPILL_DIR, _PREVIOUS_ENV
|
|
124
|
+
_PREVIOUS_ENV = os.environ.get("MATRX_SPILL_DIR")
|
|
125
|
+
_SESSION_SPILL_DIR = Path(tempfile.mkdtemp(prefix="matrx-test-spill-"))
|
|
126
|
+
os.environ["MATRX_SPILL_DIR"] = str(_SESSION_SPILL_DIR)
|
|
127
|
+
# The cascade caches its first resolution for the life of the process. If
|
|
128
|
+
# something already resolved it (a conftest that imported the ORM and
|
|
129
|
+
# spilled at import time would be its own scandal), drop the cache so the
|
|
130
|
+
# session dir wins from here on.
|
|
131
|
+
try:
|
|
132
|
+
from matrx_orm.session import fallback
|
|
133
|
+
|
|
134
|
+
fallback._spill_dir_cache = None
|
|
135
|
+
except Exception: # noqa: BLE001 — never block a session over the guard's own import
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
|
140
|
+
def pytest_runtest_teardown(item: pytest.Item):
|
|
141
|
+
# Runs after every fixture finalizer (monkeypatch reverts included), so what
|
|
142
|
+
# is left pointing elsewhere is what the test itself leaked.
|
|
143
|
+
yield
|
|
144
|
+
_reconfine(item.nodeid)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: # noqa: ARG001
|
|
148
|
+
files = spilled_files()
|
|
149
|
+
if not files:
|
|
150
|
+
return
|
|
151
|
+
# A spilled op means a real database write was attempted AND failed AND the
|
|
152
|
+
# failure could not even be recorded. That is never what a test meant to do.
|
|
153
|
+
if session.exitstatus in (pytest.ExitCode.OK, pytest.ExitCode.NO_TESTS_COLLECTED):
|
|
154
|
+
session.exitstatus = pytest.ExitCode.TESTS_FAILED
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def pytest_terminal_summary(terminalreporter, exitstatus: int, config: pytest.Config) -> None: # noqa: ARG001
|
|
158
|
+
files = spilled_files()
|
|
159
|
+
if not files:
|
|
160
|
+
return
|
|
161
|
+
lines = [
|
|
162
|
+
f"{len(files)} durable-write op(s) were SPILLED TO DISK during this test run.",
|
|
163
|
+
"A test wrote through to a real database, the write was refused, and the",
|
|
164
|
+
"failure capture itself failed — so the fallback chain parked the rows on",
|
|
165
|
+
"disk. Under pytest that is a test-isolation defect, not a persistence event:",
|
|
166
|
+
"double the coordinator/queue seam the test reaches, or install",
|
|
167
|
+
"aidream.testing.offline placeholders in the suite's conftest.",
|
|
168
|
+
f"Evidence (left in place, in {_SESSION_SPILL_DIR}):",
|
|
169
|
+
]
|
|
170
|
+
lines.extend(f" - {_describe(path)}" for path in files[:20])
|
|
171
|
+
if len(files) > 20:
|
|
172
|
+
lines.append(f" ... and {len(files) - 20} more")
|
|
173
|
+
if _HIJACKS:
|
|
174
|
+
lines.append(
|
|
175
|
+
"These tests repointed the spill cascade (MATRX_SPILL_DIR or the module "
|
|
176
|
+
"cache) without reverting it; each was re-confined at its teardown and "
|
|
177
|
+
"its directory swept — use monkeypatch so the change reverts itself:"
|
|
178
|
+
)
|
|
179
|
+
lines.extend(f" - {nodeid} -> {path}" for nodeid, path in _HIJACKS.items())
|
|
180
|
+
lines.append("The session is marked FAILED. Opt out once with -p no:matrx_write_spill_guard.")
|
|
181
|
+
terminalreporter.write_sep("=", "matrx write-spill guard", red=True, bold=True)
|
|
182
|
+
for line in lines:
|
|
183
|
+
terminalreporter.write_line(line, red=True)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def pytest_unconfigure(config: pytest.Config) -> None: # noqa: ARG001
|
|
187
|
+
global _SESSION_SPILL_DIR, _PREVIOUS_ENV
|
|
188
|
+
if _PREVIOUS_ENV is None:
|
|
189
|
+
os.environ.pop("MATRX_SPILL_DIR", None)
|
|
190
|
+
else:
|
|
191
|
+
os.environ["MATRX_SPILL_DIR"] = _PREVIOUS_ENV
|
|
192
|
+
# Remove ONLY an empty session dir; spilled evidence is left for the human.
|
|
193
|
+
if _SESSION_SPILL_DIR is not None and not spilled_files():
|
|
194
|
+
try:
|
|
195
|
+
_SESSION_SPILL_DIR.rmdir()
|
|
196
|
+
except OSError:
|
|
197
|
+
pass
|
|
198
|
+
_SESSION_SPILL_DIR = None
|
|
199
|
+
_PREVIOUS_ENV = None
|
|
200
|
+
_HIJACKS.clear()
|
|
@@ -213,6 +213,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
213
213
|
vector_null_guard: bool # auto-add IS NOT NULL guard for vector column
|
|
214
214
|
from_subquery_builder: QueryBuilder[Any] | None
|
|
215
215
|
timeout_val: float | None # timeout() — per-query statement timeout override (seconds)
|
|
216
|
+
verify_empty_val: bool
|
|
216
217
|
|
|
217
218
|
def __init__(self, model_cls: type[ModelT], database: str | None = None) -> None:
|
|
218
219
|
self.model = model_cls
|
|
@@ -239,6 +240,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
239
240
|
self.from_subquery_builder = None
|
|
240
241
|
self.from_alias_name: str | None = None
|
|
241
242
|
self.timeout_val: float | None = None
|
|
243
|
+
self.verify_empty_val = True
|
|
242
244
|
|
|
243
245
|
def _set_database(self, model_cls: type[ModelT]) -> str:
|
|
244
246
|
if hasattr(model_cls, "_database") and model_cls._database:
|
|
@@ -566,6 +568,12 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
566
568
|
new.timeout_val = seconds
|
|
567
569
|
return new
|
|
568
570
|
|
|
571
|
+
def replay_safe_empty(self) -> QueryBuilder[ModelT]:
|
|
572
|
+
"""Skip empty-read reproof for a replay-safe poll whose cursor does not advance on empty."""
|
|
573
|
+
new = self._clone()
|
|
574
|
+
new.verify_empty_val = False
|
|
575
|
+
return new
|
|
576
|
+
|
|
569
577
|
def from_alias(self, alias: str) -> QueryBuilder[ModelT]:
|
|
570
578
|
"""Alias the base (FROM) table — renders ``FROM <table> AS <alias>``.
|
|
571
579
|
|
|
@@ -737,6 +745,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
737
745
|
"from_subquery": self.from_subquery_builder,
|
|
738
746
|
"from_alias": self.from_alias_name,
|
|
739
747
|
"timeout": self.timeout_val,
|
|
748
|
+
"verify_empty": self.verify_empty_val,
|
|
740
749
|
}
|
|
741
750
|
return query
|
|
742
751
|
|
|
@@ -1028,10 +1028,17 @@ class QueryExecutor:
|
|
|
1028
1028
|
# Passed only when set so adapters that don't accept the kwarg
|
|
1029
1029
|
# keep working for un-overridden queries.
|
|
1030
1030
|
statement_timeout = self._full_query_dict.get("timeout")
|
|
1031
|
-
|
|
1031
|
+
verify_empty = self._full_query_dict.get("verify_empty", True)
|
|
1032
|
+
if statement_timeout is not None and not verify_empty:
|
|
1033
|
+
results = await self.db.execute_query(
|
|
1034
|
+
self.query, *self.params, statement_timeout=statement_timeout, verify_empty=verify_empty
|
|
1035
|
+
)
|
|
1036
|
+
elif statement_timeout is not None:
|
|
1032
1037
|
results = await self.db.execute_query(
|
|
1033
1038
|
self.query, *self.params, statement_timeout=statement_timeout
|
|
1034
1039
|
)
|
|
1040
|
+
elif not verify_empty:
|
|
1041
|
+
results = await self.db.execute_query(self.query, *self.params, verify_empty=False)
|
|
1035
1042
|
else:
|
|
1036
1043
|
results = await self.db.execute_query(self.query, *self.params)
|
|
1037
1044
|
return results
|