matrx-orm 3.1.242__tar.gz → 3.1.244__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.242 → matrx_orm-3.1.244}/CHANGELOG.md +14 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/PKG-INFO +1 -1
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/__init__.py +6 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/postgrest_client_adapter.py +9 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/FEATURE.md +15 -8
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/async_db_manager.py +59 -5
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/expressions.py +76 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/fields.py +37 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/pooler_role.py +173 -28
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/FEATURE.md +18 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/db_functions.py +11 -1
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/update.py +36 -3
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/executor.py +4 -1
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/fallback.py +7 -3
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/lifecycle.py +4 -2
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/pyproject.toml +1 -1
- matrx_orm-3.1.244/tests/level1/test_guarded_read_never_reruns_empty.py +142 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_read_connection_retry.py +24 -0
- matrx_orm-3.1.244/tests/test_instance_update_runs_expressions.py +237 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/.gitignore +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/AGENTS.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/CLAUDE.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/MODEL_API.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/docs/migrations.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/FEATURE.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/admin/FEATURE.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/admin_lane.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/foreign_session_state.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/read_write.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/session_context.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/statement_ceiling.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/core/write_retry.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/local_sqlite.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/no_org_assignment.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/catalog_select.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/platform_access.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/pytest_lock_guard.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/pytest_plugin.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/resources/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/resources/additional-assigners-2026-09-12.json +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/resources/org_assignment_trigger_baseline.json +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/excluded_targets.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/backup_restore.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/host_authorization.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/mutation_receipts.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_enable.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_lifecycle.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_passkeys.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_source.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/org_vault_gate.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/vault_history.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/matrx_orm/write_actor.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/scripts/release.sh +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/conftest.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_adapter_registry.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_admin_response_rendering.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_association_trigger_actor_naming.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_associations_wiring.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_borrowed_role_never_reaches_server_calls.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_column_scoped_writes.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_committed_row_guard.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_count_limit_param_leak.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_deadlock_retry.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_fetch_on_init_holds_whole_table.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_fk_cycle_resolution.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_local_sqlite.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_lock_contention_retry.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_org_assignment_inventory.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_orm_banners_are_client_safe.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_percentile_cont.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_persisted_count.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_person_scoped_session_bypasses_shared_cache.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pool_create_never_wedges.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_read_write_declared.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_standalone_write_session_context.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_subquery_nested_expressions.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_sync_pool_declares_read_write.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_transaction_database_identity.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_transaction_gucs_one_round_trip.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_transaction_pool_release_sends_nothing.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_write_queue_deadline.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/level2/verify_committed_row_guard_pg17.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/native_source_cases.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/native_source_oracle.rs +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_enum_ordering.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_excluded_fk_targets.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_schema_analysis.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_enum_array_column.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/schema_builder/test_never_filter_columns.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_a_failed_capture_still_names_the_failure.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_a_grouped_update_writes_only_each_rows_own_fields.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_pending_insert_honors_operator_filters.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_record_error_bounded_insert.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_record_error_storm.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_spill_guard_plugin.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/session/test_tests_never_write_live_system_error.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_backend_handed_over_mid_transaction_never_takes_our_writes.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_bulk_cache_never_stalls_the_loop.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_keyword_column_refuses_generation.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_list_of_uuids_binds_as_uuid_array.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_read_only_backend_never_refuses_our_writes.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_a_sealed_field_shows_no_character_of_its_value.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_array_arg_element_type.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_attachment_mutation_serialization.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_backup_alias_conflict.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_backup_history_restore.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_backup_restore_live.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_call_function_as.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_call_function_none_mode.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_composite_fk_is_not_a_column_fk.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_dsn_password_masking.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_dynamic_select_unregistered.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_first_asks_the_database_for_one_row.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_foreign_session_state.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_immutable_insert_or_exact_verify.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_keyed_digest.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_enable.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_lifecycle.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_lifecycle_fences.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_passkey_receipts.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_native_source.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_order_by_desc_nulls_last.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_org_vault_gate.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_package_wiring_views.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_secret_audit_names_its_organization.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_secrets_minting_keys.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_shared_lock_guard.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_state_cache_sweeps_stale.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_statement_ceiling_warning.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_sync_twins_guard.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_update_result_truthiness.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_user_visible_lookup.py +0 -0
- {matrx_orm-3.1.242 → matrx_orm-3.1.244}/tests/test_vault_aggregate_recovery_stateful.py +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- **Fix: an instance `update()` / `save()` runs a SQL expression instead of storing it.**
|
|
6
|
+
`row.update(metadata=JsonbMerge("metadata", {...}))` sent the expression through
|
|
7
|
+
`JSONField.get_db_prep_value`, and `json.dumps` wrote its attribute dict over the
|
|
8
|
+
column (1,328 live rows lost their metadata, 2026-10-22); a text column stored
|
|
9
|
+
`str(expression)`. `update_instance` and `bulk_update` now hand every expression to
|
|
10
|
+
the executor's SET compiler exactly as `update_where` does, and the instance ends
|
|
11
|
+
holding the computed value (re-read when RETURNING did not carry it). `F("col")` is
|
|
12
|
+
now a valid SET value. Every serialising path refuses an expression loudly with the
|
|
13
|
+
new `ExpressionAsDataError` (a `TypeError`): all `Field.get_db_prep_value`
|
|
14
|
+
implementations (wrapped per subclass), the json encoders (nested expressions), json
|
|
15
|
+
function arguments, and PostgREST adapter writes. New export `is_sql_expression`.
|
|
16
|
+
Guard: `tests/test_instance_update_runs_expressions.py` (9 live round trips; all 9
|
|
17
|
+
red on the prior code).
|
|
18
|
+
|
|
5
19
|
- **Feature: entity doors in the associations engine.** A registry token may resolve to a
|
|
6
20
|
*door* (`__entity_door__ = True`, `async load_by_ids(ids, *, actor_id) -> {id: row}`) instead of
|
|
7
21
|
a Model, for nouns whose rows live behind the host's read doors (a record store). The engine
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.244
|
|
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
|
|
@@ -129,7 +129,10 @@ from .core.expressions import (
|
|
|
129
129
|
Like,
|
|
130
130
|
Regex,
|
|
131
131
|
JsonbConcat,
|
|
132
|
+
ExpressionAsDataError,
|
|
132
133
|
JsonbMerge,
|
|
134
|
+
JsonbRemoveKeys,
|
|
135
|
+
is_sql_expression,
|
|
133
136
|
JsonbObjectAgg,
|
|
134
137
|
Lag,
|
|
135
138
|
LastValue,
|
|
@@ -645,7 +648,10 @@ __all__ = [
|
|
|
645
648
|
"ArrayIndex",
|
|
646
649
|
"JsonText",
|
|
647
650
|
"JsonbConcat",
|
|
651
|
+
"ExpressionAsDataError",
|
|
648
652
|
"JsonbMerge",
|
|
653
|
+
"JsonbRemoveKeys",
|
|
654
|
+
"is_sql_expression",
|
|
649
655
|
"JsonbObjectAgg",
|
|
650
656
|
"Filter",
|
|
651
657
|
"Case",
|
|
@@ -225,6 +225,15 @@ class PostgRESTClientAdapter(BaseAdapter):
|
|
|
225
225
|
filters = self._translate_filters(query_dict.get("filters", {}))
|
|
226
226
|
data: dict | list = query_dict.get("data", {})
|
|
227
227
|
|
|
228
|
+
# PostgREST takes JSON rows, not SQL: an expression object here would be
|
|
229
|
+
# serialised as data. Refuse before anything is sent.
|
|
230
|
+
from matrx_orm.core.expressions import ExpressionAsDataError, is_sql_expression
|
|
231
|
+
|
|
232
|
+
for row in data if isinstance(data, list) else [data]:
|
|
233
|
+
for column, value in (row or {}).items():
|
|
234
|
+
if is_sql_expression(value):
|
|
235
|
+
raise ExpressionAsDataError(column, value)
|
|
236
|
+
|
|
228
237
|
logger.debug("[PostgRESTClientAdapter] %s %s", operation.upper(), table)
|
|
229
238
|
|
|
230
239
|
try:
|
|
@@ -261,14 +261,21 @@ pinned server session's identity before and after `RESET SESSION AUTHORIZATION`
|
|
|
261
261
|
borrowed role. This also repairs both `SET ROLE` and `SET SESSION AUTHORIZATION`;
|
|
262
262
|
`RESET ROLE` alone does not repair the latter.
|
|
263
263
|
|
|
264
|
-
**A
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
**A read proves its own role; an empty result is never re-run** (2026-10-21). On a
|
|
265
|
+
transaction pool every single SELECT/WITH read is sent as `guarded_read_sql(query,
|
|
266
|
+
login)`: `SELECT * FROM (<query>) _matrx_guarded_read WHERE CASE WHEN current_user =
|
|
267
|
+
<login> AND session_user = <login> THEN true ELSE ('matrx_orm_borrowed_role:' ||
|
|
268
|
+
current_user)::int > 0 END`. The guard has no column references, so Postgres runs it
|
|
269
|
+
as a one-time filter before the scan (verified live with EXPLAIN; order and duplicate
|
|
270
|
+
column names preserved). A clean answer, empty or not, is proven by that one
|
|
271
|
+
statement; a borrowed role RAISES (22P02 naming the role), and only then does
|
|
272
|
+
`repair_guarded_read` emit the contamination alarm and re-read on a pinned, reset
|
|
273
|
+
session. `<login>` is learned once per pool by `learn_login_role` (pinned transaction,
|
|
274
|
+
both resets, `session_user`). Before this, every zero-row read was re-run as an
|
|
275
|
+
`EXISTS` proof: 3.5M extra statements in 5.7 h on the live database. A query that
|
|
276
|
+
cannot be wrapped (multi-statement, SHOW, VALUES) keeps the old path
|
|
277
|
+
(`prove_empty_in_one_statement`, then the pinned re-proof). Pinned by
|
|
278
|
+
`tests/level1/test_guarded_read_never_reruns_empty.py` and `tests/test_pooler_role_guard.py`.
|
|
272
279
|
|
|
273
280
|
**Round trips are the launch cost on a remote pooler.** Every top-level BEGIN sets
|
|
274
281
|
its ceilings, the host's declared session context and the opener's own settings
|
|
@@ -23,6 +23,10 @@ from matrx_orm.core.foreign_session_state import run_clearing_foreign_restrict
|
|
|
23
23
|
from matrx_orm.core.pooler_role import (
|
|
24
24
|
VERIFY_EMPTY_READS,
|
|
25
25
|
assert_no_session_role_change,
|
|
26
|
+
borrowed_role_from_error,
|
|
27
|
+
guarded_read_sql,
|
|
28
|
+
learn_login_role,
|
|
29
|
+
repair_guarded_read,
|
|
26
30
|
reprove_empty_read,
|
|
27
31
|
)
|
|
28
32
|
from matrx_orm.core.statement_ceiling import resolve_driver_timeout
|
|
@@ -454,6 +458,9 @@ class AsyncDatabaseManager:
|
|
|
454
458
|
# Only a transaction-mode pool can be handed a server connection another
|
|
455
459
|
# client left wearing a role, so a direct Postgres pays nothing.
|
|
456
460
|
_empty_read_verification: dict[str, bool] = {}
|
|
461
|
+
# Our own login role per transaction-pool config, learned ONCE on a pinned,
|
|
462
|
+
# reset session; every guarded read compares the backend's identity to it.
|
|
463
|
+
_login_roles: dict[str, str] = {}
|
|
457
464
|
|
|
458
465
|
@classmethod
|
|
459
466
|
def _verifies_empty_reads(cls, config_name: str) -> bool:
|
|
@@ -1464,11 +1471,56 @@ class AsyncDatabaseManager:
|
|
|
1464
1471
|
in_transaction=False,
|
|
1465
1472
|
)
|
|
1466
1473
|
try:
|
|
1474
|
+
# Layer 2: on a transaction pool the read carries its own
|
|
1475
|
+
# role proof (pooler_role.guarded_read_sql). A clean result —
|
|
1476
|
+
# empty or not — is proven by this one statement; the guard
|
|
1477
|
+
# RAISES on a borrowed role, and only then is the read repaired.
|
|
1478
|
+
guarded_sql: str | None = None
|
|
1479
|
+
login_role: str | None = None
|
|
1480
|
+
if (
|
|
1481
|
+
verify_empty
|
|
1482
|
+
and read_only
|
|
1483
|
+
and cls._verifies_empty_reads(config_name)
|
|
1484
|
+
):
|
|
1485
|
+
login_role = cls._login_roles.get(config_name)
|
|
1486
|
+
if login_role is None:
|
|
1487
|
+
login_role = await learn_login_role(conn)
|
|
1488
|
+
cls._login_roles[config_name] = login_role
|
|
1489
|
+
guarded_sql = guarded_read_sql(query, login_role)
|
|
1490
|
+
repaired: list[list[dict[str, Any]]] = []
|
|
1491
|
+
|
|
1492
|
+
async def _fetch() -> list[Any]:
|
|
1493
|
+
if guarded_sql is None:
|
|
1494
|
+
return await conn.fetch(query, *args, timeout=driver_timeout)
|
|
1495
|
+
try:
|
|
1496
|
+
return await conn.fetch(
|
|
1497
|
+
guarded_sql, *args, timeout=driver_timeout
|
|
1498
|
+
)
|
|
1499
|
+
except asyncpg.exceptions.InvalidTextRepresentationError as exc:
|
|
1500
|
+
borrowed = borrowed_role_from_error(exc)
|
|
1501
|
+
if borrowed is None:
|
|
1502
|
+
raise
|
|
1503
|
+
repaired.append(
|
|
1504
|
+
await repair_guarded_read(
|
|
1505
|
+
conn,
|
|
1506
|
+
config_name=config_name,
|
|
1507
|
+
query=query,
|
|
1508
|
+
args=args,
|
|
1509
|
+
statement_timeout=driver_timeout,
|
|
1510
|
+
normalize=_normalize_record,
|
|
1511
|
+
borrowed_role=borrowed,
|
|
1512
|
+
login_role=login_role or "<unknown>",
|
|
1513
|
+
)
|
|
1514
|
+
)
|
|
1515
|
+
return []
|
|
1516
|
+
|
|
1467
1517
|
results = await run_clearing_foreign_restrict(
|
|
1468
1518
|
conn,
|
|
1469
|
-
|
|
1519
|
+
_fetch,
|
|
1470
1520
|
what=f"execute_query {query[:80]!r}",
|
|
1471
1521
|
)
|
|
1522
|
+
if repaired:
|
|
1523
|
+
return repaired[0]
|
|
1472
1524
|
if read_attempt > 0:
|
|
1473
1525
|
logger.warning(
|
|
1474
1526
|
"[read-retry] SELECT HEALED on attempt %d after a "
|
|
@@ -1478,14 +1530,16 @@ class AsyncDatabaseManager:
|
|
|
1478
1530
|
)
|
|
1479
1531
|
if (
|
|
1480
1532
|
not results
|
|
1533
|
+
and guarded_sql is None
|
|
1481
1534
|
and verify_empty
|
|
1482
1535
|
and read_only
|
|
1483
1536
|
and cls._verifies_empty_reads(config_name)
|
|
1484
1537
|
):
|
|
1485
|
-
#
|
|
1486
|
-
#
|
|
1487
|
-
#
|
|
1488
|
-
#
|
|
1538
|
+
# A read that could not carry its own guard (not one
|
|
1539
|
+
# SELECT/WITH statement): an empty result from a pooled
|
|
1540
|
+
# connection is unproven until a session that can state
|
|
1541
|
+
# its own role says so. A borrowed `authenticated` role
|
|
1542
|
+
# returns ZERO ROWS on an RLS table instead of raising.
|
|
1489
1543
|
true_rows = await reprove_empty_read(
|
|
1490
1544
|
conn,
|
|
1491
1545
|
config_name=config_name,
|
|
@@ -1575,6 +1575,34 @@ class JsonbMerge(Func):
|
|
|
1575
1575
|
return f"JsonbMerge({self.args[0]!r}, {self.value!r})"
|
|
1576
1576
|
|
|
1577
1577
|
|
|
1578
|
+
class JsonbRemoveKeys(Func):
|
|
1579
|
+
"""``COALESCE(field, '{}'::jsonb) - $keys::text[]`` — atomically drop top-level
|
|
1580
|
+
keys from a JSONB column (the removal sibling of :class:`JsonbMerge`), for use as
|
|
1581
|
+
an ``.update()`` / ``update_where()`` SET value. Every other key is untouched;
|
|
1582
|
+
a key that is absent is a no-op.
|
|
1583
|
+
|
|
1584
|
+
Usage::
|
|
1585
|
+
|
|
1586
|
+
await SiteKeywordValue.update_where(
|
|
1587
|
+
{"id__in": ids}, metadata=JsonbRemoveKeys("metadata", ["serp_cluster"])
|
|
1588
|
+
)
|
|
1589
|
+
"""
|
|
1590
|
+
|
|
1591
|
+
function = "JSONB_REMOVE_KEYS"
|
|
1592
|
+
|
|
1593
|
+
def __init__(self, field: str, keys: list[str] | tuple[str, ...]) -> None:
|
|
1594
|
+
super().__init__(field)
|
|
1595
|
+
self.keys = [str(k) for k in keys]
|
|
1596
|
+
|
|
1597
|
+
def as_sql(self, params: list[Any]) -> str:
|
|
1598
|
+
inner = _render_func_arg(self.args[0], params)
|
|
1599
|
+
params.append(self.keys)
|
|
1600
|
+
return f"COALESCE({inner}, '{{}}'::jsonb) - ${len(params)}::text[]"
|
|
1601
|
+
|
|
1602
|
+
def __repr__(self) -> str:
|
|
1603
|
+
return f"JsonbRemoveKeys({self.args[0]!r}, {self.keys!r})"
|
|
1604
|
+
|
|
1605
|
+
|
|
1578
1606
|
class JsonbConcat(Func):
|
|
1579
1607
|
"""``a || b`` — jsonb concat of two RENDERABLE expressions (columns,
|
|
1580
1608
|
``F()``/``Excluded()`` references, or nested ``Func``s), top-level keys
|
|
@@ -2566,3 +2594,51 @@ class TsMatch:
|
|
|
2566
2594
|
f"TsMatch({self.column!r}, config={self.config!r}, "
|
|
2567
2595
|
f"column_is_tsvector={self.column_is_tsvector})"
|
|
2568
2596
|
)
|
|
2597
|
+
|
|
2598
|
+
|
|
2599
|
+
# ---------------------------------------------------------------------------
|
|
2600
|
+
# Expression-as-data guard
|
|
2601
|
+
# ---------------------------------------------------------------------------
|
|
2602
|
+
|
|
2603
|
+
# Expression types that carry no ``as_sql`` of their own (they are rendered by
|
|
2604
|
+
# their consumers) — listed so the predicate below still recognises them.
|
|
2605
|
+
_EXPRESSION_TYPES_WITHOUT_AS_SQL: tuple[type, ...] = (F, Q, OuterRef)
|
|
2606
|
+
|
|
2607
|
+
|
|
2608
|
+
def is_sql_expression(value: Any) -> bool:
|
|
2609
|
+
"""True when *value* is a SQL expression object, never a storable value.
|
|
2610
|
+
|
|
2611
|
+
Every class in this module is SQL: either it renders itself through
|
|
2612
|
+
``as_sql`` (``Func`` and all its subclasses — ``JsonbMerge``, ``Now``,
|
|
2613
|
+
``Coalesce``…, ``Expression``/``BinaryExpr`` increments, ``Case``,
|
|
2614
|
+
``Subquery``, ``Value``, ``Excluded``, predicates, ``Window``…) or it is
|
|
2615
|
+
one of the few renderer-side references (``F``, ``Q``, ``OuterRef``).
|
|
2616
|
+
``as_sql`` is looked up on the CLASS, so a test double whose instance
|
|
2617
|
+
answers every attribute (``MagicMock``) is not mistaken for SQL.
|
|
2618
|
+
"""
|
|
2619
|
+
if value is None or isinstance(value, type):
|
|
2620
|
+
return False
|
|
2621
|
+
if isinstance(value, _EXPRESSION_TYPES_WITHOUT_AS_SQL):
|
|
2622
|
+
return True
|
|
2623
|
+
return any("as_sql" in vars(klass) for klass in type(value).__mro__)
|
|
2624
|
+
|
|
2625
|
+
|
|
2626
|
+
class ExpressionAsDataError(TypeError):
|
|
2627
|
+
"""A SQL expression object reached a path that would store it as data.
|
|
2628
|
+
|
|
2629
|
+
Raised instead of serialising the object (a jsonb column used to receive
|
|
2630
|
+
``{"args": [...], "value": ...}`` — the 2026-10-22 metadata loss on 1,328
|
|
2631
|
+
rows). Expressions are compiled to SQL by ``Model.update_where`` /
|
|
2632
|
+
``QueryBuilder.update`` and by instance ``update()`` / ``save()``; every
|
|
2633
|
+
other write path (INSERT, upsert data, filters) refuses them here.
|
|
2634
|
+
"""
|
|
2635
|
+
|
|
2636
|
+
def __init__(self, field_name: str | None, value: Any) -> None:
|
|
2637
|
+
self.field_name = field_name
|
|
2638
|
+
self.value = value
|
|
2639
|
+
super().__init__(
|
|
2640
|
+
f"{type(value).__name__} is a SQL expression, not a value — refusing to "
|
|
2641
|
+
f"serialise {value!r} into field {field_name!r}. Expressions run as SQL only "
|
|
2642
|
+
f"in an UPDATE SET: use Model.update_where(filters, {field_name}=...) or "
|
|
2643
|
+
f"instance.update({field_name}=...)."
|
|
2644
|
+
)
|
|
@@ -44,7 +44,35 @@ def _strip_nul_bytes(value: Any) -> Any:
|
|
|
44
44
|
return value
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _refuse_expression_as_data(prep: Any) -> Any:
|
|
48
|
+
"""Wrap a ``get_db_prep_value`` so a SQL expression object can never be
|
|
49
|
+
serialised as a stored value (``json.dumps`` / ``str()`` of a ``JsonbMerge``
|
|
50
|
+
wrote its attribute dict into live jsonb columns). Every Field subclass is
|
|
51
|
+
wrapped at class creation, so a new field type cannot forget the check."""
|
|
52
|
+
import functools
|
|
53
|
+
|
|
54
|
+
if getattr(prep, "_refuses_expressions", False):
|
|
55
|
+
return prep
|
|
56
|
+
|
|
57
|
+
@functools.wraps(prep)
|
|
58
|
+
def guarded(self: Any, value: Any, *args: Any, **kwargs: Any) -> Any:
|
|
59
|
+
from matrx_orm.core.expressions import ExpressionAsDataError, is_sql_expression
|
|
60
|
+
|
|
61
|
+
if is_sql_expression(value):
|
|
62
|
+
raise ExpressionAsDataError(getattr(self, "name", None), value)
|
|
63
|
+
return prep(self, value, *args, **kwargs)
|
|
64
|
+
|
|
65
|
+
guarded._refuses_expressions = True # type: ignore[attr-defined]
|
|
66
|
+
return guarded
|
|
67
|
+
|
|
68
|
+
|
|
47
69
|
class Field:
|
|
70
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
71
|
+
super().__init_subclass__(**kwargs)
|
|
72
|
+
own = cls.__dict__.get("get_db_prep_value")
|
|
73
|
+
if own is not None:
|
|
74
|
+
cls.get_db_prep_value = _refuse_expression_as_data(own) # type: ignore[method-assign]
|
|
75
|
+
|
|
48
76
|
if TYPE_CHECKING:
|
|
49
77
|
|
|
50
78
|
@overload
|
|
@@ -147,6 +175,8 @@ class Field:
|
|
|
147
175
|
return None
|
|
148
176
|
return value
|
|
149
177
|
|
|
178
|
+
get_db_prep_value = _refuse_expression_as_data(get_db_prep_value)
|
|
179
|
+
|
|
150
180
|
def desc(self):
|
|
151
181
|
"""Mark this field for descending order."""
|
|
152
182
|
self._order_direction = "DESC"
|
|
@@ -1166,6 +1196,13 @@ class JSONBField(Field, Generic[_JT]):
|
|
|
1166
1196
|
import uuid as _uuid
|
|
1167
1197
|
from decimal import Decimal as _Decimal
|
|
1168
1198
|
|
|
1199
|
+
from matrx_orm.core.expressions import ExpressionAsDataError, is_sql_expression
|
|
1200
|
+
|
|
1201
|
+
# A SQL expression nested inside a json value is never data — degrading
|
|
1202
|
+
# it to its attribute dict is exactly how JsonbMerge objects were stored.
|
|
1203
|
+
if is_sql_expression(obj):
|
|
1204
|
+
raise ExpressionAsDataError(None, obj)
|
|
1205
|
+
|
|
1169
1206
|
# Pydantic v2 — preferred for the JSON-mode dump that handles nested
|
|
1170
1207
|
# exotic types correctly.
|
|
1171
1208
|
model_dump = getattr(obj, "model_dump", None)
|
|
@@ -35,9 +35,17 @@ TWO INDEPENDENT LAYERS, each sufficient alone, each screaming when it fires:
|
|
|
35
35
|
this guards everything else. The sanctioned ways to change role remain
|
|
36
36
|
``matrx_orm.rls_session`` and ``db/pooler_session.py::impersonate``.
|
|
37
37
|
|
|
38
|
-
2. **
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
2. **Proof in the read itself, repair only when it fires.** On a transaction-pool
|
|
39
|
+
config every read-only query is sent wrapped by ``guarded_read_sql``: the
|
|
40
|
+
statement carries a one-time filter that RAISES when the backend's
|
|
41
|
+
``current_user``/``session_user`` is not our login role (learned once per pool
|
|
42
|
+
by ``learn_login_role``). One statement runs on one server connection, so a
|
|
43
|
+
clean answer — rows or no rows — is proven by the same round trip that read
|
|
44
|
+
it; nothing is ever re-run because a result was empty (2026-10-21: the old
|
|
45
|
+
per-empty-result re-proof was 3.5M extra statements in 5.7 h). Only when the
|
|
46
|
+
guard fires, or a query cannot be wrapped (multi-statement), does the
|
|
47
|
+
repairing re-read below run: re-run inside an explicit transaction (pinned to
|
|
48
|
+
ONE server connection).
|
|
41
49
|
The pinned session's identity is captured before and after resetting session
|
|
42
50
|
authorization and role. The caller gets any rows that appeared, but a
|
|
43
51
|
contamination alarm is emitted only when the role change was directly
|
|
@@ -46,9 +54,9 @@ TWO INDEPENDENT LAYERS, each sufficient alone, each screaming when it fires:
|
|
|
46
54
|
|
|
47
55
|
Layer 2 follows the repo's guard doctrine: a guard that CAN reconcile MUST
|
|
48
56
|
reconcile — we hold the correct answer, so killing the caller's request would be
|
|
49
|
-
strictly worse than returning it loudly.
|
|
50
|
-
|
|
51
|
-
contamination cannot happen.
|
|
57
|
+
strictly worse than returning it loudly. A clean read pays one constant
|
|
58
|
+
one-time filter; a direct (session-mode) Postgres connection pays nothing at all,
|
|
59
|
+
because cross-client contamination cannot happen there.
|
|
52
60
|
"""
|
|
53
61
|
|
|
54
62
|
from __future__ import annotations
|
|
@@ -223,6 +231,115 @@ async def emit_contamination(event: EmptyReadContaminationEvent) -> None:
|
|
|
223
231
|
logger.exception("[pooler-role] contamination sink raised")
|
|
224
232
|
|
|
225
233
|
|
|
234
|
+
# The text the guard's one-time filter raises with. ``('<sentinel>' || role)::int``
|
|
235
|
+
# fails with 22P02 and names the borrowed role; nothing else in a read produces it.
|
|
236
|
+
BORROWED_ROLE_SENTINEL = "matrx_orm_borrowed_role:"
|
|
237
|
+
_SENTINEL_IN_ERROR = re.compile(re.escape(BORROWED_ROLE_SENTINEL) + r"([^\"']*)")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _sql_literal(value: str) -> str:
|
|
241
|
+
return "'" + value.replace("'", "''") + "'"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def guarded_read_sql(query: str, login_role: str | None) -> str | None:
|
|
245
|
+
"""``query`` wrapped so the read proves its own role, or ``None`` if it cannot be.
|
|
246
|
+
|
|
247
|
+
``SELECT * FROM (<query>) _q WHERE <guard>``: the guard has no column
|
|
248
|
+
references, so Postgres plans it as a ONE-TIME FILTER evaluated once before
|
|
249
|
+
the scan (measured live 2026-10-21 — ``Result / One-Time Filter`` over the
|
|
250
|
+
original plan, order and duplicate column names preserved). On our login
|
|
251
|
+
role it is ``true``; on any other ``current_user`` or ``session_user`` it
|
|
252
|
+
casts ``BORROWED_ROLE_SENTINEL || current_user`` to int and RAISES, so a
|
|
253
|
+
borrowed role can never come back as an empty or partial result. Comparing
|
|
254
|
+
both identities against the login role learned for this pool also catches a
|
|
255
|
+
``SET SESSION AUTHORIZATION`` swap, where the two agree on the wrong name.
|
|
256
|
+
|
|
257
|
+
Returns ``None`` (caller falls back to the repairing re-proof on empty) for
|
|
258
|
+
a query that is not a single SELECT/WITH statement or when the login role is
|
|
259
|
+
not yet known.
|
|
260
|
+
"""
|
|
261
|
+
if not login_role:
|
|
262
|
+
return None
|
|
263
|
+
body = query.strip().rstrip(";").strip()
|
|
264
|
+
if not body or ";" in body:
|
|
265
|
+
return None
|
|
266
|
+
head = body.lstrip("( \n\t")[:6].lower()
|
|
267
|
+
if not (head.startswith("select") or head.startswith("with")):
|
|
268
|
+
# A leading comment, VALUES, TABLE, SHOW, EXPLAIN: not provably embeddable.
|
|
269
|
+
return None
|
|
270
|
+
role = _sql_literal(login_role)
|
|
271
|
+
# Newlines around the body: a trailing ``-- comment`` must not swallow the
|
|
272
|
+
# closing parenthesis.
|
|
273
|
+
return (
|
|
274
|
+
"SELECT * FROM (\n"
|
|
275
|
+
f"{body}\n"
|
|
276
|
+
") _matrx_guarded_read WHERE CASE WHEN current_user = "
|
|
277
|
+
f"{role} AND session_user = {role} THEN true "
|
|
278
|
+
f"ELSE ({_sql_literal(BORROWED_ROLE_SENTINEL)} || current_user)::int > 0 END"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def borrowed_role_from_error(exc: BaseException) -> str | None:
|
|
283
|
+
"""The role named by a fired read guard, or ``None`` for any other error."""
|
|
284
|
+
match = _SENTINEL_IN_ERROR.search(str(exc))
|
|
285
|
+
if match is None:
|
|
286
|
+
return None
|
|
287
|
+
return match.group(1) or "<unknown>"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def learn_login_role(conn: Any) -> str:
|
|
291
|
+
"""Our own login role, read on a pinned, reset session (once per pool).
|
|
292
|
+
|
|
293
|
+
The explicit transaction pins every statement to one server connection, and
|
|
294
|
+
both resets put a borrowed role back before ``session_user`` is read, so the
|
|
295
|
+
answer is the role the backend authenticated as — never a borrowed one.
|
|
296
|
+
"""
|
|
297
|
+
async with conn.transaction():
|
|
298
|
+
await conn.execute("RESET SESSION AUTHORIZATION")
|
|
299
|
+
await conn.execute("RESET ROLE")
|
|
300
|
+
row = await conn.fetchrow("SELECT session_user AS login_, current_user AS current_")
|
|
301
|
+
login = row["login_"]
|
|
302
|
+
if not login or row["current_"] != login:
|
|
303
|
+
raise PoolerRoleContamination(
|
|
304
|
+
f"could not learn the login role: a reset session still runs as "
|
|
305
|
+
f"'{row['current_']}' (session user '{login}')"
|
|
306
|
+
)
|
|
307
|
+
return str(login)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def repair_guarded_read(
|
|
311
|
+
conn: Any,
|
|
312
|
+
*,
|
|
313
|
+
config_name: str,
|
|
314
|
+
query: str,
|
|
315
|
+
args: tuple[Any, ...],
|
|
316
|
+
statement_timeout: float | None,
|
|
317
|
+
normalize: Callable[[Any], dict[str, Any]],
|
|
318
|
+
borrowed_role: str,
|
|
319
|
+
login_role: str,
|
|
320
|
+
) -> list[dict[str, Any]]:
|
|
321
|
+
"""The read guard fired: scream, then re-read on a pinned, repaired session.
|
|
322
|
+
|
|
323
|
+
The guard's raise IS direct observation of a borrowed role on the backend
|
|
324
|
+
that served the read, so the contamination alarm is emitted unconditionally.
|
|
325
|
+
The caller gets the true rows (``[]`` when the clean read is empty).
|
|
326
|
+
"""
|
|
327
|
+
rows, _observed = await _pinned_reread(
|
|
328
|
+
conn, query=query, args=args, statement_timeout=statement_timeout, normalize=normalize
|
|
329
|
+
)
|
|
330
|
+
await emit_contamination(
|
|
331
|
+
EmptyReadContaminationEvent(
|
|
332
|
+
config_name=config_name,
|
|
333
|
+
query=query,
|
|
334
|
+
contaminated_role=borrowed_role,
|
|
335
|
+
login_role=login_role,
|
|
336
|
+
true_row_count=len(rows or []),
|
|
337
|
+
occurred_at=datetime.now(UTC),
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
return rows or []
|
|
341
|
+
|
|
342
|
+
|
|
226
343
|
def _single_statement_proof_sql(query: str) -> str | None:
|
|
227
344
|
"""The one-statement absence proof for ``query``, or ``None`` if it cannot embed.
|
|
228
345
|
|
|
@@ -306,6 +423,44 @@ async def reprove_empty_read(
|
|
|
306
423
|
):
|
|
307
424
|
return None
|
|
308
425
|
|
|
426
|
+
rows, observed = await _pinned_reread(
|
|
427
|
+
conn, query=query, args=args, statement_timeout=statement_timeout, normalize=normalize
|
|
428
|
+
)
|
|
429
|
+
if rows is None:
|
|
430
|
+
return None
|
|
431
|
+
# Rows may legitimately appear between two READ COMMITTED statements.
|
|
432
|
+
# guest_executions is the canonical example: concurrent first requests
|
|
433
|
+
# for one fingerprint race a lookup against creation. Empty -> non-empty
|
|
434
|
+
# is reconciliation, not proof of pool contamination. Only the captured
|
|
435
|
+
# before/after identity change can put this class in the repair queue.
|
|
436
|
+
if observed is not None:
|
|
437
|
+
await emit_contamination(
|
|
438
|
+
EmptyReadContaminationEvent(
|
|
439
|
+
config_name=config_name,
|
|
440
|
+
query=query,
|
|
441
|
+
contaminated_role=observed[0],
|
|
442
|
+
login_role=observed[1],
|
|
443
|
+
true_row_count=len(rows),
|
|
444
|
+
occurred_at=datetime.now(UTC),
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
return rows
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
async def _pinned_reread(
|
|
451
|
+
conn: Any,
|
|
452
|
+
*,
|
|
453
|
+
query: str,
|
|
454
|
+
args: tuple[Any, ...],
|
|
455
|
+
statement_timeout: float | None,
|
|
456
|
+
normalize: Callable[[Any], dict[str, Any]],
|
|
457
|
+
) -> tuple[list[dict[str, Any]] | None, tuple[str, str] | None]:
|
|
458
|
+
"""Re-read on ONE server connection after resetting both identities.
|
|
459
|
+
|
|
460
|
+
Returns ``(rows or None when empty, (borrowed, login) when the reset was seen
|
|
461
|
+
to change the identity else None)``. Raises ``PoolerRoleContamination`` when
|
|
462
|
+
no attempt lands on a clean session.
|
|
463
|
+
"""
|
|
309
464
|
last_role: tuple[str, str] | None = None
|
|
310
465
|
for _ in range(REPROVE_ATTEMPTS):
|
|
311
466
|
async with conn.transaction():
|
|
@@ -325,29 +480,14 @@ async def reprove_empty_read(
|
|
|
325
480
|
last_role = (current, login)
|
|
326
481
|
continue
|
|
327
482
|
rows = await conn.fetch(query, *args, timeout=statement_timeout)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
observed_contamination = (before_current, before_session) != (current, login)
|
|
333
|
-
|
|
334
|
-
# Rows may legitimately appear between two READ COMMITTED statements.
|
|
335
|
-
# guest_executions is the canonical example: concurrent first requests
|
|
336
|
-
# for one fingerprint race a lookup against creation. Empty -> non-empty
|
|
337
|
-
# is reconciliation, not proof of pool contamination. Only the captured
|
|
338
|
-
# before/after identity change can put this class in the repair queue.
|
|
339
|
-
if observed_contamination:
|
|
340
|
-
await emit_contamination(
|
|
341
|
-
EmptyReadContaminationEvent(
|
|
342
|
-
config_name=config_name,
|
|
343
|
-
query=query,
|
|
344
|
-
contaminated_role=before_current,
|
|
345
|
-
login_role=login,
|
|
346
|
-
true_row_count=len(true_rows),
|
|
347
|
-
occurred_at=datetime.now(UTC),
|
|
348
|
-
)
|
|
483
|
+
observed = (
|
|
484
|
+
(before_current, login)
|
|
485
|
+
if (before_current, before_session) != (current, login)
|
|
486
|
+
else None
|
|
349
487
|
)
|
|
350
|
-
|
|
488
|
+
if not rows:
|
|
489
|
+
return None, observed
|
|
490
|
+
return [normalize(r) for r in rows], observed
|
|
351
491
|
|
|
352
492
|
current, login = last_role if last_role is not None else ("<unknown>", "<unknown>")
|
|
353
493
|
raise PoolerRoleContamination(
|
|
@@ -358,6 +498,7 @@ async def reprove_empty_read(
|
|
|
358
498
|
|
|
359
499
|
|
|
360
500
|
__all__ = [
|
|
501
|
+
"BORROWED_ROLE_SENTINEL",
|
|
361
502
|
"ContaminationSink",
|
|
362
503
|
"EmptyReadContaminationEvent",
|
|
363
504
|
"PoolerRoleContamination",
|
|
@@ -365,8 +506,12 @@ __all__ = [
|
|
|
365
506
|
"SessionRoleChangeRefused",
|
|
366
507
|
"VERIFY_EMPTY_READS",
|
|
367
508
|
"assert_no_session_role_change",
|
|
509
|
+
"borrowed_role_from_error",
|
|
368
510
|
"emit_contamination",
|
|
511
|
+
"guarded_read_sql",
|
|
512
|
+
"learn_login_role",
|
|
369
513
|
"register_contamination_sink",
|
|
514
|
+
"repair_guarded_read",
|
|
370
515
|
"reprove_empty_read",
|
|
371
516
|
"reset_contamination_sinks",
|
|
372
517
|
]
|
|
@@ -34,6 +34,21 @@ READ depends on the database, not on the deploy. `catalog_select`:
|
|
|
34
34
|
Guard: `tests/test_dynamic_select_unregistered.py` (failing before the fallback
|
|
35
35
|
existed), plus `tests/level1/test_dynamic_crud.py` for the write-side refusals.
|
|
36
36
|
|
|
37
|
+
## SQL expressions are compiled, never stored
|
|
38
|
+
|
|
39
|
+
An expression object (`JsonbMerge`, `F(...) + 1`, `Coalesce`, `Now()`, `Case`,
|
|
40
|
+
`Subquery`, `Value`, `Excluded`, any `Func` or class with `as_sql`, plus `F`/`Q`/
|
|
41
|
+
`OuterRef`) is SQL, never data. `update_where`, `QueryBuilder.update`, instance
|
|
42
|
+
`update()`/`save()` (`update_instance`) and `bulk_update` all hand it to the
|
|
43
|
+
executor's SET compiler; the instance ends holding the database-computed value.
|
|
44
|
+
Every other path that would serialise one — INSERT data, upsert rows, a json
|
|
45
|
+
value nesting one, a json function argument, a PostgREST write — raises
|
|
46
|
+
`ExpressionAsDataError` before anything is written (`Field.get_db_prep_value` is
|
|
47
|
+
wrapped for every Field subclass, and the json encoders refuse nested ones).
|
|
48
|
+
Predicate: `matrx_orm.is_sql_expression`. Guard:
|
|
49
|
+
`tests/test_instance_update_runs_expressions.py` (live round trip on a disposable
|
|
50
|
+
loopback Postgres, `ISOLATED_PG17_DSN`; red on the prior code).
|
|
51
|
+
|
|
37
52
|
## Event-loop safety
|
|
38
53
|
|
|
39
54
|
- **Bulk row coercion never runs on the event loop.** `bulk_insert_ignore` and
|
|
@@ -51,6 +66,9 @@ Verified against `conflict_writes.py` and
|
|
|
51
66
|
|
|
52
67
|
## Change Log
|
|
53
68
|
|
|
69
|
+
- 2026-10-22 — Instance `update()`/`save()`/`bulk_update` compile SQL expressions;
|
|
70
|
+
every serialising path refuses them (`ExpressionAsDataError`).
|
|
71
|
+
|
|
54
72
|
- 2026-09-26 — Added the typed `update_where` guard to `upsert_with_conflict`.
|
|
55
73
|
|
|
56
74
|
- 2026-09-11 — Added the live-catalog read fallback so an unregistered view or a
|
|
@@ -34,6 +34,16 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
34
34
|
_FUNCTION_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$")
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _json_arg_default(obj: Any) -> str:
|
|
38
|
+
"""``default=str`` for json-bound function args — except a SQL expression,
|
|
39
|
+
which must never be stringified into data."""
|
|
40
|
+
from matrx_orm.core.expressions import ExpressionAsDataError, is_sql_expression
|
|
41
|
+
|
|
42
|
+
if is_sql_expression(obj):
|
|
43
|
+
raise ExpressionAsDataError(None, obj)
|
|
44
|
+
return str(obj)
|
|
45
|
+
|
|
46
|
+
|
|
37
47
|
def _render_call(function_name: str, args: tuple[Any, ...]) -> tuple[str, list[Any]]:
|
|
38
48
|
if not _FUNCTION_NAME_RE.match(function_name):
|
|
39
49
|
raise ValueError(f"call_db_function: invalid function name {function_name!r}")
|
|
@@ -47,7 +57,7 @@ def _render_call(function_name: str, args: tuple[Any, ...]) -> tuple[str, list[A
|
|
|
47
57
|
values.append(arg)
|
|
48
58
|
elif isinstance(arg, dict | list):
|
|
49
59
|
placeholders.append(f"${index}::jsonb")
|
|
50
|
-
values.append(json.dumps(arg, ensure_ascii=False, default=
|
|
60
|
+
values.append(json.dumps(arg, ensure_ascii=False, default=_json_arg_default))
|
|
51
61
|
else:
|
|
52
62
|
placeholders.append(f"${index}")
|
|
53
63
|
values.append(arg)
|