matrx-orm 3.1.142__tar.gz → 3.1.144__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.142 → matrx_orm-3.1.144}/PKG-INFO +1 -1
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/FEATURE.md +4 -1
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/__init__.py +10 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/async_db_manager.py +2 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/transaction.py +35 -3
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/update.py +13 -11
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/query/FEATURE.md +10 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/query/builder.py +22 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/README.md +15 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/__init__.py +10 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/errors.py +11 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/fallback.py +140 -8
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/session.py +91 -1
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/pyproject.toml +1 -1
- matrx_orm-3.1.144/tests/level1/test_committed_row_guard.py +144 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_optimistic_concurrency.py +5 -3
- matrx_orm-3.1.144/tests/level1/test_transaction_database_identity.py +114 -0
- matrx_orm-3.1.144/tests/level2/verify_committed_row_guard_pg17.py +588 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_disk_spill_fallback.py +11 -0
- matrx_orm-3.1.144/tests/test_immutable_insert_or_exact_verify.py +159 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/.gitignore +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/AGENTS.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/CLAUDE.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/MODEL_API.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/docs/migrations.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/admin/FEATURE.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/FEATURE.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/pooler_role.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/session_context.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/local_sqlite.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/no_org_assignment.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/FEATURE.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/catalog_select.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/platform_access.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/pytest_plugin.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/resources/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/resources/additional-assigners-2026-09-12.json +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/resources/org_assignment_trigger_baseline.json +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/host_authorization.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/mutation_receipts.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/scripts/release.sh +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/conftest.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_adapter_registry.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_admin_response_rendering.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_associations_wiring.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_count_limit_param_leak.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_local_sqlite.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_lock_contention_retry.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_org_assignment_inventory.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_persisted_count.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_read_connection_retry.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_standalone_write_session_context.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_subquery_nested_expressions.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_write_queue_deadline.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_enum_ordering.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_schema_analysis.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_record_error_bounded_insert.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_record_error_storm.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/session/test_spill_guard_plugin.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_array_arg_element_type.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_dynamic_select_unregistered.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_keyed_digest.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_order_by_desc_nulls_last.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_secrets_minting_keys.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.142 → matrx_orm-3.1.144}/tests/test_sync_twins_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.144
|
|
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
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
`session/fallback.py` owns both failure-capture tables — `ops.system_error`
|
|
13
13
|
(`record_error`) and `ops.system_write_failure` (`record_failures`,
|
|
14
|
-
`drain_spilled_ops`, `
|
|
14
|
+
`drain_spilled_ops`, `immutable_insert_or_exact_verify`). Both carry `organization_id NOT NULL`.
|
|
15
15
|
|
|
16
16
|
- **Every capture row is org-stamped by `_capture_org()`.** Three rungs: an org
|
|
17
17
|
the caller passed, else `None` — handing the row to the DB's
|
|
@@ -115,6 +115,9 @@
|
|
|
115
115
|
|
|
116
116
|
## Change Log
|
|
117
117
|
|
|
118
|
+
- 2026-09-12 — Added `immutable_insert_or_exact_verify`: immutable direct writes
|
|
119
|
+
normalize timestamps/JSON, verify the full row after ambiguous acknowledgements,
|
|
120
|
+
preserve absent intent under a distinct replay marker, and refuse conflicts.
|
|
118
121
|
- 2026-09-12 — Made organization-assignment retirement output an explicit,
|
|
119
122
|
non-overwriting review proposal; a checked-in resource update is required
|
|
120
123
|
before the next standalone wheel carries a retirement.
|
|
@@ -356,6 +356,7 @@ from matrx_orm.session import (
|
|
|
356
356
|
FlushError,
|
|
357
357
|
FlushReport,
|
|
358
358
|
ManagedWriteViolation,
|
|
359
|
+
PendingRowOperationError,
|
|
359
360
|
Session,
|
|
360
361
|
SessionClosedError,
|
|
361
362
|
SessionError,
|
|
@@ -367,11 +368,15 @@ from matrx_orm.session import (
|
|
|
367
368
|
allow_direct_coordinator_write,
|
|
368
369
|
coordinator_policy_tables,
|
|
369
370
|
create_or_capture,
|
|
371
|
+
immutable_insert_or_exact_verify,
|
|
372
|
+
immutable_row_differences,
|
|
370
373
|
current_session,
|
|
374
|
+
assert_no_pending_row,
|
|
371
375
|
declare_managed,
|
|
372
376
|
drain_spilled_ops,
|
|
373
377
|
is_in_session,
|
|
374
378
|
WritePreservedError,
|
|
379
|
+
ImmutableWriteConflictError,
|
|
375
380
|
record_error,
|
|
376
381
|
record_failures,
|
|
377
382
|
register_coordinator_owned_model,
|
|
@@ -675,6 +680,7 @@ __all__ = [
|
|
|
675
680
|
"FlushError",
|
|
676
681
|
"FlushReport",
|
|
677
682
|
"ManagedWriteViolation",
|
|
683
|
+
"PendingRowOperationError",
|
|
678
684
|
"Session",
|
|
679
685
|
"SessionClosedError",
|
|
680
686
|
"SessionError",
|
|
@@ -683,13 +689,17 @@ __all__ = [
|
|
|
683
689
|
"WatchedLifecycleConfig",
|
|
684
690
|
"WatchedLifecycleRegistry",
|
|
685
691
|
"current_session",
|
|
692
|
+
"assert_no_pending_row",
|
|
686
693
|
"declare_managed",
|
|
687
694
|
"is_in_session",
|
|
688
695
|
"record_error",
|
|
689
696
|
"record_failures",
|
|
690
697
|
"drain_spilled_ops",
|
|
691
698
|
"create_or_capture",
|
|
699
|
+
"immutable_insert_or_exact_verify",
|
|
700
|
+
"immutable_row_differences",
|
|
692
701
|
"WritePreservedError",
|
|
702
|
+
"ImmutableWriteConflictError",
|
|
693
703
|
"run_lifecycle_sweeper",
|
|
694
704
|
"sweep_lifecycle_once",
|
|
695
705
|
# ── Adapter layer ──────────────────────────────────────────────────────────
|
|
@@ -157,6 +157,8 @@ READ_TIMEOUT_RETRY_BACKOFF = 0.25 # seconds before the read retry
|
|
|
157
157
|
# "listConversations: deadlock detected" on /chat, and ops.app_log shows the same
|
|
158
158
|
# class crashing POST /coding-sessions/bridge with a 500. Bounded and LOUD: real
|
|
159
159
|
# contention still surfaces, it just costs two extra attempts first.
|
|
160
|
+
# KNOB MIRROR of platform.feature_knob "orm.write_retry" "contention_retries" — the database
|
|
161
|
+
# manager cannot read the database to configure itself; change the row, then re-mirror here.
|
|
160
162
|
CONTENTION_RETRIES = 2 # extra attempts for a cancelled statement; total = 1 + this
|
|
161
163
|
CONTENTION_BACKOFF = 0.06 # seconds before the first re-issue; doubled each time
|
|
162
164
|
|
|
@@ -48,6 +48,10 @@ from matrx_orm.exceptions import DatabaseError
|
|
|
48
48
|
|
|
49
49
|
# ContextVar holding the active connection (if any) for the current task tree.
|
|
50
50
|
_active_connection: ContextVar[Any | None] = ContextVar("_active_connection", default=None)
|
|
51
|
+
# The canonical registered database that owns ``_active_connection``. A raw
|
|
52
|
+
# connection has no portable database identity, so callers that need routing
|
|
53
|
+
# truth must use this ContextVar rather than infer it from connection details.
|
|
54
|
+
_active_database: ContextVar[str | None] = ContextVar("_active_database", default=None)
|
|
51
55
|
# ContextVar tracking savepoint depth (0 = top-level transaction)
|
|
52
56
|
_savepoint_depth: ContextVar[int] = ContextVar("_savepoint_depth", default=0)
|
|
53
57
|
|
|
@@ -57,6 +61,11 @@ def get_active_connection() -> Any | None:
|
|
|
57
61
|
return _active_connection.get()
|
|
58
62
|
|
|
59
63
|
|
|
64
|
+
def get_active_database_name() -> str | None:
|
|
65
|
+
"""Return the canonical database identity of the active transaction."""
|
|
66
|
+
return _active_database.get()
|
|
67
|
+
|
|
68
|
+
|
|
60
69
|
async def advisory_xact_lock(key: str | int, *, connection: Any = None) -> None:
|
|
61
70
|
"""Acquire a transaction-scoped Postgres advisory lock
|
|
62
71
|
(``pg_advisory_xact_lock``) keyed on ``key``. Auto-released at
|
|
@@ -213,6 +222,7 @@ class TransactionContext:
|
|
|
213
222
|
def __init__(self, database: str) -> None:
|
|
214
223
|
self.database = database
|
|
215
224
|
self._conn_token: Token[Any | None] | None = None
|
|
225
|
+
self._database_token: Token[str | None] | None = None
|
|
216
226
|
self._depth_token: Token[int] | None = None
|
|
217
227
|
self._is_savepoint: bool = False
|
|
218
228
|
self._savepoint_name: str = ""
|
|
@@ -227,7 +237,16 @@ class TransactionContext:
|
|
|
227
237
|
self._savepoint_name = f"sp_{uuid.uuid4().hex[:12]}"
|
|
228
238
|
depth = _savepoint_depth.get()
|
|
229
239
|
self._depth_token = _savepoint_depth.set(depth + 1)
|
|
230
|
-
|
|
240
|
+
try:
|
|
241
|
+
await existing.execute(f"SAVEPOINT {self._savepoint_name}")
|
|
242
|
+
except BaseException:
|
|
243
|
+
# Entry never completed, so __aexit__ will not run. Restore
|
|
244
|
+
# only this attempted savepoint's depth; the outer transaction
|
|
245
|
+
# owns its connection, database identity and eventual cleanup.
|
|
246
|
+
_savepoint_depth.reset(self._depth_token)
|
|
247
|
+
self._depth_token = None
|
|
248
|
+
self._is_savepoint = False
|
|
249
|
+
raise
|
|
231
250
|
else:
|
|
232
251
|
# Top-level → BEGIN
|
|
233
252
|
self._is_savepoint = False
|
|
@@ -239,15 +258,24 @@ class TransactionContext:
|
|
|
239
258
|
self._raw_conn = conn
|
|
240
259
|
try:
|
|
241
260
|
await conn.execute("BEGIN")
|
|
242
|
-
except
|
|
261
|
+
except BaseException as e:
|
|
243
262
|
await self._adapter_cm.__aexit__(type(e), e, e.__traceback__)
|
|
244
263
|
self._raw_conn = None
|
|
245
264
|
self._adapter_cm = None
|
|
265
|
+
if not isinstance(e, Exception):
|
|
266
|
+
raise
|
|
246
267
|
raise DatabaseError(
|
|
247
268
|
message=f"Failed to begin transaction: {e}",
|
|
248
269
|
details={"database": self.database},
|
|
249
270
|
) from e
|
|
271
|
+
# Keep the connection and its canonical database identity bound as
|
|
272
|
+
# one top-level transaction fact. Nested transactions are
|
|
273
|
+
# savepoints and deliberately retain the outer identity.
|
|
274
|
+
from matrx_orm.core.config import resolve_database_name
|
|
275
|
+
|
|
276
|
+
canonical_database = resolve_database_name(self.database)
|
|
250
277
|
self._conn_token = _active_connection.set(conn)
|
|
278
|
+
self._database_token = _active_database.set(canonical_database)
|
|
251
279
|
self._depth_token = _savepoint_depth.set(0)
|
|
252
280
|
# THE HOST'S DECLARED SESSION CONTEXT (provenance and friends). A
|
|
253
281
|
# transaction-local GUC only has meaning inside a transaction, so
|
|
@@ -260,8 +288,10 @@ class TransactionContext:
|
|
|
260
288
|
|
|
261
289
|
try:
|
|
262
290
|
await apply_session_context(conn)
|
|
263
|
-
except
|
|
291
|
+
except BaseException as e:
|
|
264
292
|
await self.__aexit__(type(e), e, e.__traceback__)
|
|
293
|
+
if not isinstance(e, Exception):
|
|
294
|
+
raise
|
|
265
295
|
raise DatabaseError(
|
|
266
296
|
message=f"Failed to apply the declared session context: {e}",
|
|
267
297
|
details={"database": self.database},
|
|
@@ -290,6 +320,8 @@ class TransactionContext:
|
|
|
290
320
|
# Top-level: restore ContextVars before touching the connection
|
|
291
321
|
if self._conn_token is not None:
|
|
292
322
|
_active_connection.reset(self._conn_token)
|
|
323
|
+
if self._database_token is not None:
|
|
324
|
+
_active_database.reset(self._database_token)
|
|
293
325
|
if self._depth_token is not None:
|
|
294
326
|
_savepoint_depth.reset(self._depth_token)
|
|
295
327
|
|
|
@@ -91,7 +91,7 @@ async def _conflict_actor(
|
|
|
91
91
|
)
|
|
92
92
|
.order_by("-id")
|
|
93
93
|
.limit(1)
|
|
94
|
-
.
|
|
94
|
+
.get_committed()
|
|
95
95
|
)
|
|
96
96
|
except Exception as exc:
|
|
97
97
|
vcprint(
|
|
@@ -102,10 +102,9 @@ async def _conflict_actor(
|
|
|
102
102
|
log_level="WARNING",
|
|
103
103
|
)
|
|
104
104
|
return (None, None)
|
|
105
|
-
if
|
|
105
|
+
if snapshots is None:
|
|
106
106
|
return (None, None)
|
|
107
|
-
|
|
108
|
-
return (getattr(snapshot, "actor_id", None), getattr(snapshot, "occurred_at", None))
|
|
107
|
+
return (getattr(snapshots, "actor_id", None), getattr(snapshots, "occurred_at", None))
|
|
109
108
|
|
|
110
109
|
|
|
111
110
|
async def _build_conflict(
|
|
@@ -149,10 +148,7 @@ async def _raise_stale_write(
|
|
|
149
148
|
"""
|
|
150
149
|
from matrx_orm.exceptions import DoesNotExist
|
|
151
150
|
|
|
152
|
-
|
|
153
|
-
# writes for this row — a CAS should run on the direct write paths, not
|
|
154
|
-
# Coordinator-adjacent code (see the contract's session-plane exclusion).
|
|
155
|
-
current = await QueryBuilder(model_cls).filter(**pk_filter).get_or_none()
|
|
151
|
+
current = await QueryBuilder(model_cls).filter(**pk_filter).get_committed_or_none()
|
|
156
152
|
if current is None or getattr(current, "deleted_at", None) is not None:
|
|
157
153
|
# Hard-gone or soft-deleted: for conflict classification a soft-deleted
|
|
158
154
|
# row IS gone — reporting it as "changed by someone else, refresh and
|
|
@@ -190,12 +186,15 @@ async def guarded_update(
|
|
|
190
186
|
narrowing filter that stopped matching still classifies as a conflict.
|
|
191
187
|
"""
|
|
192
188
|
_require_version_column(model_cls, version_column)
|
|
189
|
+
from matrx_orm.session import assert_no_pending_row
|
|
190
|
+
|
|
191
|
+
pk_filter = {pk: filters[pk] for pk in model_cls._meta.primary_keys}
|
|
192
|
+
assert_no_pending_row(model_cls, pk_filter)
|
|
193
193
|
data = dict(update_data)
|
|
194
194
|
attempted_fields = list(data)
|
|
195
195
|
data.setdefault(version_column, expected_version + 1)
|
|
196
196
|
result = await update(model_cls, {**filters, version_column: expected_version}, **data)
|
|
197
197
|
if result.rows_affected == 0:
|
|
198
|
-
pk_filter = {pk: filters[pk] for pk in model_cls._meta.primary_keys}
|
|
199
198
|
await _raise_stale_write(
|
|
200
199
|
model_cls, pk_filter, version_column, expected_version, attempted_fields
|
|
201
200
|
)
|
|
@@ -373,14 +372,17 @@ async def guarded_delete(
|
|
|
373
372
|
delete; deleting it anyway would discard their work unseen).
|
|
374
373
|
"""
|
|
375
374
|
_require_version_column(model_cls, version_column)
|
|
375
|
+
from matrx_orm.session import assert_no_pending_row
|
|
376
|
+
|
|
377
|
+
pk_filter = {pk: filters[pk] for pk in model_cls._meta.primary_keys}
|
|
378
|
+
assert_no_pending_row(model_cls, pk_filter)
|
|
376
379
|
deleted = (
|
|
377
380
|
await QueryBuilder(model_cls)
|
|
378
381
|
.filter(**{**filters, version_column: expected_version})
|
|
379
382
|
.delete()
|
|
380
383
|
)
|
|
381
384
|
if deleted == 0:
|
|
382
|
-
|
|
383
|
-
current = await QueryBuilder(model_cls).filter(**pk_filter).get_or_none()
|
|
385
|
+
current = await QueryBuilder(model_cls).filter(**pk_filter).get_committed_or_none()
|
|
384
386
|
# Soft-deleted counts as already gone — idempotent 0, not a conflict.
|
|
385
387
|
if current is not None and getattr(current, "deleted_at", None) is None:
|
|
386
388
|
# A delete contests no individual field (it contests the whole row),
|
|
@@ -70,6 +70,16 @@ want persisted database state call `count(include_pending=False)`. That opt-out
|
|
|
70
70
|
always uses the database aggregate and never hydrates or serializes every
|
|
71
71
|
matching model merely because a request Coordinator has queued a write.
|
|
72
72
|
|
|
73
|
+
## Committed-row evidence
|
|
74
|
+
|
|
75
|
+
`get_committed()` and `get_committed_or_none()` execute the current query through
|
|
76
|
+
the normal executor and current transaction/RLS identity, then hydrate returned
|
|
77
|
+
columns through the canonical model path. They never consult or update
|
|
78
|
+
`StateManager`, and never merge the Session stack. They retain the builder's
|
|
79
|
+
filters, projection, ordering, and ordinary zero/one/many semantics. Use them
|
|
80
|
+
only where database evidence is required, such as stale CAS diagnostics and
|
|
81
|
+
resource authorization; ordinary `get()` remains read-your-own-writes.
|
|
82
|
+
|
|
73
83
|
## A rebuilt statement binds only the params it references
|
|
74
84
|
|
|
75
85
|
`count()`, `update()` and `delete()` rebuild SQL from the WHERE tail that
|
|
@@ -909,6 +909,28 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
909
909
|
except DoesNotExist:
|
|
910
910
|
return None
|
|
911
911
|
|
|
912
|
+
async def get_committed(self) -> ModelT:
|
|
913
|
+
"""Retrieve exactly one database row, ignoring Session and identity-cache state."""
|
|
914
|
+
filters = self._merge_filters_excludes()
|
|
915
|
+
try:
|
|
916
|
+
executor = await self._get_executor_async()
|
|
917
|
+
results = await executor.all()
|
|
918
|
+
if not results:
|
|
919
|
+
raise DoesNotExist(model=self.model, filters=filters)
|
|
920
|
+
if len(results) > 1:
|
|
921
|
+
raise MultipleObjectsReturned(model=self.model, count=len(results), filters=filters)
|
|
922
|
+
return results[0]
|
|
923
|
+
except ORMException as e:
|
|
924
|
+
e.enrich(model=self.model, operation="get_committed", filters=filters)
|
|
925
|
+
raise
|
|
926
|
+
|
|
927
|
+
async def get_committed_or_none(self) -> ModelT | None:
|
|
928
|
+
"""Return a committed database row or ``None`` without Session merging."""
|
|
929
|
+
try:
|
|
930
|
+
return await self.get_committed()
|
|
931
|
+
except DoesNotExist:
|
|
932
|
+
return None
|
|
933
|
+
|
|
912
934
|
async def update(self, **kwargs: Any) -> UpdateResult:
|
|
913
935
|
"""Update matching records.
|
|
914
936
|
|
|
@@ -154,6 +154,21 @@ Complex predicates (operator suffixes, JSON paths, joins) only match
|
|
|
154
154
|
DB rows — the pending state is assumed safe (the DB still applies the
|
|
155
155
|
predicate on its side).
|
|
156
156
|
|
|
157
|
+
## Guarded committed operations beside a Session
|
|
158
|
+
|
|
159
|
+
`assert_no_pending_row(model, primary_key)` is the narrow door used by
|
|
160
|
+
synchronous guarded writes and committed-resource authorization. It refuses
|
|
161
|
+
only an exact schema/table and complete ordered primary-key collision; it
|
|
162
|
+
never flushes, drops, reorders, or otherwise changes a Session. The comparison
|
|
163
|
+
uses canonical database aliases and checks both places a queued operation can
|
|
164
|
+
land: the current transaction's database when one is active, and the owning
|
|
165
|
+
Session's explicit database (or normal transaction default) when it later
|
|
166
|
+
flushes standalone. A raw active connection without the ORM's canonical
|
|
167
|
+
database identity refuses action rather than guessing. A malformed queued
|
|
168
|
+
identity refuses only after its database destination matches; other databases,
|
|
169
|
+
rows and tables remain valid. This is not a blanket Session exclusion or a
|
|
170
|
+
SessionOp CAS.
|
|
171
|
+
|
|
157
172
|
## 🚨 Flush stubs are PARTIAL — they never enter the identity cache
|
|
158
173
|
|
|
159
174
|
`flush._execute_group` turns each queued UPDATE into a lightweight
|
|
@@ -53,10 +53,14 @@ from matrx_orm.session.errors import (
|
|
|
53
53
|
CoordinatorWriteViolation,
|
|
54
54
|
FlushError,
|
|
55
55
|
ManagedWriteViolation,
|
|
56
|
+
PendingRowOperationError,
|
|
56
57
|
SessionClosedError,
|
|
57
58
|
SessionError,
|
|
58
59
|
)
|
|
59
60
|
from matrx_orm.session.fallback import (
|
|
61
|
+
ImmutableWriteConflictError,
|
|
62
|
+
immutable_insert_or_exact_verify,
|
|
63
|
+
immutable_row_differences,
|
|
60
64
|
WritePreservedError,
|
|
61
65
|
create_or_capture,
|
|
62
66
|
drain_spilled_ops,
|
|
@@ -92,6 +96,7 @@ from matrx_orm.session.reads import (
|
|
|
92
96
|
merge_pending_with_db_one,
|
|
93
97
|
)
|
|
94
98
|
from matrx_orm.session.session import (
|
|
99
|
+
assert_no_pending_row,
|
|
95
100
|
FlushReport,
|
|
96
101
|
Session,
|
|
97
102
|
SessionPhase,
|
|
@@ -108,6 +113,7 @@ __all__ = [
|
|
|
108
113
|
"FlushError",
|
|
109
114
|
"FlushReport",
|
|
110
115
|
"ManagedWriteViolation",
|
|
116
|
+
"PendingRowOperationError",
|
|
111
117
|
"OpType",
|
|
112
118
|
"SWEEP_INTERVAL_SECS_DEFAULT",
|
|
113
119
|
"SWEEP_LIMIT_DEFAULT",
|
|
@@ -121,6 +127,7 @@ __all__ = [
|
|
|
121
127
|
"coalesce_ops",
|
|
122
128
|
"config_from_meta",
|
|
123
129
|
"current_session",
|
|
130
|
+
"assert_no_pending_row",
|
|
124
131
|
"COORDINATOR_BYPASS_ACKNOWLEDGEMENT",
|
|
125
132
|
"allow_direct_coordinator_write",
|
|
126
133
|
"coordinator_policy_tables",
|
|
@@ -139,6 +146,9 @@ __all__ = [
|
|
|
139
146
|
"drain_spilled_ops",
|
|
140
147
|
"create_or_capture",
|
|
141
148
|
"WritePreservedError",
|
|
149
|
+
"ImmutableWriteConflictError",
|
|
150
|
+
"immutable_insert_or_exact_verify",
|
|
151
|
+
"immutable_row_differences",
|
|
142
152
|
"merge_pending_with_db_one",
|
|
143
153
|
"run_sweeper",
|
|
144
154
|
"session_stack",
|
|
@@ -11,6 +11,17 @@ class SessionError(Exception):
|
|
|
11
11
|
"""Base class for all Session exceptions."""
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class PendingRowOperationError(SessionError):
|
|
15
|
+
"""An authoritative operation targets a row with queued Session work."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, table: str) -> None:
|
|
18
|
+
super().__init__(
|
|
19
|
+
f"{table} has a pending Session operation for this exact row. "
|
|
20
|
+
"Finish the current operation before authorizing or mutating this resource."
|
|
21
|
+
)
|
|
22
|
+
self.table = table
|
|
23
|
+
|
|
24
|
+
|
|
14
25
|
class LifecycleWatchdogAlarm(SessionError):
|
|
15
26
|
"""A watched lifecycle crossed its SLA and requires operator attention."""
|
|
16
27
|
|
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import json
|
|
18
18
|
import logging
|
|
19
19
|
import os
|
|
20
|
+
import re
|
|
20
21
|
import tempfile
|
|
21
22
|
import traceback as _tb
|
|
22
23
|
import uuid as _uuid
|
|
@@ -116,6 +117,7 @@ DISK_SPILL_RECOVERED_MARKER = "DiskSpillRecovered"
|
|
|
116
117
|
# matrx-ai's replay loop adds this to its RECOVERABLE_RETRY_ERRORS set — keep
|
|
117
118
|
# the string in lockstep.
|
|
118
119
|
DIRECT_WRITE_PRESERVED_MARKER = "DirectWritePreserved"
|
|
120
|
+
IMMUTABLE_WRITE_PRESERVED_MARKER = "ImmutableWritePreserved"
|
|
119
121
|
# Resolved-writable spill dir, cached after first success so we don't re-probe
|
|
120
122
|
# the cascade on every failure. None until first resolution.
|
|
121
123
|
_spill_dir_cache: Path | None = None
|
|
@@ -535,10 +537,13 @@ async def record_failures(
|
|
|
535
537
|
spilled=spilled,
|
|
536
538
|
total=len(failed_rows),
|
|
537
539
|
)
|
|
538
|
-
# A
|
|
539
|
-
#
|
|
540
|
-
#
|
|
541
|
-
|
|
540
|
+
# A spill is durable only when every failed row reached disk. Never return
|
|
541
|
+
# a handle for the DB+disk-loss case: callers use the receipt to tell a
|
|
542
|
+
# human whether the final utterance is actually recoverable.
|
|
543
|
+
if spilled == len(failed_rows):
|
|
544
|
+
return [op.op_id for op in ops]
|
|
545
|
+
failed_ids = {str(row["op_id"]) for row in failed_rows}
|
|
546
|
+
return [op.op_id for op in ops if op.op_id not in failed_ids]
|
|
542
547
|
|
|
543
548
|
|
|
544
549
|
async def _bulk_insert_failures(
|
|
@@ -901,6 +906,67 @@ def _scream_to_stdout(
|
|
|
901
906
|
# ── Keep-the-work tier: DIRECT writes that must never lose their payload ─────
|
|
902
907
|
|
|
903
908
|
|
|
909
|
+
class ImmutableWriteConflictError(RuntimeError):
|
|
910
|
+
"""An immutable row id exists, but its stored value differs from the intent."""
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _canonical_immutable_value(value: Any, *, parse_iso_timestamp: bool = False) -> Any:
|
|
914
|
+
"""Normalize JSON and timestamps before comparing immutable write intent."""
|
|
915
|
+
if isinstance(value, datetime):
|
|
916
|
+
if value.tzinfo is None:
|
|
917
|
+
value = value.replace(tzinfo=UTC)
|
|
918
|
+
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
919
|
+
if isinstance(value, _uuid.UUID):
|
|
920
|
+
return str(value)
|
|
921
|
+
if parse_iso_timestamp and isinstance(value, str) and re.fullmatch(
|
|
922
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})", value
|
|
923
|
+
):
|
|
924
|
+
try:
|
|
925
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
926
|
+
except ValueError:
|
|
927
|
+
pass
|
|
928
|
+
else:
|
|
929
|
+
return _canonical_immutable_value(parsed)
|
|
930
|
+
if hasattr(value, "model_dump"):
|
|
931
|
+
value = value.model_dump()
|
|
932
|
+
if isinstance(value, dict):
|
|
933
|
+
return {str(key): _canonical_immutable_value(item) for key, item in value.items()}
|
|
934
|
+
if isinstance(value, (list, tuple)):
|
|
935
|
+
return [_canonical_immutable_value(item) for item in value]
|
|
936
|
+
return value
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _row_value(row: Any, field: str) -> Any:
|
|
940
|
+
if isinstance(row, dict):
|
|
941
|
+
return row.get(field)
|
|
942
|
+
return getattr(row, field, None)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
async def _load_immutable_row(model_cls: type, pk_field: str, pk_value: Any) -> Any | None:
|
|
946
|
+
rows = await model_cls.filter(**{pk_field: pk_value}).limit(1).all()
|
|
947
|
+
return rows[0] if rows else None
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _immutable_differences(row: Any, payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
951
|
+
differences: dict[str, dict[str, Any]] = {}
|
|
952
|
+
for field, expected in payload.items():
|
|
953
|
+
actual = _row_value(row, field)
|
|
954
|
+
# ISO parsing is field-aware: transcript content may legitimately be a
|
|
955
|
+
# date-looking literal, while *_at columns are actual instants.
|
|
956
|
+
parse_iso_timestamp = field.endswith("_at") or field.endswith("_timestamp")
|
|
957
|
+
if _canonical_immutable_value(actual, parse_iso_timestamp=parse_iso_timestamp) != _canonical_immutable_value(expected, parse_iso_timestamp=parse_iso_timestamp):
|
|
958
|
+
differences[field] = {
|
|
959
|
+
"expected": _canonical_immutable_value(expected, parse_iso_timestamp=parse_iso_timestamp),
|
|
960
|
+
"actual": _canonical_immutable_value(actual, parse_iso_timestamp=parse_iso_timestamp),
|
|
961
|
+
}
|
|
962
|
+
return differences
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def immutable_row_differences(row: Any, payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
966
|
+
"""Public exact-comparison contract for immutable insert/replay consumers."""
|
|
967
|
+
return _immutable_differences(row, payload)
|
|
968
|
+
|
|
969
|
+
|
|
904
970
|
class WritePreservedError(RuntimeError):
|
|
905
971
|
"""A direct write failed, but its payload was PRESERVED for replay.
|
|
906
972
|
|
|
@@ -1004,11 +1070,9 @@ async def create_or_capture(
|
|
|
1004
1070
|
row[pk_field] = str(_uuid.uuid4())
|
|
1005
1071
|
pk_value = row[pk_field]
|
|
1006
1072
|
table = f"{getattr(model_cls._meta, 'schema', 'public')}.{model_cls._meta.table_name}"
|
|
1007
|
-
|
|
1008
1073
|
try:
|
|
1009
1074
|
return await model_cls.create_item(**row)
|
|
1010
1075
|
except Exception as exc:
|
|
1011
|
-
# (3) The attempt failing is NOT proof the row is missing.
|
|
1012
1076
|
try:
|
|
1013
1077
|
landed = await model_cls.filter(**{pk_field: pk_value}).limit(1).all()
|
|
1014
1078
|
except Exception:
|
|
@@ -1018,11 +1082,79 @@ async def create_or_capture(
|
|
|
1018
1082
|
f"[matrx_orm.create_or_capture] {table} {pk_value} raised "
|
|
1019
1083
|
f"{type(exc).__name__} but the row IS committed — the write "
|
|
1020
1084
|
f"succeeded and the error was the acknowledgement, not the write. "
|
|
1021
|
-
|
|
1085
|
+
"Investigate: this means a client gave up on a landed write.",
|
|
1022
1086
|
color="yellow",
|
|
1023
1087
|
)
|
|
1024
1088
|
logger.warning("create_or_capture_landed_despite_error", exc_info=exc)
|
|
1025
1089
|
return landed[0]
|
|
1090
|
+
op = make_insert(model_cls, row, pk_value=pk_value)
|
|
1091
|
+
op_ids = await record_failures(
|
|
1092
|
+
[op], exc, request_id=request_id, user_id=user_id,
|
|
1093
|
+
conversation_id=conversation_id,
|
|
1094
|
+
organization_id=organization_id or payload.get("organization_id"), database=database,
|
|
1095
|
+
error_prefix=DIRECT_WRITE_PRESERVED_MARKER,
|
|
1096
|
+
)
|
|
1097
|
+
op_id = op_ids[0] if op_ids else None
|
|
1098
|
+
vcprint(
|
|
1099
|
+
f"[matrx_orm.create_or_capture] {table} {pk_value} FAILED "
|
|
1100
|
+
f"({type(exc).__name__}) — payload preserved as system_write_failure "
|
|
1101
|
+
f"op {op_id}; the auto-replay loop will re-apply it.",
|
|
1102
|
+
color="yellow",
|
|
1103
|
+
)
|
|
1104
|
+
raise WritePreservedError(
|
|
1105
|
+
f"{table} write failed ({type(exc).__name__}: {exc}); the payload was preserved for automatic replay",
|
|
1106
|
+
table=table, pk_value=pk_value, op_id=op_id, captured=op_id is not None, cause=exc,
|
|
1107
|
+
) from exc
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
async def immutable_insert_or_exact_verify(
|
|
1111
|
+
model_cls: type,
|
|
1112
|
+
payload: dict[str, Any],
|
|
1113
|
+
*,
|
|
1114
|
+
pk_field: str = "id",
|
|
1115
|
+
request_id: str | None = None,
|
|
1116
|
+
user_id: str | None = None,
|
|
1117
|
+
conversation_id: str | None = None,
|
|
1118
|
+
organization_id: str | None = None,
|
|
1119
|
+
database: str | None = None,
|
|
1120
|
+
) -> Any:
|
|
1121
|
+
"""Insert an immutable row, or verify the complete intended row exactly.
|
|
1122
|
+
|
|
1123
|
+
A failed acknowledgement is ambiguous: a fresh read either proves the exact
|
|
1124
|
+
row landed, proves a conflict, or preserves the complete insert for replay.
|
|
1125
|
+
"""
|
|
1126
|
+
from matrx_orm.session.op import make_insert
|
|
1127
|
+
|
|
1128
|
+
row = dict(payload)
|
|
1129
|
+
if not row.get(pk_field):
|
|
1130
|
+
row[pk_field] = str(_uuid.uuid4())
|
|
1131
|
+
pk_value = row[pk_field]
|
|
1132
|
+
table = f"{getattr(model_cls._meta, 'schema', 'public')}.{model_cls._meta.table_name}"
|
|
1133
|
+
|
|
1134
|
+
try:
|
|
1135
|
+
# Deliberately no Session: this primitive owns one direct write boundary.
|
|
1136
|
+
return await model_cls.create_item(**row)
|
|
1137
|
+
except Exception as exc:
|
|
1138
|
+
# (3) The attempt failing is NOT proof the row is missing.
|
|
1139
|
+
try:
|
|
1140
|
+
landed = await _load_immutable_row(model_cls, pk_field, pk_value)
|
|
1141
|
+
except Exception:
|
|
1142
|
+
landed = None
|
|
1143
|
+
if landed:
|
|
1144
|
+
differences = _immutable_differences(landed, row)
|
|
1145
|
+
if differences:
|
|
1146
|
+
raise ImmutableWriteConflictError(
|
|
1147
|
+
f"immutable write conflict for {table} {pk_value}: {sorted(differences)}"
|
|
1148
|
+
) from exc
|
|
1149
|
+
vcprint(
|
|
1150
|
+
f"[matrx_orm.create_or_capture] {table} {pk_value} raised "
|
|
1151
|
+
f"{type(exc).__name__} but the row IS committed — the write "
|
|
1152
|
+
f"succeeded and the error was the acknowledgement, not the write. "
|
|
1153
|
+
f"Investigate: this means a client gave up on a landed write.",
|
|
1154
|
+
color="yellow",
|
|
1155
|
+
)
|
|
1156
|
+
logger.warning("create_or_capture_landed_despite_error", exc_info=exc)
|
|
1157
|
+
return landed
|
|
1026
1158
|
|
|
1027
1159
|
# (4) Preserve the payload where the replay loop will find it.
|
|
1028
1160
|
op = make_insert(model_cls, row, pk_value=pk_value)
|
|
@@ -1034,7 +1166,7 @@ async def create_or_capture(
|
|
|
1034
1166
|
conversation_id=conversation_id,
|
|
1035
1167
|
organization_id=organization_id or payload.get("organization_id"),
|
|
1036
1168
|
database=database,
|
|
1037
|
-
error_prefix=
|
|
1169
|
+
error_prefix=IMMUTABLE_WRITE_PRESERVED_MARKER,
|
|
1038
1170
|
)
|
|
1039
1171
|
op_id = op_ids[0] if op_ids else None
|
|
1040
1172
|
vcprint(
|