matrx-orm 3.1.35__tar.gz → 3.1.36__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.35 → matrx_orm-3.1.36}/PKG-INFO +1 -1
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/base.py +11 -5
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/README.md +18 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/flush.py +11 -2
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/state.py +84 -1
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/pyproject.toml +1 -1
- matrx_orm-3.1.36/tests/session/test_cache_no_downgrade.py +97 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/.gitignore +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/AGENTS.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/CLAUDE.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/MODEL_API.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/docs/migrations.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/scripts/release.sh +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/conftest.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.35 → matrx_orm-3.1.36}/tests/test_sync_twins_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.36
|
|
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
|
|
@@ -1155,24 +1155,30 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1155
1155
|
return pks[0]
|
|
1156
1156
|
|
|
1157
1157
|
@classmethod
|
|
1158
|
-
async def load_by_id(cls, item_id: Any) -> Self:
|
|
1158
|
+
async def load_by_id(cls, item_id: Any, use_cache: bool = True) -> Self:
|
|
1159
1159
|
"""Load a row by its primary key. Raises DoesNotExist if absent.
|
|
1160
1160
|
|
|
1161
1161
|
Alias for ``get(**{pk: item_id})`` — mirrors ``BaseManager.load_by_id``.
|
|
1162
1162
|
Composite-PK models accept a dict ``{pk: val}`` or a PK-ordered
|
|
1163
1163
|
tuple/list (a bare scalar raises — see ``_build_pk_filter``).
|
|
1164
1164
|
For a None-returning lookup use :meth:`load_by_id_or_none`.
|
|
1165
|
+
|
|
1166
|
+
``use_cache=False`` is the AUTHORITATIVE read: pass it whenever the row
|
|
1167
|
+
decides identity, ownership, or whether the request may run at all. It
|
|
1168
|
+
also refreshes the cache with what it read, so a bypass never leaves a
|
|
1169
|
+
stale entry behind.
|
|
1165
1170
|
"""
|
|
1166
|
-
return await cls.get(**cls._build_pk_filter(item_id))
|
|
1171
|
+
return await cls.get(use_cache=use_cache, **cls._build_pk_filter(item_id))
|
|
1167
1172
|
|
|
1168
1173
|
@classmethod
|
|
1169
|
-
async def load_by_id_or_none(cls, item_id: Any) -> Self | None:
|
|
1174
|
+
async def load_by_id_or_none(cls, item_id: Any, use_cache: bool = True) -> Self | None:
|
|
1170
1175
|
"""Load a row by its primary key, or None if absent.
|
|
1171
1176
|
|
|
1172
1177
|
Alias for ``get_or_none(**{pk: item_id})``. Composite-PK models accept
|
|
1173
|
-
a dict / PK-ordered tuple, same as :meth:`load_by_id`.
|
|
1178
|
+
a dict / PK-ordered tuple, same as :meth:`load_by_id`. See
|
|
1179
|
+
:meth:`load_by_id` for when to pass ``use_cache=False``.
|
|
1174
1180
|
"""
|
|
1175
|
-
return await cls.get_or_none(**cls._build_pk_filter(item_id))
|
|
1181
|
+
return await cls.get_or_none(use_cache=use_cache, **cls._build_pk_filter(item_id))
|
|
1176
1182
|
|
|
1177
1183
|
@classmethod
|
|
1178
1184
|
async def filter_items(cls, *args: Any, **kwargs: Any) -> list[Self]:
|
|
@@ -134,6 +134,24 @@ Complex predicates (operator suffixes, JSON paths, joins) only match
|
|
|
134
134
|
DB rows — the pending state is assumed safe (the DB still applies the
|
|
135
135
|
predicate on its side).
|
|
136
136
|
|
|
137
|
+
## 🚨 Flush stubs are PARTIAL — they never enter the identity cache
|
|
138
|
+
|
|
139
|
+
`flush._execute_group` turns each queued UPDATE into a lightweight
|
|
140
|
+
`model_cls(**{pk, **payload})` instance so `bulk_update` can build per-row SQL.
|
|
141
|
+
That instance is **not a row snapshot**: every column outside the payload holds a
|
|
142
|
+
model default (usually `None`). Each one is therefore stamped by
|
|
143
|
+
`matrx_orm.state.mark_partial(...)`, and `ModelState.cache` **evicts** instead of
|
|
144
|
+
storing any partial — the next read refetches the whole row.
|
|
145
|
+
|
|
146
|
+
This is load-bearing, not hygiene. `bulk_update` write-through used to install
|
|
147
|
+
those stubs as the row's cache entry, so for the whole cache TTL (SHORT_TERM = 10
|
|
148
|
+
min) a freshly-updated row read back with NULLs in every untouched column. On
|
|
149
|
+
2026-07-25 that made a saved conversation read back with no `organization_id`
|
|
150
|
+
seconds after its own turn-1 title/`message_count` UPDATE flushed, and aidream's
|
|
151
|
+
identity guard hard-stopped the next turn. Any new code path that builds a model
|
|
152
|
+
instance from a column subset MUST call `mark_partial` — an undeclared downgrade
|
|
153
|
+
is evicted anyway and SCREAMS (see `state._scream_downgrade`).
|
|
154
|
+
|
|
137
155
|
## Nested Sessions
|
|
138
156
|
|
|
139
157
|
Sub-agents, parallel-execute branches, workflow super-steps — every
|
|
@@ -29,6 +29,7 @@ from collections.abc import Sequence
|
|
|
29
29
|
from contextvars import ContextVar
|
|
30
30
|
|
|
31
31
|
from matrx_orm.session.op import SessionOp
|
|
32
|
+
from matrx_orm.state import mark_partial
|
|
32
33
|
|
|
33
34
|
logger = logging.getLogger("matrx_orm.session.flush")
|
|
34
35
|
|
|
@@ -122,7 +123,13 @@ async def _execute_group(
|
|
|
122
123
|
for op in ops:
|
|
123
124
|
kwargs = {pk_field: op.pk_value}
|
|
124
125
|
kwargs.update(op.payload)
|
|
125
|
-
|
|
126
|
+
# These are stubs, NOT row snapshots: every column outside `kwargs`
|
|
127
|
+
# is a model default (None), not the row's value. Declare that, or
|
|
128
|
+
# bulk_update's write-through would install one as the row's cache
|
|
129
|
+
# entry and blank every other column for the TTL (this is the bug
|
|
130
|
+
# that made a saved conversation read back with no organization_id
|
|
131
|
+
# on its next turn). See matrx_orm.state.mark_partial.
|
|
132
|
+
instances.append(mark_partial(model_cls(**kwargs), kwargs))
|
|
126
133
|
|
|
127
134
|
await model_cls.bulk_update(instances, fields=all_fields)
|
|
128
135
|
return len(instances)
|
|
@@ -132,7 +139,9 @@ async def _execute_group(
|
|
|
132
139
|
pks = getattr(meta, "primary_keys", None) if meta else None
|
|
133
140
|
pk_field = pks[0] if pks else ops[0].pk_field
|
|
134
141
|
|
|
135
|
-
instances = [
|
|
142
|
+
instances = [
|
|
143
|
+
mark_partial(model_cls(**{pk_field: op.pk_value}), (pk_field,)) for op in ops
|
|
144
|
+
]
|
|
136
145
|
await model_cls.bulk_delete(instances)
|
|
137
146
|
return len(instances)
|
|
138
147
|
|
|
@@ -15,6 +15,48 @@ from matrx_orm.exceptions import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
# Attribute a write-through instance sets to declare "I am a {pk + payload}
|
|
19
|
+
# stub, not a row snapshot". Set by matrx_orm.session.flush; honored by
|
|
20
|
+
# ModelState.cache (evict, never store) and by mark_partial() below.
|
|
21
|
+
PARTIAL_FIELDS_ATTR = "_matrx_partial_fields"
|
|
22
|
+
|
|
23
|
+
# One scream per (model, blanked-field-set) — a downgrade is a code defect, so
|
|
24
|
+
# it must be visible, but it must not drown a hot loop in identical banners.
|
|
25
|
+
_SCREAMED_DOWNGRADES: set[tuple[str, tuple[str, ...]]] = set()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mark_partial(instance, fields):
|
|
29
|
+
"""Declare ``instance`` a partial write-through stub of its row.
|
|
30
|
+
|
|
31
|
+
Every caller that builds a model instance from a PK plus a subset of
|
|
32
|
+
columns (session flush, targeted bulk_update) MUST call this. Undeclared
|
|
33
|
+
partials trip the no-downgrade scream in :meth:`ModelState.cache`.
|
|
34
|
+
"""
|
|
35
|
+
setattr(instance, PARTIAL_FIELDS_ATTR, frozenset(fields))
|
|
36
|
+
return instance
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _scream_downgrade(model_class, blanked: list[str]) -> None:
|
|
40
|
+
signature = (model_class.__name__, tuple(sorted(blanked)))
|
|
41
|
+
if signature in _SCREAMED_DOWNGRADES:
|
|
42
|
+
return
|
|
43
|
+
_SCREAMED_DOWNGRADES.add(signature)
|
|
44
|
+
vcprint(
|
|
45
|
+
"\n"
|
|
46
|
+
"══════════════════════════════════════════════════════════════════════\n"
|
|
47
|
+
" MATRX ORM — CACHE DOWNGRADE BLOCKED (no-downgrade law)\n"
|
|
48
|
+
"══════════════════════════════════════════════════════════════════════\n"
|
|
49
|
+
f" model: {model_class.__name__}\n"
|
|
50
|
+
f" would blank: {', '.join(sorted(blanked))}\n"
|
|
51
|
+
" action: cache entry EVICTED (next read refetches the full row).\n"
|
|
52
|
+
" Nothing was lost, but a PARTIAL model instance reached a\n"
|
|
53
|
+
" write-through cache path without calling\n"
|
|
54
|
+
" matrx_orm.state.mark_partial() — fix the writer.\n"
|
|
55
|
+
"══════════════════════════════════════════════════════════════════════",
|
|
56
|
+
color="red",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
18
60
|
class CachePolicy(Enum):
|
|
19
61
|
PERMANENT = "permanent"
|
|
20
62
|
LONG_TERM = "long_term"
|
|
@@ -192,7 +234,26 @@ class ModelState:
|
|
|
192
234
|
return True
|
|
193
235
|
|
|
194
236
|
async def cache(self, record):
|
|
195
|
-
"""Cache a record with proper error handling.
|
|
237
|
+
"""Cache a record with proper error handling.
|
|
238
|
+
|
|
239
|
+
THE NO-DOWNGRADE LAW: a cache entry may only ever be REPLACED by a
|
|
240
|
+
record that is at least as informed as the one already there. A
|
|
241
|
+
write-through instance built from ``{pk, **payload}`` is NOT a row
|
|
242
|
+
snapshot — every column outside the payload holds a model default
|
|
243
|
+
(usually ``None``), not the row's real value. Storing one blanks the
|
|
244
|
+
row for every cached reader until the TTL expires.
|
|
245
|
+
|
|
246
|
+
That is exactly how a conversation with a perfectly good
|
|
247
|
+
``organization_id`` in Postgres read back as org-less for 10 minutes
|
|
248
|
+
after its own turn-1 title/message_count UPDATE flushed
|
|
249
|
+
(``session/flush.py`` builds partial instances; ``bulk_update`` cached
|
|
250
|
+
them), which then hard-failed the next turn's identity restore.
|
|
251
|
+
|
|
252
|
+
So: partial records EVICT instead of caching (the next read refetches
|
|
253
|
+
the whole row), and an unflagged record that would blank a populated
|
|
254
|
+
field evicts + SCREAMS — a downgrade means a partial instance reached
|
|
255
|
+
a write-through path without declaring itself.
|
|
256
|
+
"""
|
|
196
257
|
try:
|
|
197
258
|
if not record:
|
|
198
259
|
raise ValidationError(model=self.model_class, reason="Cannot cache None record")
|
|
@@ -204,6 +265,20 @@ class ModelState:
|
|
|
204
265
|
reason="Could not generate cache key for record",
|
|
205
266
|
)
|
|
206
267
|
|
|
268
|
+
if getattr(record, PARTIAL_FIELDS_ATTR, None) is not None:
|
|
269
|
+
# Declared partial (session flush write-through). Designed
|
|
270
|
+
# path — evict silently so the next read hits the DB.
|
|
271
|
+
self.evict_by_id(cache_key)
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
existing = self._cache.get(cache_key)
|
|
275
|
+
if existing is not None and existing is not record:
|
|
276
|
+
blanked = self._downgraded_fields(existing, record)
|
|
277
|
+
if blanked:
|
|
278
|
+
self.evict_by_id(cache_key)
|
|
279
|
+
_scream_downgrade(self.model_class, blanked)
|
|
280
|
+
return
|
|
281
|
+
|
|
207
282
|
self._cache[cache_key] = record
|
|
208
283
|
self._cache_times[cache_key] = datetime.now()
|
|
209
284
|
|
|
@@ -222,6 +297,14 @@ class ModelState:
|
|
|
222
297
|
original_error=e,
|
|
223
298
|
) from e
|
|
224
299
|
|
|
300
|
+
def _downgraded_fields(self, existing, incoming) -> list[str]:
|
|
301
|
+
"""Column names the incoming record would blank on the cached one."""
|
|
302
|
+
blanked: list[str] = []
|
|
303
|
+
for name in getattr(self.model_class, "_fields", {}):
|
|
304
|
+
if getattr(existing, name, None) is not None and getattr(incoming, name, None) is None:
|
|
305
|
+
blanked.append(name)
|
|
306
|
+
return blanked
|
|
307
|
+
|
|
225
308
|
async def remove(self, record):
|
|
226
309
|
cache_key = self._get_record_cache_key(record)
|
|
227
310
|
self._cache.pop(cache_key, None)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.36"
|
|
4
4
|
description = "Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""THE NO-DOWNGRADE LAW — a cache entry may never lose information.
|
|
2
|
+
|
|
3
|
+
Regression guard for the 2026-07-25 incident: a conversation with a correct
|
|
4
|
+
``organization_id`` in Postgres read back org-less for the whole 10-minute
|
|
5
|
+
cache TTL, because ``session/flush.py`` builds ``{pk + payload}`` stubs for
|
|
6
|
+
queued UPDATEs and ``operations/update.py::bulk_update`` wrote them straight
|
|
7
|
+
into the identity cache. The next turn's identity restore then STOPPED the
|
|
8
|
+
request on data that did not exist.
|
|
9
|
+
|
|
10
|
+
Two independent guarantees are locked in here:
|
|
11
|
+
1. flush declares its stubs partial (``mark_partial``).
|
|
12
|
+
2. ``ModelState.cache`` never stores a partial or downgrading record — it
|
|
13
|
+
evicts, so the next read refetches the whole row.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from matrx_orm.state import PARTIAL_FIELDS_ATTR, CachePolicy, ModelState, mark_partial
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Conversation:
|
|
24
|
+
"""Minimal Model stand-in with the fields the incident turned on."""
|
|
25
|
+
|
|
26
|
+
_fields = {
|
|
27
|
+
"id": object(),
|
|
28
|
+
"organization_id": object(),
|
|
29
|
+
"title": object(),
|
|
30
|
+
"message_count": object(),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class _Meta:
|
|
34
|
+
primary_keys = ["id"]
|
|
35
|
+
|
|
36
|
+
_meta = _Meta()
|
|
37
|
+
|
|
38
|
+
def __init__(self, **kwargs):
|
|
39
|
+
for name in self._fields:
|
|
40
|
+
setattr(self, name, kwargs.get(name))
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get_database_name(cls):
|
|
44
|
+
return "test_db"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _state() -> ModelState:
|
|
48
|
+
state = ModelState(_Conversation)
|
|
49
|
+
state.policy = CachePolicy.SHORT_TERM
|
|
50
|
+
return state
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _full_row() -> _Conversation:
|
|
54
|
+
return _Conversation(
|
|
55
|
+
id="c1", organization_id="org-1", title="Anthropic Model Sync", message_count=3
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_flush_update_stub_is_declared_partial():
|
|
61
|
+
stub = mark_partial(_Conversation(id="c1", message_count=5), {"id", "message_count"})
|
|
62
|
+
assert getattr(stub, PARTIAL_FIELDS_ATTR) == frozenset({"id", "message_count"})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_partial_stub_evicts_instead_of_blanking_the_row():
|
|
67
|
+
state = _state()
|
|
68
|
+
await state.cache(_full_row())
|
|
69
|
+
assert (await state.get(id="c1")).organization_id == "org-1"
|
|
70
|
+
|
|
71
|
+
# Exactly what a queued conversation UPDATE flush produces on turn 1.
|
|
72
|
+
await state.cache(mark_partial(_Conversation(id="c1", message_count=5), {"id", "message_count"}))
|
|
73
|
+
|
|
74
|
+
# The org-less stub must NOT be servable. Cache miss → caller refetches.
|
|
75
|
+
assert await state.get(id="c1") is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_undeclared_downgrade_is_refused_too():
|
|
80
|
+
state = _state()
|
|
81
|
+
await state.cache(_full_row())
|
|
82
|
+
await state.cache(_Conversation(id="c1", message_count=7)) # no mark_partial
|
|
83
|
+
assert await state.get(id="c1") is None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.asyncio
|
|
87
|
+
async def test_equally_informed_record_still_replaces():
|
|
88
|
+
state = _state()
|
|
89
|
+
await state.cache(_full_row())
|
|
90
|
+
fresh = _Conversation(
|
|
91
|
+
id="c1", organization_id="org-1", title="Renamed", message_count=9
|
|
92
|
+
)
|
|
93
|
+
await state.cache(fresh)
|
|
94
|
+
cached = await state.get(id="c1")
|
|
95
|
+
assert cached is fresh
|
|
96
|
+
assert cached.title == "Renamed"
|
|
97
|
+
assert cached.organization_id == "org-1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|