matrx-orm 3.1.4__tar.gz → 3.1.6__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.4 → matrx_orm-3.1.6}/PKG-INFO +1 -1
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/base.py +24 -12
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/expressions.py +133 -5
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/extended.py +52 -16
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/relations.py +19 -5
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/delete.py +27 -6
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/read.py +7 -1
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/update.py +18 -13
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/query/builder.py +13 -6
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/op.py +38 -6
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/pyproject.toml +1 -1
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_case_when.py +145 -8
- matrx_orm-3.1.6/tests/level1/test_composite_pk_write_paths.py +238 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/.gitignore +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/CLAUDE.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/MODEL_API.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/docs/migrations.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/scripts/release.sh +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/conftest.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_execute_admin_sql.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_filter_raw.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_querybuilder_mutation_semantics.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.4 → matrx_orm-3.1.6}/tests/test_sql_param_casts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.6
|
|
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
|
|
@@ -1145,12 +1145,15 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1145
1145
|
|
|
1146
1146
|
@classmethod
|
|
1147
1147
|
def _single_pk_name(cls) -> str:
|
|
1148
|
+
"""The one PK column name, for callers that structurally need a single
|
|
1149
|
+
scalar key (e.g. ``{pk}__in`` filters). RAISES on composite-PK models —
|
|
1150
|
+
use ``_build_pk_filter`` for by-id access instead."""
|
|
1148
1151
|
pks = cls._meta.primary_keys
|
|
1149
1152
|
if len(pks) != 1:
|
|
1150
1153
|
raise ValueError(
|
|
1151
|
-
f"{cls.__name__}
|
|
1154
|
+
f"{cls.__name__} requires a single-column primary key here, "
|
|
1152
1155
|
f"but the model has primary keys {pks!r}. Use get(...)/get_or_none(...) "
|
|
1153
|
-
f"with explicit key columns
|
|
1156
|
+
f"with explicit key columns, or pass a dict/tuple to the by-id methods."
|
|
1154
1157
|
)
|
|
1155
1158
|
return pks[0]
|
|
1156
1159
|
|
|
@@ -1159,17 +1162,20 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1159
1162
|
"""Load a row by its primary key. Raises DoesNotExist if absent.
|
|
1160
1163
|
|
|
1161
1164
|
Alias for ``get(**{pk: item_id})`` — mirrors ``BaseManager.load_by_id``.
|
|
1165
|
+
Composite-PK models accept a dict ``{pk: val}`` or a PK-ordered
|
|
1166
|
+
tuple/list (a bare scalar raises — see ``_build_pk_filter``).
|
|
1162
1167
|
For a None-returning lookup use :meth:`load_by_id_or_none`.
|
|
1163
1168
|
"""
|
|
1164
|
-
return await cls.get(**
|
|
1169
|
+
return await cls.get(**cls._build_pk_filter(item_id))
|
|
1165
1170
|
|
|
1166
1171
|
@classmethod
|
|
1167
1172
|
async def load_by_id_or_none(cls, item_id: Any) -> Self | None:
|
|
1168
1173
|
"""Load a row by its primary key, or None if absent.
|
|
1169
1174
|
|
|
1170
|
-
Alias for ``get_or_none(**{pk: item_id})``.
|
|
1175
|
+
Alias for ``get_or_none(**{pk: item_id})``. Composite-PK models accept
|
|
1176
|
+
a dict / PK-ordered tuple, same as :meth:`load_by_id`.
|
|
1171
1177
|
"""
|
|
1172
|
-
return await cls.get_or_none(**
|
|
1178
|
+
return await cls.get_or_none(**cls._build_pk_filter(item_id))
|
|
1173
1179
|
|
|
1174
1180
|
@classmethod
|
|
1175
1181
|
async def filter_items(cls, *args: Any, **kwargs: Any) -> list[Self]:
|
|
@@ -1189,7 +1195,7 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1189
1195
|
returned instance isn't needed, prefer ``update_where({pk: id}, ...)``
|
|
1190
1196
|
(a single statement).
|
|
1191
1197
|
"""
|
|
1192
|
-
instance = await cls.get(**
|
|
1198
|
+
instance = await cls.get(**cls._build_pk_filter(item_id))
|
|
1193
1199
|
return await instance.update(**updates)
|
|
1194
1200
|
|
|
1195
1201
|
@classmethod
|
|
@@ -1292,9 +1298,17 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1292
1298
|
raise TypeError(error_message)
|
|
1293
1299
|
|
|
1294
1300
|
try:
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1301
|
+
pks = self._meta.primary_keys
|
|
1302
|
+
if len(pks) > 1:
|
|
1303
|
+
# Natural composite keys are fully known BEFORE the row exists
|
|
1304
|
+
# (a join row is constructed with both ids in hand), so "all
|
|
1305
|
+
# PKs set" cannot distinguish INSERT from UPDATE. Hydration is
|
|
1306
|
+
# the truth: only a row that came from the DB is an update.
|
|
1307
|
+
is_update = bool(self.__dict__.get("_from_db"))
|
|
1308
|
+
else:
|
|
1309
|
+
is_update = bool(pks) and all(
|
|
1310
|
+
getattr(self, pk, None) is not None for pk in pks
|
|
1311
|
+
)
|
|
1298
1312
|
if is_update:
|
|
1299
1313
|
# Write ONLY loaded/changed columns — never blindly re-write the
|
|
1300
1314
|
# whole row. A full-row write from a partially-loaded instance
|
|
@@ -1424,9 +1438,7 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1424
1438
|
await delete.delete_instance(self)
|
|
1425
1439
|
await StateManager.remove(self.__class__, self)
|
|
1426
1440
|
except ORMException as e:
|
|
1427
|
-
pk = (
|
|
1428
|
-
getattr(self, self._meta.primary_keys[0], None) if self._meta.primary_keys else None
|
|
1429
|
-
)
|
|
1441
|
+
pk = {p: getattr(self, p, None) for p in self._meta.primary_keys} or None
|
|
1430
1442
|
e.enrich(model=self.__class__, operation="delete", args={"pk": pk})
|
|
1431
1443
|
raise
|
|
1432
1444
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from decimal import Decimal
|
|
3
5
|
from typing import Any, TYPE_CHECKING
|
|
6
|
+
from uuid import UUID
|
|
4
7
|
|
|
5
8
|
if TYPE_CHECKING:
|
|
6
9
|
pass
|
|
@@ -533,6 +536,115 @@ def _render_func_arg(arg: Any, params: list[Any]) -> str:
|
|
|
533
536
|
return f"${len(params)}"
|
|
534
537
|
|
|
535
538
|
|
|
539
|
+
# ---------------------------------------------------------------------------
|
|
540
|
+
# Type-anchoring for bound literals in a CASE branch
|
|
541
|
+
# ---------------------------------------------------------------------------
|
|
542
|
+
#
|
|
543
|
+
# Postgres infers a bare `$N`'s type from its surrounding context. A CASE whose
|
|
544
|
+
# THEN/ELSE branches are ALL bound params has NO anchor to infer from, so it
|
|
545
|
+
# falls back to `text` — and asyncpg then refuses to encode a Python int/bool/
|
|
546
|
+
# float into that text slot:
|
|
547
|
+
#
|
|
548
|
+
# invalid input for query argument $5: 0 (expected str, got int)
|
|
549
|
+
#
|
|
550
|
+
# Verified live (project txzxabzwovsujtloxrus):
|
|
551
|
+
# PREPARE p AS SELECT 1 FROM t ORDER BY CASE WHEN lower(name) = $1
|
|
552
|
+
# THEN $2 ELSE $3 END;
|
|
553
|
+
# -> parameter_types = {text,text,text} # $2/$3 silently become text
|
|
554
|
+
# ... THEN $2::bigint ELSE $3::bigint END
|
|
555
|
+
# -> parameter_types = {text,bigint,bigint} # anchored
|
|
556
|
+
#
|
|
557
|
+
# So an UNANCHORED CASE-branch literal is bound WITH an explicit cast: the value
|
|
558
|
+
# stays parameterized (zero injection surface) and Postgres gets its type anchor.
|
|
559
|
+
#
|
|
560
|
+
# 🚨 BUT THE CAST IS ONLY EMITTED WHEN THE CASE IS ACTUALLY UNANCHORED — i.e.
|
|
561
|
+
# when EVERY branch is a bound literal. If ANY branch is a column or expression,
|
|
562
|
+
# THAT branch already supplies the type and adding a cast can CHANGE the CASE's
|
|
563
|
+
# resolved type. This is not hypothetical (live-verified type resolution):
|
|
564
|
+
#
|
|
565
|
+
# CASE WHEN .. THEN bigint ELSE numeric END -> numeric ✅
|
|
566
|
+
# CASE WHEN .. THEN double precision ELSE numeric END -> double precision ❌
|
|
567
|
+
#
|
|
568
|
+
# The auto-ingest cost UPSERT does exactly this:
|
|
569
|
+
# SET daily_auto_rag_cost_used_usd = -- a NUMERIC (money) column
|
|
570
|
+
# CASE WHEN .. THEN $7 ELSE (col + $8) END -- ELSE anchors it: numeric
|
|
571
|
+
# Blindly casting `$7::double precision` there promotes the whole CASE to double
|
|
572
|
+
# precision and pushes MONEY THROUGH A FLOAT. So: literal-only CASE -> cast;
|
|
573
|
+
# any column/expression branch -> leave the params bare and let it anchor them.
|
|
574
|
+
#
|
|
575
|
+
# Order matters: bool before int (bool IS a subclass of int), datetime before
|
|
576
|
+
# date (datetime IS a subclass of date).
|
|
577
|
+
_PG_LITERAL_CASTS: tuple[tuple[type, str], ...] = (
|
|
578
|
+
(bool, "boolean"),
|
|
579
|
+
(int, "bigint"),
|
|
580
|
+
(float, "double precision"),
|
|
581
|
+
(Decimal, "numeric"),
|
|
582
|
+
(datetime, "timestamptz"),
|
|
583
|
+
(date, "date"),
|
|
584
|
+
(UUID, "uuid"),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _pg_cast_for_literal(value: Any) -> str | None:
|
|
589
|
+
"""The Postgres type to anchor a bound Python literal to, or None if unknown."""
|
|
590
|
+
for py_type, pg_type in _PG_LITERAL_CASTS:
|
|
591
|
+
if isinstance(value, py_type):
|
|
592
|
+
return pg_type
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _is_bound_literal_branch(arg: Any) -> bool:
|
|
597
|
+
"""True when a CASE branch is a value we BIND as a param.
|
|
598
|
+
|
|
599
|
+
A bare string is a COLUMN NAME (the Func-arg convention), an ``F``/``Func``/
|
|
600
|
+
nested ``Case`` is an expression, and ``None`` renders as a literal ``NULL``
|
|
601
|
+
— none of those are bound. Everything else is a value we bind.
|
|
602
|
+
"""
|
|
603
|
+
if arg is None or isinstance(arg, str) or isinstance(arg, F) or hasattr(arg, "as_sql"):
|
|
604
|
+
return False
|
|
605
|
+
return True
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _branch_anchors_type(arg: Any) -> bool:
|
|
609
|
+
"""True when a CASE branch SUPPLIES a type Postgres can resolve the CASE by.
|
|
610
|
+
|
|
611
|
+
A column reference or an expression does. A bound literal does not (it is the
|
|
612
|
+
thing that needs anchoring). ``None`` does not either — an untyped ``NULL``
|
|
613
|
+
resolves to whatever its siblings do, so it must NOT suppress the cast (a
|
|
614
|
+
`THEN NULL ELSE $1` CASE is still unanchored and still degrades to text).
|
|
615
|
+
"""
|
|
616
|
+
return isinstance(arg, str) or isinstance(arg, F) or hasattr(arg, "as_sql")
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def case_is_unanchored(branches: list[Any]) -> bool:
|
|
620
|
+
"""True when NO CASE branch supplies a type — only then may we cast literals.
|
|
621
|
+
|
|
622
|
+
If even one branch is a column or an expression, it anchors the CASE and a
|
|
623
|
+
cast could change the resolved type (bigint/numeric -> numeric, but double
|
|
624
|
+
precision/numeric -> double precision, which silently degrades a money
|
|
625
|
+
column). See the block comment above.
|
|
626
|
+
"""
|
|
627
|
+
return not any(_branch_anchors_type(b) for b in branches)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _render_case_result(arg: Any, params: list[Any], *, cast_literals: bool) -> str:
|
|
631
|
+
"""Render a CASE THEN/ELSE branch.
|
|
632
|
+
|
|
633
|
+
Identical to :func:`_render_func_arg` for columns/expressions. A bound
|
|
634
|
+
literal gets an explicit ``::type`` cast ONLY when ``cast_literals`` is set —
|
|
635
|
+
i.e. only when the CASE has no other branch to anchor its type.
|
|
636
|
+
"""
|
|
637
|
+
if arg is None:
|
|
638
|
+
# An unadorned NULL is unknown-typed too, but it unifies with whatever
|
|
639
|
+
# the sibling branch resolves to — no cast needed, and none is possible.
|
|
640
|
+
return "NULL"
|
|
641
|
+
if not _is_bound_literal_branch(arg):
|
|
642
|
+
return _render_func_arg(arg, params)
|
|
643
|
+
params.append(arg)
|
|
644
|
+
cast = _pg_cast_for_literal(arg) if cast_literals else None
|
|
645
|
+
return f"${len(params)}::{cast}" if cast else f"${len(params)}"
|
|
646
|
+
|
|
647
|
+
|
|
536
648
|
# ---------------------------------------------------------------------------
|
|
537
649
|
# Case / When — conditional CASE WHEN ... THEN ... ELSE ... END expression
|
|
538
650
|
# ---------------------------------------------------------------------------
|
|
@@ -552,8 +664,13 @@ class When:
|
|
|
552
664
|
|
|
553
665
|
Usage::
|
|
554
666
|
|
|
555
|
-
When("lower(e.name) = $1", phrase, then=0) # -> WHEN lower(e.name) = $1 THEN $2
|
|
667
|
+
When("lower(e.name) = $1", phrase, then=0) # -> WHEN lower(e.name) = $1 THEN $2::bigint
|
|
556
668
|
When("ed.src_id = ANY($1::uuid[])", ids, then="ed.dst_id") # then = bare column, not bound
|
|
669
|
+
|
|
670
|
+
A bound ``then``/``default`` literal is rendered WITH an explicit ``::type``
|
|
671
|
+
cast — a CASE branch gives Postgres no type to infer from, so an unadorned
|
|
672
|
+
``$N`` silently resolves to ``text`` and asyncpg then rejects a Python int
|
|
673
|
+
("expected str, got int"). See :func:`_render_case_result`.
|
|
557
674
|
"""
|
|
558
675
|
|
|
559
676
|
def __init__(self, condition: str, *condition_params: Any, then: Any) -> None:
|
|
@@ -561,7 +678,7 @@ class When:
|
|
|
561
678
|
self.condition_params = condition_params
|
|
562
679
|
self.then = then
|
|
563
680
|
|
|
564
|
-
def as_sql(self, params: list[Any]) -> str:
|
|
681
|
+
def as_sql(self, params: list[Any], *, cast_literals: bool = True) -> str:
|
|
565
682
|
if self.condition_params:
|
|
566
683
|
from matrx_orm.query.executor import _shift_param_refs
|
|
567
684
|
|
|
@@ -569,7 +686,7 @@ class When:
|
|
|
569
686
|
params.extend(self.condition_params)
|
|
570
687
|
else:
|
|
571
688
|
cond_sql = self.condition
|
|
572
|
-
then_sql =
|
|
689
|
+
then_sql = _render_case_result(self.then, params, cast_literals=cast_literals)
|
|
573
690
|
return f"WHEN {cond_sql} THEN {then_sql}"
|
|
574
691
|
|
|
575
692
|
def __repr__(self) -> str:
|
|
@@ -611,11 +728,22 @@ class Case:
|
|
|
611
728
|
self.default = default
|
|
612
729
|
|
|
613
730
|
def as_sql(self, params: list[Any]) -> str:
|
|
731
|
+
# Cast the bound literals ONLY if nothing else in the CASE anchors its
|
|
732
|
+
# type. A column/expression branch (e.g. `ELSE col + $8` in the money
|
|
733
|
+
# UPSERT) is the anchor — casting alongside it can promote the whole
|
|
734
|
+
# CASE to a different type. See case_is_unanchored().
|
|
735
|
+
branches: list[Any] = [w.then for w in self.whens]
|
|
736
|
+
if self.default is not None:
|
|
737
|
+
branches.append(self.default)
|
|
738
|
+
cast_literals = case_is_unanchored(branches)
|
|
739
|
+
|
|
614
740
|
parts = ["CASE"]
|
|
615
741
|
for w in self.whens:
|
|
616
|
-
parts.append(w.as_sql(params))
|
|
742
|
+
parts.append(w.as_sql(params, cast_literals=cast_literals))
|
|
617
743
|
if self.default is not None:
|
|
618
|
-
parts.append(
|
|
744
|
+
parts.append(
|
|
745
|
+
f"ELSE {_render_case_result(self.default, params, cast_literals=cast_literals)}"
|
|
746
|
+
)
|
|
619
747
|
parts.append("END")
|
|
620
748
|
return " ".join(parts)
|
|
621
749
|
|
|
@@ -196,7 +196,7 @@ class BaseManager(Generic[ModelT]):
|
|
|
196
196
|
async def _initialize_item_runtime(self, item: ModelT | None) -> ModelT | None:
|
|
197
197
|
if not item:
|
|
198
198
|
return None
|
|
199
|
-
item_pk =
|
|
199
|
+
item_pk = self._item_identity(item)
|
|
200
200
|
if item_pk is not None and item_pk in self._skipped_item_ids:
|
|
201
201
|
return None
|
|
202
202
|
existing_dto = getattr(item, "dto", None)
|
|
@@ -274,7 +274,7 @@ class BaseManager(Generic[ModelT]):
|
|
|
274
274
|
async def _process_item(self, item: ModelT | None) -> ModelT | None:
|
|
275
275
|
if item:
|
|
276
276
|
await self._initialize_item_runtime(item)
|
|
277
|
-
self._add_to_active(item_id=
|
|
277
|
+
self._add_to_active(item_id=item)
|
|
278
278
|
return item
|
|
279
279
|
|
|
280
280
|
@handle_errors
|
|
@@ -404,7 +404,12 @@ class BaseManager(Generic[ModelT]):
|
|
|
404
404
|
|
|
405
405
|
@property
|
|
406
406
|
def _pk_name(self) -> str:
|
|
407
|
-
"""The primary key field name for this manager's model.
|
|
407
|
+
"""The primary key field name for this manager's model.
|
|
408
|
+
|
|
409
|
+
⚠️ Returns the FIRST PK column only — never use it to identify a row
|
|
410
|
+
on a composite-PK model. Use ``_pk_filter`` (by-id filters) or
|
|
411
|
+
``_item_identity`` (row identity) instead.
|
|
412
|
+
"""
|
|
408
413
|
pks = self.model._meta.primary_keys
|
|
409
414
|
return pks[0] if pks else "id"
|
|
410
415
|
|
|
@@ -434,12 +439,30 @@ class BaseManager(Generic[ModelT]):
|
|
|
434
439
|
|
|
435
440
|
return governed_write(self.model)
|
|
436
441
|
|
|
442
|
+
def _item_identity(self, item: Any) -> Any:
|
|
443
|
+
"""Full-primary-key identity of a model instance.
|
|
444
|
+
|
|
445
|
+
Single-PK: the scalar PK value. Composite-PK: a PK-ordered tuple.
|
|
446
|
+
Returns None when any PK component is unset — first-column-only
|
|
447
|
+
identity conflated distinct composite-PK rows.
|
|
448
|
+
"""
|
|
449
|
+
pks = self.model._meta.primary_keys or ["id"]
|
|
450
|
+
if len(pks) == 1:
|
|
451
|
+
return getattr(item, pks[0], None)
|
|
452
|
+
values = tuple(getattr(item, pk, None) for pk in pks)
|
|
453
|
+
return None if any(v is None for v in values) else values
|
|
454
|
+
|
|
437
455
|
def _active_item_key(self, item_or_id: Any) -> Any:
|
|
438
|
-
"""Normalize an active-set entry to a primary-key value."""
|
|
439
|
-
|
|
440
|
-
if hasattr(item_or_id, pk):
|
|
441
|
-
return
|
|
442
|
-
|
|
456
|
+
"""Normalize an active-set entry to a full-primary-key value."""
|
|
457
|
+
pks = self.model._meta.primary_keys or ["id"]
|
|
458
|
+
if all(hasattr(item_or_id, pk) for pk in pks):
|
|
459
|
+
return self._item_identity(item_or_id)
|
|
460
|
+
if len(pks) == 1:
|
|
461
|
+
return item_or_id
|
|
462
|
+
# Composite raw id (dict / PK-ordered tuple) → normalized PK-ordered
|
|
463
|
+
# tuple. A bare scalar raises via _build_pk_filter.
|
|
464
|
+
pk_filter = self.model._build_pk_filter(item_or_id)
|
|
465
|
+
return tuple(pk_filter[pk] for pk in pks)
|
|
443
466
|
|
|
444
467
|
def _add_to_active(self, item_id: Any) -> Any:
|
|
445
468
|
"""Add an item ID to the active set."""
|
|
@@ -517,7 +540,14 @@ class BaseManager(Generic[ModelT]):
|
|
|
517
540
|
return await self.load_item_with_retry(**self._pk_filter(item_id))
|
|
518
541
|
|
|
519
542
|
async def load_items_by_ids(self, item_ids: list[Any]) -> list[ModelT]:
|
|
520
|
-
"""Load multiple items by their primary keys.
|
|
543
|
+
"""Load multiple items by their primary keys.
|
|
544
|
+
|
|
545
|
+
Composite-PK models: each id is a dict / PK-ordered tuple (no
|
|
546
|
+
single-column ``__in`` exists for a composite key).
|
|
547
|
+
"""
|
|
548
|
+
if len(self.model._meta.primary_keys or ["id"]) > 1:
|
|
549
|
+
items = await asyncio.gather(*(self.load_by_id(item_id) for item_id in item_ids))
|
|
550
|
+
return [item for item in items if item]
|
|
521
551
|
return await self.load_items(**{f"{self._pk_name}__in": item_ids})
|
|
522
552
|
|
|
523
553
|
async def add_active_by_id(self, item_id: Any) -> ModelT:
|
|
@@ -548,10 +578,9 @@ class BaseManager(Generic[ModelT]):
|
|
|
548
578
|
@handle_errors
|
|
549
579
|
async def get_active_items(self) -> list[ModelT]:
|
|
550
580
|
"""Get all active items, initialized."""
|
|
551
|
-
pk = self._pk_name
|
|
552
581
|
item_ids = [self._active_item_key(item_id) for item_id in self._active_items]
|
|
553
582
|
items: list[ModelT] = await asyncio.gather(
|
|
554
|
-
*(self._get_item_or_raise(**
|
|
583
|
+
*(self._get_item_or_raise(**self._pk_filter(item_id)) for item_id in item_ids)
|
|
555
584
|
)
|
|
556
585
|
return list(items)
|
|
557
586
|
|
|
@@ -671,7 +700,14 @@ class BaseManager(Generic[ModelT]):
|
|
|
671
700
|
|
|
672
701
|
config_name = self.model.get_database_name()
|
|
673
702
|
table = self.model._meta.qualified_table_name
|
|
674
|
-
|
|
703
|
+
pks = self.model._meta.primary_keys or ["id"]
|
|
704
|
+
if len(pks) > 1:
|
|
705
|
+
raise ValueError(
|
|
706
|
+
f"{self.model.__name__} has a composite primary key {tuple(pks)}; "
|
|
707
|
+
f"patch_jsonb_path supports single-column primary keys only. "
|
|
708
|
+
f"Use update_item_fields with an explicit pk dict/tuple instead."
|
|
709
|
+
)
|
|
710
|
+
pk = pks[0]
|
|
675
711
|
|
|
676
712
|
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
677
713
|
|
|
@@ -801,8 +837,8 @@ class BaseManager(Generic[ModelT]):
|
|
|
801
837
|
rows_affected = await self.model.bulk_delete(objects)
|
|
802
838
|
|
|
803
839
|
for item in objects:
|
|
804
|
-
pk_val =
|
|
805
|
-
if pk_val:
|
|
840
|
+
pk_val = self._item_identity(item)
|
|
841
|
+
if pk_val is not None:
|
|
806
842
|
self._remove_from_active(pk_val)
|
|
807
843
|
|
|
808
844
|
vcprint(
|
|
@@ -822,8 +858,8 @@ class BaseManager(Generic[ModelT]):
|
|
|
822
858
|
for item in objects:
|
|
823
859
|
try:
|
|
824
860
|
success = await self._delete_item(item)
|
|
825
|
-
pk_val =
|
|
826
|
-
if success and pk_val:
|
|
861
|
+
pk_val = self._item_identity(item)
|
|
862
|
+
if success and pk_val is not None:
|
|
827
863
|
self._remove_from_active(pk_val)
|
|
828
864
|
success_count += 1
|
|
829
865
|
except Exception as individual_error:
|
|
@@ -365,12 +365,26 @@ class ManyToManyReference:
|
|
|
365
365
|
"target_model": self.target_model if isinstance(self.target_model, str) else getattr(self.target_model, '__name__', str(self.target_model)),
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
@staticmethod
|
|
369
|
+
def _single_pk(model_cls: type[Model], side: str) -> str:
|
|
370
|
+
"""M2M traversal keys each side by ONE column. A composite-PK endpoint
|
|
371
|
+
must raise — silently using the first PK column would join/mutate the
|
|
372
|
+
wrong junction rows."""
|
|
373
|
+
pks = model_cls._meta.primary_keys
|
|
374
|
+
if len(pks) != 1:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"ManyToMany {side} model {model_cls.__name__} has a composite "
|
|
377
|
+
f"primary key {tuple(pks)}; M2M traversal supports single-column "
|
|
378
|
+
f"primary keys only. Query the junction table's model directly."
|
|
379
|
+
)
|
|
380
|
+
return pks[0]
|
|
381
|
+
|
|
368
382
|
async def fetch_related(self, instance: Model) -> list[Model]:
|
|
369
383
|
"""Single-query JOIN: junction table + target model in one round-trip."""
|
|
370
384
|
from matrx_orm.adapters import AdapterRegistry
|
|
371
385
|
from matrx_orm.exceptions import ORMException
|
|
372
386
|
|
|
373
|
-
pk_field = instance.
|
|
387
|
+
pk_field = self._single_pk(instance.__class__, "source")
|
|
374
388
|
source_value = getattr(instance, pk_field)
|
|
375
389
|
if source_value is None:
|
|
376
390
|
return []
|
|
@@ -383,7 +397,7 @@ class ManyToManyReference:
|
|
|
383
397
|
target_table = target_model._meta.qualified_table_name
|
|
384
398
|
|
|
385
399
|
# Determine the target PK column name (nearly always "id")
|
|
386
|
-
target_pk = target_model
|
|
400
|
+
target_pk = self._single_pk(target_model, "target")
|
|
387
401
|
|
|
388
402
|
sql = (
|
|
389
403
|
f"SELECT t.* FROM {target_table} t "
|
|
@@ -408,7 +422,7 @@ class ManyToManyReference:
|
|
|
408
422
|
from matrx_orm.adapters import AdapterRegistry
|
|
409
423
|
from matrx_orm.exceptions import ORMException
|
|
410
424
|
|
|
411
|
-
pk_field = instance.
|
|
425
|
+
pk_field = self._single_pk(instance.__class__, "source")
|
|
412
426
|
source_value = getattr(instance, pk_field)
|
|
413
427
|
|
|
414
428
|
try:
|
|
@@ -443,7 +457,7 @@ class ManyToManyReference:
|
|
|
443
457
|
from matrx_orm.adapters import AdapterRegistry
|
|
444
458
|
from matrx_orm.exceptions import ORMException
|
|
445
459
|
|
|
446
|
-
pk_field = instance.
|
|
460
|
+
pk_field = self._single_pk(instance.__class__, "source")
|
|
447
461
|
source_value = getattr(instance, pk_field)
|
|
448
462
|
|
|
449
463
|
try:
|
|
@@ -473,7 +487,7 @@ class ManyToManyReference:
|
|
|
473
487
|
from matrx_orm.adapters import AdapterRegistry
|
|
474
488
|
from matrx_orm.exceptions import ORMException
|
|
475
489
|
|
|
476
|
-
pk_field = instance.
|
|
490
|
+
pk_field = self._single_pk(instance.__class__, "source")
|
|
477
491
|
source_value = getattr(instance, pk_field)
|
|
478
492
|
|
|
479
493
|
try:
|
|
@@ -25,7 +25,24 @@ async def bulk_delete(model_cls: type[Model], objects: Sequence[Model]) -> int:
|
|
|
25
25
|
if not objects:
|
|
26
26
|
return 0
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
pk_list = model_cls._meta.primary_keys or ["id"]
|
|
29
|
+
|
|
30
|
+
if len(pk_list) > 1:
|
|
31
|
+
# Composite PK: no single-column `__in` exists. Delete per-row on the
|
|
32
|
+
# FULL key — filtering on the first column alone would wipe every row
|
|
33
|
+
# sharing that value.
|
|
34
|
+
rows_affected = 0
|
|
35
|
+
for obj in objects:
|
|
36
|
+
pk_filter = {pk: getattr(obj, pk, None) for pk in pk_list}
|
|
37
|
+
if any(v is None for v in pk_filter.values()):
|
|
38
|
+
continue
|
|
39
|
+
deleted = await delete(model_cls, **pk_filter)
|
|
40
|
+
if deleted:
|
|
41
|
+
rows_affected += deleted
|
|
42
|
+
await StateManager.remove(model_cls, obj)
|
|
43
|
+
return rows_affected
|
|
44
|
+
|
|
45
|
+
pk_name = pk_list[0]
|
|
29
46
|
ids = [
|
|
30
47
|
getattr(obj, pk_name)
|
|
31
48
|
for obj in objects
|
|
@@ -66,11 +83,15 @@ async def delete_instance(instance: Model) -> None:
|
|
|
66
83
|
pk_list = model_cls._meta.primary_keys
|
|
67
84
|
if not pk_list:
|
|
68
85
|
raise ValueError(f"Cannot delete {model_cls.__name__} with no primary key.")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
# WHERE must carry EVERY primary-key column — first-column-only deletes
|
|
87
|
+
# every row sharing that value on a composite-PK table.
|
|
88
|
+
pk_filter: dict[str, Any] = {}
|
|
89
|
+
for pk in pk_list:
|
|
90
|
+
value = getattr(instance, pk, None)
|
|
91
|
+
if value is None:
|
|
92
|
+
raise ValueError(f"Cannot delete {model_cls.__name__}, primary key {pk} is None")
|
|
93
|
+
pk_filter[pk] = value
|
|
73
94
|
|
|
74
95
|
await pre_delete.send(model_cls, instance=instance)
|
|
75
|
-
await delete(model_cls, **
|
|
96
|
+
await delete(model_cls, **pk_filter)
|
|
76
97
|
await post_delete.send(model_cls, instance=instance)
|
|
@@ -43,7 +43,13 @@ async def values_list(model_cls, *fields, flat=False, **kwargs):
|
|
|
43
43
|
|
|
44
44
|
async def in_bulk(model_cls, id_list, field=None):
|
|
45
45
|
if field is None:
|
|
46
|
-
|
|
46
|
+
pks = model_cls._meta.primary_keys
|
|
47
|
+
if len(pks) > 1:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"{model_cls.__name__} has a composite primary key {tuple(pks)} — "
|
|
50
|
+
f"in_bulk() needs an explicit single `field` to key results on."
|
|
51
|
+
)
|
|
52
|
+
field = pks[0] if pks else "id"
|
|
47
53
|
objects = await QueryBuilder(model_cls).filter(**{f"{field}__in": id_list}).all()
|
|
48
54
|
return {getattr(obj, field): obj for obj in objects}
|
|
49
55
|
|
|
@@ -34,9 +34,9 @@ async def bulk_update(model_cls: type[Model], objects: Sequence[Model], fields:
|
|
|
34
34
|
vcprint(model_cls._fields, "Model Fields", color="yellow")
|
|
35
35
|
raise ValueError(f"Invalid fields for {model_cls.__name__}: {invalid_fields}")
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
pk_list = model_cls._meta.primary_keys or ["id"]
|
|
38
38
|
valid_objects = [
|
|
39
|
-
obj for obj in objects if getattr(obj,
|
|
39
|
+
obj for obj in objects if all(getattr(obj, pk, None) is not None for pk in pk_list)
|
|
40
40
|
]
|
|
41
41
|
if not valid_objects:
|
|
42
42
|
return 0
|
|
@@ -47,7 +47,8 @@ async def bulk_update(model_cls: type[Model], objects: Sequence[Model], fields:
|
|
|
47
47
|
failed_updates: list[dict[str, Any]] = []
|
|
48
48
|
|
|
49
49
|
for i, obj in enumerate(valid_objects):
|
|
50
|
-
|
|
50
|
+
pk_filter = {pk: getattr(obj, pk) for pk in pk_list}
|
|
51
|
+
pk_value = pk_filter if len(pk_list) > 1 else next(iter(pk_filter.values()))
|
|
51
52
|
try:
|
|
52
53
|
update_data: dict[str, Any] = {}
|
|
53
54
|
for field_name in fields:
|
|
@@ -59,7 +60,7 @@ async def bulk_update(model_cls: type[Model], objects: Sequence[Model], fields:
|
|
|
59
60
|
|
|
60
61
|
if update_data:
|
|
61
62
|
result = (
|
|
62
|
-
await QueryBuilder(model_cls).filter(**
|
|
63
|
+
await QueryBuilder(model_cls).filter(**pk_filter).update(**update_data)
|
|
63
64
|
)
|
|
64
65
|
if result.rows_affected > 0:
|
|
65
66
|
rows_affected += 1
|
|
@@ -140,10 +141,14 @@ async def update_instance(instance: Model, fields: Iterable[str] | None = None)
|
|
|
140
141
|
pk_list = model_cls._meta.primary_keys
|
|
141
142
|
if not pk_list:
|
|
142
143
|
raise ValueError(f"Cannot update {model_cls.__name__} with no primary key.")
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
# WHERE must carry EVERY primary-key column. Filtering on pk_list[0] alone
|
|
145
|
+
# updated every row sharing the first key value on a composite-PK table.
|
|
146
|
+
pk_filter: dict[str, Any] = {}
|
|
147
|
+
for pk in pk_list:
|
|
148
|
+
value = getattr(instance, pk, None)
|
|
149
|
+
if value is None:
|
|
150
|
+
raise ValueError(f"Cannot update {model_cls.__name__}, primary key {pk} is None")
|
|
151
|
+
pk_filter[pk] = value
|
|
147
152
|
|
|
148
153
|
version_field_name: str | None = None
|
|
149
154
|
current_version: int | None = None
|
|
@@ -157,10 +162,10 @@ async def update_instance(instance: Model, fields: Iterable[str] | None = None)
|
|
|
157
162
|
field_names: Iterable[str] = (
|
|
158
163
|
fields
|
|
159
164
|
if fields is not None
|
|
160
|
-
else [f for f in model_cls._fields if f
|
|
165
|
+
else [f for f in model_cls._fields if f not in pk_filter]
|
|
161
166
|
)
|
|
162
167
|
for field_name in field_names:
|
|
163
|
-
if field_name
|
|
168
|
+
if field_name in pk_filter:
|
|
164
169
|
continue
|
|
165
170
|
if field_name not in model_cls._fields:
|
|
166
171
|
continue
|
|
@@ -182,7 +187,7 @@ async def update_instance(instance: Model, fields: Iterable[str] | None = None)
|
|
|
182
187
|
if version_field_name is not None and update_data:
|
|
183
188
|
update_data[version_field_name] = (current_version or 1) + 1
|
|
184
189
|
|
|
185
|
-
filters: dict[str, Any] =
|
|
190
|
+
filters: dict[str, Any] = dict(pk_filter)
|
|
186
191
|
if version_field_name is not None and current_version is not None:
|
|
187
192
|
filters[version_field_name] = current_version
|
|
188
193
|
|
|
@@ -198,11 +203,11 @@ async def update_instance(instance: Model, fields: Iterable[str] | None = None)
|
|
|
198
203
|
if version_field_name is not None:
|
|
199
204
|
raise OptimisticLockError(
|
|
200
205
|
model=model_cls,
|
|
201
|
-
pk=
|
|
206
|
+
pk=pk_filter if len(pk_filter) > 1 else next(iter(pk_filter.values())),
|
|
202
207
|
expected_version=current_version,
|
|
203
208
|
)
|
|
204
209
|
raise ValueError(
|
|
205
|
-
f"No rows were updated for {model_cls.__name__} with {
|
|
210
|
+
f"No rows were updated for {model_cls.__name__} with {pk_filter}"
|
|
206
211
|
)
|
|
207
212
|
|
|
208
213
|
if result.updated_rows:
|
|
@@ -80,12 +80,18 @@ def _merge_pending_into_models(model_cls: type, db_models: list, filters: dict[s
|
|
|
80
80
|
filters,
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
# Telemetry: count rows that came from Session state.
|
|
84
|
-
|
|
83
|
+
# Telemetry: count rows that came from Session state. Row identity is the
|
|
84
|
+
# FULL primary key — first-column-only would conflate composite-PK rows.
|
|
85
|
+
_pk_names = model_cls._meta.primary_keys or ["id"]
|
|
86
|
+
|
|
87
|
+
def _row_key(d: dict) -> tuple:
|
|
88
|
+
return tuple(d.get(pk) for pk in _pk_names)
|
|
89
|
+
|
|
90
|
+
db_pks = {_row_key(d) for d in db_dicts if isinstance(d, dict)}
|
|
85
91
|
merged_extra = sum(
|
|
86
92
|
1
|
|
87
93
|
for d in merged_dicts
|
|
88
|
-
if isinstance(d, dict) and d
|
|
94
|
+
if isinstance(d, dict) and _row_key(d) not in db_pks
|
|
89
95
|
)
|
|
90
96
|
if merged_extra:
|
|
91
97
|
try:
|
|
@@ -805,9 +811,10 @@ class QueryBuilder(Generic[ModelT]):
|
|
|
805
811
|
raise
|
|
806
812
|
|
|
807
813
|
async def last(self) -> ModelT | None:
|
|
808
|
-
"""Order by primary key
|
|
809
|
-
|
|
810
|
-
self.
|
|
814
|
+
"""Order by primary key (all columns, for composite PKs) descending
|
|
815
|
+
and fetch the first record."""
|
|
816
|
+
pk_names = self.model._meta.primary_keys or ["id"]
|
|
817
|
+
self.order_by(*(f"-{pk}" for pk in pk_names))
|
|
811
818
|
return await self.first()
|
|
812
819
|
|
|
813
820
|
async def get(self) -> ModelT:
|