matrx-orm 3.1.78__tar.gz → 3.1.80__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.78 → matrx_orm-3.1.80}/CLAUDE.md +1 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/PKG-INFO +1 -1
- matrx_orm-3.1.80/matrx_orm/FEATURE.md +19 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/__init__.py +2 -1
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/FEATURE.md +21 -3
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/async_db_manager.py +58 -15
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/exceptions.py +54 -28
- matrx_orm-3.1.80/matrx_orm/local_sqlite.py +71 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/query/FEATURE.md +13 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/query/builder.py +22 -12
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/query/executor.py +2 -2
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/pyproject.toml +1 -1
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_connection_poison_guard.py +31 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_exceptions.py +38 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_local_sqlite.py +25 -1
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_pool_maintenance.py +33 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_query_builder.py +28 -0
- matrx_orm-3.1.78/matrx_orm/local_sqlite.py +0 -40
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/.gitignore +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/AGENTS.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/MODEL_API.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/docs/migrations.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/admin/FEATURE.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/pooler_role.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/FEATURE.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/scripts/release.sh +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/conftest.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_admin_response_rendering.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.78 → matrx_orm-3.1.80}/tests/test_sync_twins_guard.py +0 -0
|
@@ -29,6 +29,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
|
|
|
29
29
|
- **Read-only models** (`_read_only = True`): a model that can be queried but NEVER written through the ORM — every write path raises `ReadOnlyModelError` (enforced at `QueryExecutor.insert/bulk_insert/upsert/update/delete`, so it covers all of `create`/`save`/`update`/`delete`/`update_where`/`delete_where`/`upsert`/`bulk_*`). Used for DB-managed tables the app must not mutate. The canonical **`auth.users`** model is baked into the schema generator (`get_string_user_model`) as the full, curated Supabase auth schema (safe columns only — no password/token fields), `_read_only`, bound per-database. Read a user's profile/metadata there; mutate users only through Supabase Auth (GoTrue).
|
|
30
30
|
- **Connection-level type codecs** (`async_db_manager._init_connection_default`): every pooled connection decodes **uuid → `str`**, **vector → `list[float]`**, and **json/jsonb → `dict`/`list`** at the driver boundary — the TYPE decides the decode, never the call path. So `call_function` scalar/rows, raw fetches, and field hydration all agree; a caller must **never `json.loads` a fetched json/jsonb value** (it's already decoded; the 2026-07-13 `build_agent_context` crash was a jsonb RPC fetched pre-codec as a raw string). Encoding: a pre-serialised JSON `str` passes through untouched; a native dict/list is dumped (`_encode_json`). Pinned by `tests/level1/test_connection_codecs.py`.
|
|
31
31
|
- **Connection pools are event-loop-owned.** `AsyncDatabaseManager.get_pool()` recreates a pool when a caller arrives on a different loop. If the former owner loop is already closed, stale-pool termination and cleanup discard that pool without trying to schedule callbacks onto the dead loop; unrelated runtime errors still raise. Pinned by `tests/level1/test_pool_loop_recreation.py`.
|
|
32
|
+
- **Exception diagnostics never read source files.** `ORMException` captures compact caller metadata by walking live frames; `traceback.extract_stack()` is forbidden because its `linecache` filesystem checks can stall an async server. Contract: [`matrx_orm/FEATURE.md`](matrx_orm/FEATURE.md).
|
|
32
33
|
- **A zero-connection pool is dead, not busy.** After bounded SQL-safe acquire retries, `AsyncDatabaseManager.get_connection()` atomically replaces a pool whose physical size is still zero, then tries once against the replacement. It never replaces a busy `size > 0` pool. Contract: [`matrx_orm/core/FEATURE.md`](matrx_orm/core/FEATURE.md).
|
|
33
34
|
- **Query layer**: `QueryBuilder`, typed predicates/projections (`F`, `Q`,
|
|
34
35
|
`Predicate`, `Projection`), window functions, QueryBuilder-backed CTEs, and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.80
|
|
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
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Matrx ORM runtime contracts
|
|
2
|
+
|
|
3
|
+
## Exception diagnostics stay event-loop safe
|
|
4
|
+
|
|
5
|
+
- **Caller capture is metadata-only.** `_capture_caller_frames()` walks live Python frames and reads only code filename, function name, and line number.
|
|
6
|
+
- **Source-file reads are forbidden during exception construction.** Never use `traceback.extract_stack()` here: it enters `linecache.checkcache()` and synchronously stats source files, which can freeze every request in an async server.
|
|
7
|
+
- **Diagnostic value is preserved.** `ORMException._caller_frames` retains up to three external application frames in oldest-to-newest order while excluding ORM, site-package, frozen-import, and runpy frames.
|
|
8
|
+
- **The regression guard is direct.** `tests/level1/test_exceptions.py::TestORMExceptionBase::test_caller_capture_never_reads_source_files` makes traceback extraction fail and proves exception construction still captures the application call site.
|
|
9
|
+
|
|
10
|
+
## Entry points
|
|
11
|
+
|
|
12
|
+
- Runtime hierarchy and formatting: `exceptions.py`
|
|
13
|
+
- Focused unit coverage: `tests/level1/test_exceptions.py`
|
|
14
|
+
|
|
15
|
+
## Change Log
|
|
16
|
+
|
|
17
|
+
- 2026-08-18 — Replaced traceback extraction with live-frame metadata walking so ordinary ORM exceptions cannot synchronously stat source files on the event loop.
|
|
18
|
+
|
|
19
|
+
Verified against `exceptions.py` and `tests/level1/test_exceptions.py` on 2026-08-18.
|
|
@@ -43,7 +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
|
+
from .local_sqlite import SQLiteValue, check_local_sqlite_integrity, read_local_sqlite_rows
|
|
47
47
|
from .core.diagnostics import collect_pool_diagnostics, log_pool_diagnostics
|
|
48
48
|
from .core.introspection import introspect_columns, introspect_rls_policies, schema_exists
|
|
49
49
|
from .core.one_database import OneDatabaseReport, PoolIdentity, audit_one_database
|
|
@@ -639,6 +639,7 @@ __all__ = [
|
|
|
639
639
|
"open_listener_connection",
|
|
640
640
|
# ── Read-only local SQLite artifacts ──────────────────────────────────────
|
|
641
641
|
"SQLiteValue",
|
|
642
|
+
"check_local_sqlite_integrity",
|
|
642
643
|
"read_local_sqlite_rows",
|
|
643
644
|
# ── Cache ──────────────────────────────────────────────────────────────────
|
|
644
645
|
"CachePolicy",
|
|
@@ -36,7 +36,12 @@ labels (`unique`, `foreign_key`, `not_null`, `check`, `exclusion`, and
|
|
|
36
36
|
It is diagnostic and intentionally verbose, so a known PostgreSQL error must
|
|
37
37
|
never reach that fallback.
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
`QueryTimeoutError` diagnoses by SQL verb. A fresh `INSERT` points first to a
|
|
40
|
+
relation/DDL lock convoy, FK parent lock, or trigger work—not same-row fan-out.
|
|
41
|
+
`UPDATE`/`DELETE` retain the hot-row diagnosis while also naming a database-wide
|
|
42
|
+
DDL convoy when unrelated relations fail in the same window.
|
|
43
|
+
|
|
44
|
+
Verified against code: 2026-08-17.
|
|
40
45
|
|
|
41
46
|
## Pool recovery
|
|
42
47
|
|
|
@@ -66,12 +71,19 @@ census, so retry/persistence casualties preserve the diagnosis.
|
|
|
66
71
|
shielded task with a 10-second asyncpg release timeout. A holder remains visible
|
|
67
72
|
as `releasing` until reset really finishes. If reset exceeds the bound, asyncpg
|
|
68
73
|
discards the physical connection and the failure is logged loudly rather than
|
|
69
|
-
leaving an invisible checkout forever.
|
|
74
|
+
leaving an invisible checkout forever. **A release timeout never replaces an
|
|
75
|
+
already-completed query result** with a request failure; the discarded connection
|
|
76
|
+
is the isolation boundary.
|
|
77
|
+
|
|
78
|
+
**Pool growth never blocks reuse.** Only physical connection setup is serialized.
|
|
79
|
+
Waiters behind a slow TLS/database handshake continue checking for healthy holders
|
|
80
|
+
released by normal traffic, so an available connection cannot sit idle until the
|
|
81
|
+
outer acquire deadline expires.
|
|
70
82
|
|
|
71
83
|
Pinned by `tests/level1/test_pool_holder_census.py` and
|
|
72
84
|
`tests/level1/test_pool_maintenance.py`.
|
|
73
85
|
|
|
74
|
-
Verified against code: 2026-08-
|
|
86
|
+
Verified against code: 2026-08-18.
|
|
75
87
|
|
|
76
88
|
## Transaction-pooled role scope
|
|
77
89
|
|
|
@@ -107,6 +119,12 @@ Verified against code: 2026-08-17.
|
|
|
107
119
|
|
|
108
120
|
## Change log
|
|
109
121
|
|
|
122
|
+
- 2026-08-18: Preserved successful query results when asyncpg times out while
|
|
123
|
+
resetting and discarding the released connection.
|
|
124
|
+
- 2026-08-18: Let waiters reuse healthy connections released behind a slow
|
|
125
|
+
serialized-growth handshake.
|
|
126
|
+
- 2026-08-17: Corrected INSERT timeout guidance to identify relation/DDL lock
|
|
127
|
+
convoys before application row contention.
|
|
110
128
|
- 2026-08-17: Required direct before/after role evidence before recording
|
|
111
129
|
contamination; concurrent empty-to-nonempty reads reconcile without a false
|
|
112
130
|
repair-queue row, and session-authorization contamination is reset too.
|
|
@@ -120,6 +120,7 @@ READ_TIMEOUT_RETRY_BACKOFF = 0.25 # seconds before the read retry
|
|
|
120
120
|
POOL_MAINTENANCE_SWEEP_SECONDS = 45.0
|
|
121
121
|
POOL_MAINTENANCE_MIN_INTERVAL = 0.25
|
|
122
122
|
POOL_MAINTENANCE_MAX_BACKOFF = 30.0
|
|
123
|
+
POOL_GROWTH_GATE_POLL_SECONDS = 0.05
|
|
123
124
|
|
|
124
125
|
|
|
125
126
|
try:
|
|
@@ -708,21 +709,45 @@ class AsyncDatabaseManager:
|
|
|
708
709
|
try:
|
|
709
710
|
ready = await acquire_connected_holder(pool)
|
|
710
711
|
except UnsupportedPoolInternalsError:
|
|
711
|
-
|
|
712
|
+
lock = cls._growth_locks.setdefault(config_name, asyncio.Lock())
|
|
713
|
+
async with lock:
|
|
714
|
+
return await pool.acquire()
|
|
712
715
|
if ready is not None:
|
|
713
716
|
return ready
|
|
714
717
|
|
|
715
718
|
lock = cls._growth_locks.setdefault(config_name, asyncio.Lock())
|
|
716
|
-
|
|
717
|
-
# Maintenance or the previous waiter may have established/released
|
|
718
|
-
# a ready holder while this caller waited for the growth gate.
|
|
719
|
+
while True:
|
|
719
720
|
try:
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
721
|
+
await asyncio.wait_for(
|
|
722
|
+
lock.acquire(),
|
|
723
|
+
timeout=POOL_GROWTH_GATE_POLL_SECONDS,
|
|
724
|
+
)
|
|
725
|
+
except TimeoutError:
|
|
726
|
+
# A slow TLS/database handshake owns the growth gate. Traffic
|
|
727
|
+
# must still consume healthy connections released meanwhile;
|
|
728
|
+
# otherwise those slots sit idle while every waiter times out
|
|
729
|
+
# behind work that has no bearing on them.
|
|
730
|
+
try:
|
|
731
|
+
ready = await acquire_connected_holder(pool)
|
|
732
|
+
except UnsupportedPoolInternalsError:
|
|
733
|
+
async with lock:
|
|
734
|
+
return await pool.acquire()
|
|
735
|
+
if ready is not None:
|
|
736
|
+
return ready
|
|
737
|
+
continue
|
|
738
|
+
|
|
739
|
+
try:
|
|
740
|
+
# Maintenance or the previous waiter may have established or
|
|
741
|
+
# released a ready holder while this caller waited briefly.
|
|
742
|
+
try:
|
|
743
|
+
ready = await acquire_connected_holder(pool)
|
|
744
|
+
except UnsupportedPoolInternalsError:
|
|
745
|
+
return await pool.acquire()
|
|
746
|
+
if ready is not None:
|
|
747
|
+
return ready
|
|
748
|
+
return await pool.acquire()
|
|
749
|
+
finally:
|
|
750
|
+
lock.release()
|
|
726
751
|
|
|
727
752
|
@classmethod
|
|
728
753
|
@asynccontextmanager
|
|
@@ -1019,13 +1044,30 @@ class AsyncDatabaseManager:
|
|
|
1019
1044
|
except Exception:
|
|
1020
1045
|
pass
|
|
1021
1046
|
|
|
1022
|
-
release_task.add_done_callback(_release_done)
|
|
1023
1047
|
try:
|
|
1024
1048
|
await asyncio.shield(release_task)
|
|
1025
1049
|
except asyncio.CancelledError:
|
|
1026
1050
|
# The dedicated release task keeps running to completion and its
|
|
1027
1051
|
# body/callback own census cleanup and recovery-latch reset.
|
|
1052
|
+
release_task.add_done_callback(_release_done)
|
|
1028
1053
|
raise
|
|
1054
|
+
except TimeoutError as release_exc:
|
|
1055
|
+
# asyncpg terminates a connection whose reset exceeds the
|
|
1056
|
+
# release timeout. The query already completed successfully, so
|
|
1057
|
+
# this cleanup failure must not replace its result with a false
|
|
1058
|
+
# request crash. Keep the pool-health signal loud.
|
|
1059
|
+
logger.error(
|
|
1060
|
+
"[matrx_orm] Connection release timed out for '%s'; "
|
|
1061
|
+
"asyncpg discarded the connection: %s",
|
|
1062
|
+
config_name,
|
|
1063
|
+
release_exc,
|
|
1064
|
+
)
|
|
1065
|
+
else:
|
|
1066
|
+
try:
|
|
1067
|
+
if pool.get_idle_size() > 0:
|
|
1068
|
+
note_pool_recovered(config_name)
|
|
1069
|
+
except Exception:
|
|
1070
|
+
pass
|
|
1029
1071
|
|
|
1030
1072
|
@classmethod
|
|
1031
1073
|
async def execute_query(
|
|
@@ -1102,8 +1144,9 @@ class AsyncDatabaseManager:
|
|
|
1102
1144
|
# asyncpg's command_timeout fired — query held a connection
|
|
1103
1145
|
# but didn't return in time. NOT a pool-acquire timeout
|
|
1104
1146
|
# (that's PoolAcquireTimeoutError, raised in get_connection
|
|
1105
|
-
# before the connection is held).
|
|
1106
|
-
#
|
|
1147
|
+
# before the connection is held). Diagnose by query verb:
|
|
1148
|
+
# INSERT commonly means relation/DDL/FK/trigger waits, while
|
|
1149
|
+
# UPDATE/DELETE commonly means a hot-row lock.
|
|
1107
1150
|
raise QueryTimeoutError(
|
|
1108
1151
|
operation="execute_query",
|
|
1109
1152
|
query=query,
|
|
@@ -1225,8 +1268,8 @@ class AsyncDatabaseManager:
|
|
|
1225
1268
|
) from None
|
|
1226
1269
|
except TimeoutError as e:
|
|
1227
1270
|
# asyncpg's command_timeout fired — query held a
|
|
1228
|
-
# connection but didn't return in time. See twin
|
|
1229
|
-
# in the tx_conn branch above
|
|
1271
|
+
# connection but didn't return in time. See the twin
|
|
1272
|
+
# query-verb diagnosis in the tx_conn branch above.
|
|
1230
1273
|
# A READ is side-effect-free → retry once on a fresh
|
|
1231
1274
|
# connection before surfacing the timeout (self-heal for
|
|
1232
1275
|
# starvation blips). Writes fall straight through to raise.
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import re
|
|
3
3
|
import socket
|
|
4
4
|
import ssl
|
|
5
|
+
import sys
|
|
5
6
|
import traceback as _tb
|
|
6
7
|
from collections.abc import Mapping
|
|
7
8
|
from dataclasses import dataclass
|
|
@@ -304,12 +305,17 @@ def _capture_caller_frames() -> list[str]:
|
|
|
304
305
|
section only contains application code written by the user.
|
|
305
306
|
"""
|
|
306
307
|
frames: list[str] = []
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
frame = sys._getframe(1)
|
|
309
|
+
try:
|
|
310
|
+
while frame is not None and len(frames) < 3:
|
|
311
|
+
filename = frame.f_code.co_filename
|
|
312
|
+
if not any(marker in filename for marker in _EXCLUDED_FRAME_MARKERS):
|
|
313
|
+
frames.append(
|
|
314
|
+
f' File "{filename}", line {frame.f_lineno}, in {frame.f_code.co_name}'
|
|
315
|
+
)
|
|
316
|
+
frame = frame.f_back
|
|
317
|
+
finally:
|
|
318
|
+
del frame
|
|
313
319
|
frames.reverse()
|
|
314
320
|
return frames
|
|
315
321
|
|
|
@@ -1008,10 +1014,11 @@ class QueryTimeoutError(DatabaseError):
|
|
|
1008
1014
|
timeout = self.details.get("timeout")
|
|
1009
1015
|
query = self._str("query")
|
|
1010
1016
|
args = self.details.get("args", [])
|
|
1011
|
-
# First-token heuristic: SELECT
|
|
1012
|
-
#
|
|
1013
|
-
#
|
|
1014
|
-
#
|
|
1017
|
+
# First-token heuristic: SELECT, INSERT, and UPDATE/DELETE timeouts
|
|
1018
|
+
# have different root-cause distributions, so the hint changes
|
|
1019
|
+
# accordingly. In particular, a fresh INSERT cannot be contending on
|
|
1020
|
+
# the row it is creating; relation/DDL locks and FK/trigger work must
|
|
1021
|
+
# be investigated before blaming application fan-out.
|
|
1015
1022
|
query_kind = _classify_query_shape(query)
|
|
1016
1023
|
lines = ["\n" + "-" * 80]
|
|
1017
1024
|
lines.append("Matrx ORM | QueryTimeoutError")
|
|
@@ -1054,25 +1061,40 @@ class QueryTimeoutError(DatabaseError):
|
|
|
1054
1061
|
lines.append(" - Safe to retry ONCE for cold-cache / transient cases. If it times out")
|
|
1055
1062
|
lines.append(" twice in a row, fix the index — retrying won't help.")
|
|
1056
1063
|
elif query_kind == "write":
|
|
1057
|
-
|
|
1058
|
-
lines.append(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
else (query.lstrip().split(None, 1)[0].upper() if query else "WRITE")
|
|
1064
|
+
write_verb = query.lstrip().split(None, 1)[0].upper() if query else "WRITE"
|
|
1065
|
+
lines.append(f" - Write query ({write_verb}).")
|
|
1066
|
+
if write_verb == "INSERT":
|
|
1067
|
+
lines.append(
|
|
1068
|
+
" - A fresh INSERT is not waiting on the row it is creating. Check, in"
|
|
1063
1069
|
)
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1070
|
+
lines.append(
|
|
1071
|
+
" order: a queued live DDL/relation lock; FK parent-row locks; trigger"
|
|
1072
|
+
)
|
|
1073
|
+
lines.append(
|
|
1074
|
+
" work; then database-wide overload. Correlate other unrelated query"
|
|
1075
|
+
)
|
|
1076
|
+
lines.append(
|
|
1077
|
+
" timeouts in the same window before changing application concurrency."
|
|
1078
|
+
)
|
|
1079
|
+
else:
|
|
1080
|
+
lines.append(
|
|
1081
|
+
" - UPDATE/DELETE commonly wait on a hot row. If N tasks change the same"
|
|
1082
|
+
)
|
|
1083
|
+
lines.append(
|
|
1084
|
+
" row, batch them, use one atomic SQL update, or gate that write lane."
|
|
1085
|
+
)
|
|
1086
|
+
lines.append(
|
|
1087
|
+
" - Also check for a queued live DDL/relation lock when unrelated tables"
|
|
1088
|
+
)
|
|
1089
|
+
lines.append(
|
|
1090
|
+
" time out in the same window; that is a database-wide lock convoy."
|
|
1091
|
+
)
|
|
1092
|
+
lines.append(" - A missing WHERE-clause index, trigger work, or sustained server load")
|
|
1093
|
+
lines.append(" can also exceed command_timeout.")
|
|
1072
1094
|
lines.append(
|
|
1073
|
-
" - NOT safe to retry blindly —
|
|
1095
|
+
" - NOT safe to retry blindly — a write may have ambiguous completion, and"
|
|
1074
1096
|
)
|
|
1075
|
-
lines.append("
|
|
1097
|
+
lines.append(" an unchanged lock convoy will time out again.")
|
|
1076
1098
|
else:
|
|
1077
1099
|
# DDL / CALL / unknown — keep both hints concise.
|
|
1078
1100
|
lines.append(" - Couldn't classify the query shape — see the Query line above. Common")
|
|
@@ -1204,11 +1226,15 @@ class IntegrityError(DatabaseError):
|
|
|
1204
1226
|
lines.append(' `null value in column "<name>"` and the rest of the message.')
|
|
1205
1227
|
lines.append(" - Pass the field explicitly (or set a default on the model).")
|
|
1206
1228
|
elif constraint == "check":
|
|
1207
|
-
lines.append(
|
|
1229
|
+
lines.append(
|
|
1230
|
+
" - A CHECK constraint rejected values that violate a database invariant."
|
|
1231
|
+
)
|
|
1208
1232
|
lines.append(" - Read the DB error above for the invariant and offending row.")
|
|
1209
1233
|
lines.append(" - This is not transient; correct the input or the write ordering.")
|
|
1210
1234
|
elif constraint == "exclusion":
|
|
1211
|
-
lines.append(
|
|
1235
|
+
lines.append(
|
|
1236
|
+
" - This row conflicts with an existing row under an exclusion constraint."
|
|
1237
|
+
)
|
|
1212
1238
|
lines.append(" - Read the DB error above for the conflicting key or range.")
|
|
1213
1239
|
lines.append(" - This is not safe to blindly retry with unchanged input.")
|
|
1214
1240
|
elif constraint == "restrict":
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sqlite3
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
|
|
9
|
+
type SQLiteValue = str | int | float | bytes | None
|
|
10
|
+
|
|
11
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _identifier(value: str) -> str:
|
|
15
|
+
if not _IDENTIFIER.fullmatch(value):
|
|
16
|
+
raise ValueError(f"invalid SQLite identifier: {value!r}")
|
|
17
|
+
return f'"{value}"'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_local_sqlite_rows(
|
|
21
|
+
database: str | Path,
|
|
22
|
+
*,
|
|
23
|
+
table: str,
|
|
24
|
+
columns: Sequence[str],
|
|
25
|
+
order_by: Sequence[str] = (),
|
|
26
|
+
where_not_null: Sequence[str] = (),
|
|
27
|
+
min_lengths: Mapping[str, int] | None = None,
|
|
28
|
+
limit: int | None = None,
|
|
29
|
+
) -> list[tuple[SQLiteValue, ...]]:
|
|
30
|
+
"""Read a projected table from a local SQLite artifact in read-only mode."""
|
|
31
|
+
if not columns:
|
|
32
|
+
raise ValueError("columns must contain at least one identifier")
|
|
33
|
+
|
|
34
|
+
projection = ", ".join(_identifier(column) for column in columns)
|
|
35
|
+
statement = f"SELECT {projection} FROM {_identifier(table)}"
|
|
36
|
+
predicates: list[str] = []
|
|
37
|
+
parameters: list[int] = []
|
|
38
|
+
for column in where_not_null:
|
|
39
|
+
predicates.append(f"{_identifier(column)} IS NOT NULL")
|
|
40
|
+
for column, minimum in (min_lengths or {}).items():
|
|
41
|
+
if minimum < 0:
|
|
42
|
+
raise ValueError("minimum lengths must be non-negative")
|
|
43
|
+
predicates.append(f"length({_identifier(column)}) >= ?")
|
|
44
|
+
parameters.append(minimum)
|
|
45
|
+
if predicates:
|
|
46
|
+
statement += " WHERE " + " AND ".join(predicates)
|
|
47
|
+
if order_by:
|
|
48
|
+
statement += " ORDER BY " + ", ".join(_identifier(column) for column in order_by)
|
|
49
|
+
if limit is not None:
|
|
50
|
+
if limit <= 0:
|
|
51
|
+
raise ValueError("limit must be positive")
|
|
52
|
+
statement += " LIMIT ?"
|
|
53
|
+
parameters.append(limit)
|
|
54
|
+
|
|
55
|
+
path = Path(database).expanduser().resolve()
|
|
56
|
+
connection = sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)
|
|
57
|
+
try:
|
|
58
|
+
return [tuple(row) for row in connection.execute(statement, parameters)]
|
|
59
|
+
finally:
|
|
60
|
+
connection.close()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_local_sqlite_integrity(database: str | Path) -> str:
|
|
64
|
+
"""Return SQLite's read-only quick-check result for a local artifact."""
|
|
65
|
+
path = Path(database).expanduser().resolve()
|
|
66
|
+
connection = sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)
|
|
67
|
+
try:
|
|
68
|
+
row = connection.execute("PRAGMA quick_check").fetchone()
|
|
69
|
+
return str(row[0]) if row else ""
|
|
70
|
+
finally:
|
|
71
|
+
connection.close()
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
`QueryBuilder` is the typed terminal-read and write layer behind every model.
|
|
4
4
|
Application packages compose queries here; they never bypass it with SQL.
|
|
5
5
|
|
|
6
|
+
## Async query compilation
|
|
7
|
+
|
|
8
|
+
**Every async terminal compiles SQL in a worker thread.** `all`, `first`,
|
|
9
|
+
`get`, `update`, `delete`, `count`, `exists`, `aggregate`, `values`,
|
|
10
|
+
`values_list`, `fetch_via`, and async iteration must reach their first database
|
|
11
|
+
await without running `QueryBuilder._build_query` or `QueryExecutor._build_query`
|
|
12
|
+
on the event-loop thread. Query shapes are usually small, but concurrent or
|
|
13
|
+
expression-heavy builders are synchronous Python work and cannot freeze every
|
|
14
|
+
request in the process.
|
|
15
|
+
|
|
16
|
+
`compile()` remains deliberately synchronous for synchronous callers.
|
|
17
|
+
|
|
6
18
|
## Large scans and the identity cache
|
|
7
19
|
|
|
8
20
|
`.all()` populates `StateManager` by default so ordinary repeated identity reads
|
|
@@ -34,5 +46,6 @@ without forcing callers to add a redundant projection.
|
|
|
34
46
|
|
|
35
47
|
## Change log
|
|
36
48
|
|
|
49
|
+
- 2026-08-18 — Moved async terminal SQL compilation off the event loop.
|
|
37
50
|
- 2026-08-15 — Moved bulk hydration off the event loop, made complete-primary-key
|
|
38
51
|
misses constant-time, and documented cache bypass for request-local scans.
|
|
@@ -805,6 +805,9 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
805
805
|
def _get_executor(self) -> QueryExecutor:
|
|
806
806
|
return QueryExecutor(self._build_query())
|
|
807
807
|
|
|
808
|
+
async def _get_executor_async(self) -> QueryExecutor:
|
|
809
|
+
return await asyncio.to_thread(self._get_executor)
|
|
810
|
+
|
|
808
811
|
# ------------------------------------------------------------------
|
|
809
812
|
# Terminal async methods
|
|
810
813
|
# ------------------------------------------------------------------
|
|
@@ -822,7 +825,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
822
825
|
See matrx_orm.session.reads.merge_pending_with_db.
|
|
823
826
|
"""
|
|
824
827
|
try:
|
|
825
|
-
executor = self.
|
|
828
|
+
executor = await self._get_executor_async()
|
|
826
829
|
results = await executor.all()
|
|
827
830
|
if use_cache:
|
|
828
831
|
await StateManager.cache_bulk(self.model, results)
|
|
@@ -851,7 +854,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
851
854
|
|
|
852
855
|
Session-aware via the same merge pathway as :meth:`all`."""
|
|
853
856
|
try:
|
|
854
|
-
executor = self.
|
|
857
|
+
executor = await self._get_executor_async()
|
|
855
858
|
results = await executor.all()
|
|
856
859
|
merged = _merge_pending_into_models(
|
|
857
860
|
self.model,
|
|
@@ -878,7 +881,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
878
881
|
"""
|
|
879
882
|
filters = self._merge_filters_excludes()
|
|
880
883
|
try:
|
|
881
|
-
executor = self.
|
|
884
|
+
executor = await self._get_executor_async()
|
|
882
885
|
results = await executor.all()
|
|
883
886
|
results = _merge_pending_into_models(self.model, results, filters)
|
|
884
887
|
if not results:
|
|
@@ -908,7 +911,8 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
908
911
|
try:
|
|
909
912
|
if not kwargs:
|
|
910
913
|
raise ValidationError(model=self.model, reason="No update data provided")
|
|
911
|
-
|
|
914
|
+
executor = await self._get_executor_async()
|
|
915
|
+
result = await executor.update(**kwargs)
|
|
912
916
|
await StateManager.refresh_from_rows(self.model, result.updated_rows)
|
|
913
917
|
return result
|
|
914
918
|
except ValidationError:
|
|
@@ -927,7 +931,8 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
927
931
|
free, serving a deleted row for 10 minutes is not.
|
|
928
932
|
"""
|
|
929
933
|
try:
|
|
930
|
-
|
|
934
|
+
executor = await self._get_executor_async()
|
|
935
|
+
deleted = await executor.delete()
|
|
931
936
|
if deleted:
|
|
932
937
|
self._invalidate_cache_for_filters()
|
|
933
938
|
return deleted
|
|
@@ -958,7 +963,8 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
958
963
|
try:
|
|
959
964
|
if self._stack_has_pending():
|
|
960
965
|
return len(await self.all())
|
|
961
|
-
|
|
966
|
+
executor = await self._get_executor_async()
|
|
967
|
+
return await executor.count()
|
|
962
968
|
except ORMException as e:
|
|
963
969
|
e.enrich(model=self.model, operation="count", filters=self._merge_filters_excludes())
|
|
964
970
|
raise
|
|
@@ -970,7 +976,8 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
970
976
|
try:
|
|
971
977
|
if self._stack_has_pending():
|
|
972
978
|
return len(await self.all()) > 0
|
|
973
|
-
|
|
979
|
+
executor = await self._get_executor_async()
|
|
980
|
+
return await executor.exists()
|
|
974
981
|
except ORMException as e:
|
|
975
982
|
e.enrich(model=self.model, operation="exists", filters=self._merge_filters_excludes())
|
|
976
983
|
raise
|
|
@@ -991,7 +998,8 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
991
998
|
for key, value in kwargs.items():
|
|
992
999
|
target.aggregations.append({"name": key, "function": value})
|
|
993
1000
|
try:
|
|
994
|
-
|
|
1001
|
+
executor = await target._get_executor_async()
|
|
1002
|
+
return await executor.aggregate()
|
|
995
1003
|
except ORMException as e:
|
|
996
1004
|
e.enrich(
|
|
997
1005
|
model=self.model, operation="aggregate", filters=self._merge_filters_excludes()
|
|
@@ -1002,7 +1010,8 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
1002
1010
|
target = self._clone() if fields else self
|
|
1003
1011
|
if fields:
|
|
1004
1012
|
target.select_fields = list(fields)
|
|
1005
|
-
|
|
1013
|
+
executor = await target._get_executor_async()
|
|
1014
|
+
return await executor.values()
|
|
1006
1015
|
|
|
1007
1016
|
def compile(self, *fields: str) -> tuple[str, list[Any]]:
|
|
1008
1017
|
"""Build the SELECT SQL + bound-parameter list WITHOUT executing.
|
|
@@ -1057,7 +1066,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
1057
1066
|
.fetch_via(conn, "id", set_local={"hnsw.iterative_scan": "relaxed_order"})
|
|
1058
1067
|
)
|
|
1059
1068
|
"""
|
|
1060
|
-
sql, params = self.compile
|
|
1069
|
+
sql, params = await asyncio.to_thread(self.compile, *fields)
|
|
1061
1070
|
if set_local:
|
|
1062
1071
|
async with conn.transaction():
|
|
1063
1072
|
for guc_name, guc_value in set_local.items():
|
|
@@ -1075,7 +1084,8 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
1075
1084
|
target = self._clone() if fields else self
|
|
1076
1085
|
if fields:
|
|
1077
1086
|
target.select_fields = list(fields)
|
|
1078
|
-
|
|
1087
|
+
executor = await target._get_executor_async()
|
|
1088
|
+
return await executor.values_list(flat=flat)
|
|
1079
1089
|
|
|
1080
1090
|
# ------------------------------------------------------------------
|
|
1081
1091
|
# Synchronous wrappers
|
|
@@ -1144,7 +1154,7 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
1144
1154
|
# ------------------------------------------------------------------
|
|
1145
1155
|
|
|
1146
1156
|
async def __aiter__(self) -> Any:
|
|
1147
|
-
executor = self.
|
|
1157
|
+
executor = await self._get_executor_async()
|
|
1148
1158
|
async for item in executor:
|
|
1149
1159
|
yield item
|
|
1150
1160
|
|
|
@@ -1540,7 +1540,7 @@ class QueryExecutor:
|
|
|
1540
1540
|
async def first(self) -> Any:
|
|
1541
1541
|
"""Fetch the first matching row as a model instance."""
|
|
1542
1542
|
self._full_query_dict["limit"] = 1
|
|
1543
|
-
self.query, self.params = self._build_query
|
|
1543
|
+
self.query, self.params = await asyncio.to_thread(self._build_query)
|
|
1544
1544
|
results = await self._execute()
|
|
1545
1545
|
if not results:
|
|
1546
1546
|
return None
|
|
@@ -1578,7 +1578,7 @@ class QueryExecutor:
|
|
|
1578
1578
|
async def exists(self) -> bool:
|
|
1579
1579
|
"""EXISTS check for the current WHERE clause."""
|
|
1580
1580
|
self._full_query_dict["limit"] = 1
|
|
1581
|
-
self.query, self.params = self._build_query
|
|
1581
|
+
self.query, self.params = await asyncio.to_thread(self._build_query)
|
|
1582
1582
|
results = await self._execute()
|
|
1583
1583
|
return len(results) > 0
|
|
1584
1584
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.80"
|
|
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" }
|
|
@@ -33,6 +33,16 @@ class _FakeConn:
|
|
|
33
33
|
self._in_tx = False
|
|
34
34
|
return "OK"
|
|
35
35
|
|
|
36
|
+
async def fetch(
|
|
37
|
+
self,
|
|
38
|
+
sql: str,
|
|
39
|
+
*args: object,
|
|
40
|
+
timeout: float | None = None, # noqa: ASYNC109 - mirrors asyncpg
|
|
41
|
+
) -> list[dict[str, int]]:
|
|
42
|
+
del args, timeout
|
|
43
|
+
self.calls.append(sql)
|
|
44
|
+
return [{"value": 1}]
|
|
45
|
+
|
|
36
46
|
def terminate(self) -> None:
|
|
37
47
|
self.calls.append("TERMINATE")
|
|
38
48
|
self._in_tx = False
|
|
@@ -73,6 +83,16 @@ class _FakePool:
|
|
|
73
83
|
return 1
|
|
74
84
|
|
|
75
85
|
|
|
86
|
+
class _ReleaseTimeoutPool(_FakePool):
|
|
87
|
+
async def release(
|
|
88
|
+
self,
|
|
89
|
+
conn: _FakeConn,
|
|
90
|
+
timeout: float | None = None, # noqa: ASYNC109 - mirrors asyncpg
|
|
91
|
+
) -> None:
|
|
92
|
+
del conn, timeout
|
|
93
|
+
raise TimeoutError("reset exceeded release budget")
|
|
94
|
+
|
|
95
|
+
|
|
76
96
|
def _patch_pool(monkeypatch, pool: _FakePool) -> None:
|
|
77
97
|
async def fake_get_pool(cls, config_name): # noqa: ANN001
|
|
78
98
|
return pool
|
|
@@ -116,3 +136,14 @@ async def test_unhealable_connection_is_terminated_not_reused(monkeypatch):
|
|
|
116
136
|
pass
|
|
117
137
|
assert "TERMINATE" in conn.calls
|
|
118
138
|
assert pool.released == [conn] # slot freed, conn discarded
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def test_release_timeout_does_not_replace_successful_query_result(monkeypatch, caplog):
|
|
142
|
+
conn = _FakeConn(in_tx=False)
|
|
143
|
+
pool = _ReleaseTimeoutPool(conn)
|
|
144
|
+
_patch_pool(monkeypatch, pool)
|
|
145
|
+
|
|
146
|
+
result = await AsyncDatabaseManager.execute_query("x", "SELECT 1 AS value")
|
|
147
|
+
|
|
148
|
+
assert result == [{"value": 1}]
|
|
149
|
+
assert "Connection release timed out for 'x'" in caplog.text
|