matrx-orm 3.1.2__tar.gz → 3.1.4__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.2 → matrx_orm-3.1.4}/CLAUDE.md +1 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/PKG-INFO +1 -1
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/async_db_manager.py +40 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/db_function.py +11 -7
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/expressions.py +4 -3
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/query/executor.py +53 -8
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/pyproject.toml +1 -1
- matrx_orm-3.1.4/tests/level1/test_connection_codecs.py +93 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_subquery_in_filter.py +16 -4
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/schema/test_composite_pk_fk_generation.py +1 -1
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/.gitignore +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/MODEL_API.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/docs/migrations.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/scripts/release.sh +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/conftest.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_execute_admin_sql.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_filter_raw.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_querybuilder_mutation_semantics.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.2 → matrx_orm-3.1.4}/tests/test_sql_param_casts.py +0 -0
|
@@ -20,6 +20,7 @@ matrx-orm is a standalone async Postgres ORM (`Model`, `QueryBuilder`, migration
|
|
|
20
20
|
- **`MatrxEntity`** (`matrx_orm/entity.py`) — **the platform entity contract as an ORM primitive.** A table registered in `platform.entity_types` generates as a `MatrxEntity` (everything else stays a plain `Model`) and gets the behavior instead of every consumer hand-wiring it: `await page.versions()` (not a query on `history.row_versions`), `await page.restore(3)` (not a `version_restore` RPC + a cache bust — `restore()` busts the cache itself), `Note.alive()` / `.dead()` / `.soft_delete()` / `.undelete()`, and `Page.capabilities()`. Capability flags (`_entity_token`, `_is_versioned`, `_has_soft_delete`, `_is_org_scoped`, `_rls_variant`) are **stamped by the generator from the LIVE DATABASE** (does the table really carry the `_version_capture` trigger? a `deleted_at` column?) — never from `entity_types`' own flags, which are wrong on 61 of its 242 rows. **The database is truth; a flag is a claim.** Using a capability an entity does not declare RAISES (`EntityCapabilityError`) and names the fix — it never returns silently-wrong data, and `is_deleted` refuses to answer from a column a partial fetch never loaded. Drift guard: `scripts/check_entity_drift.py` (loud, non-blocking, in `release.sh`).
|
|
21
21
|
- **Change tracking** — every instance records which columns were **loaded** (a SELECTed row, or the caller's kwargs) and which were **changed**. `save()` writes only those, so `.only("id","email")` + `save()` can never NULL the columns it never fetched (it used to — silent data loss). Mutable containers (jsonb/arrays) are always rewritten when loaded, because `obj.metadata["k"]=1` cannot be seen by `__setattr__`; **reassigning** (`obj.metadata = {...}`) is tracked precisely and is the sanctioned way. `Model._hydrate()` is the ONE row→model entry point.
|
|
22
22
|
- **Read-only models** (`_read_only = True`): a model that can be queried but NEVER written through the ORM — every write path raises `ReadOnlyModelError` (enforced at `QueryExecutor.insert/bulk_insert/upsert/update/delete`, so it covers all of `create`/`save`/`update`/`delete`/`update_where`/`delete_where`/`upsert`/`bulk_*`). Used for DB-managed tables the app must not mutate. The canonical **`auth.users`** model is baked into the schema generator (`get_string_user_model`) as the full, curated Supabase auth schema (safe columns only — no password/token fields), `_read_only`, bound per-database. Read a user's profile/metadata there; mutate users only through Supabase Auth (GoTrue).
|
|
23
|
+
- **Connection-level type codecs** (`async_db_manager._init_connection_default`): every pooled connection decodes **uuid → `str`**, **vector → `list[float]`**, and **json/jsonb → `dict`/`list`** at the driver boundary — the TYPE decides the decode, never the call path. So `call_function` scalar/rows, raw fetches, and field hydration all agree; a caller must **never `json.loads` a fetched json/jsonb value** (it's already decoded; the 2026-07-13 `build_agent_context` crash was a jsonb RPC fetched pre-codec as a raw string). Encoding: a pre-serialised JSON `str` passes through untouched; a native dict/list is dumped (`_encode_json`). Pinned by `tests/level1/test_connection_codecs.py`.
|
|
23
24
|
- **Query layer**: `QueryBuilder`, expressions (`F`, `Q`), window functions, CTEs, subqueries.
|
|
24
25
|
- **Migrations**: `MigrationDB`, `MigrationLoader`, `MigrationExecutor`, `makemigrations`, `migrate` — migrations declare explicit `dependencies` and apply in topological order (no multi-head/merge support yet).
|
|
25
26
|
- **Admin router** (FastAPI): `admin_router` exposes read/write endpoints for any registered model.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.4
|
|
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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import json
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
import traceback
|
|
@@ -143,6 +144,23 @@ def _decode_vector(value: str) -> list[float]:
|
|
|
143
144
|
return [float(x) for x in inner.split(",")]
|
|
144
145
|
|
|
145
146
|
|
|
147
|
+
def _encode_json(value: Any) -> str:
|
|
148
|
+
"""Encode a Python value bound to a ``json``/``jsonb`` parameter.
|
|
149
|
+
|
|
150
|
+
A ``str`` passes through untouched — every existing ORM write path
|
|
151
|
+
(``JSONField.get_db_prep_value``, ``JsonbMerge``, ``call_function``'s
|
|
152
|
+
``_bind_args``) pre-serialises to a JSON string, and asyncpg's built-in
|
|
153
|
+
codec (which this replaces) also required ``str``. Anything else (dict,
|
|
154
|
+
list, int, ...) is serialised here with the same coercion hook the field
|
|
155
|
+
layer uses, so binding a native container directly now also works.
|
|
156
|
+
"""
|
|
157
|
+
if isinstance(value, str):
|
|
158
|
+
return value
|
|
159
|
+
from matrx_orm.core.fields import JSONBField
|
|
160
|
+
|
|
161
|
+
return json.dumps(value, default=JSONBField._json_default)
|
|
162
|
+
|
|
163
|
+
|
|
146
164
|
async def _init_connection_default(conn: asyncpg.Connection) -> None:
|
|
147
165
|
"""Register custom type codecs on each new connection.
|
|
148
166
|
|
|
@@ -166,6 +184,28 @@ async def _init_connection_default(conn: asyncpg.Connection) -> None:
|
|
|
166
184
|
format="text",
|
|
167
185
|
)
|
|
168
186
|
|
|
187
|
+
# json/jsonb → dict/list (decode) and str-or-native → json text (encode).
|
|
188
|
+
#
|
|
189
|
+
# Without this codec asyncpg hands json/jsonb values back as RAW JSON
|
|
190
|
+
# STRINGS. The field layer papered over that (``JSONField.to_python`` does
|
|
191
|
+
# ``json.loads``), but every fetch path that bypasses field hydration —
|
|
192
|
+
# ``call_function`` scalar/rows, ``field=`` dereference, sql_executor rows
|
|
193
|
+
# — returned a ``str`` where the caller expected a ``dict``. That is
|
|
194
|
+
# exactly the 2026-07-13 ``build_agent_context`` crash ("'str' object has
|
|
195
|
+
# no attribute 'get'"): a jsonb-returning RPC moved from PostgREST (which
|
|
196
|
+
# decodes) to ``call_function`` (which didn't). Decoding at the driver
|
|
197
|
+
# boundary kills the whole class: the DRIVER knows the value is jsonb, so
|
|
198
|
+
# the type — not the call path — decides the decode. ``to_python`` already
|
|
199
|
+
# passes non-str values through, so field hydration is unaffected.
|
|
200
|
+
for _json_type in ("json", "jsonb"):
|
|
201
|
+
await conn.set_type_codec(
|
|
202
|
+
_json_type,
|
|
203
|
+
encoder=_encode_json,
|
|
204
|
+
decoder=json.loads,
|
|
205
|
+
schema="pg_catalog",
|
|
206
|
+
format="text",
|
|
207
|
+
)
|
|
208
|
+
|
|
169
209
|
# pgvector → list[float]
|
|
170
210
|
#
|
|
171
211
|
# The ``vector`` type lives in whichever schema the pgvector extension was
|
|
@@ -14,7 +14,11 @@ hand-write `SELECT schema.func(...)` itself — call this instead.
|
|
|
14
14
|
|
|
15
15
|
Three return modes:
|
|
16
16
|
* ``mode="scalar"`` (default) — the function returns one usable value
|
|
17
|
-
(text, integer, boolean, ...); we ``fetchval`` it.
|
|
17
|
+
(text, integer, boolean, ...); we ``fetchval`` it. A ``json``/``jsonb``
|
|
18
|
+
return value arrives as a decoded ``dict``/``list`` (never a raw JSON
|
|
19
|
+
string) — the pool's connection-level codec
|
|
20
|
+
(``async_db_manager._init_connection_default``) decodes at the driver
|
|
21
|
+
boundary. Callers must NOT ``json.loads`` a result.
|
|
18
22
|
* ``mode="none"`` — the function's return value is discarded (e.g. it
|
|
19
23
|
returns a composite/row type the caller doesn't need decoded). We
|
|
20
24
|
``execute`` the statement instead of ``fetchval``, which never attempts
|
|
@@ -37,12 +41,12 @@ composite; the single field still comes back scalar, through ``fetchval``.
|
|
|
37
41
|
alongside a row-set, is a caller bug, not a valid combination).
|
|
38
42
|
|
|
39
43
|
A ``dict``/``list`` positional argument is JSON-serialised and its
|
|
40
|
-
placeholder is cast explicitly (``$N::jsonb``)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ORM-generated ``UPDATE``
|
|
44
|
-
``matrx_orm.core.expressions.JsonbMerge``) — it is not a
|
|
45
|
-
how every jsonb-typed function parameter
|
|
44
|
+
placeholder is cast explicitly (``$N::jsonb``). The pool's connection-level
|
|
45
|
+
json codec (``async_db_manager._encode_json``) passes the pre-serialised
|
|
46
|
+
string through untouched, so this never double-encodes. This mirrors the
|
|
47
|
+
same trick ``JsonbMerge`` already uses for ORM-generated ``UPDATE``
|
|
48
|
+
statements (see ``matrx_orm.core.expressions.JsonbMerge``) — it is not a
|
|
49
|
+
special case, it is how every jsonb-typed function parameter is bound.
|
|
46
50
|
|
|
47
51
|
A genuine Postgres **array** parameter (``uuid[]``, ``text[]``, ...) is a
|
|
48
52
|
different shape from jsonb — asyncpg natively encodes a Python ``list`` as a
|
|
@@ -708,9 +708,10 @@ class JsonbMerge(Func):
|
|
|
708
708
|
sibling, expressible directly on ``Model.update_where`` (no manager
|
|
709
709
|
needed).
|
|
710
710
|
|
|
711
|
-
``value`` is JSON-serialised here (never left to the caller)
|
|
712
|
-
as a
|
|
713
|
-
|
|
711
|
+
``value`` is JSON-serialised here (never left to the caller) and binds
|
|
712
|
+
as a string param cast to ``jsonb``. The pool's connection-level json
|
|
713
|
+
codec (``async_db_manager._encode_json``) passes pre-serialised strings
|
|
714
|
+
through untouched, so this never double-encodes.
|
|
714
715
|
|
|
715
716
|
Usage::
|
|
716
717
|
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
+
from types import NotImplementedType
|
|
5
6
|
from typing import TYPE_CHECKING, Any
|
|
6
7
|
|
|
7
8
|
from matrx_utils import vcprint
|
|
@@ -27,6 +28,55 @@ _SKIP_PARAM = object()
|
|
|
27
28
|
|
|
28
29
|
_PARAM_RE = re.compile(r"\$(\d+)")
|
|
29
30
|
|
|
31
|
+
_TOP_LEVEL_WHERE_STOPPERS = (
|
|
32
|
+
"ORDER BY",
|
|
33
|
+
"GROUP BY",
|
|
34
|
+
"HAVING",
|
|
35
|
+
"LIMIT",
|
|
36
|
+
"OFFSET",
|
|
37
|
+
"FOR UPDATE",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _strip_top_level_trailing_clauses(sql_fragment: str) -> str:
|
|
42
|
+
"""Drop outer-query ORDER BY / LIMIT / … suffixes from a WHERE tail.
|
|
43
|
+
|
|
44
|
+
``_extract_where_clause`` peels the WHERE tail off a built SELECT. The tail
|
|
45
|
+
must not include the SELECT's own trailing clauses — but it MUST keep any
|
|
46
|
+
LIMIT/ORDER BY that lives *inside* a subquery (``id IN (SELECT … LIMIT n)``).
|
|
47
|
+
Naïve ``.split("LIMIT")`` truncates at the inner LIMIT and breaks bounded
|
|
48
|
+
batch updates (admin_persistence dismiss/resolve).
|
|
49
|
+
"""
|
|
50
|
+
if not sql_fragment:
|
|
51
|
+
return sql_fragment
|
|
52
|
+
depth = 0
|
|
53
|
+
cut_at: int | None = None
|
|
54
|
+
i = 0
|
|
55
|
+
n = len(sql_fragment)
|
|
56
|
+
while i < n:
|
|
57
|
+
ch = sql_fragment[i]
|
|
58
|
+
if ch == "(":
|
|
59
|
+
depth += 1
|
|
60
|
+
i += 1
|
|
61
|
+
continue
|
|
62
|
+
if ch == ")":
|
|
63
|
+
depth = max(0, depth - 1)
|
|
64
|
+
i += 1
|
|
65
|
+
continue
|
|
66
|
+
if depth == 0:
|
|
67
|
+
for stopper in _TOP_LEVEL_WHERE_STOPPERS:
|
|
68
|
+
slen = len(stopper)
|
|
69
|
+
if sql_fragment[i : i + slen] == stopper:
|
|
70
|
+
before = sql_fragment[i - 1] if i > 0 else " "
|
|
71
|
+
if before.isalnum() or before == "_":
|
|
72
|
+
break
|
|
73
|
+
cut_at = i if cut_at is None else min(cut_at, i)
|
|
74
|
+
break
|
|
75
|
+
i += 1
|
|
76
|
+
if cut_at is not None:
|
|
77
|
+
return sql_fragment[:cut_at].strip()
|
|
78
|
+
return sql_fragment.strip()
|
|
79
|
+
|
|
30
80
|
|
|
31
81
|
def _escape_like(value: Any) -> str:
|
|
32
82
|
"""Escape LIKE/ILIKE metacharacters so user input matches LITERALLY.
|
|
@@ -820,11 +870,7 @@ class QueryExecutor:
|
|
|
820
870
|
where_clause = ""
|
|
821
871
|
if "WHERE" in base_query:
|
|
822
872
|
after_where = base_query.split("WHERE", 1)[1].strip()
|
|
823
|
-
|
|
824
|
-
for stopper in ("ORDER BY", "GROUP BY", "HAVING", "LIMIT", "OFFSET", "FOR UPDATE"):
|
|
825
|
-
if stopper in after_where:
|
|
826
|
-
after_where = after_where.split(stopper)[0].strip()
|
|
827
|
-
where_clause = after_where
|
|
873
|
+
where_clause = _strip_top_level_trailing_clauses(after_where)
|
|
828
874
|
return where_clause, where_params
|
|
829
875
|
|
|
830
876
|
async def _execute(self) -> list[dict[str, Any]]:
|
|
@@ -904,15 +950,14 @@ class QueryExecutor:
|
|
|
904
950
|
|
|
905
951
|
async def _try_write_dict(
|
|
906
952
|
self, query: dict[str, Any]
|
|
907
|
-
) -> list[dict[str, Any]] |
|
|
953
|
+
) -> list[dict[str, Any]] | NotImplementedType:
|
|
908
954
|
"""Call execute_write_dict on the adapter.
|
|
909
955
|
|
|
910
956
|
Returns ``NotImplemented`` if the adapter does not override the hook
|
|
911
957
|
(default ``BaseAdapter`` behaviour) so callers can fall through to the
|
|
912
958
|
SQL path.
|
|
913
959
|
"""
|
|
914
|
-
|
|
915
|
-
return result # type: ignore[return-value]
|
|
960
|
+
return await self.db.execute_write_dict(query)
|
|
916
961
|
|
|
917
962
|
async def insert(self, query: dict[str, Any]) -> dict[str, Any]:
|
|
918
963
|
"""INSERT a single row, RETURNING *."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.4"
|
|
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,93 @@
|
|
|
1
|
+
"""Level 1: connection-level type codecs (``_init_connection_default``).
|
|
2
|
+
|
|
3
|
+
The pool registers codecs so driver-boundary decoding is uniform across EVERY
|
|
4
|
+
fetch path — model hydration, ``call_function``, sql_executor. The json/jsonb
|
|
5
|
+
codec exists because asyncpg's default returns jsonb as a RAW JSON STRING;
|
|
6
|
+
only the field layer decoded it, so any path that bypassed field hydration
|
|
7
|
+
(e.g. ``call_function(mode="scalar")`` on a jsonb-returning RPC) handed
|
|
8
|
+
callers a ``str`` where they expected a ``dict`` — the 2026-07-13
|
|
9
|
+
``build_agent_context`` crash. These tests pin the registration and the
|
|
10
|
+
encoder's str-pass-through contract so that class of bug stays extinct.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from unittest.mock import AsyncMock
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from matrx_orm.core.async_db_manager import (
|
|
21
|
+
_encode_json,
|
|
22
|
+
_init_connection_default,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _FakeConn:
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self.set_type_codec = AsyncMock()
|
|
29
|
+
# The vector-schema lookup in _init_connection_default; None = pgvector
|
|
30
|
+
# not installed, the only legitimate skip.
|
|
31
|
+
self.fetchval = AsyncMock(return_value=None)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestJsonCodecRegistration:
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_registers_json_and_jsonb_decoders(self) -> None:
|
|
37
|
+
conn = _FakeConn()
|
|
38
|
+
await _init_connection_default(conn) # type: ignore[arg-type]
|
|
39
|
+
registered = {
|
|
40
|
+
call.args[0]: call.kwargs for call in conn.set_type_codec.await_args_list
|
|
41
|
+
}
|
|
42
|
+
for type_name in ("json", "jsonb"):
|
|
43
|
+
assert type_name in registered, (
|
|
44
|
+
f"{type_name} codec not registered — jsonb values fetched outside "
|
|
45
|
+
f"field hydration (call_function scalar/rows) will come back as "
|
|
46
|
+
f"raw JSON strings again (the build_agent_context crash class)."
|
|
47
|
+
)
|
|
48
|
+
kwargs = registered[type_name]
|
|
49
|
+
assert kwargs["decoder"] is json.loads
|
|
50
|
+
assert kwargs["encoder"] is _encode_json
|
|
51
|
+
assert kwargs["schema"] == "pg_catalog"
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_uuid_codec_still_registered(self) -> None:
|
|
55
|
+
conn = _FakeConn()
|
|
56
|
+
await _init_connection_default(conn) # type: ignore[arg-type]
|
|
57
|
+
assert "uuid" in {c.args[0] for c in conn.set_type_codec.await_args_list}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestEncodeJson:
|
|
61
|
+
def test_str_passes_through_untouched(self) -> None:
|
|
62
|
+
# Every existing write path (JSONField.get_db_prep_value, JsonbMerge,
|
|
63
|
+
# call_function._bind_args) pre-serialises to str; double-encoding
|
|
64
|
+
# would corrupt every jsonb write in the platform.
|
|
65
|
+
assert _encode_json('{"a": 1}') == '{"a": 1}'
|
|
66
|
+
assert _encode_json("plain") == "plain"
|
|
67
|
+
|
|
68
|
+
def test_dict_and_list_serialised(self) -> None:
|
|
69
|
+
assert json.loads(_encode_json({"a": 1})) == {"a": 1}
|
|
70
|
+
assert json.loads(_encode_json([1, "b"])) == [1, "b"]
|
|
71
|
+
|
|
72
|
+
def test_non_native_values_degrade_via_field_hook(self) -> None:
|
|
73
|
+
# Same coercion hook the field layer uses (JSONBField._json_default):
|
|
74
|
+
# a weird value degrades instead of crashing the write.
|
|
75
|
+
class Weird:
|
|
76
|
+
__slots__ = ()
|
|
77
|
+
|
|
78
|
+
def __repr__(self) -> str:
|
|
79
|
+
return "<weird>"
|
|
80
|
+
|
|
81
|
+
# The exact degrade shape belongs to _json_default's own contract;
|
|
82
|
+
# what THIS codec guarantees is: never crash, always emit valid JSON.
|
|
83
|
+
out = _encode_json({"w": Weird()})
|
|
84
|
+
assert isinstance(json.loads(out), dict)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestDecodeContract:
|
|
88
|
+
def test_decoder_yields_native_containers(self) -> None:
|
|
89
|
+
# json.loads is the registered decoder — pin the shapes callers rely on.
|
|
90
|
+
assert json.loads('{"variables": {}}') == {"variables": {}}
|
|
91
|
+
assert json.loads("[1, 2]") == [1, 2]
|
|
92
|
+
assert json.loads('"scalar"') == "scalar"
|
|
93
|
+
assert json.loads("null") is None
|
|
@@ -51,10 +51,7 @@ class TestSubqueryInFilter:
|
|
|
51
51
|
def test_renders_in_not_equals(self):
|
|
52
52
|
Model_ = _make_model()
|
|
53
53
|
inner = (
|
|
54
|
-
Model_.filter(recovered_at__isnull=True)
|
|
55
|
-
.order_by("-failed_at")
|
|
56
|
-
.limit(50)
|
|
57
|
-
.select("id")
|
|
54
|
+
Model_.filter(recovered_at__isnull=True).order_by("-failed_at").limit(50).select("id")
|
|
58
55
|
)
|
|
59
56
|
|
|
60
57
|
qb = Model_.filter(id__in=Subquery(inner))
|
|
@@ -98,3 +95,18 @@ class TestSubqueryInFilter:
|
|
|
98
95
|
assert "recovered_at IS NULL" in cond
|
|
99
96
|
assert "table_target = $" in cond
|
|
100
97
|
assert "cx_message" in params
|
|
98
|
+
|
|
99
|
+
def test_extract_where_clause_preserves_inner_limit(self):
|
|
100
|
+
"""Bounded batch dismiss/resolve: update_where(id__in=Subquery(...))
|
|
101
|
+
must not truncate the inner SELECT at its LIMIT."""
|
|
102
|
+
Model_ = _make_model()
|
|
103
|
+
inner = (
|
|
104
|
+
Model_.filter(recovered_at__isnull=True).order_by("-failed_at").limit(50).select("id")
|
|
105
|
+
)
|
|
106
|
+
qb = Model_.filter(id__in=Subquery(inner))
|
|
107
|
+
executor = qb._get_executor()
|
|
108
|
+
where_clause, where_params = executor._extract_where_clause()
|
|
109
|
+
assert where_clause.endswith(")")
|
|
110
|
+
assert "LIMIT $" in where_clause
|
|
111
|
+
assert "ORDER BY failed_at DESC" in where_clause
|
|
112
|
+
assert where_params == [50]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Pins the generator behavior for composite primary keys whose columns are ALSO
|
|
2
2
|
# foreign keys (live shape: platform.association_types — PK (source_type,
|
|
3
3
|
# target_type), both FKs to platform.entity_types(token)). Regression context:
|
|
4
|
-
#
|
|
4
|
+
# FOUND_DEFECTS 2026-07-06 — a regenerate emitted AssociationTypes with no
|
|
5
5
|
# primary_key=True and every db.models import crashed at boot. Root cause was a
|
|
6
6
|
# PK-less live table; these tests pin BOTH layers: (1) FK+PK columns must emit
|
|
7
7
|
# primary_key=True, (2) a PK-less table must fail generation loudly instead of
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|