matrx-orm 3.1.65__tar.gz → 3.1.67__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.65 → matrx_orm-3.1.67}/PKG-INFO +1 -1
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/__init__.py +4 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/associations.py +28 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/FEATURE.md +14 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/base.py +1 -3
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/README.md +29 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/__init__.py +9 -1
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/fallback.py +182 -3
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/pyproject.toml +1 -1
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_derived_table.py +1 -1
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_expression_primitives.py +5 -2
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_join_and_aggregate.py +1 -1
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_managed_write_guard.py +11 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_model_meta.py +1 -1
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_query_builder.py +1 -1
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_relational_backfill.py +4 -4
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_typed_join_projection.py +2 -1
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_typed_predicates.py +10 -7
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/.gitignore +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/AGENTS.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/CLAUDE.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/MODEL_API.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/docs/migrations.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/scripts/release.sh +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/conftest.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.65 → matrx_orm-3.1.67}/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.67
|
|
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
|
|
@@ -317,10 +317,12 @@ from matrx_orm.session import (
|
|
|
317
317
|
COORDINATOR_BYPASS_ACKNOWLEDGEMENT,
|
|
318
318
|
allow_direct_coordinator_write,
|
|
319
319
|
coordinator_policy_tables,
|
|
320
|
+
create_or_capture,
|
|
320
321
|
current_session,
|
|
321
322
|
declare_managed,
|
|
322
323
|
drain_spilled_ops,
|
|
323
324
|
is_in_session,
|
|
325
|
+
WritePreservedError,
|
|
324
326
|
record_error,
|
|
325
327
|
record_failures,
|
|
326
328
|
register_coordinator_owned_model,
|
|
@@ -599,6 +601,8 @@ __all__ = [
|
|
|
599
601
|
"record_error",
|
|
600
602
|
"record_failures",
|
|
601
603
|
"drain_spilled_ops",
|
|
604
|
+
"create_or_capture",
|
|
605
|
+
"WritePreservedError",
|
|
602
606
|
"run_lifecycle_sweeper",
|
|
603
607
|
"sweep_lifecycle_once",
|
|
604
608
|
# ── Adapter layer ──────────────────────────────────────────────────────────
|
|
@@ -186,6 +186,7 @@ class Associations:
|
|
|
186
186
|
target_id_field: str = "target_id",
|
|
187
187
|
org_field: str = "org_id",
|
|
188
188
|
role_field: str = "role",
|
|
189
|
+
soft_delete_field: str = "deleted_at",
|
|
189
190
|
) -> None:
|
|
190
191
|
self._assoc = association_model
|
|
191
192
|
self._reg = registry
|
|
@@ -196,6 +197,14 @@ class Associations:
|
|
|
196
197
|
self._org = org_field
|
|
197
198
|
self._role = role_field
|
|
198
199
|
self._role_available = True # refined by _validate_field_bindings when introspectable
|
|
200
|
+
# An edge table MAY tombstone edges rather than delete them: the host's
|
|
201
|
+
# entity soft-delete carries its edges down with it, and the entity's
|
|
202
|
+
# restore brings exactly those edges back. Where the column exists EVERY
|
|
203
|
+
# read here is live-only — a tombstoned edge conveys nothing, so handing
|
|
204
|
+
# one back is an access leak. A minimal host edge table without the
|
|
205
|
+
# column keeps the old unfiltered behaviour.
|
|
206
|
+
self._soft_delete = soft_delete_field
|
|
207
|
+
self._soft_delete_available = False # refined by _validate_field_bindings
|
|
199
208
|
# Optional edge-document columns (platform.associations carries them;
|
|
200
209
|
# a minimal host edge table may not). Passing the kwarg against a
|
|
201
210
|
# table without the column raises loudly at call time.
|
|
@@ -239,9 +248,14 @@ class Associations:
|
|
|
239
248
|
# role is OPTIONAL on the edge table — engines over role-less edge
|
|
240
249
|
# tables keep working; passing a role against one raises at call time.
|
|
241
250
|
self._role_available = self._role in fields
|
|
251
|
+
self._soft_delete_available = self._soft_delete in fields
|
|
242
252
|
for col in self._optional_cols:
|
|
243
253
|
self._optional_cols[col] = col in fields
|
|
244
254
|
|
|
255
|
+
def _live_filter(self) -> dict[str, Any]:
|
|
256
|
+
"""Restrict a read to non-tombstoned edges (no-op without the column)."""
|
|
257
|
+
return {self._soft_delete: None} if self._soft_delete_available else {}
|
|
258
|
+
|
|
245
259
|
# ---- writes ----------------------------------------------------------
|
|
246
260
|
|
|
247
261
|
def _role_filter(self, role: Any) -> dict[str, Any]:
|
|
@@ -332,6 +346,14 @@ class Associations:
|
|
|
332
346
|
for col, value in {"metadata": metadata, **extras}.items()
|
|
333
347
|
if value is not None and getattr(edge, col, None) != value
|
|
334
348
|
}
|
|
349
|
+
# Re-attaching over a TOMBSTONE revives it. The lookup deliberately
|
|
350
|
+
# matches tombstoned edges (the DB's unique index does too), so
|
|
351
|
+
# without this the caller would get back an edge nothing can see.
|
|
352
|
+
if self._soft_delete_available and getattr(edge, self._soft_delete, None) is not None:
|
|
353
|
+
updates[self._soft_delete] = None
|
|
354
|
+
for stamp in ("deleted_via_type", "deleted_via_id"):
|
|
355
|
+
if stamp in getattr(getattr(self._assoc, "_meta", None), "fields", ()):
|
|
356
|
+
updates[stamp] = None
|
|
335
357
|
if updates:
|
|
336
358
|
edge = await edge.update(**updates)
|
|
337
359
|
return edge
|
|
@@ -352,6 +374,10 @@ class Associations:
|
|
|
352
374
|
self._ti: target.id,
|
|
353
375
|
self._org: org_id,
|
|
354
376
|
**self._role_filter(role),
|
|
377
|
+
# Never purge a tombstone: the user is detaching what they can
|
|
378
|
+
# SEE, and the tombstone must survive to be restored with its
|
|
379
|
+
# entity. A hard entity DELETE is what purges tombstones.
|
|
380
|
+
**self._live_filter(),
|
|
355
381
|
}
|
|
356
382
|
)
|
|
357
383
|
for edge in edges:
|
|
@@ -374,6 +400,7 @@ class Associations:
|
|
|
374
400
|
if target_type is not None:
|
|
375
401
|
filters[self._tt] = target_type
|
|
376
402
|
filters.update(self._role_filter(role))
|
|
403
|
+
filters.update(self._live_filter())
|
|
377
404
|
return await self._assoc.filter_items(**filters)
|
|
378
405
|
|
|
379
406
|
async def edges_to(
|
|
@@ -390,6 +417,7 @@ class Associations:
|
|
|
390
417
|
if source_type is not None:
|
|
391
418
|
filters[self._st] = source_type
|
|
392
419
|
filters.update(self._role_filter(role))
|
|
420
|
+
filters.update(self._live_filter())
|
|
393
421
|
return await self._assoc.filter_items(**filters)
|
|
394
422
|
|
|
395
423
|
async def targets_from(
|
|
@@ -13,6 +13,18 @@ columns present in the row as loaded.
|
|
|
13
13
|
Pinned by `tests/level1/test_parameter_name_hydration.py` and
|
|
14
14
|
`tests/level1/test_dirty_tracking.py`.
|
|
15
15
|
|
|
16
|
+
## Relation identity
|
|
17
|
+
|
|
18
|
+
**Every model renders an exact `schema.table` SQL target.** Models whose
|
|
19
|
+
generated metadata omits `_db_schema` are public-schema models, so
|
|
20
|
+
`ModelOptions.qualified_table_name` returns `public.<table>` rather than a bare
|
|
21
|
+
name. This keeps ordinary public-model writes compatible with the managed-write
|
|
22
|
+
guard and ensures diagnostic sinks such as `public.app_log` and
|
|
23
|
+
`public.system_write_failure` remain writable after schema-identity hardening.
|
|
24
|
+
|
|
25
|
+
Pinned by `tests/level1/test_model_meta.py` and
|
|
26
|
+
`tests/level1/test_managed_write_guard.py`.
|
|
27
|
+
|
|
16
28
|
## Database execution errors
|
|
17
29
|
|
|
18
30
|
`AsyncDatabaseManager.execute_query` must classify every PostgreSQL integrity
|
|
@@ -45,6 +57,8 @@ Verified against code: 2026-08-13.
|
|
|
45
57
|
|
|
46
58
|
## Change log
|
|
47
59
|
|
|
60
|
+
- 2026-08-14: Public models now render `public.table` SQL targets; bare public
|
|
61
|
+
relation names can no longer disable application logs or write-failure capture.
|
|
48
62
|
- 2026-08-13: Registry, admin, recovery, catalog, and diagnostic identities are now exact `schema.table`; bare names are accepted only as uncached discovery terms and ambiguity fails loudly.
|
|
49
63
|
- 2026-08-13: Added atomic recovery from a pool left at zero connections after an upstream database restart.
|
|
50
64
|
- 2026-08-09: Pinned positional-mapping hydration for parameter-shaped column names.
|
|
@@ -176,9 +176,7 @@ class ModelOptions:
|
|
|
176
176
|
|
|
177
177
|
@property
|
|
178
178
|
def qualified_table_name(self) -> str:
|
|
179
|
-
|
|
180
|
-
return f"{self.db_schema}.{self.table_name}"
|
|
181
|
-
return self.table_name
|
|
179
|
+
return f"{self.db_schema or 'public'}.{self.table_name}"
|
|
182
180
|
|
|
183
181
|
|
|
184
182
|
class ModelMeta(type):
|
|
@@ -231,6 +231,35 @@ matrx-ai/persistence module.
|
|
|
231
231
|
See [docs/persistence/ORM_SESSION_VISION.md](../../../../docs/persistence/ORM_SESSION_VISION.md)
|
|
232
232
|
for the full migration plan.
|
|
233
233
|
|
|
234
|
+
## Keep-the-work: `create_or_capture` for DIRECT writes
|
|
235
|
+
|
|
236
|
+
A Session-queued write that fails is captured to `system_write_failure` and
|
|
237
|
+
replayed automatically. A **direct** write (`Model.create_item(...)`) had no
|
|
238
|
+
such tier — when it raised, the payload evaporated. That destroyed a paid agent
|
|
239
|
+
result in production (matrx-frontend FOUND_DEFECTS D183): the model had already
|
|
240
|
+
answered, the user had already been billed, and a 10s statement timeout on the
|
|
241
|
+
single INSERT deleted the only copy.
|
|
242
|
+
|
|
243
|
+
`create_or_capture(model_cls, payload, ...)` closes it, in this order:
|
|
244
|
+
|
|
245
|
+
1. **Mint the PK first**, so one id spans the attempt, the verification, and any
|
|
246
|
+
replay.
|
|
247
|
+
2. **Fire** `create_item`.
|
|
248
|
+
3. **Verify before declaring loss** — a statement timeout can fire *after* the
|
|
249
|
+
row committed, so re-read by PK; if it is there the write SUCCEEDED (logged
|
|
250
|
+
loudly: a client gave up on a landed write).
|
|
251
|
+
4. **Preserve** — capture the intended INSERT to `system_write_failure` stamped
|
|
252
|
+
`DirectWritePreserved`, which `matrx_ai`'s auto-replay loop treats as
|
|
253
|
+
self-healing and re-applies with no human in the loop.
|
|
254
|
+
5. **Raise `WritePreservedError`** carrying the op handle, so the caller's
|
|
255
|
+
failure record can say *your work survived* (`as_payload()` is the JSON-safe
|
|
256
|
+
shape for a run row's `error` column and for a stream event).
|
|
257
|
+
|
|
258
|
+
**Reach for it only where a lost row means lost money or lost human effort** —
|
|
259
|
+
an agent's structured output, a provider's response, a rendered document.
|
|
260
|
+
Ordinary writes stay on the Session/Coordinator path, which already has this
|
|
261
|
+
guarantee. Live proof: `scripts/_verify_keep_the_work_live.py`.
|
|
262
|
+
|
|
234
263
|
## Doctrine references
|
|
235
264
|
|
|
236
265
|
- `docs/persistence/DOCTRINE.md` — the persistence rules
|
|
@@ -56,7 +56,13 @@ from matrx_orm.session.errors import (
|
|
|
56
56
|
SessionClosedError,
|
|
57
57
|
SessionError,
|
|
58
58
|
)
|
|
59
|
-
from matrx_orm.session.fallback import
|
|
59
|
+
from matrx_orm.session.fallback import (
|
|
60
|
+
WritePreservedError,
|
|
61
|
+
create_or_capture,
|
|
62
|
+
drain_spilled_ops,
|
|
63
|
+
record_error,
|
|
64
|
+
record_failures,
|
|
65
|
+
)
|
|
60
66
|
from matrx_orm.session.lifecycle import (
|
|
61
67
|
SWEEP_INTERVAL_SECS_DEFAULT,
|
|
62
68
|
SWEEP_LIMIT_DEFAULT,
|
|
@@ -131,6 +137,8 @@ __all__ = [
|
|
|
131
137
|
"record_error",
|
|
132
138
|
"record_failures",
|
|
133
139
|
"drain_spilled_ops",
|
|
140
|
+
"create_or_capture",
|
|
141
|
+
"WritePreservedError",
|
|
134
142
|
"merge_pending_with_db_one",
|
|
135
143
|
"run_sweeper",
|
|
136
144
|
"session_stack",
|
|
@@ -50,6 +50,15 @@ _SPILL_PACKAGED_DEFAULT = "/var/lib/matrx/spill"
|
|
|
50
50
|
# which is definitionally recoverable once the DB returns). matrx-ai's replay
|
|
51
51
|
# loop adds this to its RECOVERABLE_RETRY_ERRORS set — keep the string in lockstep.
|
|
52
52
|
DISK_SPILL_RECOVERED_MARKER = "DiskSpillRecovered"
|
|
53
|
+
# Signature stamped into a row captured by :func:`create_or_capture` — a DIRECT
|
|
54
|
+
# (non-Session) write that failed after work was already done and paid for. It
|
|
55
|
+
# is stamped for the same reason as the disk-spill marker: the failure classes
|
|
56
|
+
# that kill a direct write (statement timeout, connection reset, pool
|
|
57
|
+
# exhaustion) are ENVIRONMENTAL and self-heal, so the auto-replay loop should
|
|
58
|
+
# re-apply the row rather than leave a paid artifact sitting in a failure table.
|
|
59
|
+
# matrx-ai's replay loop adds this to its RECOVERABLE_RETRY_ERRORS set — keep
|
|
60
|
+
# the string in lockstep.
|
|
61
|
+
DIRECT_WRITE_PRESERVED_MARKER = "DirectWritePreserved"
|
|
53
62
|
# Resolved-writable spill dir, cached after first success so we don't re-probe
|
|
54
63
|
# the cascade on every failure. None until first resolution.
|
|
55
64
|
_spill_dir_cache: Path | None = None
|
|
@@ -300,19 +309,31 @@ async def record_failures(
|
|
|
300
309
|
user_id: str | None = None,
|
|
301
310
|
conversation_id: str | None = None,
|
|
302
311
|
database: str | None = None,
|
|
303
|
-
|
|
312
|
+
error_prefix: str | None = None,
|
|
313
|
+
) -> list[str]:
|
|
304
314
|
"""Write every dropped op to ``system_write_failure`` on a fresh connection.
|
|
305
315
|
|
|
306
316
|
Never raises. If the fresh-connection write itself fails, dumps every
|
|
307
317
|
op to stdout in red and to the structured logger.
|
|
318
|
+
|
|
319
|
+
``error_prefix`` stamps a recoverability marker onto ``error_text`` (the
|
|
320
|
+
same mechanism ``drain_spilled_ops`` uses for ``DISK_SPILL_RECOVERED_MARKER``)
|
|
321
|
+
so the auto-replay loop can classify this capture without pattern-matching
|
|
322
|
+
a driver message.
|
|
323
|
+
|
|
324
|
+
Returns the ``op_id`` of every op it captured — the handle a caller quotes
|
|
325
|
+
back to a human ("your work is preserved as <id>") and the key the admin
|
|
326
|
+
persistence surface replays by.
|
|
308
327
|
"""
|
|
309
328
|
if not ops:
|
|
310
|
-
return
|
|
329
|
+
return []
|
|
311
330
|
|
|
312
331
|
# Never blank: resolve the deepest driver error (SQLSTATE + message + diag)
|
|
313
332
|
# so the system_write_failure row records WHY the write was dropped, even
|
|
314
333
|
# when the exception stringifies to "".
|
|
315
334
|
error_text = describe_db_exception(exc).as_text()
|
|
335
|
+
if error_prefix:
|
|
336
|
+
error_text = f"{error_prefix}: {error_text}"
|
|
316
337
|
tb_text = "".join(_tb.format_exception(type(exc), exc, exc.__traceback__))
|
|
317
338
|
|
|
318
339
|
# UUID-typed columns must never crash the capture itself (request_id is
|
|
@@ -351,7 +372,7 @@ async def record_failures(
|
|
|
351
372
|
f"system_write_failure (flush exc: {error_text})",
|
|
352
373
|
color="yellow",
|
|
353
374
|
)
|
|
354
|
-
return
|
|
375
|
+
return [op.op_id for op in ops]
|
|
355
376
|
|
|
356
377
|
# Anything that didn't reach the table is spilled to disk — last resort
|
|
357
378
|
# before data loss. drain_spilled_ops() re-lands it when the DB returns.
|
|
@@ -376,6 +397,10 @@ async def record_failures(
|
|
|
376
397
|
spilled=spilled,
|
|
377
398
|
total=len(failed_rows),
|
|
378
399
|
)
|
|
400
|
+
# A spilled op is still preserved (drain_spilled_ops re-lands it), so it
|
|
401
|
+
# counts as captured. Only ops that reached neither the table nor the disk
|
|
402
|
+
# are lost, and those already screamed above.
|
|
403
|
+
return [op.op_id for op in ops]
|
|
379
404
|
|
|
380
405
|
|
|
381
406
|
async def _bulk_insert_failures(
|
|
@@ -657,3 +682,157 @@ def _scream_to_stdout(
|
|
|
657
682
|
)
|
|
658
683
|
except Exception:
|
|
659
684
|
vcprint(f" LOST OP {op.op_id} (could not serialize)", color="red")
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# ── Keep-the-work tier: DIRECT writes that must never lose their payload ─────
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class WritePreservedError(RuntimeError):
|
|
691
|
+
"""A direct write failed, but its payload was PRESERVED for replay.
|
|
692
|
+
|
|
693
|
+
Raised by :func:`create_or_capture` so a caller can tell a human the truth
|
|
694
|
+
that matters: the expensive work is not gone. ``op_id`` is the
|
|
695
|
+
``system_write_failure`` handle the auto-replay loop retries and the admin
|
|
696
|
+
persistence surface replays by.
|
|
697
|
+
|
|
698
|
+
``captured=False`` means even the capture failed (the op was screamed to
|
|
699
|
+
stdout/structlog) — the ONLY case where a caller should say the work is
|
|
700
|
+
genuinely at risk.
|
|
701
|
+
"""
|
|
702
|
+
|
|
703
|
+
def __init__(
|
|
704
|
+
self,
|
|
705
|
+
message: str,
|
|
706
|
+
*,
|
|
707
|
+
table: str,
|
|
708
|
+
pk_value: Any,
|
|
709
|
+
op_id: str | None,
|
|
710
|
+
captured: bool,
|
|
711
|
+
cause: BaseException,
|
|
712
|
+
) -> None:
|
|
713
|
+
super().__init__(message)
|
|
714
|
+
self.table = table
|
|
715
|
+
self.pk_value = pk_value
|
|
716
|
+
self.op_id = op_id
|
|
717
|
+
self.captured = captured
|
|
718
|
+
self.cause = cause
|
|
719
|
+
|
|
720
|
+
def as_payload(self) -> dict[str, Any]:
|
|
721
|
+
"""JSON-safe description for a run row's ``error`` column / a stream
|
|
722
|
+
event. Never a bare traceback — this is what a SURFACE renders."""
|
|
723
|
+
return {
|
|
724
|
+
"type": "WritePreservedError",
|
|
725
|
+
"message": str(self),
|
|
726
|
+
"work_preserved": self.captured,
|
|
727
|
+
"preserved_op_id": self.op_id,
|
|
728
|
+
"preserved_table": self.table,
|
|
729
|
+
"preserved_row_id": str(self.pk_value),
|
|
730
|
+
"user_message": (
|
|
731
|
+
"The result was generated successfully but could not be saved "
|
|
732
|
+
"to its final home. It has been preserved and will be recovered "
|
|
733
|
+
"automatically — you will not be charged to produce it again."
|
|
734
|
+
if self.captured
|
|
735
|
+
else "The result was generated but could not be saved, and the "
|
|
736
|
+
"preservation write also failed. Support has the full payload "
|
|
737
|
+
"in the server logs."
|
|
738
|
+
),
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
async def create_or_capture(
|
|
743
|
+
model_cls: type,
|
|
744
|
+
payload: dict[str, Any],
|
|
745
|
+
*,
|
|
746
|
+
pk_field: str = "id",
|
|
747
|
+
request_id: str | None = None,
|
|
748
|
+
user_id: str | None = None,
|
|
749
|
+
conversation_id: str | None = None,
|
|
750
|
+
database: str | None = None,
|
|
751
|
+
) -> Any:
|
|
752
|
+
"""Create ONE row, and never lose its payload if the write fails.
|
|
753
|
+
|
|
754
|
+
THE HOLE THIS CLOSES: a Session-queued write that fails is captured to
|
|
755
|
+
``system_write_failure`` and replayed automatically. A DIRECT write
|
|
756
|
+
(``Model.create_item(...)``) had no such tier — when it raised, the payload
|
|
757
|
+
simply evaporated. That is fine for a row we can trivially recompute and
|
|
758
|
+
catastrophic for the ONE class of write this function exists for: **the
|
|
759
|
+
durable home of an artifact that was already generated and already paid
|
|
760
|
+
for** — an agent's structured output, a provider's response, a rendered
|
|
761
|
+
document. Losing that write does not just fail the request, it destroys
|
|
762
|
+
work the user was billed for and forces them to buy it again (D183).
|
|
763
|
+
|
|
764
|
+
The contract, in order:
|
|
765
|
+
|
|
766
|
+
1. **Stable identity first.** The PK is minted here when absent, so the
|
|
767
|
+
same id spans the attempt, the verification, and any later replay.
|
|
768
|
+
2. **Fire.** ``model_cls.create_item(**payload)``.
|
|
769
|
+
3. **Verify on failure.** A statement timeout can fire AFTER the row
|
|
770
|
+
committed, so a failed attempt is not proof of a missing row. Re-read by
|
|
771
|
+
PK on a fresh read before declaring anything lost; if it is there, the
|
|
772
|
+
write SUCCEEDED and we return it (loudly — a recovery firing means a
|
|
773
|
+
real defect got past the proactive layer).
|
|
774
|
+
4. **Preserve.** Otherwise capture the intended INSERT into
|
|
775
|
+
``system_write_failure`` stamped ``DirectWritePreserved``, so the
|
|
776
|
+
existing auto-replay loop re-applies it with no human in the loop.
|
|
777
|
+
5. **Tell the truth.** Raise :class:`WritePreservedError` carrying the
|
|
778
|
+
handle, so the caller's failure record says "your work survived" instead
|
|
779
|
+
of only "it broke".
|
|
780
|
+
|
|
781
|
+
This is a KEEP-THE-WORK primitive, not a general write path: reach for it
|
|
782
|
+
where a lost row means lost money or lost human effort. Ordinary writes
|
|
783
|
+
stay on the Session/Coordinator path, which already has this guarantee.
|
|
784
|
+
"""
|
|
785
|
+
from matrx_orm.session.op import make_insert
|
|
786
|
+
|
|
787
|
+
row = dict(payload)
|
|
788
|
+
if not row.get(pk_field):
|
|
789
|
+
row[pk_field] = str(_uuid.uuid4())
|
|
790
|
+
pk_value = row[pk_field]
|
|
791
|
+
table = f"{getattr(model_cls._meta, 'schema', 'public')}.{model_cls._meta.table_name}"
|
|
792
|
+
|
|
793
|
+
try:
|
|
794
|
+
return await model_cls.create_item(**row)
|
|
795
|
+
except Exception as exc:
|
|
796
|
+
# (3) The attempt failing is NOT proof the row is missing.
|
|
797
|
+
try:
|
|
798
|
+
landed = await model_cls.filter(**{pk_field: pk_value}).limit(1).all()
|
|
799
|
+
except Exception:
|
|
800
|
+
landed = []
|
|
801
|
+
if landed:
|
|
802
|
+
vcprint(
|
|
803
|
+
f"[matrx_orm.create_or_capture] {table} {pk_value} raised "
|
|
804
|
+
f"{type(exc).__name__} but the row IS committed — the write "
|
|
805
|
+
f"succeeded and the error was the acknowledgement, not the write. "
|
|
806
|
+
f"Investigate: this means a client gave up on a landed write.",
|
|
807
|
+
color="yellow",
|
|
808
|
+
)
|
|
809
|
+
logger.warning("create_or_capture_landed_despite_error", exc_info=exc)
|
|
810
|
+
return landed[0]
|
|
811
|
+
|
|
812
|
+
# (4) Preserve the payload where the replay loop will find it.
|
|
813
|
+
op = make_insert(model_cls, row, pk_value=pk_value)
|
|
814
|
+
op_ids = await record_failures(
|
|
815
|
+
[op],
|
|
816
|
+
exc,
|
|
817
|
+
request_id=request_id,
|
|
818
|
+
user_id=user_id,
|
|
819
|
+
conversation_id=conversation_id,
|
|
820
|
+
database=database,
|
|
821
|
+
error_prefix=DIRECT_WRITE_PRESERVED_MARKER,
|
|
822
|
+
)
|
|
823
|
+
op_id = op_ids[0] if op_ids else None
|
|
824
|
+
vcprint(
|
|
825
|
+
f"[matrx_orm.create_or_capture] {table} {pk_value} FAILED "
|
|
826
|
+
f"({type(exc).__name__}) — payload preserved as system_write_failure "
|
|
827
|
+
f"op {op_id}; the auto-replay loop will re-apply it.",
|
|
828
|
+
color="yellow",
|
|
829
|
+
)
|
|
830
|
+
raise WritePreservedError(
|
|
831
|
+
f"{table} write failed ({type(exc).__name__}: {exc}); the payload was "
|
|
832
|
+
f"preserved for automatic replay",
|
|
833
|
+
table=table,
|
|
834
|
+
pk_value=pk_value,
|
|
835
|
+
op_id=op_id,
|
|
836
|
+
captured=op_id is not None,
|
|
837
|
+
cause=exc,
|
|
838
|
+
) from exc
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.67"
|
|
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" }
|
|
@@ -86,7 +86,7 @@ def test_correlated_subquery_aggregate_params_shift_past_outer_params() -> None:
|
|
|
86
86
|
sql, params = QueryExecutor(query._build_query())._build_query()
|
|
87
87
|
|
|
88
88
|
assert "COALESCE(SUM(amount), $1)" in sql
|
|
89
|
-
assert "id = parents.id" in sql
|
|
89
|
+
assert "id = public.parents.id" in sql
|
|
90
90
|
assert "kind = $2" in sql
|
|
91
91
|
assert "WHERE kind = $3" in sql
|
|
92
92
|
assert params == [0, "child", "parent"]
|
|
@@ -137,7 +137,7 @@ def test_exists_in_filter_now_works_after_list_fix(_dummy_adapter):
|
|
|
137
137
|
Site, Run = _models()
|
|
138
138
|
# Before the fix this raised AttributeError (.items() on a list).
|
|
139
139
|
sql = _sql(Site.filter(Exists(Run.filter(site_id=OuterRef("id")))))
|
|
140
|
-
assert "EXISTS (SELECT 1 FROM runs WHERE site_id = sites.id)" in sql
|
|
140
|
+
assert "EXISTS (SELECT 1 FROM public.runs WHERE site_id = public.sites.id)" in sql
|
|
141
141
|
finally:
|
|
142
142
|
ModelRegistry.clear()
|
|
143
143
|
|
|
@@ -151,7 +151,10 @@ def test_correlated_count_subquery_in_annotate(_dummy_adapter):
|
|
|
151
151
|
inner = Run.filter(site_id=OuterRef("id")).annotate(c=Count())
|
|
152
152
|
sql = _sql(Site.filter().annotate(run_count=Subquery(inner)))
|
|
153
153
|
# The correlated scalar rollup: (SELECT COUNT(*) FROM runs WHERE ...) AS run_count
|
|
154
|
-
assert
|
|
154
|
+
assert (
|
|
155
|
+
"(SELECT COUNT(*) FROM public.runs WHERE site_id = public.sites.id) AS run_count"
|
|
156
|
+
in sql
|
|
157
|
+
)
|
|
155
158
|
finally:
|
|
156
159
|
ModelRegistry.clear()
|
|
157
160
|
|
|
@@ -310,7 +310,7 @@ class TestBaseManagerAggregate:
|
|
|
310
310
|
assert "SELECT *" not in sql.split(" FROM ")[0]
|
|
311
311
|
assert "SUM(amount) AS total" in sql
|
|
312
312
|
assert "COUNT(id) AS n" in sql
|
|
313
|
-
assert "FROM orders" in sql
|
|
313
|
+
assert "FROM public.orders" in sql
|
|
314
314
|
assert "WHERE status = $1" in sql
|
|
315
315
|
assert params == ["paid"]
|
|
316
316
|
|
|
@@ -95,6 +95,17 @@ class TestStrictGuard:
|
|
|
95
95
|
M._STRICT_TABLES.add("public.cx_message")
|
|
96
96
|
M.assert_managed_write_allowed("WITH t AS (...) UPDATE cx_message ...") # no raise
|
|
97
97
|
|
|
98
|
+
def test_public_model_sql_is_schema_qualified_at_the_source(self):
|
|
99
|
+
model = _make_model(
|
|
100
|
+
"PublicDiagnostic",
|
|
101
|
+
{"id": UUIDField(primary_key=True), "_table_name": "diagnostic_sink"},
|
|
102
|
+
)
|
|
103
|
+
assert model._meta.qualified_table_name == "public.diagnostic_sink"
|
|
104
|
+
M._STRICT_TABLES.add("chat.message")
|
|
105
|
+
M.assert_managed_write_allowed(
|
|
106
|
+
f"INSERT INTO {model._meta.qualified_table_name} (id) VALUES ($1)"
|
|
107
|
+
)
|
|
108
|
+
|
|
98
109
|
|
|
99
110
|
class TestDeclareManagedRegistersStrictTable:
|
|
100
111
|
def test_strict_declaration_registers_table(self):
|
|
@@ -110,7 +110,7 @@ class TestBuildQuery:
|
|
|
110
110
|
qb = QueryBuilder(M).filter(name="Alice").limit(10)
|
|
111
111
|
q = qb._build_query()
|
|
112
112
|
assert q["model"] is M
|
|
113
|
-
assert q["table"] == "qb_table"
|
|
113
|
+
assert q["table"] == "public.qb_table"
|
|
114
114
|
# filters is now a list of dicts/Q objects — find the dict that contains "name"
|
|
115
115
|
filter_items = q["filters"]
|
|
116
116
|
combined = {}
|
|
@@ -100,13 +100,13 @@ async def test_set_based_backfills_render_model_owned_statements(
|
|
|
100
100
|
) == 1
|
|
101
101
|
|
|
102
102
|
assert "ensure_personal_organization(target.owner_id)" in statements[0]
|
|
103
|
-
assert "UPDATE mentions AS target" in statements[1]
|
|
104
|
-
assert "FROM chunks AS source" in statements[1]
|
|
103
|
+
assert "UPDATE public.mentions AS target" in statements[1]
|
|
104
|
+
assert "FROM public.chunks AS source" in statements[1]
|
|
105
105
|
dominant = statements[2]
|
|
106
|
-
assert "JOIN chunks AS authority" in dominant
|
|
106
|
+
assert "JOIN public.chunks AS authority" in dominant
|
|
107
107
|
assert "authority.organization_id AS value" in dominant
|
|
108
108
|
assert "ORDER BY n DESC, value ASC" in dominant
|
|
109
|
-
assert "UPDATE entities AS target" in dominant
|
|
109
|
+
assert "UPDATE public.entities AS target" in dominant
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
@pytest.mark.asyncio
|
|
@@ -79,7 +79,8 @@ def test_join_accepts_composed_typed_predicate() -> None:
|
|
|
79
79
|
sql, params = executor.query, executor.params
|
|
80
80
|
|
|
81
81
|
assert (
|
|
82
|
-
"JOIN joined ON (public.base.id = public.joined.base_id
|
|
82
|
+
"JOIN public.joined ON (public.base.id = public.joined.base_id "
|
|
83
|
+
"AND public.joined.active IS TRUE)"
|
|
83
84
|
) in sql
|
|
84
85
|
assert params == []
|
|
85
86
|
|
|
@@ -104,7 +104,10 @@ def test_exists_is_composable_and_outer_ref_is_correlated() -> None:
|
|
|
104
104
|
sql, params = _sql(Parent.filter(Is(F("enabled"), True) | ~child_exists))
|
|
105
105
|
|
|
106
106
|
assert "enabled IS TRUE" in sql
|
|
107
|
-
assert
|
|
107
|
+
assert (
|
|
108
|
+
"NOT (EXISTS (SELECT 1 FROM public.children "
|
|
109
|
+
"WHERE parent_id = public.parents.id))"
|
|
110
|
+
) in sql
|
|
108
111
|
assert params == []
|
|
109
112
|
|
|
110
113
|
|
|
@@ -129,9 +132,9 @@ def test_nested_exists_correlates_to_immediately_enclosing_query() -> None:
|
|
|
129
132
|
|
|
130
133
|
sql, _ = _sql(Parent.filter(nested))
|
|
131
134
|
|
|
132
|
-
assert "parent_id = parents.id" in sql
|
|
135
|
+
assert "parent_id = public.parents.id" in sql
|
|
133
136
|
assert "child_id = c.id" in sql
|
|
134
|
-
assert "child_id = parents.id" not in sql
|
|
137
|
+
assert "child_id = public.parents.id" not in sql
|
|
135
138
|
|
|
136
139
|
|
|
137
140
|
def test_nested_exists_shifts_parameters_at_every_query_level() -> None:
|
|
@@ -249,10 +252,10 @@ def test_outer_ref_is_preserved_through_cast_json_path_and_function() -> None:
|
|
|
249
252
|
|
|
250
253
|
sql, params = _sql(Parent.filter(nested))
|
|
251
254
|
|
|
252
|
-
assert "CAST(parents.id AS text) = parent_id" in sql
|
|
253
|
-
assert "LOWER(parents.name) = LOWER(id)" in sql
|
|
254
|
-
assert "((parents.metadata -> $1) ->> $2) IS NOT NULL" in sql
|
|
255
|
-
assert "public.can_read_parent(parents.id, id) = $3" in sql
|
|
255
|
+
assert "CAST(public.parents.id AS text) = parent_id" in sql
|
|
256
|
+
assert "LOWER(public.parents.name) = LOWER(id)" in sql
|
|
257
|
+
assert "((public.parents.metadata -> $1) ->> $2) IS NOT NULL" in sql
|
|
258
|
+
assert "public.can_read_parent(public.parents.id, id) = $3" in sql
|
|
256
259
|
assert params == ["source", "id", True]
|
|
257
260
|
|
|
258
261
|
|
|
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
|