matrx-orm 3.1.243__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.243 → matrx_orm-3.1.244}/CHANGELOG.md +14 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/PKG-INFO +1 -1
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/__init__.py +6 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/adapters/postgrest_client_adapter.py +9 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/expressions.py +76 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/fields.py +37 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/FEATURE.md +18 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/db_functions.py +11 -1
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/update.py +36 -3
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/query/executor.py +4 -1
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/pyproject.toml +1 -1
- matrx_orm-3.1.244/tests/test_instance_update_runs_expressions.py +237 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/.gitignore +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/AGENTS.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/CLAUDE.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/MODEL_API.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/docs/migrations.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/FEATURE.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/admin/FEATURE.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/FEATURE.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/admin_lane.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/foreign_session_state.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/pooler_role.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/read_write.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/session_context.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/statement_ceiling.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/core/write_retry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/local_sqlite.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/no_org_assignment.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/catalog_select.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/platform_access.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/pytest_lock_guard.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/pytest_plugin.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/resources/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/resources/additional-assigners-2026-09-12.json +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/resources/org_assignment_trigger_baseline.json +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/excluded_targets.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/backup_restore.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/host_authorization.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/mutation_receipts.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_enable.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_lifecycle.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_passkeys.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/native_source.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/org_vault_gate.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/secrets_battery/vault_history.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/matrx_orm/write_actor.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/scripts/release.sh +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/conftest.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_adapter_registry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_admin_response_rendering.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_association_trigger_actor_naming.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_associations_wiring.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_borrowed_role_never_reaches_server_calls.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_column_scoped_writes.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_committed_row_guard.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_count_limit_param_leak.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_deadlock_retry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_fetch_on_init_holds_whole_table.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_fk_cycle_resolution.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_guarded_read_never_reruns_empty.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_local_sqlite.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_lock_contention_retry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_org_assignment_inventory.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_orm_banners_are_client_safe.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_percentile_cont.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_persisted_count.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_person_scoped_session_bypasses_shared_cache.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_pool_create_never_wedges.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_read_connection_retry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_read_write_declared.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_standalone_write_session_context.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_subquery_nested_expressions.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_sync_pool_declares_read_write.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_transaction_database_identity.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_transaction_gucs_one_round_trip.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_transaction_pool_release_sends_nothing.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_write_queue_deadline.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/level2/verify_committed_row_guard_pg17.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/native_source_cases.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/native_source_oracle.rs +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_enum_ordering.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_excluded_fk_targets.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_schema_analysis.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema_builder/test_enum_array_column.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/schema_builder/test_never_filter_columns.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_a_failed_capture_still_names_the_failure.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_a_grouped_update_writes_only_each_rows_own_fields.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_pending_insert_honors_operator_filters.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_record_error_bounded_insert.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_record_error_storm.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_spill_guard_plugin.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/session/test_tests_never_write_live_system_error.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_a_backend_handed_over_mid_transaction_never_takes_our_writes.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_a_bulk_cache_never_stalls_the_loop.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_a_keyword_column_refuses_generation.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_a_list_of_uuids_binds_as_uuid_array.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_a_read_only_backend_never_refuses_our_writes.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_a_sealed_field_shows_no_character_of_its_value.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_array_arg_element_type.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_attachment_mutation_serialization.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_backup_alias_conflict.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_backup_history_restore.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_backup_restore_live.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_call_function_as.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_call_function_none_mode.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_composite_fk_is_not_a_column_fk.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_dsn_password_masking.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_dynamic_select_unregistered.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_first_asks_the_database_for_one_row.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_foreign_session_state.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_immutable_insert_or_exact_verify.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_keyed_digest.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_native_enable.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_native_lifecycle.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_native_lifecycle_fences.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_native_passkey_receipts.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_native_source.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_order_by_desc_nulls_last.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_org_vault_gate.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_package_wiring_views.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_secret_audit_names_its_organization.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_secrets_minting_keys.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_shared_lock_guard.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_state_cache_sweeps_stale.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_statement_ceiling_warning.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_sync_twins_guard.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_update_result_truthiness.py +0 -0
- {matrx_orm-3.1.243 → matrx_orm-3.1.244}/tests/test_user_visible_lookup.py +0 -0
- {matrx_orm-3.1.243 → 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:
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -9,7 +9,7 @@ from matrx_orm.core.types import UpdateResult
|
|
|
9
9
|
from matrx_orm.exceptions import describe_db_exception
|
|
10
10
|
from matrx_orm.state import StateManager
|
|
11
11
|
|
|
12
|
-
from ..core.expressions import F
|
|
12
|
+
from ..core.expressions import F, is_sql_expression
|
|
13
13
|
from ..query.builder import QueryBuilder
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
@@ -286,7 +286,9 @@ async def bulk_update(
|
|
|
286
286
|
if field_name in model_cls._fields and hasattr(obj, field_name):
|
|
287
287
|
field = model_cls._fields[field_name]
|
|
288
288
|
value = getattr(obj, field_name)
|
|
289
|
-
if value
|
|
289
|
+
if is_sql_expression(value):
|
|
290
|
+
update_data[field_name] = value # compiled by the executor
|
|
291
|
+
elif value is not None:
|
|
290
292
|
update_data[field_name] = field.get_db_prep_value(value)
|
|
291
293
|
|
|
292
294
|
row_filter = dict(pk_filter)
|
|
@@ -310,6 +312,14 @@ async def bulk_update(
|
|
|
310
312
|
)
|
|
311
313
|
if result.rows_affected > 0:
|
|
312
314
|
rows_affected += 1
|
|
315
|
+
# An expression field holds the database-computed value
|
|
316
|
+
# afterwards, never the expression (a later save would
|
|
317
|
+
# re-run it).
|
|
318
|
+
if result.updated_rows:
|
|
319
|
+
returned = result.updated_rows[0]
|
|
320
|
+
for field_name, sent in update_data.items():
|
|
321
|
+
if is_sql_expression(sent) and field_name in returned:
|
|
322
|
+
setattr(obj, field_name, returned[field_name])
|
|
313
323
|
# Do NOT cache `obj` here. It is the CALLER's instance and is
|
|
314
324
|
# routinely a partial {pk + payload} stub (see
|
|
315
325
|
# session/flush.py), so writing it into the identity cache
|
|
@@ -461,6 +471,7 @@ async def update_instance(
|
|
|
461
471
|
current_version = expected_version
|
|
462
472
|
|
|
463
473
|
update_data: dict[str, Any] = {}
|
|
474
|
+
expression_fields: set[str] = set()
|
|
464
475
|
field_names: Iterable[str] = (
|
|
465
476
|
fields if fields is not None else [f for f in model_cls._fields if f not in pk_filter]
|
|
466
477
|
)
|
|
@@ -473,7 +484,14 @@ async def update_instance(
|
|
|
473
484
|
continue # handled below — never depends on being in `fields`
|
|
474
485
|
field = model_cls._fields[field_name]
|
|
475
486
|
value = getattr(instance, field_name, None)
|
|
476
|
-
if value
|
|
487
|
+
if is_sql_expression(value):
|
|
488
|
+
# SQL, not data: hand it to the executor untouched so it compiles
|
|
489
|
+
# into the SET clause exactly as update_where() does. Running it
|
|
490
|
+
# through get_db_prep_value is what json-dumped JsonbMerge objects
|
|
491
|
+
# into live jsonb columns.
|
|
492
|
+
update_data[field_name] = value
|
|
493
|
+
expression_fields.add(field_name)
|
|
494
|
+
elif value is not None:
|
|
477
495
|
update_data[field_name] = field.get_db_prep_value(value)
|
|
478
496
|
elif field.nullable:
|
|
479
497
|
update_data[field_name] = None
|
|
@@ -517,5 +535,20 @@ async def update_instance(
|
|
|
517
535
|
elif version_field_name is not None and version_field_name in update_data:
|
|
518
536
|
setattr(instance, version_field_name, update_data[version_field_name])
|
|
519
537
|
|
|
538
|
+
# An expression field must end holding the value the database computed —
|
|
539
|
+
# never the expression object, which a later save() would re-run (a second
|
|
540
|
+
# increment) or a reader would mistake for data. RETURNING normally covers
|
|
541
|
+
# it; re-read whatever it did not.
|
|
542
|
+
unresolved = [f for f in expression_fields if is_sql_expression(getattr(instance, f, None))]
|
|
543
|
+
if unresolved:
|
|
544
|
+
fresh = await QueryBuilder(model_cls).filter(**pk_filter).first()
|
|
545
|
+
if fresh is None:
|
|
546
|
+
raise ValueError(
|
|
547
|
+
f"{model_cls.__name__} {pk_filter}: updated, but the row could not be "
|
|
548
|
+
f"re-read to resolve expression fields {sorted(unresolved)}"
|
|
549
|
+
)
|
|
550
|
+
for f in unresolved:
|
|
551
|
+
setattr(instance, f, getattr(fresh, f, None))
|
|
552
|
+
|
|
520
553
|
await post_save.send(model_cls, instance=instance, created=False)
|
|
521
554
|
return instance
|
|
@@ -1564,7 +1564,7 @@ class QueryExecutor:
|
|
|
1564
1564
|
invalid_fields: list[str] = []
|
|
1565
1565
|
skipped_fields: list[str] = []
|
|
1566
1566
|
|
|
1567
|
-
from matrx_orm.core.expressions import Expression, Func, Subquery
|
|
1567
|
+
from matrx_orm.core.expressions import F, Expression, Func, Subquery
|
|
1568
1568
|
|
|
1569
1569
|
for k, v in kwargs.items():
|
|
1570
1570
|
if k in self.model._fields:
|
|
@@ -1600,6 +1600,9 @@ class QueryExecutor:
|
|
|
1600
1600
|
expr_sql = value.as_sql(params)
|
|
1601
1601
|
set_clause.append(f"{field_name} = {expr_sql}")
|
|
1602
1602
|
param_index = len(params) + 1
|
|
1603
|
+
elif isinstance(value, F):
|
|
1604
|
+
# SET col = other_col (F validates the identifier).
|
|
1605
|
+
set_clause.append(f"{field_name} = {value.field_name}")
|
|
1603
1606
|
elif isinstance(value, Expression):
|
|
1604
1607
|
expr_sql = _render_update_expression(value, params)
|
|
1605
1608
|
set_clause.append(f"{field_name} = {expr_sql}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.244"
|
|
4
4
|
description = "Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""An instance ``update()`` / ``save()`` RUNS a SQL expression — it never stores one.
|
|
2
|
+
|
|
3
|
+
THE INCIDENT (2026-10-22). ``row.update(metadata=JsonbMerge("metadata", {...}))`` sent
|
|
4
|
+
the JsonbMerge object through ``JSONField.get_db_prep_value``; ``json.dumps`` degraded
|
|
5
|
+
it to its attribute dict and wrote ``{"args": [...], "value": ...}`` over the metadata
|
|
6
|
+
of 1,328 live rows. ``Model.update_where`` compiled the same object to SQL correctly;
|
|
7
|
+
the instance path did not. A text column had the same hole (``str(expression)``).
|
|
8
|
+
|
|
9
|
+
THE GUARD. Instance ``update()``/``save()`` and ``bulk_update`` hand expressions to the
|
|
10
|
+
executor, which compiles them into the SET clause exactly as ``update_where`` does, and
|
|
11
|
+
the instance ends holding the value the database computed. Every other path that would
|
|
12
|
+
serialise an expression (INSERT data, a json value that nests one) raises
|
|
13
|
+
``ExpressionAsDataError`` before anything is written.
|
|
14
|
+
|
|
15
|
+
Every test does a real round trip and re-reads the row with raw SQL, so a stored
|
|
16
|
+
expression cannot hide behind the in-memory instance. Needs a DISPOSABLE loopback
|
|
17
|
+
Postgres (``ISOLATED_PG17_DSN``); it creates and drops its own tables.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import uuid
|
|
25
|
+
from ipaddress import ip_address
|
|
26
|
+
from urllib.parse import unquote, urlparse
|
|
27
|
+
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
from matrx_orm import (
|
|
31
|
+
Coalesce,
|
|
32
|
+
ExpressionAsDataError,
|
|
33
|
+
F,
|
|
34
|
+
JsonbMerge,
|
|
35
|
+
Upper,
|
|
36
|
+
)
|
|
37
|
+
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
38
|
+
from matrx_orm.core.base import Model
|
|
39
|
+
from matrx_orm.core.config import DatabaseProjectConfig, register_database, registry
|
|
40
|
+
from matrx_orm.core.fields import CharField, IntegerField, JSONBField, TextField, UUIDField
|
|
41
|
+
from matrx_orm.operations.update import bulk_update
|
|
42
|
+
from matrx_orm.state import StateManager
|
|
43
|
+
|
|
44
|
+
pytestmark = pytest.mark.asyncio
|
|
45
|
+
|
|
46
|
+
_DB = "expr_guard_db"
|
|
47
|
+
_SUFFIX = uuid.uuid4().hex[:8]
|
|
48
|
+
_TABLE = f"expr_guard_{_SUFFIX}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ExprRow(Model):
|
|
52
|
+
id = UUIDField(primary_key=True)
|
|
53
|
+
label = CharField(max_length=100, null=True)
|
|
54
|
+
note = TextField(null=True)
|
|
55
|
+
counter = IntegerField(null=True)
|
|
56
|
+
metadata = JSONBField(null=True)
|
|
57
|
+
_database = _DB
|
|
58
|
+
_table_name = _TABLE
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _loopback_dsn() -> dict[str, str]:
|
|
62
|
+
dsn = os.environ.get("ISOLATED_PG17_DSN")
|
|
63
|
+
if not dsn:
|
|
64
|
+
pytest.skip("ISOLATED_PG17_DSN (a disposable loopback Postgres) is required")
|
|
65
|
+
u = urlparse(dsn)
|
|
66
|
+
host = (u.hostname or "").strip("[]")
|
|
67
|
+
try:
|
|
68
|
+
loopback = ip_address(host).is_loopback
|
|
69
|
+
except ValueError:
|
|
70
|
+
loopback = host == "localhost"
|
|
71
|
+
if not loopback:
|
|
72
|
+
raise RuntimeError(f"REFUSED: {host!r} is not loopback — this test creates tables.")
|
|
73
|
+
return {
|
|
74
|
+
"host": host,
|
|
75
|
+
"port": str(u.port or 5432),
|
|
76
|
+
"user": unquote(u.username or "postgres"),
|
|
77
|
+
"password": unquote(u.password or ""),
|
|
78
|
+
"database_name": (u.path or "/postgres").lstrip("/") or "postgres",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.fixture
|
|
83
|
+
async def db():
|
|
84
|
+
target = _loopback_dsn()
|
|
85
|
+
# Other suites clear the process-wide state registry; re-register this model.
|
|
86
|
+
StateManager.register_model(ExprRow)
|
|
87
|
+
registry._configs.pop(_DB, None)
|
|
88
|
+
if f"{_DB}_alias" in registry._used_aliases:
|
|
89
|
+
registry._used_aliases.remove(f"{_DB}_alias")
|
|
90
|
+
register_database(
|
|
91
|
+
DatabaseProjectConfig(
|
|
92
|
+
name=_DB,
|
|
93
|
+
alias=f"{_DB}_alias",
|
|
94
|
+
host=target["host"],
|
|
95
|
+
port=target["port"],
|
|
96
|
+
database_name=target["database_name"],
|
|
97
|
+
user=target["user"],
|
|
98
|
+
password=target["password"],
|
|
99
|
+
pool_min=1,
|
|
100
|
+
pool_max=2,
|
|
101
|
+
ssl=False,
|
|
102
|
+
write_queue_enabled=False,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
async with AsyncDatabaseManager.get_connection(_DB) as conn:
|
|
106
|
+
await conn.execute(f"DROP TABLE IF EXISTS public.{_TABLE}")
|
|
107
|
+
await conn.execute(
|
|
108
|
+
f"CREATE TABLE public.{_TABLE} (id uuid PRIMARY KEY, label varchar(100), "
|
|
109
|
+
f"note text, counter integer, metadata jsonb)"
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
yield
|
|
113
|
+
finally:
|
|
114
|
+
async with AsyncDatabaseManager.get_connection(_DB) as conn:
|
|
115
|
+
await conn.execute(f"DROP TABLE IF EXISTS public.{_TABLE}")
|
|
116
|
+
pool = AsyncDatabaseManager._pools.pop(_DB, None)
|
|
117
|
+
if pool is not None:
|
|
118
|
+
await pool.close()
|
|
119
|
+
AsyncDatabaseManager._pool_loops.pop(_DB, None)
|
|
120
|
+
AsyncDatabaseManager._locks.pop(_DB, None)
|
|
121
|
+
AsyncDatabaseManager._growth_locks.pop(_DB, None)
|
|
122
|
+
task = AsyncDatabaseManager._keepalive_tasks.pop(_DB, None)
|
|
123
|
+
if task is not None:
|
|
124
|
+
task.cancel()
|
|
125
|
+
registry._configs.pop(_DB, None)
|
|
126
|
+
if f"{_DB}_alias" in registry._used_aliases:
|
|
127
|
+
registry._used_aliases.remove(f"{_DB}_alias")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def _raw(row_id: str) -> dict | None:
|
|
131
|
+
async with AsyncDatabaseManager.get_connection(_DB) as conn:
|
|
132
|
+
rec = await conn.fetchrow(
|
|
133
|
+
f"SELECT label, note, counter, metadata::text AS metadata FROM public.{_TABLE} "
|
|
134
|
+
f"WHERE id = $1::uuid",
|
|
135
|
+
row_id,
|
|
136
|
+
)
|
|
137
|
+
if rec is None:
|
|
138
|
+
return None
|
|
139
|
+
out = dict(rec)
|
|
140
|
+
out["metadata"] = json.loads(out["metadata"]) if out["metadata"] is not None else None
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def _all_stored_text() -> list[str]:
|
|
145
|
+
async with AsyncDatabaseManager.get_connection(_DB) as conn:
|
|
146
|
+
rows = await conn.fetch(
|
|
147
|
+
f"SELECT coalesce(label,'') || '|' || coalesce(note,'') || '|' || "
|
|
148
|
+
f"coalesce(metadata::text,'') AS t FROM public.{_TABLE}"
|
|
149
|
+
)
|
|
150
|
+
return [r["t"] for r in rows]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _serialised_expression(text: str) -> bool:
|
|
154
|
+
return any(m in text for m in ('"args"', "JsonbMerge", "Expression(", "Func", "Upper("))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def _new(**values) -> ExprRow:
|
|
158
|
+
return await ExprRow.create(id=str(uuid.uuid4()), **values)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def test_instance_update_with_jsonb_merge_merges(db) -> None:
|
|
162
|
+
row = await _new(metadata={"views": 100, "keep": True})
|
|
163
|
+
await row.update(metadata=JsonbMerge("metadata", {"placement": {"slot": 3}}))
|
|
164
|
+
|
|
165
|
+
stored = (await _raw(row.id))["metadata"]
|
|
166
|
+
assert stored == {"views": 100, "keep": True, "placement": {"slot": 3}}
|
|
167
|
+
assert row.metadata == stored # the computed value, not the expression
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def test_instance_update_with_jsonb_merge_on_a_null_column(db) -> None:
|
|
171
|
+
row = await _new(metadata=None)
|
|
172
|
+
await row.update(metadata=JsonbMerge("metadata", {"classification": "a"}))
|
|
173
|
+
assert (await _raw(row.id))["metadata"] == {"classification": "a"}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def test_save_with_a_jsonb_merge_attribute_merges(db) -> None:
|
|
177
|
+
row = await _new(metadata={"views": 1})
|
|
178
|
+
row.metadata = JsonbMerge("metadata", {"extra": 2})
|
|
179
|
+
await row.save()
|
|
180
|
+
stored = (await _raw(row.id))["metadata"]
|
|
181
|
+
assert stored == {"views": 1, "extra": 2}
|
|
182
|
+
assert row.metadata == stored
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def test_increments_run_once_and_a_later_save_does_not_rerun_them(db) -> None:
|
|
186
|
+
row = await _new(counter=None)
|
|
187
|
+
await row.update(counter=Coalesce(F("counter"), 0) + 7) # BinaryExpr
|
|
188
|
+
assert row.counter == 7
|
|
189
|
+
await row.update(counter=F("counter") * 2) # Expression
|
|
190
|
+
assert row.counter == 14
|
|
191
|
+
await row.save() # nothing changed: must not re-apply anything
|
|
192
|
+
assert (await _raw(row.id))["counter"] == 14
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def test_text_functions_and_column_references_run_as_sql(db) -> None:
|
|
196
|
+
row = await _new(label="quiet", note="n")
|
|
197
|
+
await row.update(label=Upper(F("label")))
|
|
198
|
+
assert (await _raw(row.id))["label"] == "QUIET"
|
|
199
|
+
await row.update(note=F("label"))
|
|
200
|
+
assert (await _raw(row.id))["note"] == "QUIET"
|
|
201
|
+
assert row.note == "QUIET"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def test_bulk_update_runs_expressions(db) -> None:
|
|
205
|
+
row = await _new(metadata={"a": 1})
|
|
206
|
+
row.metadata = JsonbMerge("metadata", {"b": 2})
|
|
207
|
+
assert await bulk_update(ExprRow, [row], ["metadata"]) == 1
|
|
208
|
+
stored = (await _raw(row.id))["metadata"]
|
|
209
|
+
assert stored == {"a": 1, "b": 2}
|
|
210
|
+
assert row.metadata == stored
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def test_insert_with_an_expression_refuses_and_writes_nothing(db) -> None:
|
|
214
|
+
row_id = str(uuid.uuid4())
|
|
215
|
+
with pytest.raises(ExpressionAsDataError):
|
|
216
|
+
await ExprRow.create(id=row_id, metadata=JsonbMerge("metadata", {"a": 1}))
|
|
217
|
+
with pytest.raises(ExpressionAsDataError):
|
|
218
|
+
await ExprRow.create(id=row_id, label=Upper(F("label")))
|
|
219
|
+
assert await _raw(row_id) is None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def test_an_expression_nested_inside_json_refuses(db) -> None:
|
|
223
|
+
row = await _new(metadata={"a": 1})
|
|
224
|
+
with pytest.raises(ExpressionAsDataError):
|
|
225
|
+
await row.update(metadata={"placement": JsonbMerge("metadata", {"x": 1})})
|
|
226
|
+
assert (await _raw(row.id))["metadata"] == {"a": 1}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def test_no_stored_value_is_ever_a_serialised_expression(db) -> None:
|
|
230
|
+
"""Drive every write shape at one table, then scan what Postgres holds."""
|
|
231
|
+
a = await _new(label="x", note="y", counter=1, metadata={"k": 1})
|
|
232
|
+
await a.update(metadata=JsonbMerge("metadata", {"m": 1}), label=Upper(F("label")))
|
|
233
|
+
a.note = F("label")
|
|
234
|
+
a.counter = F("counter") + 1
|
|
235
|
+
await a.save()
|
|
236
|
+
for text in await _all_stored_text():
|
|
237
|
+
assert not _serialised_expression(text), text
|
|
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
|