matrx-orm 3.1.20__tar.gz → 3.1.22__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.20 → matrx_orm-3.1.22}/PKG-INFO +12 -1
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/README.md +11 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/admin/router.py +72 -8
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/config.py +1 -1
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/columns.py +10 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/common.py +10 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/generator.py +1 -1
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/schema.py +6 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/views.py +35 -8
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/pyproject.toml +1 -1
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_admin_db_columns.py +79 -0
- matrx_orm-3.1.22/tests/schema/test_view_generation.py +72 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/.gitignore +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/AGENTS.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/CLAUDE.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/MODEL_API.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/docs/migrations.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/scripts/release.sh +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/conftest.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_execute_admin_sql.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_filter_raw.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.20 → matrx_orm-3.1.22}/tests/test_sync_twins_guard.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.22
|
|
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
|
|
@@ -113,6 +113,17 @@ The migration system tracks which branch a migration originated on and refuses t
|
|
|
113
113
|
|
|
114
114
|
### Mount the admin router
|
|
115
115
|
|
|
116
|
+
Rows with composite primary keys expose an opaque `__matrx_row_id` in list
|
|
117
|
+
responses and a matching virtual primary-key column descriptor. Pass that value
|
|
118
|
+
unchanged to row-detail, update, delete, and cache-eviction routes; the router
|
|
119
|
+
decodes it into the complete composite key. This also gives generated read-only
|
|
120
|
+
views collision-safe row navigation.
|
|
121
|
+
|
|
122
|
+
Generated views default to `id` only when they project it. Every other view must
|
|
123
|
+
declare bounded, unique columns in
|
|
124
|
+
`generate[].output.view_primary_keys`; generation fails instead of guessing a
|
|
125
|
+
first column or embedding an unbounded projected row in an identifier.
|
|
126
|
+
|
|
116
127
|
```python
|
|
117
128
|
from fastapi import FastAPI
|
|
118
129
|
from matrx_orm import admin_router
|
|
@@ -76,6 +76,17 @@ The migration system tracks which branch a migration originated on and refuses t
|
|
|
76
76
|
|
|
77
77
|
### Mount the admin router
|
|
78
78
|
|
|
79
|
+
Rows with composite primary keys expose an opaque `__matrx_row_id` in list
|
|
80
|
+
responses and a matching virtual primary-key column descriptor. Pass that value
|
|
81
|
+
unchanged to row-detail, update, delete, and cache-eviction routes; the router
|
|
82
|
+
decodes it into the complete composite key. This also gives generated read-only
|
|
83
|
+
views collision-safe row navigation.
|
|
84
|
+
|
|
85
|
+
Generated views default to `id` only when they project it. Every other view must
|
|
86
|
+
declare bounded, unique columns in
|
|
87
|
+
`generate[].output.view_primary_keys`; generation fails instead of guessing a
|
|
88
|
+
first column or embedding an unbounded projected row in an identifier.
|
|
89
|
+
|
|
79
90
|
```python
|
|
80
91
|
from fastapi import FastAPI
|
|
81
92
|
from matrx_orm import admin_router
|
|
@@ -24,6 +24,9 @@ Effective routes (relative to the mount prefix):
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
+
import base64
|
|
28
|
+
import binascii
|
|
29
|
+
import json
|
|
27
30
|
import logging
|
|
28
31
|
import math
|
|
29
32
|
import re
|
|
@@ -44,6 +47,8 @@ log = logging.getLogger(__name__)
|
|
|
44
47
|
|
|
45
48
|
# Must stay in sync with dashboard `ADMIN_LIST_MAX_PAGE_SIZE` (orm-proxy.ts).
|
|
46
49
|
ADMIN_LIST_MAX_PAGE_SIZE = 2000
|
|
50
|
+
COMPOSITE_ROW_ID_FIELD = "__matrx_row_id"
|
|
51
|
+
_COMPOSITE_ROW_ID_PREFIX = "cmp1_"
|
|
47
52
|
|
|
48
53
|
|
|
49
54
|
# ---------------------------------------------------------------------------
|
|
@@ -116,9 +121,59 @@ def _serialize(instance: Any) -> dict[str, Any]:
|
|
|
116
121
|
if key in complete:
|
|
117
122
|
continue
|
|
118
123
|
complete[key] = serialize_value(value) if callable(serialize_value) else value
|
|
124
|
+
primary_keys = list(getattr(meta, "primary_keys", ()) or ())
|
|
125
|
+
if len(primary_keys) > 1:
|
|
126
|
+
values = [complete.get(primary_key) for primary_key in primary_keys]
|
|
127
|
+
payload = json.dumps(
|
|
128
|
+
values,
|
|
129
|
+
separators=(",", ":"),
|
|
130
|
+
sort_keys=True,
|
|
131
|
+
default=str,
|
|
132
|
+
).encode("utf-8")
|
|
133
|
+
encoded = base64.urlsafe_b64encode(payload).decode("ascii").rstrip("=")
|
|
134
|
+
complete[COMPOSITE_ROW_ID_FIELD] = f"{_COMPOSITE_ROW_ID_PREFIX}{encoded}"
|
|
119
135
|
return complete
|
|
120
136
|
|
|
121
137
|
|
|
138
|
+
def _decode_row_id(model: type, row_id: str) -> str | dict[str, Any]:
|
|
139
|
+
primary_keys = list(model._meta.primary_keys or ())
|
|
140
|
+
if len(primary_keys) <= 1:
|
|
141
|
+
return row_id
|
|
142
|
+
if not row_id.startswith(_COMPOSITE_ROW_ID_PREFIX):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"{model.__name__} has a composite primary key; use the opaque row identity "
|
|
145
|
+
f"{COMPOSITE_ROW_ID_FIELD} returned by the list endpoint"
|
|
146
|
+
)
|
|
147
|
+
encoded = row_id.removeprefix(_COMPOSITE_ROW_ID_PREFIX)
|
|
148
|
+
try:
|
|
149
|
+
payload = base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4))
|
|
150
|
+
values = json.loads(payload)
|
|
151
|
+
except (binascii.Error, ValueError, json.JSONDecodeError) as exc:
|
|
152
|
+
raise ValueError("Invalid composite row id") from exc
|
|
153
|
+
if not isinstance(values, list) or len(values) != len(primary_keys):
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Invalid composite row id: expected {len(primary_keys)} key values"
|
|
156
|
+
)
|
|
157
|
+
return dict(zip(primary_keys, values))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _composite_row_id_descriptor() -> dict[str, Any]:
|
|
161
|
+
return {
|
|
162
|
+
"column_name": COMPOSITE_ROW_ID_FIELD,
|
|
163
|
+
"name": COMPOSITE_ROW_ID_FIELD,
|
|
164
|
+
"field_type": "TextField",
|
|
165
|
+
"data_type": "text",
|
|
166
|
+
"is_nullable": False,
|
|
167
|
+
"is_primary_key": True,
|
|
168
|
+
"is_unique": True,
|
|
169
|
+
"default": None,
|
|
170
|
+
"has_default": False,
|
|
171
|
+
"is_fk": False,
|
|
172
|
+
"is_array": False,
|
|
173
|
+
"description": "Opaque identity for a row with a composite primary key.",
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
122
177
|
def _safe_scalar(value: Any) -> str | int | float | bool | None:
|
|
123
178
|
"""Return value only if it is a JSON-safe scalar; drop everything else."""
|
|
124
179
|
if value is None or callable(value):
|
|
@@ -307,8 +362,17 @@ async def _admin_column_descriptors(model: type) -> list[dict[str, Any]]:
|
|
|
307
362
|
db_rows = await _fetch_db_columns(model)
|
|
308
363
|
orm_fields = model._meta.fields
|
|
309
364
|
if not db_rows:
|
|
310
|
-
|
|
311
|
-
|
|
365
|
+
columns = [_field_descriptor(name, field) for name, field in orm_fields.items()]
|
|
366
|
+
else:
|
|
367
|
+
columns = [
|
|
368
|
+
_db_row_to_descriptor(row, orm_fields.get(str(row["column_name"])))
|
|
369
|
+
for row in db_rows
|
|
370
|
+
]
|
|
371
|
+
if len(model._meta.primary_keys or ()) > 1:
|
|
372
|
+
# Put the opaque composite identity first so clients that select the
|
|
373
|
+
# first primary-key descriptor cannot accidentally use one component.
|
|
374
|
+
columns.insert(0, _composite_row_id_descriptor())
|
|
375
|
+
return columns
|
|
312
376
|
|
|
313
377
|
|
|
314
378
|
# Regex to extract values from CHECK constraint definitions like:
|
|
@@ -666,7 +730,7 @@ async def get_row(
|
|
|
666
730
|
log.info("get_row: table=%s id=%s", table_name, row_id)
|
|
667
731
|
|
|
668
732
|
try:
|
|
669
|
-
instance = await model.get_by_id(row_id, use_cache=False)
|
|
733
|
+
instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
|
|
670
734
|
return JSONResponse(content=_serialize(instance))
|
|
671
735
|
except Exception as exc:
|
|
672
736
|
log.error("get_row: %s/%s failed: %s", table_name, row_id, exc)
|
|
@@ -704,7 +768,7 @@ async def update_row(
|
|
|
704
768
|
log.info("update_row: table=%s id=%s fields=%s", table_name, row_id, list(body.keys()))
|
|
705
769
|
|
|
706
770
|
try:
|
|
707
|
-
instance = await model.get_by_id(row_id, use_cache=False)
|
|
771
|
+
instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
|
|
708
772
|
await instance.update(**body)
|
|
709
773
|
log.info("update_row: %s/%s updated", table_name, row_id)
|
|
710
774
|
return JSONResponse(content=_serialize(instance))
|
|
@@ -723,7 +787,7 @@ async def delete_row(
|
|
|
723
787
|
log.info("delete_row: table=%s id=%s", table_name, row_id)
|
|
724
788
|
|
|
725
789
|
try:
|
|
726
|
-
instance = await model.get_by_id(row_id, use_cache=False)
|
|
790
|
+
instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
|
|
727
791
|
await instance.delete()
|
|
728
792
|
log.info("delete_row: %s/%s deleted", table_name, row_id)
|
|
729
793
|
return JSONResponse(status_code=200, content={"deleted": True, "id": row_id})
|
|
@@ -760,7 +824,7 @@ async def bulk_delete_rows(
|
|
|
760
824
|
for raw_id in ids:
|
|
761
825
|
row_id = str(raw_id)
|
|
762
826
|
try:
|
|
763
|
-
instance = await model.get_by_id(row_id, use_cache=False)
|
|
827
|
+
instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
|
|
764
828
|
await instance.delete()
|
|
765
829
|
deleted.append(row_id)
|
|
766
830
|
except Exception as exc:
|
|
@@ -819,7 +883,7 @@ async def bulk_update_rows(
|
|
|
819
883
|
for raw_id in ids:
|
|
820
884
|
row_id = str(raw_id)
|
|
821
885
|
try:
|
|
822
|
-
instance = await model.get_by_id(row_id, use_cache=False)
|
|
886
|
+
instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
|
|
823
887
|
await instance.update(**data)
|
|
824
888
|
updated.append(row_id)
|
|
825
889
|
except Exception as exc:
|
|
@@ -892,7 +956,7 @@ async def evict_row_cache(
|
|
|
892
956
|
log.info("evict_row_cache: %s/%s", table_name, row_id)
|
|
893
957
|
|
|
894
958
|
try:
|
|
895
|
-
instance = await model.get_by_id(row_id, use_cache=False)
|
|
959
|
+
instance = await model.get_by_id(_decode_row_id(model, row_id), use_cache=False)
|
|
896
960
|
await StateManager.remove(model, instance)
|
|
897
961
|
return JSONResponse(content={"evicted": True, "table_name": table_name, "id": row_id})
|
|
898
962
|
except Exception as exc:
|
|
@@ -34,7 +34,7 @@ class DatabaseProjectConfig:
|
|
|
34
34
|
field_overrides: dict[str, Any] = field(default_factory=dict)
|
|
35
35
|
|
|
36
36
|
# Extra PostgreSQL schemas to introspect and include in schema builder output.
|
|
37
|
-
#
|
|
37
|
+
# Hosted projects may add private platform schemas as needed.
|
|
38
38
|
# These schemas live in the same database but outside the default "public" schema.
|
|
39
39
|
additional_schemas: list = field(default_factory=list)
|
|
40
40
|
|
|
@@ -588,6 +588,16 @@ class Column:
|
|
|
588
588
|
elif self.full_type in [
|
|
589
589
|
"tsvector",
|
|
590
590
|
"tsquery",
|
|
591
|
+
"regclass",
|
|
592
|
+
"regproc",
|
|
593
|
+
"regprocedure",
|
|
594
|
+
"regoper",
|
|
595
|
+
"regoperator",
|
|
596
|
+
"regtype",
|
|
597
|
+
"regrole",
|
|
598
|
+
"regnamespace",
|
|
599
|
+
"regconfig",
|
|
600
|
+
"regdictionary",
|
|
591
601
|
"geometry",
|
|
592
602
|
"geography",
|
|
593
603
|
"box",
|
|
@@ -78,6 +78,10 @@ class GenerationOutputOverrides:
|
|
|
78
78
|
exclude_views names of individual views to skip even when
|
|
79
79
|
include_views is True (e.g. a view with a column shape
|
|
80
80
|
the codegen can't represent). Default: none excluded.
|
|
81
|
+
view_primary_keys mapping of view name to bounded, unique projected
|
|
82
|
+
columns used as the read-only model identity. Views
|
|
83
|
+
projecting ``id`` default to ``[id]``; every other
|
|
84
|
+
generated view must declare this explicitly.
|
|
81
85
|
|
|
82
86
|
All fields default to current behavior; an absent yaml block is a no-op.
|
|
83
87
|
"""
|
|
@@ -90,6 +94,7 @@ class GenerationOutputOverrides:
|
|
|
90
94
|
emit_managers: bool = True
|
|
91
95
|
include_views: bool = True
|
|
92
96
|
exclude_views: list[str] | None = None
|
|
97
|
+
view_primary_keys: dict[str, list[str]] | None = None
|
|
93
98
|
|
|
94
99
|
@classmethod
|
|
95
100
|
def from_dict(cls, d: dict | None) -> GenerationOutputOverrides:
|
|
@@ -103,6 +108,11 @@ class GenerationOutputOverrides:
|
|
|
103
108
|
emit_managers=bool(d.get("emit_managers", True)),
|
|
104
109
|
include_views=bool(d.get("include_views", True)),
|
|
105
110
|
exclude_views=list(d.get("exclude_views") or []) or None,
|
|
111
|
+
view_primary_keys={
|
|
112
|
+
str(view_name): [str(column) for column in columns]
|
|
113
|
+
for view_name, columns in (d.get("view_primary_keys") or {}).items()
|
|
114
|
+
}
|
|
115
|
+
or None,
|
|
106
116
|
)
|
|
107
117
|
|
|
108
118
|
|
|
@@ -65,6 +65,7 @@ class Schema:
|
|
|
65
65
|
self._emit_managers = self.output_overrides.emit_managers
|
|
66
66
|
self._include_views = self.output_overrides.include_views
|
|
67
67
|
self._exclude_views = set(self.output_overrides.exclude_views or [])
|
|
68
|
+
self._view_primary_keys = self.output_overrides.view_primary_keys or {}
|
|
68
69
|
self.tables = {}
|
|
69
70
|
self.views = {}
|
|
70
71
|
self.model_export_names: list[str] = []
|
|
@@ -1054,7 +1055,12 @@ class Schema:
|
|
|
1054
1055
|
try:
|
|
1055
1056
|
view_src, view_model_name, view_field_types = view.to_python_model(
|
|
1056
1057
|
model_database_name=self._model_database_name,
|
|
1058
|
+
primary_keys=self._view_primary_keys.get(view_name),
|
|
1057
1059
|
)
|
|
1060
|
+
except ValueError:
|
|
1061
|
+
# Missing/invalid view identity is a generation contract
|
|
1062
|
+
# failure, never a relation to silently omit.
|
|
1063
|
+
raise
|
|
1058
1064
|
except Exception as e: # noqa: BLE001 — one bad view must not kill the run
|
|
1059
1065
|
vcprint(
|
|
1060
1066
|
f"[MATRX ORM] Skipping view '{self.name}.{view_name}' — "
|
|
@@ -90,9 +90,14 @@ class View:
|
|
|
90
90
|
except (IndexError, ValueError):
|
|
91
91
|
return "TextField()"
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
# pg_catalog identifier/reference types and enum-typed view columns are
|
|
94
|
+
# read as strings. Views are read-only, so generating writable EnumField
|
|
95
|
+
# metadata would add complexity without preserving any extra behavior.
|
|
96
|
+
if base_type == "name" or col.get("enum_labels"):
|
|
97
|
+
return "TextField()"
|
|
98
|
+
|
|
99
|
+
field_class = self.utils.sql_to_python_models_field.get(base_type)
|
|
100
|
+
if field_class is None:
|
|
96
101
|
vcprint(
|
|
97
102
|
f"[MATRX ORM] View {self.schema}.{self.name}: column "
|
|
98
103
|
f"'{col.get('name')}' has unmapped type {base_type!r} "
|
|
@@ -107,13 +112,16 @@ class View:
|
|
|
107
112
|
self,
|
|
108
113
|
*,
|
|
109
114
|
model_database_name: str | None = None,
|
|
115
|
+
primary_keys: list[str] | None = None,
|
|
110
116
|
) -> tuple[str, str, set[str]]:
|
|
111
117
|
"""Return (class_source, model_class_name, field_type_names) for a
|
|
112
118
|
read-only Model bound to this view.
|
|
113
119
|
|
|
114
|
-
Views have no Postgres PK
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
Views have no Postgres PK, so their identity must come from a bounded,
|
|
121
|
+
unique projection. ``id`` is the safe default when projected; otherwise
|
|
122
|
+
the generation config must explicitly name the grouping/identity columns.
|
|
123
|
+
This prevents first-column collisions and avoids embedding arbitrary row
|
|
124
|
+
payloads in dashboard URLs. No ForeignKey — views carry no FK metadata.
|
|
117
125
|
"""
|
|
118
126
|
columns = self.column_data or []
|
|
119
127
|
py_fields: list[str] = []
|
|
@@ -140,14 +148,33 @@ class View:
|
|
|
140
148
|
if not column_names:
|
|
141
149
|
raise ValueError(f"View {self.schema}.{self.name} has no columns to model.")
|
|
142
150
|
|
|
143
|
-
|
|
151
|
+
resolved_primary_keys = list(primary_keys or (["id"] if "id" in column_names else []))
|
|
152
|
+
if not resolved_primary_keys:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"View {self.schema}.{self.name} has no projected id; declare "
|
|
155
|
+
"output.view_primary_keys for a bounded unique identity."
|
|
156
|
+
)
|
|
157
|
+
if len(set(resolved_primary_keys)) != len(resolved_primary_keys):
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"View {self.schema}.{self.name} declares duplicate identity columns: "
|
|
160
|
+
f"{resolved_primary_keys}"
|
|
161
|
+
)
|
|
162
|
+
missing_primary_keys = [
|
|
163
|
+
column for column in resolved_primary_keys if column not in column_names
|
|
164
|
+
]
|
|
165
|
+
if missing_primary_keys:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"View {self.schema}.{self.name} identity columns are not projected: "
|
|
168
|
+
f"{missing_primary_keys}"
|
|
169
|
+
)
|
|
144
170
|
|
|
145
171
|
from matrx_orm.schema_builder.common import safe_model_class_name
|
|
146
172
|
|
|
147
173
|
model_class_name = safe_model_class_name(self.name, self.schema, self.utils.to_pascal_case)
|
|
148
174
|
db_name = model_database_name or self.database
|
|
149
175
|
|
|
150
|
-
py_fields.append(
|
|
176
|
+
py_fields.append("_read_only = True")
|
|
177
|
+
py_fields.append(f"_primary_keys = {resolved_primary_keys!r}")
|
|
151
178
|
py_fields.append(f'_table_name = "{self.name}"')
|
|
152
179
|
py_fields.append(f'_db_schema = "{self.schema}"')
|
|
153
180
|
py_fields.append(f'_database = "{db_name}"')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.22"
|
|
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" }
|
|
@@ -5,8 +5,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
from matrx_orm.admin.router import (
|
|
8
|
+
COMPOSITE_ROW_ID_FIELD,
|
|
8
9
|
_admin_column_descriptors,
|
|
9
10
|
_db_row_to_descriptor,
|
|
11
|
+
_decode_row_id,
|
|
10
12
|
_serialize,
|
|
11
13
|
)
|
|
12
14
|
|
|
@@ -25,6 +27,69 @@ def test_serialize_includes_extra_data() -> None:
|
|
|
25
27
|
assert out["role"] == "authenticated"
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
def test_composite_row_identity_round_trips() -> None:
|
|
31
|
+
meta = SimpleNamespace(
|
|
32
|
+
fields={"site_id": object(), "page_id": object()},
|
|
33
|
+
primary_keys=["site_id", "page_id"],
|
|
34
|
+
)
|
|
35
|
+
instance = SimpleNamespace(
|
|
36
|
+
to_dict=lambda: {"site_id": "site-1", "page_id": "page/2"},
|
|
37
|
+
_meta=meta,
|
|
38
|
+
_extra_data={},
|
|
39
|
+
)
|
|
40
|
+
model = SimpleNamespace(__name__="CostByPage", _meta=meta)
|
|
41
|
+
|
|
42
|
+
out = _serialize(instance)
|
|
43
|
+
|
|
44
|
+
assert out[COMPOSITE_ROW_ID_FIELD].startswith("cmp1_")
|
|
45
|
+
assert _decode_row_id(model, out[COMPOSITE_ROW_ID_FIELD]) == {
|
|
46
|
+
"site_id": "site-1",
|
|
47
|
+
"page_id": "page/2",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_composite_row_identity_rejects_scalar() -> None:
|
|
52
|
+
model = SimpleNamespace(
|
|
53
|
+
__name__="CostByPage",
|
|
54
|
+
_meta=SimpleNamespace(primary_keys=["site_id", "page_id"]),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with pytest.raises(ValueError, match="opaque row identity"):
|
|
58
|
+
_decode_row_id(model, "site-1")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_composite_row_identity_contains_only_declared_key_values() -> None:
|
|
62
|
+
metadata = {"private": "x" * 10_000}
|
|
63
|
+
meta = SimpleNamespace(
|
|
64
|
+
fields={"site_id": object(), "page_id": object(), "metadata": object()},
|
|
65
|
+
primary_keys=["site_id", "page_id"],
|
|
66
|
+
)
|
|
67
|
+
instance = SimpleNamespace(
|
|
68
|
+
to_dict=lambda: {"site_id": "site-1", "page_id": "page-2", "metadata": metadata},
|
|
69
|
+
_meta=meta,
|
|
70
|
+
_extra_data={},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
encoded = _serialize(instance)[COMPOSITE_ROW_ID_FIELD]
|
|
74
|
+
|
|
75
|
+
assert len(encoded) < 100
|
|
76
|
+
assert _decode_row_id(SimpleNamespace(__name__="View", _meta=meta), encoded) == {
|
|
77
|
+
"site_id": "site-1",
|
|
78
|
+
"page_id": "page-2",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize("row_id", ["cmp1_not-base64!", "cmp1_WzFd"])
|
|
83
|
+
def test_composite_row_identity_rejects_malformed_payload(row_id: str) -> None:
|
|
84
|
+
model = SimpleNamespace(
|
|
85
|
+
__name__="CostByPage",
|
|
86
|
+
_meta=SimpleNamespace(primary_keys=["site_id", "page_id"]),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
with pytest.raises(ValueError, match="Invalid composite row id"):
|
|
90
|
+
_decode_row_id(model, row_id)
|
|
91
|
+
|
|
92
|
+
|
|
28
93
|
def test_db_row_to_descriptor_without_orm_field() -> None:
|
|
29
94
|
row = {
|
|
30
95
|
"column_name": "last_sign_in_at",
|
|
@@ -84,3 +149,17 @@ async def test_admin_column_descriptors_prefers_live_db() -> None:
|
|
|
84
149
|
cols = await _admin_column_descriptors(model)
|
|
85
150
|
assert [c["column_name"] for c in cols] == ["id", "email", "created_at"]
|
|
86
151
|
assert cols[2]["field_type"] == "DateTimeField"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.mark.asyncio
|
|
155
|
+
async def test_admin_columns_expose_composite_row_identity_first() -> None:
|
|
156
|
+
model = MagicMock()
|
|
157
|
+
model._meta.fields = {"site_id": MagicMock(), "page_id": MagicMock()}
|
|
158
|
+
model._meta.primary_keys = ["site_id", "page_id"]
|
|
159
|
+
model._meta.table_name = "v_cost_by_page"
|
|
160
|
+
model.raw_sql = AsyncMock(return_value=[])
|
|
161
|
+
|
|
162
|
+
columns = await _admin_column_descriptors(model)
|
|
163
|
+
|
|
164
|
+
assert columns[0]["column_name"] == COMPOSITE_ROW_ID_FIELD
|
|
165
|
+
assert columns[0]["is_primary_key"] is True
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from matrx_orm.schema_builder.views import View
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _view(*, base_type: str = "text", multiple_columns: bool = False) -> View:
|
|
5
|
+
columns = [
|
|
6
|
+
{
|
|
7
|
+
"name": "id",
|
|
8
|
+
"base_type": base_type,
|
|
9
|
+
"full_type": base_type,
|
|
10
|
+
"nullable": False,
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
if multiple_columns:
|
|
14
|
+
columns.append(
|
|
15
|
+
{
|
|
16
|
+
"name": "item_id",
|
|
17
|
+
"base_type": "uuid",
|
|
18
|
+
"full_type": "uuid",
|
|
19
|
+
"nullable": False,
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
return View(
|
|
23
|
+
oid=1,
|
|
24
|
+
name="current_items",
|
|
25
|
+
type_="view",
|
|
26
|
+
schema="example",
|
|
27
|
+
database="test_database",
|
|
28
|
+
owner="postgres",
|
|
29
|
+
size_bytes=0,
|
|
30
|
+
description=None,
|
|
31
|
+
view_definition="select id from example.items",
|
|
32
|
+
column_data=columns,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_generated_view_model_is_structurally_read_only() -> None:
|
|
37
|
+
source, _, _ = _view().to_python_model()
|
|
38
|
+
|
|
39
|
+
assert "_read_only = True" in source
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_generated_view_uses_declared_composite_identity() -> None:
|
|
43
|
+
source, _, _ = _view(multiple_columns=True).to_python_model(
|
|
44
|
+
primary_keys=["id", "item_id"]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert "_primary_keys = ['id', 'item_id']" in source
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_generated_view_defaults_to_projected_id() -> None:
|
|
51
|
+
source, _, _ = _view(multiple_columns=True).to_python_model()
|
|
52
|
+
|
|
53
|
+
assert "_primary_keys = ['id']" in source
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_generated_view_without_id_requires_declared_identity() -> None:
|
|
57
|
+
view = _view()
|
|
58
|
+
view.column_data[0]["name"] = "group_name"
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
view.to_python_model()
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
assert "view_primary_keys" in str(exc)
|
|
64
|
+
else:
|
|
65
|
+
raise AssertionError("missing view identity must fail generation")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_catalog_name_view_column_maps_to_text_without_error() -> None:
|
|
69
|
+
source, _, field_types = _view(base_type="name").to_python_model()
|
|
70
|
+
|
|
71
|
+
assert "id = TextField(null=False)" in source
|
|
72
|
+
assert field_types == {"TextField"}
|
|
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
|