matrx-orm 3.1.199__tar.gz → 3.1.201__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.199 → matrx_orm-3.1.201}/CHANGELOG.md +5 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/PKG-INFO +1 -1
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/__init__.py +2 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/FEATURE.md +52 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/async_db_manager.py +34 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/pooler_role.py +64 -0
- matrx_orm-3.1.201/matrx_orm/core/read_write.py +221 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/rls_session.py +22 -31
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/session_context.py +35 -2
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/transaction.py +53 -10
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/pytest_lock_guard.py +12 -4
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/flush.py +2 -3
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/write_actor.py +2 -3
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/pyproject.toml +1 -1
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_association_trigger_actor_naming.py +3 -3
- matrx_orm-3.1.201/tests/level1/test_read_write_declared.py +108 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_rls_session_scope.py +39 -21
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_transaction_database_identity.py +7 -3
- matrx_orm-3.1.201/tests/level1/test_transaction_gucs_one_round_trip.py +91 -0
- matrx_orm-3.1.201/tests/level1/test_transaction_pool_release_sends_nothing.py +93 -0
- matrx_orm-3.1.201/tests/test_a_backend_handed_over_mid_transaction_never_takes_our_writes.py +311 -0
- matrx_orm-3.1.201/tests/test_a_read_only_backend_never_refuses_our_writes.py +187 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_pooler_role_guard.py +111 -4
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/.gitignore +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/AGENTS.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/CLAUDE.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/MODEL_API.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/docs/migrations.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/FEATURE.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/admin/FEATURE.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/admin_lane.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/statement_ceiling.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/core/write_retry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/local_sqlite.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/no_org_assignment.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/FEATURE.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/catalog_select.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/platform_access.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/pytest_live_db_guard.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/pytest_plugin.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/resources/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/resources/additional-assigners-2026-09-12.json +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/resources/org_assignment_trigger_baseline.json +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/backup_restore.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/host_authorization.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/mutation_receipts.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/native_enable.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/native_lifecycle.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/native_passkeys.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/native_source.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/org_vault_gate.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/secrets_battery/vault_history.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/scripts/release.sh +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/conftest.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_adapter_registry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_admin_response_rendering.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_associations_wiring.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_column_scoped_writes.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_committed_row_guard.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_count_limit_param_leak.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_deadlock_retry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_fk_cycle_resolution.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_local_sqlite.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_lock_contention_retry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_org_assignment_inventory.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_orm_banners_are_client_safe.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_percentile_cont.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_persisted_count.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_person_scoped_session_bypasses_shared_cache.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_pool_create_never_wedges.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_read_connection_retry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_standalone_write_session_context.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_subquery_nested_expressions.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_write_queue_deadline.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/level2/verify_committed_row_guard_pg17.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/native_source_cases.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/native_source_oracle.rs +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_enum_ordering.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_schema_analysis.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/schema_builder/test_never_filter_columns.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_a_failed_capture_still_names_the_failure.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_live_db_guard_plugin.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_record_error_bounded_insert.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_record_error_storm.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/session/test_spill_guard_plugin.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_array_arg_element_type.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_attachment_mutation_serialization.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_backup_alias_conflict.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_backup_history_restore.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_backup_restore_live.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_call_function_as.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_call_function_none_mode.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_composite_fk_is_not_a_column_fk.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_dsn_password_masking.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_dynamic_select_unregistered.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_immutable_insert_or_exact_verify.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_keyed_digest.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_native_enable.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_native_lifecycle.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_native_lifecycle_fences.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_native_passkey_receipts.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_native_source.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_order_by_desc_nulls_last.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_org_vault_gate.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_secret_audit_names_its_organization.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_secrets_minting_keys.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_shared_lock_guard.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_statement_ceiling_warning.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_sync_twins_guard.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_update_result_truthiness.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_user_visible_lookup.py +0 -0
- {matrx_orm-3.1.199 → matrx_orm-3.1.201}/tests/test_vault_aggregate_recovery_stateful.py +0 -0
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- **Fix: the shared-lock sampler no longer leaves its timeouts on a pooled backend.**
|
|
6
|
+
`pytest_lock_guard` sent session-level `set lock_timeout` / `set statement_timeout`
|
|
7
|
+
through the transaction pooler, where they stay on the backend for the next client
|
|
8
|
+
(both found leaked on the clone's pool, 2026-10-01). It now uses `SET LOCAL` inside
|
|
9
|
+
each sample's own transaction.
|
|
5
10
|
- **Fix: a driver timeout longer than the server's statement ceiling screams.**
|
|
6
11
|
`call_function` / `call_function_as` / `execute_query` read the connection's
|
|
7
12
|
effective `statement_timeout` and log one WARNING per call site when the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.201
|
|
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
|
|
@@ -298,6 +298,7 @@ from .core.transaction import (
|
|
|
298
298
|
advisory_unlock,
|
|
299
299
|
advisory_xact_lock,
|
|
300
300
|
set_local_guc,
|
|
301
|
+
set_local_gucs,
|
|
301
302
|
transaction,
|
|
302
303
|
try_advisory_lock,
|
|
303
304
|
try_advisory_xact_lock,
|
|
@@ -697,6 +698,7 @@ __all__ = [
|
|
|
697
698
|
"transaction",
|
|
698
699
|
"acquire_connection",
|
|
699
700
|
"set_local_guc",
|
|
701
|
+
"set_local_gucs",
|
|
700
702
|
"SessionContextProvider",
|
|
701
703
|
"ACTOR_AGENT_GUC",
|
|
702
704
|
"ACTOR_SYSTEM_GUC",
|
|
@@ -261,6 +261,55 @@ pinned server session's identity before and after `RESET SESSION AUTHORIZATION`
|
|
|
261
261
|
borrowed role. This also repairs both `SET ROLE` and `SET SESSION AUTHORIZATION`;
|
|
262
262
|
`RESET ROLE` alone does not repair the latter.
|
|
263
263
|
|
|
264
|
+
**A clean, genuinely empty read is proved in ONE statement.** Before the pinned
|
|
265
|
+
re-proof, `prove_empty_in_one_statement` runs `current_user`, `session_user`, the
|
|
266
|
+
backend's login role (`pg_stat_activity.usename`, which `SET SESSION AUTHORIZATION`
|
|
267
|
+
does not change) and `EXISTS (<query>)` as one statement — one server connection,
|
|
268
|
+
one role. All three identities equal and no row: proven empty, one round trip
|
|
269
|
+
instead of seven. Any other outcome (borrowed role, session-authorization swap,
|
|
270
|
+
unreadable login, rows that appeared, a query that cannot embed, any error) runs
|
|
271
|
+
the full repairing re-proof unchanged. Pinned by `tests/test_pooler_role_guard.py`.
|
|
272
|
+
|
|
273
|
+
**Round trips are the launch cost on a remote pooler.** Every top-level BEGIN sets
|
|
274
|
+
its ceilings, the host's declared session context and the opener's own settings
|
|
275
|
+
(`transaction(opening_gucs=…)`; `rls_session` passes claims + timeouts + role,
|
|
276
|
+
role LAST) in ONE `set_config` statement; `set_local_gucs` does the same inside a
|
|
277
|
+
transaction. A transaction-mode pool releases connections with no reset statement
|
|
278
|
+
(`_transaction_pool_release`); a session-mode pool keeps asyncpg's full reset.
|
|
279
|
+
Measured on one agent launch against the clone through the pooler (170 ms per round
|
|
280
|
+
trip from a laptop): 435 → ~200 round trips. Pinned by
|
|
281
|
+
`tests/level1/test_transaction_gucs_one_round_trip.py`,
|
|
282
|
+
`tests/level1/test_transaction_pool_release_sends_nothing.py` and
|
|
283
|
+
`tests/level1/test_rls_session_scope.py`.
|
|
284
|
+
|
|
285
|
+
**A backend another client left read-only never refuses our writes.** A transaction
|
|
286
|
+
pooler shares server connections with every client of the same login and never resets
|
|
287
|
+
one between clients, so a session-level `SET default_transaction_read_only = on` (or
|
|
288
|
+
`SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY`) sent outside a transaction — the
|
|
289
|
+
shape of an ad-hoc "read-only production" helper — leaves that backend read-only for
|
|
290
|
+
everyone (2026-10-01 incident: dispatchers, workflow claims, request audit, chat writes;
|
|
291
|
+
recurring since 2026-09-12). A transaction-mode pool's connections are
|
|
292
|
+
`ReadWriteConnection` (`read_write.py`): every top-level BEGIN naming no access mode is
|
|
293
|
+
sent as `... READ WRITE` (zero extra round trips), and an autocommit statement refused as
|
|
294
|
+
read-only re-runs once inside `BEGIN READ WRITE` with a WARNING naming the cause. An
|
|
295
|
+
explicit `READ ONLY` is kept; the session is never rewritten. Read-only work through the
|
|
296
|
+
pooler belongs in `BEGIN READ ONLY`, never a session SET. Pinned by
|
|
297
|
+
`tests/test_a_read_only_backend_never_refuses_our_writes.py` (loopback `ISOLATED_PG17_DSN`)
|
|
298
|
+
and `tests/level1/test_read_write_declared.py`.
|
|
299
|
+
|
|
300
|
+
**A backend handed over inside another client's transaction never takes our writes.** The
|
|
301
|
+
second shape of the same incident: a probe sent `begin read only; …; rollback;` through the
|
|
302
|
+
pooler, an error or timeout skipped the `rollback`, and the pooler handed the open READ ONLY
|
|
303
|
+
transaction to the server. The pooler answers our handshake "idle", so asyncpg learns of it
|
|
304
|
+
only from the reply. A statement that comes back inside a transaction this connection never
|
|
305
|
+
opened is the stranger's: rolled back, logged at ERROR with the remedy, re-run once in our own
|
|
306
|
+
`BEGIN READ WRITE … COMMIT` (the rollback undid it, so exactly once). A top-level BEGIN answered
|
|
307
|
+
with 25001 is the same: rolled back and begun again; a second stranger raises
|
|
308
|
+
`InheritedTransactionError` by name. Pinned by
|
|
309
|
+
`tests/test_a_backend_handed_over_mid_transaction_never_takes_our_writes.py`, which runs an
|
|
310
|
+
in-process transaction pooler that never resets its one server connection (loopback
|
|
311
|
+
`ISOLATED_PG17_DSN`; 4 of 5 fail on the code before it).
|
|
312
|
+
|
|
264
313
|
**The destructive live proof is loopback-only.**
|
|
265
314
|
`tests/live_pooler_role.py::assert_isolated_pooler_target` runs before DSN
|
|
266
315
|
construction or connection and refuses every shared or remote database. The proof
|
|
@@ -339,6 +388,9 @@ can never disagree about who the blocker was.
|
|
|
339
388
|
Verified against the live database: 2026-08-21.
|
|
340
389
|
|
|
341
390
|
## Change log
|
|
391
|
+
- 2026-10-01: `ReadWriteConnection` rolls back a stranger's open transaction it was handed (statement reply or 25001 on BEGIN), re-runs our statement in its own transaction, and names a repeat `InheritedTransactionError`.
|
|
392
|
+
- 2026-10-01: `ReadWriteConnection` for transaction-mode pools — every BEGIN declares READ WRITE and a read-only-refused autocommit statement re-runs once in one; a pooled backend another client left read-only at session level no longer fails the platform's writes.
|
|
393
|
+
- 2026-09-30: Fewer round trips per request (agent-launch latency, lane deep): one-statement proof of a clean empty read; ceilings + declared context + `rls_session` settings in ONE BEGIN-time `set_config` statement (`transaction(opening_gucs=)`, `set_local_gucs`, `emit_local_gucs`); transaction-mode pools release with no reset statement. 435 → ~200 round trips on a flashcard launch.
|
|
342
394
|
- 2026-09-30: `statement_ceiling.py` — a driver timeout above the server's statement ceiling is logged once per call site; `SERVER_CEILING` sentinel. ~19 callers passed 60-600 s under the 30 s `postgres` ceiling (the nightly keyword classifier was cancelled at 30 s for eight days).
|
|
343
395
|
- 2026-09-12 (later): ONE retry mechanism. Two independent lock-retry
|
|
344
396
|
implementations for the same class landed hours apart — a `LockContentionError`
|
|
@@ -25,6 +25,7 @@ from matrx_orm.core.pooler_role import (
|
|
|
25
25
|
reprove_empty_read,
|
|
26
26
|
)
|
|
27
27
|
from matrx_orm.core.statement_ceiling import resolve_driver_timeout
|
|
28
|
+
from matrx_orm.core.read_write import ReadWriteConnection
|
|
28
29
|
from matrx_orm.core.tls import resolve_asyncpg_ssl
|
|
29
30
|
from matrx_orm.exceptions import (
|
|
30
31
|
AdapterError,
|
|
@@ -421,6 +422,25 @@ class ConnectionIdentity:
|
|
|
421
422
|
return f"{cluster}/{self.database}"
|
|
422
423
|
|
|
423
424
|
|
|
425
|
+
async def _transaction_pool_release(_conn: Any) -> None:
|
|
426
|
+
"""Release a transaction-pooler connection with NO session reset statement.
|
|
427
|
+
|
|
428
|
+
asyncpg's default release runs ``pg_advisory_unlock_all(); CLOSE ALL;
|
|
429
|
+
UNLISTEN *; RESET ALL`` — one round trip on EVERY release. Behind a
|
|
430
|
+
transaction-mode pooler (Supavisor / PgBouncer, port 6543) that statement
|
|
431
|
+
lands on whichever server connection the pooler picks, so it cannot reset
|
|
432
|
+
anything the next statement from this client will see: there is no session
|
|
433
|
+
state of ours to reset (every GUC here is ``SET LOCAL``, LISTEN rides its own
|
|
434
|
+
session connection, cursors die with their transaction). It cost ~88 of the
|
|
435
|
+
~435 round trips of one agent launch (measured 2026-09-30, ~15 s at a
|
|
436
|
+
laptop's 170 ms). asyncpg still rolls back an open transaction before this
|
|
437
|
+
hook (``Connection._reset``), and a session-mode pool keeps the full default
|
|
438
|
+
reset. ``RESET ALL`` never reset a borrowed ROLE either (``role`` is
|
|
439
|
+
``GUC_NO_RESET_ALL``) — that defence is ``pooler_role``.
|
|
440
|
+
"""
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
|
|
424
444
|
class AsyncDatabaseManager:
|
|
425
445
|
_instance = None
|
|
426
446
|
_pools = {}
|
|
@@ -669,6 +689,20 @@ class AsyncDatabaseManager:
|
|
|
669
689
|
server_settings=server_settings,
|
|
670
690
|
init=_init_connection_default,
|
|
671
691
|
timeout=connect_timeout,
|
|
692
|
+
reset=(
|
|
693
|
+
_transaction_pool_release
|
|
694
|
+
if effective_mode == "transaction"
|
|
695
|
+
else None
|
|
696
|
+
),
|
|
697
|
+
# A transaction pooler hands us server connections other
|
|
698
|
+
# clients may have left read-only at SESSION level; every
|
|
699
|
+
# BEGIN declares READ WRITE and a refused autocommit
|
|
700
|
+
# statement re-runs once in one (core/read_write.py).
|
|
701
|
+
connection_class=(
|
|
702
|
+
ReadWriteConnection
|
|
703
|
+
if effective_mode == "transaction"
|
|
704
|
+
else asyncpg.Connection
|
|
705
|
+
),
|
|
672
706
|
)
|
|
673
707
|
try:
|
|
674
708
|
async with asyncio.timeout(create_timeout):
|
|
@@ -223,6 +223,61 @@ async def emit_contamination(event: EmptyReadContaminationEvent) -> None:
|
|
|
223
223
|
logger.exception("[pooler-role] contamination sink raised")
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
def _single_statement_proof_sql(query: str) -> str | None:
|
|
227
|
+
"""The one-statement absence proof for ``query``, or ``None`` if it cannot embed.
|
|
228
|
+
|
|
229
|
+
One statement executes on ONE server connection, so the identity columns and
|
|
230
|
+
the ``EXISTS`` are evaluated by the same backend under the same role. The
|
|
231
|
+
login column comes from ``pg_stat_activity`` (the role the backend
|
|
232
|
+
authenticated as), which ``SET SESSION AUTHORIZATION`` does not change — so a
|
|
233
|
+
session-authorization swap, where ``current_user`` and ``session_user`` agree
|
|
234
|
+
on the WRONG identity, still fails the proof.
|
|
235
|
+
"""
|
|
236
|
+
body = query.strip().rstrip(";").strip()
|
|
237
|
+
if not body or ";" in body:
|
|
238
|
+
return None
|
|
239
|
+
return (
|
|
240
|
+
"SELECT current_user AS current_user_, session_user AS session_user_, "
|
|
241
|
+
"(SELECT usename FROM pg_catalog.pg_stat_activity WHERE pid = pg_backend_pid()) "
|
|
242
|
+
"AS login_user_, "
|
|
243
|
+
f"EXISTS ({body}) AS has_rows_"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def prove_empty_in_one_statement(
|
|
248
|
+
conn: Any,
|
|
249
|
+
*,
|
|
250
|
+
query: str,
|
|
251
|
+
args: tuple[Any, ...],
|
|
252
|
+
statement_timeout: float | None,
|
|
253
|
+
) -> bool:
|
|
254
|
+
"""True only when ONE statement proves the read is genuinely empty.
|
|
255
|
+
|
|
256
|
+
The fast path of Layer 2. A clean session (``current_user`` = ``session_user``
|
|
257
|
+
= the backend's login role) that finds no row IS the proof the full re-proof
|
|
258
|
+
would reach — in one round trip instead of seven (BEGIN, two identity reads,
|
|
259
|
+
two resets, the re-read, COMMIT). Every other outcome — a borrowed role, a
|
|
260
|
+
session-authorization swap, rows that appeared since, a query that cannot be
|
|
261
|
+
embedded, any error — returns False and the full repairing re-proof runs, so
|
|
262
|
+
no contaminated or unprovable read ever short-circuits here.
|
|
263
|
+
"""
|
|
264
|
+
sql = _single_statement_proof_sql(query)
|
|
265
|
+
if sql is None:
|
|
266
|
+
return False
|
|
267
|
+
try:
|
|
268
|
+
row = await conn.fetchrow(sql, *args, timeout=statement_timeout)
|
|
269
|
+
except Exception: # noqa: BLE001 — any failure means "not proven"; the full path decides
|
|
270
|
+
return False
|
|
271
|
+
if row is None:
|
|
272
|
+
return False
|
|
273
|
+
current = row["current_user_"]
|
|
274
|
+
session = row["session_user_"]
|
|
275
|
+
login = row["login_user_"]
|
|
276
|
+
if not current or current != session or current != login:
|
|
277
|
+
return False
|
|
278
|
+
return row["has_rows_"] is False
|
|
279
|
+
|
|
280
|
+
|
|
226
281
|
async def reprove_empty_read(
|
|
227
282
|
conn: Any,
|
|
228
283
|
*,
|
|
@@ -238,10 +293,19 @@ async def reprove_empty_read(
|
|
|
238
293
|
the read was genuinely empty. Raises ``PoolerRoleContamination`` when a clean
|
|
239
294
|
session cannot be obtained at all — an unprovable absence is never a result.
|
|
240
295
|
|
|
296
|
+
The common case — a genuinely empty read on a clean pool — is settled by
|
|
297
|
+
``prove_empty_in_one_statement`` in one round trip. Anything it cannot prove
|
|
298
|
+
falls through to the pinned, repairing re-proof below.
|
|
299
|
+
|
|
241
300
|
``conn`` must be a connection not currently inside a transaction; the explicit
|
|
242
301
|
transaction opened here is what pins every statement below to ONE server
|
|
243
302
|
connection, which is the only thing that makes ``RESET ROLE`` meaningful.
|
|
244
303
|
"""
|
|
304
|
+
if await prove_empty_in_one_statement(
|
|
305
|
+
conn, query=query, args=args, statement_timeout=statement_timeout
|
|
306
|
+
):
|
|
307
|
+
return None
|
|
308
|
+
|
|
245
309
|
last_role: tuple[str, str] | None = None
|
|
246
310
|
for _ in range(REPROVE_ATTEMPTS):
|
|
247
311
|
async with conn.transaction():
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""A pooled backend another client left read-only never refuses our writes.
|
|
2
|
+
|
|
3
|
+
A transaction-mode pooler (Supavisor / PgBouncer, port 6543) shares its server
|
|
4
|
+
connections between EVERY client that logs in as the same user, and never resets
|
|
5
|
+
one between clients. A client that runs a session-level read-only setting outside a
|
|
6
|
+
transaction — ``SET default_transaction_read_only = on``,
|
|
7
|
+
``SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY`` (the shape of an ad-hoc
|
|
8
|
+
"read-only production" helper) — leaves it on whichever server connection the pooler
|
|
9
|
+
handed it. Every later transaction routed there starts read-only, and every write
|
|
10
|
+
fails with ``cannot execute ... in a read-only transaction`` while the database is
|
|
11
|
+
writable. (2026-10-01: SMS and notification dispatchers, workflow claims, the request
|
|
12
|
+
audit and chat writes; recurring since at least 2026-09-12. Proven live: one of 21
|
|
13
|
+
pooled backends carried ``default_transaction_read_only = on``, ``source = session``.)
|
|
14
|
+
|
|
15
|
+
``ReadWriteConnection`` is the connection class of every transaction-mode pool:
|
|
16
|
+
|
|
17
|
+
* every top-level ``BEGIN`` that names no access mode is sent as ``BEGIN ... READ
|
|
18
|
+
WRITE`` — the ORM's own ``transaction()`` and any raw ``conn.transaction()``
|
|
19
|
+
alike, at no extra round trip. An explicit ``READ ONLY`` is kept.
|
|
20
|
+
* an AUTOCOMMIT statement the backend refuses as read-only (SQLSTATE 25006) is re-run
|
|
21
|
+
once inside one explicit ``BEGIN READ WRITE`` transaction, and the event is logged
|
|
22
|
+
with its remedy. The refusal proves the statement never ran: Postgres checks the
|
|
23
|
+
access mode before a statement modifies anything, and an implicit transaction
|
|
24
|
+
rolls back whole.
|
|
25
|
+
|
|
26
|
+
THE SECOND SHAPE — a server connection handed over INSIDE another client's transaction
|
|
27
|
+
(2026-10-01: live probes sent ``begin read only; …; rollback;`` as one string through the
|
|
28
|
+
pooler; an error or timeout skipped the ``rollback`` and the open READ ONLY transaction went to
|
|
29
|
+
the platform server). A transaction pooler answers our handshake with "idle" itself, so asyncpg
|
|
30
|
+
only learns of the stranger's transaction from the reply to our next statement:
|
|
31
|
+
|
|
32
|
+
* a statement that comes back inside a transaction this connection never opened (asyncpg's
|
|
33
|
+
``_top_xact`` unset, no BEGIN of ours open) ran inside a STRANGER's transaction: that
|
|
34
|
+
transaction is rolled back, logged at ERROR with its remedy, and the statement re-runs once in
|
|
35
|
+
our own ``BEGIN READ WRITE … COMMIT``. The rollback undid whatever the statement did, so the
|
|
36
|
+
re-run is exactly once.
|
|
37
|
+
* a top-level ``BEGIN`` the server answers with SQLSTATE 25001 ("there is already a transaction
|
|
38
|
+
in progress" / "read-write mode must be set before any query") is the same stranger: rolled
|
|
39
|
+
back, begun again once; a second stranger raises ``InheritedTransactionError`` by name —
|
|
40
|
+
never a bare 25006 three statements later.
|
|
41
|
+
|
|
42
|
+
It never touches the session it was handed — the poison stays visible to whoever
|
|
43
|
+
investigates (``SHOW default_transaction_read_only``) and the guard stays per
|
|
44
|
+
transaction. Session-mode pools keep the plain asyncpg connection: their server
|
|
45
|
+
connection is ours alone and asyncpg's release reset clears it.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import logging
|
|
51
|
+
import re
|
|
52
|
+
from collections.abc import Awaitable, Callable
|
|
53
|
+
from typing import Any
|
|
54
|
+
|
|
55
|
+
import asyncpg
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
# One statement that opens a top-level transaction: BEGIN / BEGIN WORK / BEGIN
|
|
60
|
+
# TRANSACTION / START TRANSACTION, optionally with modes and one trailing ';'.
|
|
61
|
+
_BEGIN_STATEMENT = re.compile(
|
|
62
|
+
r"^\s*(?:BEGIN|START\s+TRANSACTION)\b(?P<modes>[^;]*?)\s*;?\s*$",
|
|
63
|
+
re.IGNORECASE | re.DOTALL,
|
|
64
|
+
)
|
|
65
|
+
_ACCESS_MODE = re.compile(r"\bREAD\s+(?:ONLY|WRITE)\b", re.IGNORECASE)
|
|
66
|
+
# A statement that ends the top-level transaction (not ROLLBACK TO / RELEASE a savepoint).
|
|
67
|
+
_END_STATEMENT = re.compile(
|
|
68
|
+
r"^\s*(?:COMMIT|END|ABORT|ROLLBACK)\b(?!\s+(?:TO|PREPARED)\b)(?!\s+(?:WORK|TRANSACTION)\s+TO\b)",
|
|
69
|
+
re.IGNORECASE,
|
|
70
|
+
)
|
|
71
|
+
_ACTIVE_SQL_TRANSACTION = "25001"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def declare_read_write(query: str) -> str:
|
|
75
|
+
"""``query`` with ``READ WRITE`` declared when it is a BEGIN naming no access mode.
|
|
76
|
+
|
|
77
|
+
Anything else — a savepoint, a multi-statement script, a BEGIN that already says
|
|
78
|
+
``READ ONLY`` or ``READ WRITE`` — is returned unchanged.
|
|
79
|
+
"""
|
|
80
|
+
match = _BEGIN_STATEMENT.match(query)
|
|
81
|
+
if match is None or _ACCESS_MODE.search(match.group("modes")):
|
|
82
|
+
return query
|
|
83
|
+
return query.strip().rstrip(";").rstrip() + " READ WRITE"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_REFUSED_REMEDY = (
|
|
87
|
+
"[matrx_orm] A pooled server connection refused an autocommit statement as "
|
|
88
|
+
"read-only: another client of the transaction pooler left a SESSION-level "
|
|
89
|
+
"read-only default on it (SET default_transaction_read_only / SET SESSION "
|
|
90
|
+
"CHARACTERISTICS AS TRANSACTION READ ONLY sent outside a transaction). The "
|
|
91
|
+
"statement never ran and was re-run inside BEGIN READ WRITE. Remedy: find the "
|
|
92
|
+
"client sending that SET through the pooler — read-only work belongs in "
|
|
93
|
+
"BEGIN READ ONLY, one transaction at a time. Statement: %s"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
_STRANGER_REMEDY = (
|
|
98
|
+
"[matrx_orm] The pooler handed this connection a server connection still inside "
|
|
99
|
+
"another client's open transaction (a client of the transaction pooler left without "
|
|
100
|
+
"COMMIT/ROLLBACK, e.g. a 'begin read only; ...; rollback;' probe that errored or timed "
|
|
101
|
+
"out). That transaction was rolled back and our statement re-runs in its own "
|
|
102
|
+
"transaction. Remedy: find the client — probes belong on a DIRECT connection (:5432) "
|
|
103
|
+
"with ROLLBACK in a finally. Statement: %s"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class InheritedTransactionError(RuntimeError):
|
|
108
|
+
"""Every server connection the pooler offered was inside another client's transaction."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ReadWriteConnection(asyncpg.Connection): # type: ignore[misc]
|
|
112
|
+
"""The connection class of a transaction-mode pool (see module docstring)."""
|
|
113
|
+
|
|
114
|
+
_matrx_own_xact = False # a top-level BEGIN of ours is open
|
|
115
|
+
_matrx_saw_25001 = False # the server warned "already a transaction in progress"
|
|
116
|
+
|
|
117
|
+
def _process_log_message(self, fields: Any, last_query: Any) -> None:
|
|
118
|
+
if fields.get("C") == _ACTIVE_SQL_TRANSACTION:
|
|
119
|
+
self._matrx_saw_25001 = True
|
|
120
|
+
super()._process_log_message(fields, last_query)
|
|
121
|
+
|
|
122
|
+
async def _reset(self) -> None:
|
|
123
|
+
self._matrx_own_xact = False
|
|
124
|
+
await super()._reset()
|
|
125
|
+
|
|
126
|
+
def _in_strangers_transaction(self) -> bool:
|
|
127
|
+
return self.is_in_transaction() and self._top_xact is None and not self._matrx_own_xact
|
|
128
|
+
|
|
129
|
+
async def _drop_strangers_transaction(self, query: str) -> None:
|
|
130
|
+
logger.error(_STRANGER_REMEDY, " ".join(str(query).split())[:160])
|
|
131
|
+
self._matrx_own_xact = False
|
|
132
|
+
await asyncpg.Connection.execute(self, "ROLLBACK")
|
|
133
|
+
|
|
134
|
+
async def _begin(self, query: str) -> str:
|
|
135
|
+
"""A top-level BEGIN that never silently joins a stranger's transaction."""
|
|
136
|
+
if self._in_strangers_transaction():
|
|
137
|
+
await self._drop_strangers_transaction(query)
|
|
138
|
+
for _ in range(2):
|
|
139
|
+
self._matrx_saw_25001 = False
|
|
140
|
+
try:
|
|
141
|
+
result = await asyncpg.Connection.execute(self, query)
|
|
142
|
+
except asyncpg.exceptions.ActiveSQLTransactionError:
|
|
143
|
+
pass
|
|
144
|
+
else:
|
|
145
|
+
if not self._matrx_saw_25001:
|
|
146
|
+
self._matrx_own_xact = True
|
|
147
|
+
return result
|
|
148
|
+
await self._drop_strangers_transaction(query)
|
|
149
|
+
raise InheritedTransactionError(
|
|
150
|
+
"Two server connections in a row were inside another client's open transaction; "
|
|
151
|
+
"nothing was written. A client of the transaction pooler is leaving transactions "
|
|
152
|
+
"open (see matrx_orm/core/read_write.py)."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def _in_own_transaction(
|
|
156
|
+
self, call: Callable[..., Awaitable[Any]], query: str, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
157
|
+
) -> Any:
|
|
158
|
+
await self._begin("BEGIN READ WRITE")
|
|
159
|
+
try:
|
|
160
|
+
result = await call(query, *args, **kwargs)
|
|
161
|
+
except BaseException:
|
|
162
|
+
self._matrx_own_xact = False
|
|
163
|
+
try:
|
|
164
|
+
await asyncpg.Connection.execute(self, "ROLLBACK")
|
|
165
|
+
except Exception: # noqa: BLE001 — the pool's release rolls back what this cannot
|
|
166
|
+
pass
|
|
167
|
+
raise
|
|
168
|
+
self._matrx_own_xact = False
|
|
169
|
+
await asyncpg.Connection.execute(self, "COMMIT")
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
async def _healed(
|
|
173
|
+
self, call: Callable[..., Awaitable[Any]], query: str, args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
174
|
+
) -> Any:
|
|
175
|
+
if self._in_strangers_transaction():
|
|
176
|
+
await self._drop_strangers_transaction(query)
|
|
177
|
+
if self.is_in_transaction():
|
|
178
|
+
result = await call(query, *args, **kwargs)
|
|
179
|
+
if self._top_xact is None and _END_STATEMENT.match(str(query)):
|
|
180
|
+
self._matrx_own_xact = False
|
|
181
|
+
return result
|
|
182
|
+
try:
|
|
183
|
+
result = await call(query, *args, **kwargs)
|
|
184
|
+
except asyncpg.exceptions.ReadOnlySQLTransactionError:
|
|
185
|
+
if self.is_in_transaction():
|
|
186
|
+
await self._drop_strangers_transaction(query)
|
|
187
|
+
else:
|
|
188
|
+
logger.warning(_REFUSED_REMEDY, " ".join(str(query).split())[:160])
|
|
189
|
+
except Exception:
|
|
190
|
+
if not self._in_strangers_transaction():
|
|
191
|
+
raise
|
|
192
|
+
await self._drop_strangers_transaction(query)
|
|
193
|
+
else:
|
|
194
|
+
if not self._in_strangers_transaction():
|
|
195
|
+
return result
|
|
196
|
+
# The reply came from inside a stranger's transaction; rolling it back undid it.
|
|
197
|
+
await self._drop_strangers_transaction(query)
|
|
198
|
+
return await self._in_own_transaction(call, query, args, kwargs)
|
|
199
|
+
|
|
200
|
+
async def execute(self, query: str, *args: Any, **kwargs: Any) -> str:
|
|
201
|
+
if not args:
|
|
202
|
+
declared = declare_read_write(query)
|
|
203
|
+
if _BEGIN_STATEMENT.match(declared) and not (self.is_in_transaction() and not self._in_strangers_transaction()):
|
|
204
|
+
return await self._begin(declared)
|
|
205
|
+
query = declared
|
|
206
|
+
return await self._healed(super().execute, query, args, kwargs)
|
|
207
|
+
|
|
208
|
+
async def fetch(self, query: str, *args: Any, **kwargs: Any) -> list[Any]:
|
|
209
|
+
return await self._healed(super().fetch, query, args, kwargs)
|
|
210
|
+
|
|
211
|
+
async def fetchrow(self, query: str, *args: Any, **kwargs: Any) -> Any:
|
|
212
|
+
return await self._healed(super().fetchrow, query, args, kwargs)
|
|
213
|
+
|
|
214
|
+
async def fetchval(self, query: str, *args: Any, **kwargs: Any) -> Any:
|
|
215
|
+
return await self._healed(super().fetchval, query, args, kwargs)
|
|
216
|
+
|
|
217
|
+
async def executemany(self, command: str, args: Any, **kwargs: Any) -> None:
|
|
218
|
+
return await self._healed(super().executemany, command, (args,), kwargs)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
__all__ = ["InheritedTransactionError", "ReadWriteConnection", "declare_read_write"]
|
|
@@ -115,43 +115,34 @@ async def rls_session(
|
|
|
115
115
|
|
|
116
116
|
claims_json = json.dumps(claims, separators=(",", ":"))
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
# Set the verified claims FIRST (while still the privileged role), then drop to
|
|
119
|
+
# the restricted role. Order mirrors PostgREST; all of them are transaction-local
|
|
120
|
+
# (`set_config(..., true)` IS `SET LOCAL`) and survive the role switch within the
|
|
121
|
+
# same transaction.
|
|
122
|
+
#
|
|
123
|
+
# ONE ROUND TRIP FOR THE WHOLE OPENING. 2026-09-19 folded these four settings
|
|
124
|
+
# into one statement (0.437 s -> 0.192 s against the main database); 2026-09-30
|
|
125
|
+
# folded that statement into the BEGIN-time one that already carries the
|
|
126
|
+
# ceilings and the declared session context (`transaction(opening_gucs=...)`),
|
|
127
|
+
# saving one more round trip on every RLS session. `role` stays LAST — the
|
|
128
|
+
# opening statement always emits it last — so the claims land before the role
|
|
129
|
+
# drops, and the role still changes here and nowhere else.
|
|
130
|
+
gucs: dict[str, str] = {"request.jwt.claims": claims_json}
|
|
131
|
+
if statement_timeout_ms is not None:
|
|
132
|
+
gucs["statement_timeout"] = str(int(statement_timeout_ms))
|
|
133
|
+
if lock_timeout_ms is not None:
|
|
134
|
+
gucs["lock_timeout"] = str(int(lock_timeout_ms))
|
|
135
|
+
if idle_in_transaction_timeout_ms is not None:
|
|
136
|
+
gucs["idle_in_transaction_session_timeout"] = str(int(idle_in_transaction_timeout_ms))
|
|
137
|
+
gucs["role"] = role
|
|
138
|
+
|
|
139
|
+
async with transaction(database=database, opening_gucs=gucs):
|
|
119
140
|
conn = get_active_connection()
|
|
120
141
|
if conn is None: # pragma: no cover - transaction always sets it on first entry
|
|
121
142
|
raise DatabaseError(
|
|
122
143
|
message="rls_session: no active connection after opening transaction",
|
|
123
144
|
details={"role": role},
|
|
124
145
|
)
|
|
125
|
-
# Set the verified claims FIRST (while still the privileged role), then drop to
|
|
126
|
-
# the restricted role. Order mirrors PostgREST; all of them are transaction-local
|
|
127
|
-
# (`set_config(..., true)` IS `SET LOCAL`) and survive the role switch within the
|
|
128
|
-
# same transaction.
|
|
129
|
-
#
|
|
130
|
-
# ONE ROUND TRIP, NOT FOUR. These were four sequential `execute` calls, and a
|
|
131
|
-
# round trip to a hosted pooler is not free: measured 2026-09-19 against the main
|
|
132
|
-
# database, the three that always run cost 0.247 + 0.085 + 0.105 = 0.437 s
|
|
133
|
-
# separately and 0.192 s as one statement — so every RLS session on the platform
|
|
134
|
-
# paid a quarter of a second to say three things it could say at once. They are
|
|
135
|
-
# emitted in the SAME ORDER inside one target list, which is the order that
|
|
136
|
-
# matters: the claims land before the role drops. `SET LOCAL ROLE x` and
|
|
137
|
-
# `set_config('role', 'x', true)` are the same GUC write, so the role still
|
|
138
|
-
# changes here and nowhere else, and `role` stays LAST.
|
|
139
|
-
settings: list[str] = ["set_config('request.jwt.claims', $1, true)"]
|
|
140
|
-
bound: list[str] = [claims_json]
|
|
141
|
-
if statement_timeout_ms is not None:
|
|
142
|
-
bound.append(str(int(statement_timeout_ms)))
|
|
143
|
-
settings.append(f"set_config('statement_timeout', ${len(bound)}, true)")
|
|
144
|
-
if lock_timeout_ms is not None:
|
|
145
|
-
bound.append(str(int(lock_timeout_ms)))
|
|
146
|
-
settings.append(f"set_config('lock_timeout', ${len(bound)}, true)")
|
|
147
|
-
if idle_in_transaction_timeout_ms is not None:
|
|
148
|
-
bound.append(str(int(idle_in_transaction_timeout_ms)))
|
|
149
|
-
settings.append(
|
|
150
|
-
f"set_config('idle_in_transaction_session_timeout', ${len(bound)}, true)"
|
|
151
|
-
)
|
|
152
|
-
bound.append(role)
|
|
153
|
-
settings.append(f"set_config('role', ${len(bound)}, true)")
|
|
154
|
-
await conn.execute("select " + ", ".join(settings), *bound)
|
|
155
146
|
# RLS decides every read in this block, so the process-wide by-id cache
|
|
156
147
|
# (keyed by primary key alone) is bypassed both ways — see
|
|
157
148
|
# ``transaction.in_person_scoped_session``.
|
|
@@ -87,10 +87,41 @@ def session_context_gucs() -> dict[str, str]:
|
|
|
87
87
|
return out
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
def local_gucs_statement(gucs: Mapping[str, str]) -> tuple[str, tuple[str, ...]] | None:
|
|
91
|
+
"""ONE statement that sets every GUC in ``gucs`` transaction-locally.
|
|
92
|
+
|
|
93
|
+
``None`` when there is nothing to set. Names are validated like
|
|
94
|
+
``set_local_guc`` validates them; values travel as bind parameters.
|
|
95
|
+
"""
|
|
96
|
+
if not gucs:
|
|
97
|
+
return None
|
|
98
|
+
targets: list[str] = []
|
|
99
|
+
args: list[str] = []
|
|
100
|
+
for name, value in gucs.items():
|
|
101
|
+
if not isinstance(name, str) or not _GUC_NAME_RE.match(name):
|
|
102
|
+
raise ValueError(f"invalid transaction-local GUC name: {name!r}")
|
|
103
|
+
targets.append(f"set_config(${len(args) + 1}, ${len(args) + 2}, true)")
|
|
104
|
+
args.extend((name, "" if value is None else str(value)))
|
|
105
|
+
return "SELECT " + ", ".join(targets), tuple(args)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def emit_local_gucs(connection: Any, gucs: Mapping[str, str]) -> None:
|
|
109
|
+
"""Set every GUC in ``gucs`` on ``connection`` (inside a BEGIN) in ONE round trip.
|
|
110
|
+
|
|
111
|
+
One ``set_config`` statement per GUC cost a round trip each on EVERY top-level
|
|
112
|
+
transaction — three per BEGIN once the actor declaration landed, ~45 per agent
|
|
113
|
+
launch, ~7.5 s at a laptop's 170 ms to the pooler (measured 2026-09-30).
|
|
114
|
+
"""
|
|
115
|
+
statement = local_gucs_statement(gucs)
|
|
116
|
+
if statement is None:
|
|
117
|
+
return
|
|
118
|
+
sql, args = statement
|
|
119
|
+
await connection.execute(sql, *args)
|
|
120
|
+
|
|
121
|
+
|
|
90
122
|
async def apply_session_context(connection: Any) -> None:
|
|
91
123
|
"""Emit the declared GUCs on ``connection`` (already inside a BEGIN)."""
|
|
92
|
-
|
|
93
|
-
await connection.execute("SELECT set_config($1, $2, true)", name, value)
|
|
124
|
+
await emit_local_gucs(connection, session_context_gucs())
|
|
94
125
|
|
|
95
126
|
|
|
96
127
|
# ---------------------------------------------------------------------------
|
|
@@ -256,6 +287,8 @@ __all__ = [
|
|
|
256
287
|
"ActorTier",
|
|
257
288
|
"SessionContextProvider",
|
|
258
289
|
"apply_session_context",
|
|
290
|
+
"emit_local_gucs",
|
|
291
|
+
"local_gucs_statement",
|
|
259
292
|
"configure_session_context",
|
|
260
293
|
"current_actor",
|
|
261
294
|
"current_tier",
|