matrx-orm 3.1.145__tar.gz → 3.1.147__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.145 → matrx_orm-3.1.147}/.gitignore +9 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/BASE_CLASS_METHODS.md +13 -1
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/PKG-INFO +2 -2
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/adapters/async_postgresql.py +7 -1
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/adapters/base_adapter.py +6 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/adapters/postgrest_client_adapter.py +1 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/adapters/supabase_adapter.py +7 -1
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/FEATURE.md +75 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/async_db_manager.py +111 -91
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/base.py +33 -3
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/db_function.py +31 -4
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/expressions.py +1 -0
- matrx_orm-3.1.147/matrx_orm/core/write_retry.py +297 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/exceptions.py +34 -15
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/create.py +11 -5
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/update.py +26 -3
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/query/FEATURE.md +23 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/query/builder.py +36 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/query/executor.py +110 -2
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/FEATURE.md +16 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/org_service.py +13 -1
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/pyproject.toml +2 -2
- matrx_orm-3.1.147/tests/level1/test_deadlock_retry.py +487 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_lock_contention_retry.py +23 -4
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_optimistic_concurrency.py +60 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_query_timeout.py +80 -1
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_write_retry_semantics.py +7 -1
- matrx_orm-3.1.147/tests/test_user_visible_lookup.py +52 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/AGENTS.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/CLAUDE.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/MODEL_API.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/docs/migrations.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/FEATURE.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/admin/FEATURE.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/pooler_role.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/session_context.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/local_sqlite.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/no_org_assignment.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/FEATURE.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/catalog_select.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/platform_access.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/pytest_plugin.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/resources/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/resources/additional-assigners-2026-09-12.json +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/resources/org_assignment_trigger_baseline.json +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/host_authorization.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/mutation_receipts.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/scripts/release.sh +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/conftest.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_adapter_registry.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_admin_response_rendering.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_associations_wiring.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_committed_row_guard.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_count_limit_param_leak.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_local_sqlite.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_org_assignment_inventory.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_persisted_count.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_read_connection_retry.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_standalone_write_session_context.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_subquery_nested_expressions.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_transaction_database_identity.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level1/test_write_queue_deadline.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/level2/verify_committed_row_guard_pg17.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_enum_ordering.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_schema_analysis.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_record_error_bounded_insert.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_record_error_storm.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/session/test_spill_guard_plugin.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_array_arg_element_type.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_dynamic_select_unregistered.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_immutable_insert_or_exact_verify.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_keyed_digest.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_order_by_desc_nulls_last.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_secrets_minting_keys.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.145 → matrx_orm-3.1.147}/tests/test_sync_twins_guard.py +0 -0
|
@@ -304,3 +304,12 @@ _kind_marker_law_scratch*/
|
|
|
304
304
|
|
|
305
305
|
# packed npm tarballs are build output, never source (doctrine: typescript-package-standard)
|
|
306
306
|
*.tgz
|
|
307
|
+
|
|
308
|
+
# Peers' kit scratch witnesses. A `.kit-<name>-witness.XXXXXX/` directory is a
|
|
309
|
+
# full COPY of kit/src planted inside the packages tree — three of them held a
|
|
310
|
+
# duplicate `format.ts` on 2026-09-12 — and it sits in the `check:package-twins`
|
|
311
|
+
# scan ROOT. `git ls-files` only reaches TRACKED files, so the guard never saw
|
|
312
|
+
# them; the risk was a broad `git add` committing three formatter twins in one
|
|
313
|
+
# stroke, which is the exact class that guard exists to stop. Ignored, never
|
|
314
|
+
# deleted: they are another agent's evidence.
|
|
315
|
+
apps/shared/.kit-*-witness.*/
|
|
@@ -59,9 +59,21 @@ The following methods are available directly from all manager classes:
|
|
|
59
59
|
- `upsert_item(data, conflict_fields, update_fields=None)`
|
|
60
60
|
- `upsert_items(items_data, conflict_fields, update_fields=None)`
|
|
61
61
|
- `count(**filters)`
|
|
62
|
-
- `update_where(filters, *, expected_version=None, **updates)`
|
|
62
|
+
- `update_where(filters, *, expected_version=None, lock_rows_in_pk_order=False, **updates)`
|
|
63
63
|
- `delete_where(*, expected_version=None, **filters)`
|
|
64
64
|
|
|
65
|
+
### A multi-row UPDATE that racing callers share — `lock_rows_in_pk_order=True`
|
|
66
|
+
|
|
67
|
+
Pass it whenever concurrent callers can run the SAME multi-row `update_where`
|
|
68
|
+
over an **overlapping** row set. The statement then takes its row locks
|
|
69
|
+
ascending by primary key (`WHERE pk IN (SELECT pk ... ORDER BY pk FOR UPDATE)`)
|
|
70
|
+
instead of in plan-scan order, which is what stops two copies of one statement
|
|
71
|
+
from deadlocking each other (SQLSTATE 40P01). Sorting the id list yourself does
|
|
72
|
+
NOT do this — the plan decides lock order, not the array. Needs a single-column
|
|
73
|
+
primary key; contradictory with `expected_version=` (that addresses ONE row, so
|
|
74
|
+
there is no order to fix) and refused. Full reasoning and the incident:
|
|
75
|
+
`matrx_orm/query/FEATURE.md` § A multi-row UPDATE can pin its lock ORDER.
|
|
76
|
+
|
|
65
77
|
### Optimistic concurrency — `expected_version=`
|
|
66
78
|
|
|
67
79
|
**Opt-in compare-and-swap on the canonical `version` int column.** Pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.147
|
|
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.40
|
|
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
|
|
@@ -60,11 +60,17 @@ class AsyncPostgreSQLAdapter(BaseAdapter):
|
|
|
60
60
|
*args: Any,
|
|
61
61
|
timeout: float = 10.0,
|
|
62
62
|
max_retries: int = 2,
|
|
63
|
+
statement_timeout: float | None = None,
|
|
63
64
|
) -> list[dict[str, Any]]:
|
|
64
65
|
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
65
66
|
|
|
66
67
|
return await AsyncDatabaseManager.execute_write(
|
|
67
|
-
self._config_name,
|
|
68
|
+
self._config_name,
|
|
69
|
+
sql,
|
|
70
|
+
*args,
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
max_retries=max_retries,
|
|
73
|
+
statement_timeout=statement_timeout,
|
|
68
74
|
)
|
|
69
75
|
|
|
70
76
|
# ------------------------------------------------------------------
|
|
@@ -50,10 +50,16 @@ class BaseAdapter(ABC):
|
|
|
50
50
|
*args: Any,
|
|
51
51
|
timeout: float = 10.0,
|
|
52
52
|
max_retries: int = 2,
|
|
53
|
+
statement_timeout: float | None = None,
|
|
53
54
|
) -> list[dict[str, Any]]:
|
|
54
55
|
"""Execute a write (INSERT/UPDATE/DELETE) query and return rows as dicts.
|
|
55
56
|
|
|
56
57
|
Implementations should handle retries and write-queue routing as needed.
|
|
58
|
+
``timeout`` is the connection-acquire timeout; ``statement_timeout``
|
|
59
|
+
(when set) overrides the pool's ``command_timeout`` for THIS statement
|
|
60
|
+
only — the write-side twin of ``QueryBuilder.timeout()``, for a write
|
|
61
|
+
whose size is known to need longer than the pool default (a workflow
|
|
62
|
+
checkpoint carrying a fan-out's inline payloads).
|
|
57
63
|
"""
|
|
58
64
|
|
|
59
65
|
# ------------------------------------------------------------------
|
|
@@ -338,6 +338,7 @@ class PostgRESTClientAdapter(BaseAdapter):
|
|
|
338
338
|
*args: Any,
|
|
339
339
|
timeout: float = 10.0,
|
|
340
340
|
max_retries: int = 2,
|
|
341
|
+
statement_timeout: float | None = None,
|
|
341
342
|
) -> list[dict[str, Any]]:
|
|
342
343
|
"""Raw SQL writes are not supported via PostgREST."""
|
|
343
344
|
raise NotImplementedError(
|
|
@@ -122,11 +122,17 @@ class SupabaseAdapter(BaseAdapter):
|
|
|
122
122
|
*args: Any,
|
|
123
123
|
timeout: float = 10.0,
|
|
124
124
|
max_retries: int = 2,
|
|
125
|
+
statement_timeout: float | None = None,
|
|
125
126
|
) -> list[dict[str, Any]]:
|
|
126
127
|
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
127
128
|
|
|
128
129
|
return await AsyncDatabaseManager.execute_write(
|
|
129
|
-
self._config_name,
|
|
130
|
+
self._config_name,
|
|
131
|
+
sql,
|
|
132
|
+
*args,
|
|
133
|
+
timeout=timeout,
|
|
134
|
+
max_retries=max_retries,
|
|
135
|
+
statement_timeout=statement_timeout,
|
|
130
136
|
)
|
|
131
137
|
|
|
132
138
|
# ------------------------------------------------------------------
|
|
@@ -45,6 +45,67 @@ Guard: `tests/level1/test_read_connection_retry.py`.
|
|
|
45
45
|
rejects the statement before execution, so this narrow failure is replay-safe;
|
|
46
46
|
ambiguous write failures remain single-attempt.
|
|
47
47
|
|
|
48
|
+
**A write may set its own statement ceiling — `Model.create(statement_timeout=…)`
|
|
49
|
+
(seconds, keyword-only).** The read side has had `QueryBuilder.timeout()` for a
|
|
50
|
+
while; the write door had nothing, so the pool's `command_timeout` (10s) was the
|
|
51
|
+
only ceiling any INSERT could have. It threads
|
|
52
|
+
`Model.create` → `operations/create` → `query["statement_timeout"]` →
|
|
53
|
+
`QueryExecutor.insert` → `adapter.execute_write(statement_timeout=)` →
|
|
54
|
+
`execute_query(statement_timeout=)` → asyncpg `fetch(..., timeout=)`, so it
|
|
55
|
+
overrides — never widens — anything else: pool-acquire timeout and every other
|
|
56
|
+
statement are untouched. It raises no retry rights: an ambiguous write is still
|
|
57
|
+
single-attempt HERE, and a caller that re-issues one must own an idempotency key
|
|
58
|
+
(matrx-graph's checkpoint INSERT is keyed by checkpoint id and reads the row back).
|
|
59
|
+
Added 2026-09-12 after a 1.1 MB `workflow.checkpoint` row timed out at 10s under
|
|
60
|
+
load and killed a paid-for workflow run. Guard:
|
|
61
|
+
`tests/level1/test_query_timeout.py::TestInsertStatementTimeout`.
|
|
62
|
+
|
|
63
|
+
**A statement the SERVER cancelled to break a lock collision is re-issued — the
|
|
64
|
+
second and last replay-safe failure, and it covers READS.** SQLSTATE `40P01`
|
|
65
|
+
(`deadlock_detected`), `40001` (`serialization_failure`) and `55P03`
|
|
66
|
+
(`lock_not_available`) are the statuses where Postgres aborted the statement IN
|
|
67
|
+
FULL: the effects are gone with certainty, so a replay is exactly-once, not the
|
|
68
|
+
at-least-once hazard that makes a statement timeout or a mid-query disconnect
|
|
69
|
+
unretryable. Class-40 siblings stay out by name — `40003`
|
|
70
|
+
(`StatementCompletionUnknownError`) is literally "the server cannot say whether
|
|
71
|
+
this completed", and `40002` is a constraint failure wearing a class-40 code.
|
|
72
|
+
The victim is often innocent of any write: a multi-table READ is cancelled
|
|
73
|
+
whenever a concurrent DDL session wants AccessExclusiveLock on a table it already
|
|
74
|
+
holds a lock on, which reached a person as "listConversations: deadlock detected"
|
|
75
|
+
on /chat.
|
|
76
|
+
|
|
77
|
+
🚨 **THERE IS ONE LOOP AND ONE BUDGET.** `matrx_orm/core/write_retry.py` owns the
|
|
78
|
+
classification (`is_confidently_aborted_lock_contention`), the knob policy, and
|
|
79
|
+
the loop (`run_with_lock_retry`); both doors into the database run through it —
|
|
80
|
+
`execute_query` for every statement and `call_function` for RPCs (an RPC reaches
|
|
81
|
+
Postgres on a raw connection, bypassing `execute_query` entirely, so before this a
|
|
82
|
+
deadlock inside any `SECURITY DEFINER` write RPC failed its caller outright).
|
|
83
|
+
`_execute_with_retry` retries only the nothing-was-sent classes above it and must
|
|
84
|
+
never grow a lock budget of its own: on 2026-09-12 two independent lock-retry
|
|
85
|
+
mechanisms landed within hours and nested, making the real ceiling the product of
|
|
86
|
+
two budgets nobody had chosen. Statement-level replay is sound only because both
|
|
87
|
+
doors return early when a `transaction()` is active: every attempt the loop
|
|
88
|
+
retries WAS its own transaction. Inside a caller's transaction the rollback took
|
|
89
|
+
the whole block and only that transaction's owner can redo the unit, so the
|
|
90
|
+
refusal is typed `LockContentionError(in_transaction=True)`.
|
|
91
|
+
|
|
92
|
+
The budget is `platform.feature_knob` feature `orm.write_retry`
|
|
93
|
+
(`deadlock_max_retries` 4, `deadlock_backoff_base_ms` 25,
|
|
94
|
+
`deadlock_backoff_max_ms` 400 — seeded by
|
|
95
|
+
`db/migrations/0647_orm_write_retry_knobs.sql`, re-read every 60s, not
|
|
96
|
+
org-overridable). Backoff is FULL jitter: the victims are N invocations that
|
|
97
|
+
started together, so a fixed sleep re-collides them in lockstep. Every retry and
|
|
98
|
+
every heal is logged `[lock-retry]`, and when the budget is spent the
|
|
99
|
+
`LockContentionError` NAMES the attempts — and points at the lock ORDER
|
|
100
|
+
(`Model.update_where(..., lock_rows_in_pk_order=True)`) rather than a bigger
|
|
101
|
+
retry count. A database whose knob rows cannot be read (a standalone install on a
|
|
102
|
+
bare Postgres) runs on the recorded starting values and logs one ERROR naming the
|
|
103
|
+
remedy — a raise here would turn a survivable deadlock into a hard failure on the
|
|
104
|
+
very path that exists to survive it. Guards:
|
|
105
|
+
`tests/level1/test_lock_contention_retry.py` (the loop's semantics per statement)
|
|
106
|
+
+ `tests/level1/test_deadlock_retry.py` (the knob budget, the RPC door, the lock
|
|
107
|
+
order).
|
|
108
|
+
|
|
48
109
|
`AsyncDatabaseManager.execute_query` must classify every PostgreSQL integrity
|
|
49
110
|
constraint violation as `IntegrityError`. Known categories retain their useful
|
|
50
111
|
labels (`unique`, `foreign_key`, `not_null`, `check`, `exclusion`, and
|
|
@@ -220,6 +281,20 @@ can never disagree about who the blocker was.
|
|
|
220
281
|
Verified against the live database: 2026-08-21.
|
|
221
282
|
|
|
222
283
|
## Change log
|
|
284
|
+
- 2026-09-12 (later): ONE retry mechanism. Two independent lock-retry
|
|
285
|
+
implementations for the same class landed hours apart — a `LockContentionError`
|
|
286
|
+
+ constant-budget loop inside `execute_query`, and a `TransientTransactionError`
|
|
287
|
+
+ knob-budget loop wrapped around `_execute_with_retry` — and merged into
|
|
288
|
+
nesting. Reconciled to: `LockContentionError` survives (it covers reads and
|
|
289
|
+
55P03 too), the loop lives once in `write_retry.run_with_lock_retry` and is used
|
|
290
|
+
by `execute_query` AND `call_function`, its budget is the `orm.write_retry`
|
|
291
|
+
knob rows (never a constant), and `TransientTransactionError` is deleted.
|
|
292
|
+
- 2026-09-12: A statement Postgres cancelled to break a lock collision
|
|
293
|
+
(40P01/40001/55P03) is re-issued on a fresh connection instead of failing the
|
|
294
|
+
caller. Before this it fell to the catch-all as `UnknownDatabaseError` and
|
|
295
|
+
killed the request: four parallel `ai.agent.start` invocations deadlocked on
|
|
296
|
+
`users.user_secrets` and errored a whole workflow run, and a pure `/chat`
|
|
297
|
+
conversation-list READ showed "deadlock detected" to a person.
|
|
223
298
|
- 2026-08-30: Classified server restart/failover rejection during pool checkout
|
|
224
299
|
as retryable connection failure before any caller SQL is sent.
|
|
225
300
|
- 2026-08-24: Recovered read-only disconnects masked by empty-read reproof's
|
|
@@ -6,7 +6,6 @@ from dataclasses import dataclass
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
|
-
import random
|
|
10
9
|
import traceback
|
|
11
10
|
from contextlib import asynccontextmanager
|
|
12
11
|
from typing import Any
|
|
@@ -44,6 +43,15 @@ from matrx_orm.exceptions import (
|
|
|
44
43
|
StateError,
|
|
45
44
|
UnknownDatabaseError,
|
|
46
45
|
)
|
|
46
|
+
from matrx_orm.core.write_retry import (
|
|
47
|
+
CONTENTION_EXC as _CONTENTION_EXC,
|
|
48
|
+
)
|
|
49
|
+
from matrx_orm.core.write_retry import (
|
|
50
|
+
is_confidently_aborted_lock_contention as _is_confidently_aborted_lock_contention,
|
|
51
|
+
)
|
|
52
|
+
from matrx_orm.core.write_retry import (
|
|
53
|
+
run_with_lock_retry,
|
|
54
|
+
)
|
|
47
55
|
|
|
48
56
|
logger = logging.getLogger("matrx_orm.async_db_manager")
|
|
49
57
|
|
|
@@ -75,24 +83,6 @@ _SCHEMA_MISMATCH_EXC = (
|
|
|
75
83
|
asyncpg.exceptions.InvalidSchemaNameError,
|
|
76
84
|
)
|
|
77
85
|
|
|
78
|
-
# Cancelled to break a lock collision. ``TransactionRollbackError`` is an
|
|
79
|
-
# umbrella for every SQLSTATE 40xxx, including 40003 (completion unknown) and
|
|
80
|
-
# 40002 (transaction integrity constraint violation). Only the three explicit
|
|
81
|
-
# statuses below establish that PostgreSQL aborted this statement in full.
|
|
82
|
-
_CONTENTION_EXC = (
|
|
83
|
-
asyncpg.exceptions.TransactionRollbackError,
|
|
84
|
-
asyncpg.exceptions.LockNotAvailableError,
|
|
85
|
-
)
|
|
86
|
-
_CONTENTION_SQLSTATES = frozenset({"40P01", "40001", "55P03"})
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _is_confidently_aborted_lock_contention(exc: BaseException) -> bool:
|
|
90
|
-
"""Whether ``exc`` proves a standalone statement can be re-issued."""
|
|
91
|
-
return (
|
|
92
|
-
isinstance(exc, _CONTENTION_EXC)
|
|
93
|
-
and getattr(exc, "sqlstate", None) in _CONTENTION_SQLSTATES
|
|
94
|
-
)
|
|
95
|
-
|
|
96
86
|
# PostgreSQL emits these SQLSTATE class-57 errors while asyncpg is opening a
|
|
97
87
|
# physical connection during a server restart/failover. They happen before a
|
|
98
88
|
# checkout exists, so no caller SQL was sent and a replay-safe owner may retry.
|
|
@@ -156,11 +146,13 @@ READ_TIMEOUT_RETRY_BACKOFF = 0.25 # seconds before the read retry
|
|
|
156
146
|
# traffic). On 2026-09-12 that reached a person as the banner
|
|
157
147
|
# "listConversations: deadlock detected" on /chat, and ops.app_log shows the same
|
|
158
148
|
# class crashing POST /coding-sessions/bridge with a 500. Bounded and LOUD: real
|
|
159
|
-
# contention still surfaces, it just costs
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
|
|
149
|
+
# contention still surfaces, it just costs extra attempts first.
|
|
150
|
+
#
|
|
151
|
+
# The budget is NOT a constant here: it is the ``orm.write_retry``
|
|
152
|
+
# ``platform.feature_knob`` rows an admin turns, read by
|
|
153
|
+
# ``matrx_orm.core.write_retry``, which also owns THE ONE retry loop
|
|
154
|
+
# (``run_with_lock_retry``) that this door and the RPC door (``call_function``)
|
|
155
|
+
# both run through. Never grow a second loop or a second budget.
|
|
164
156
|
|
|
165
157
|
# ── idle-in-transaction ceiling (the platform-wide DDL blocker) ───────────────
|
|
166
158
|
# A session that BEGINs and then goes quiet is the single most expensive thing a
|
|
@@ -1330,23 +1322,18 @@ class AsyncDatabaseManager:
|
|
|
1330
1322
|
|
|
1331
1323
|
read_only = is_read_only_sql(query)
|
|
1332
1324
|
|
|
1333
|
-
async
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1325
|
+
async def _attempt() -> list[dict[str, Any]]:
|
|
1326
|
+
"""One full try of this statement, on its own fresh connection.
|
|
1327
|
+
|
|
1328
|
+
Re-issuing it after a lock collision is NOT decided here: a raw
|
|
1329
|
+
asyncpg contention error escapes unwrapped so THE ONE retry loop
|
|
1330
|
+
(``run_with_lock_retry``, knob-driven) owns that budget for this door
|
|
1331
|
+
and the RPC door alike.
|
|
1332
|
+
"""
|
|
1341
1333
|
# A timed-out READ retries ONCE on a fresh connection (self-heal for
|
|
1342
1334
|
# a starvation-induced command_timeout). Each iteration re-acquires,
|
|
1343
1335
|
# so a poisoned connection is never reused. Writes never loop here.
|
|
1344
|
-
# Two independent budgets, each re-acquiring a FRESH connection: a
|
|
1345
|
-
# timed-out read (below) and a statement Postgres cancelled to break
|
|
1346
|
-
# a lock collision (which applies to writes too — the victim is
|
|
1347
|
-
# rolled back in full).
|
|
1348
1336
|
read_attempt = 0
|
|
1349
|
-
contention_attempt = 0
|
|
1350
1337
|
while True:
|
|
1351
1338
|
async with cls.get_connection(config_name, timeout) as conn:
|
|
1352
1339
|
try:
|
|
@@ -1358,14 +1345,6 @@ class AsyncDatabaseManager:
|
|
|
1358
1345
|
read_attempt + 1,
|
|
1359
1346
|
query[:120],
|
|
1360
1347
|
)
|
|
1361
|
-
if contention_attempt > 0:
|
|
1362
|
-
logger.warning(
|
|
1363
|
-
"[lock-retry] HEALED on attempt %d after Postgres "
|
|
1364
|
-
"cancelled the statement to break a lock collision "
|
|
1365
|
-
"(rolled back, safe to re-run): %s",
|
|
1366
|
-
contention_attempt + 1,
|
|
1367
|
-
query[:120],
|
|
1368
|
-
)
|
|
1369
1348
|
if (
|
|
1370
1349
|
not results
|
|
1371
1350
|
and verify_empty
|
|
@@ -1387,36 +1366,19 @@ class AsyncDatabaseManager:
|
|
|
1387
1366
|
if true_rows is not None:
|
|
1388
1367
|
return true_rows
|
|
1389
1368
|
return [_normalize_record(r) for r in results]
|
|
1390
|
-
except _CONTENTION_EXC
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
#
|
|
1394
|
-
# on a
|
|
1395
|
-
#
|
|
1396
|
-
#
|
|
1397
|
-
#
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
"collision (%s) — re-running in %.3fs: %s",
|
|
1404
|
-
contention_attempt + 1,
|
|
1405
|
-
1 + CONTENTION_RETRIES,
|
|
1406
|
-
getattr(e, "sqlstate", None) or e.__class__.__name__,
|
|
1407
|
-
delay,
|
|
1408
|
-
query[:120],
|
|
1409
|
-
)
|
|
1410
|
-
await asyncio.sleep(delay)
|
|
1411
|
-
contention_attempt += 1
|
|
1412
|
-
continue
|
|
1413
|
-
raise LockContentionError(
|
|
1414
|
-
original_error=e,
|
|
1415
|
-
query=query,
|
|
1416
|
-
args=args,
|
|
1417
|
-
config_name=config_name,
|
|
1418
|
-
attempts=1 + contention_attempt,
|
|
1419
|
-
) from None
|
|
1369
|
+
except _CONTENTION_EXC:
|
|
1370
|
+
# Rolled back in full (40P01/40001/55P03) → the SAME
|
|
1371
|
+
# statement is provably safe to re-issue, writes
|
|
1372
|
+
# included, and ``run_with_lock_retry`` around this
|
|
1373
|
+
# attempt does it on a FRESH connection with the knob
|
|
1374
|
+
# budget and full jitter. Raised RAW on purpose: this
|
|
1375
|
+
# clause exists only to keep the catch-all below from
|
|
1376
|
+
# wrapping it as UnknownDatabaseError, which is what made
|
|
1377
|
+
# the class unretryable and cost a live workflow run
|
|
1378
|
+
# ($0.35, Newsroom Desk, 2026-09-12). An ambiguous
|
|
1379
|
+
# class-40 status (40002/40003) leaves here the same way
|
|
1380
|
+
# and the loop declines to retry it.
|
|
1381
|
+
raise
|
|
1420
1382
|
except asyncpg.exceptions.PostgresSyntaxError as e:
|
|
1421
1383
|
raise QueryError(
|
|
1422
1384
|
model=None,
|
|
@@ -1565,6 +1527,21 @@ class AsyncDatabaseManager:
|
|
|
1565
1527
|
original_error=e,
|
|
1566
1528
|
) from e
|
|
1567
1529
|
|
|
1530
|
+
# handle_orm_operation stays OUTSIDE the retry loop: it wraps a non-ORM
|
|
1531
|
+
# exception as CacheError, which would hide the raw asyncpg contention
|
|
1532
|
+
# error the loop classifies on.
|
|
1533
|
+
async with handle_orm_operation(
|
|
1534
|
+
operation_name="execute_query",
|
|
1535
|
+
model=None,
|
|
1536
|
+
config_name=config_name,
|
|
1537
|
+
query=query,
|
|
1538
|
+
args=args,
|
|
1539
|
+
timeout=timeout,
|
|
1540
|
+
):
|
|
1541
|
+
return await run_with_lock_retry(
|
|
1542
|
+
_attempt, config_name=config_name, query=query, args=args
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1568
1545
|
@classmethod
|
|
1569
1546
|
async def execute_write(
|
|
1570
1547
|
cls,
|
|
@@ -1573,6 +1550,7 @@ class AsyncDatabaseManager:
|
|
|
1573
1550
|
*args: Any,
|
|
1574
1551
|
timeout: float = 10.0,
|
|
1575
1552
|
max_retries: int = 2,
|
|
1553
|
+
statement_timeout: float | None = None,
|
|
1576
1554
|
) -> list[dict[str, Any]]:
|
|
1577
1555
|
"""Execute a write query, routing through the write queue when enabled.
|
|
1578
1556
|
|
|
@@ -1604,7 +1582,13 @@ class AsyncDatabaseManager:
|
|
|
1604
1582
|
from matrx_orm.core.transaction import get_active_connection
|
|
1605
1583
|
|
|
1606
1584
|
if get_active_connection() is not None:
|
|
1607
|
-
return await cls.execute_query(
|
|
1585
|
+
return await cls.execute_query(
|
|
1586
|
+
config_name,
|
|
1587
|
+
query,
|
|
1588
|
+
*args,
|
|
1589
|
+
timeout=timeout,
|
|
1590
|
+
statement_timeout=statement_timeout,
|
|
1591
|
+
)
|
|
1608
1592
|
except ImportError:
|
|
1609
1593
|
pass
|
|
1610
1594
|
|
|
@@ -1633,9 +1617,17 @@ class AsyncDatabaseManager:
|
|
|
1633
1617
|
if session_context_gucs():
|
|
1634
1618
|
from matrx_orm.core.transaction import transaction
|
|
1635
1619
|
|
|
1620
|
+
config_name = resolve_database_name(config_name)
|
|
1621
|
+
|
|
1636
1622
|
async def _declared_write() -> list[dict[str, Any]]:
|
|
1637
1623
|
async with transaction(config_name):
|
|
1638
|
-
return await cls.execute_query(
|
|
1624
|
+
return await cls.execute_query(
|
|
1625
|
+
config_name,
|
|
1626
|
+
query,
|
|
1627
|
+
*args,
|
|
1628
|
+
timeout=timeout,
|
|
1629
|
+
statement_timeout=statement_timeout,
|
|
1630
|
+
)
|
|
1639
1631
|
|
|
1640
1632
|
if wq is None:
|
|
1641
1633
|
return await _declared_write()
|
|
@@ -1652,11 +1644,18 @@ class AsyncDatabaseManager:
|
|
|
1652
1644
|
if wq is not None:
|
|
1653
1645
|
return await wq.submit(
|
|
1654
1646
|
lambda: cls._execute_with_retry(
|
|
1655
|
-
config_name,
|
|
1647
|
+
config_name,
|
|
1648
|
+
query,
|
|
1649
|
+
*args,
|
|
1650
|
+
timeout=timeout,
|
|
1651
|
+
max_retries=max_retries,
|
|
1652
|
+
statement_timeout=statement_timeout,
|
|
1656
1653
|
)
|
|
1657
1654
|
)
|
|
1658
1655
|
|
|
1659
|
-
return await cls.execute_query(
|
|
1656
|
+
return await cls.execute_query(
|
|
1657
|
+
config_name, query, *args, timeout=timeout, statement_timeout=statement_timeout
|
|
1658
|
+
)
|
|
1660
1659
|
|
|
1661
1660
|
@classmethod
|
|
1662
1661
|
async def _execute_with_retry(
|
|
@@ -1666,12 +1665,21 @@ class AsyncDatabaseManager:
|
|
|
1666
1665
|
*args: Any,
|
|
1667
1666
|
timeout: float = 10.0,
|
|
1668
1667
|
max_retries: int = 2,
|
|
1668
|
+
statement_timeout: float | None = None,
|
|
1669
1669
|
) -> list[dict[str, Any]]:
|
|
1670
1670
|
"""Execute a query with exponential-backoff retry — write-safe.
|
|
1671
1671
|
|
|
1672
|
-
Retries
|
|
1673
|
-
``PoolAcquireTimeoutError`` (no connection acquired) and
|
|
1674
|
-
``ReadOnlyTransactionError`` (PostgreSQL refused the statement
|
|
1672
|
+
Retries exactly ONE class here — **nothing was sent**:
|
|
1673
|
+
``PoolAcquireTimeoutError`` (no connection was ever acquired) and
|
|
1674
|
+
``ReadOnlyTransactionError`` (PostgreSQL refused the statement before
|
|
1675
|
+
executing it). Both leave the database provably untouched.
|
|
1676
|
+
|
|
1677
|
+
The other provably-safe class — **the server rolled it back itself**
|
|
1678
|
+
(SQLSTATE 40P01 / 40001 / 55P03) — is retried ONE LAYER DOWN, inside
|
|
1679
|
+
``execute_query``, by THE ONE lock-retry loop on the ``orm.write_retry``
|
|
1680
|
+
knob budget (:mod:`matrx_orm.core.write_retry`); it covers reads as well
|
|
1681
|
+
as writes and surfaces as ``LockContentionError`` when spent. This loop
|
|
1682
|
+
must never grow a second budget for it.
|
|
1675
1683
|
|
|
1676
1684
|
Every other transient-looking error is propagated immediately:
|
|
1677
1685
|
|
|
@@ -1692,18 +1700,29 @@ class AsyncDatabaseManager:
|
|
|
1692
1700
|
``"timeout" in str(exc).lower() or isinstance(exc, ConnectionError)``
|
|
1693
1701
|
treated all four classes above as "transient" and retried them,
|
|
1694
1702
|
which silently produced cx_request_pkey / cx_message_pkey
|
|
1695
|
-
duplicate-key errors under burst load.
|
|
1696
|
-
structurally extinct
|
|
1697
|
-
|
|
1703
|
+
duplicate-key errors under burst load. That class of failure is now
|
|
1704
|
+
structurally extinct: a retry fires only when no SQL was sent
|
|
1705
|
+
(``PoolAcquireTimeoutError`` — see its docstring for the contract), or,
|
|
1706
|
+
a layer down, when the server states it rolled the statement back.
|
|
1707
|
+
Never on "the error message contains the word timeout".
|
|
1698
1708
|
"""
|
|
1699
|
-
last_exc: BaseException | None = None
|
|
1700
1709
|
for attempt in range(1 + max_retries):
|
|
1701
1710
|
try:
|
|
1702
|
-
|
|
1711
|
+
# A lock collision is NOT retried here: ``execute_query`` already
|
|
1712
|
+
# re-issues the statement on the ONE knob-driven loop
|
|
1713
|
+
# (``run_with_lock_retry``), one layer down and for reads too. A
|
|
1714
|
+
# second budget living here is the duplicate this was reconciled
|
|
1715
|
+
# out of.
|
|
1716
|
+
return await cls.execute_query(
|
|
1717
|
+
config_name,
|
|
1718
|
+
query,
|
|
1719
|
+
*args,
|
|
1720
|
+
timeout=timeout,
|
|
1721
|
+
statement_timeout=statement_timeout,
|
|
1722
|
+
)
|
|
1703
1723
|
except (PoolAcquireTimeoutError, ReadOnlyTransactionError) as exc:
|
|
1704
1724
|
if attempt >= max_retries:
|
|
1705
1725
|
raise
|
|
1706
|
-
last_exc = exc
|
|
1707
1726
|
backoff = 0.5 * (2**attempt)
|
|
1708
1727
|
logger.info(
|
|
1709
1728
|
"[matrx_orm] Write retry %d/%d for '%s' after %s (backoff %.1fs)",
|
|
@@ -1715,9 +1734,10 @@ class AsyncDatabaseManager:
|
|
|
1715
1734
|
)
|
|
1716
1735
|
await asyncio.sleep(backoff)
|
|
1717
1736
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1737
|
+
# Unreachable: the handler above re-raises on its last attempt rather than
|
|
1738
|
+
# falling through. Kept explicit so no future edit can turn a spent retry
|
|
1739
|
+
# budget into an empty result that reads as "the UPDATE matched no rows".
|
|
1740
|
+
raise AssertionError("write retry loop fell through without a result")
|
|
1721
1741
|
|
|
1722
1742
|
@classmethod
|
|
1723
1743
|
async def cleanup(cls):
|
|
@@ -658,8 +658,18 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
658
658
|
)
|
|
659
659
|
|
|
660
660
|
@classmethod
|
|
661
|
-
async def create(cls, **kwargs: Any) -> Self:
|
|
662
|
-
|
|
661
|
+
async def create(cls, *, statement_timeout: float | None = None, **kwargs: Any) -> Self:
|
|
662
|
+
"""INSERT one row.
|
|
663
|
+
|
|
664
|
+
``statement_timeout`` (seconds, keyword-only) overrides the pool's
|
|
665
|
+
``command_timeout`` for THIS insert only — the write-side twin of
|
|
666
|
+
``QueryBuilder.timeout()``. Use it when the row's size is known to need
|
|
667
|
+
longer than the pool default; it never widens the pool-acquire timeout
|
|
668
|
+
and never affects another statement.
|
|
669
|
+
"""
|
|
670
|
+
instance = await create.create_instance(
|
|
671
|
+
model_cls=cls, statement_timeout=statement_timeout, **kwargs
|
|
672
|
+
)
|
|
663
673
|
await StateManager.cache(cls, instance)
|
|
664
674
|
return cast(Self, instance)
|
|
665
675
|
|
|
@@ -980,12 +990,22 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
980
990
|
filters: dict[str, Any],
|
|
981
991
|
*,
|
|
982
992
|
expected_version: int | None = None,
|
|
993
|
+
lock_rows_in_pk_order: bool = False,
|
|
983
994
|
**updates: Any,
|
|
984
995
|
) -> UpdateResult:
|
|
985
996
|
"""Bulk-update rows matching filters without fetching them first.
|
|
986
997
|
|
|
987
998
|
Args:
|
|
988
999
|
filters: Lookup kwargs (same syntax as filter()).
|
|
1000
|
+
lock_rows_in_pk_order: take the row locks ascending by primary key
|
|
1001
|
+
instead of in plan-scan order, by routing the WHERE through
|
|
1002
|
+
``... IN (SELECT pk ... ORDER BY pk FOR UPDATE)``. Pass it on
|
|
1003
|
+
any multi-row UPDATE that concurrent callers can run over an
|
|
1004
|
+
OVERLAPPING row set — without it the lock order is whatever the
|
|
1005
|
+
plan scans (and heap order moves as the updates themselves
|
|
1006
|
+
rewrite rows), which is how two identical statements deadlock
|
|
1007
|
+
(SQLSTATE 40P01). Needs a single-column primary key. Details
|
|
1008
|
+
and the incident: ``QueryBuilder.lock_rows_in_pk_order``.
|
|
989
1009
|
expected_version: opt-in optimistic concurrency — compare-and-swap
|
|
990
1010
|
against the canonical ``version`` column. Requires the filters
|
|
991
1011
|
to address ONE row (every primary-key column present as a plain
|
|
@@ -1006,6 +1026,13 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1006
1026
|
print(result.rows_affected)
|
|
1007
1027
|
"""
|
|
1008
1028
|
if expected_version is not None:
|
|
1029
|
+
if lock_rows_in_pk_order:
|
|
1030
|
+
raise ValueError(
|
|
1031
|
+
f"{cls.__name__}.update_where(expected_version=..., "
|
|
1032
|
+
f"lock_rows_in_pk_order=True) is contradictory: a "
|
|
1033
|
+
f"compare-and-swap already addresses exactly ONE row, so "
|
|
1034
|
+
f"there is no lock ORDER to fix."
|
|
1035
|
+
)
|
|
1009
1036
|
missing = [pk for pk in cls._meta.primary_keys if pk not in filters]
|
|
1010
1037
|
if missing:
|
|
1011
1038
|
raise ValueError(
|
|
@@ -1016,7 +1043,10 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1016
1043
|
return await update.guarded_update(
|
|
1017
1044
|
cls, dict(filters), dict(updates), expected_version=expected_version
|
|
1018
1045
|
)
|
|
1019
|
-
|
|
1046
|
+
qb = QueryBuilder(cls).filter(**filters)
|
|
1047
|
+
if lock_rows_in_pk_order:
|
|
1048
|
+
qb = qb.lock_rows_in_pk_order()
|
|
1049
|
+
return await qb.update(**updates)
|
|
1020
1050
|
|
|
1021
1051
|
@classmethod
|
|
1022
1052
|
async def delete_where(cls, *, expected_version: int | None = None, **filters: Any) -> int:
|
|
@@ -69,7 +69,11 @@ from typing import Any, Literal
|
|
|
69
69
|
|
|
70
70
|
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
71
71
|
from matrx_orm.core.transaction import get_active_connection
|
|
72
|
-
from matrx_orm.
|
|
72
|
+
from matrx_orm.core.write_retry import (
|
|
73
|
+
is_confidently_aborted_lock_contention,
|
|
74
|
+
run_with_lock_retry,
|
|
75
|
+
)
|
|
76
|
+
from matrx_orm.exceptions import LockContentionError, QueryTimeoutError
|
|
73
77
|
|
|
74
78
|
__all__ = ["call_function", "call_function_sync", "ArrayArg"]
|
|
75
79
|
|
|
@@ -263,10 +267,33 @@ async def call_function( # noqa: ASYNC109 — maps onto asyncpg's per-call time
|
|
|
263
267
|
|
|
264
268
|
active_conn = get_active_connection()
|
|
265
269
|
if active_conn is not None:
|
|
266
|
-
|
|
270
|
+
# Inside a caller's transaction a rollback took the whole block; only the
|
|
271
|
+
# transaction's owner can redo the unit, so no retry here — the refusal is
|
|
272
|
+
# typed the same way ``execute_query``'s transaction branch types it.
|
|
273
|
+
try:
|
|
274
|
+
return await invoke(active_conn)
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
if not is_confidently_aborted_lock_contention(exc):
|
|
277
|
+
raise
|
|
278
|
+
raise LockContentionError(
|
|
279
|
+
original_error=exc,
|
|
280
|
+
query=sql,
|
|
281
|
+
args=bound_args,
|
|
282
|
+
config_name=database,
|
|
283
|
+
attempts=1,
|
|
284
|
+
in_transaction=True,
|
|
285
|
+
) from None
|
|
286
|
+
|
|
287
|
+
async def _attempt() -> Any:
|
|
288
|
+
async with AsyncDatabaseManager.get_connection(database) as conn:
|
|
289
|
+
return await invoke(conn)
|
|
267
290
|
|
|
268
|
-
|
|
269
|
-
|
|
291
|
+
# An RPC reaches Postgres on a RAW connection, bypassing execute_query
|
|
292
|
+
# entirely, so it needs THE ONE loop explicitly — otherwise a deadlock inside
|
|
293
|
+
# any SECURITY DEFINER write RPC still costs the caller its request.
|
|
294
|
+
return await run_with_lock_retry(
|
|
295
|
+
_attempt, config_name=database, query=sql, args=bound_args
|
|
296
|
+
)
|
|
270
297
|
|
|
271
298
|
|
|
272
299
|
def call_function_sync(
|