matrx-orm 3.0.26__tar.gz → 3.0.28__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.0.26 → matrx_orm-3.0.28}/PKG-INFO +3 -1
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/README.md +2 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/pyproject.toml +1 -1
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/__init__.py +2 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/adapters/__init__.py +1 -0
- matrx_orm-3.0.28/src/matrx_orm/adapters/postgrest_client_adapter.py +392 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/base.py +18 -9
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/extended.py +35 -18
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/relations.py +12 -10
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/query/executor.py +72 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/schema.py +27 -12
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/.arman/pending/versioning/INITIAL.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/.env.example +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/.github/workflows/publish.yml +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/.gitignore +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/.python-version +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/.vscode/settings.json +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/CLAUDE.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/DESKTOP-API-INTEGRATION.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/MIGRATIONS.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/RESERVED_NAMES.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/TASKS-TEMPLATE.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/TASKS.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/TRANSACTION_POOLER_PLAN.MD +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/WRITE-QUEUE-UPGRADE.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/docs/migrations.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/extended_jan5_backup.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/main.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/release.sh +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/scripts/publish.sh +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/scripts/release.sh +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/py.typed +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/state.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/src/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/conftest.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.0.26 → matrx_orm-3.0.28}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.28
|
|
4
4
|
Summary: Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching. Works with any PostgreSQL database.
|
|
5
5
|
Project-URL: Homepage, https://github.com/armanisadeghi/matrx-orm
|
|
6
6
|
Project-URL: Repository, https://github.com/armanisadeghi/matrx-orm
|
|
@@ -1838,6 +1838,8 @@ When you run the schema builder against a database that has `vector(n)` columns,
|
|
|
1838
1838
|
|
|
1839
1839
|
| Version | Highlights |
|
|
1840
1840
|
|---|---|
|
|
1841
|
+
| **v3.0.28** | Patch release |
|
|
1842
|
+
| **v3.0.27** | Patch release |
|
|
1841
1843
|
| **v3.0.26** | Patch release |
|
|
1842
1844
|
| **v3.0.25** | Patch release |
|
|
1843
1845
|
| **v3.0.24** | Patch release |
|
|
@@ -1795,6 +1795,8 @@ When you run the schema builder against a database that has `vector(n)` columns,
|
|
|
1795
1795
|
|
|
1796
1796
|
| Version | Highlights |
|
|
1797
1797
|
|---|---|
|
|
1798
|
+
| **v3.0.28** | Patch release |
|
|
1799
|
+
| **v3.0.27** | Patch release |
|
|
1798
1800
|
| **v3.0.26** | Patch release |
|
|
1799
1801
|
| **v3.0.25** | Patch release |
|
|
1800
1802
|
| **v3.0.24** | Patch release |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.0.
|
|
3
|
+
version = "3.0.28"
|
|
4
4
|
description = "Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching. Works with any PostgreSQL database."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -151,6 +151,7 @@ from .core.signals import (
|
|
|
151
151
|
from .core.transaction import transaction
|
|
152
152
|
from .adapters import AdapterRegistry, BaseAdapter, AsyncPostgreSQLAdapter
|
|
153
153
|
from .adapters.supabase_adapter import SupabaseAdapter
|
|
154
|
+
from .adapters.postgrest_client_adapter import PostgRESTClientAdapter
|
|
154
155
|
from .exceptions import (
|
|
155
156
|
ORMException,
|
|
156
157
|
ValidationError,
|
|
@@ -358,6 +359,7 @@ __all__ = [
|
|
|
358
359
|
"BaseAdapter",
|
|
359
360
|
"AsyncPostgreSQLAdapter",
|
|
360
361
|
"SupabaseAdapter",
|
|
362
|
+
"PostgRESTClientAdapter",
|
|
361
363
|
# ── Cache ──────────────────────────────────────────────────────────────────
|
|
362
364
|
"CachePolicy",
|
|
363
365
|
"StateManager",
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""PostgREST client adapter — routes all ORM queries through Supabase PostgREST.
|
|
2
|
+
|
|
3
|
+
This adapter allows the full BaseManager / QueryBuilder / QueryExecutor stack
|
|
4
|
+
to work transparently against a Supabase project using only the publishable
|
|
5
|
+
anon key + the user's JWT. No asyncpg connection is opened; every operation
|
|
6
|
+
goes through the Supabase REST API, which enforces Row-Level Security
|
|
7
|
+
automatically.
|
|
8
|
+
|
|
9
|
+
Typical setup (called by matrx_ai.initialize(client_mode=True, ...))::
|
|
10
|
+
|
|
11
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
12
|
+
from matrx_orm.adapters.postgrest_client_adapter import PostgRESTClientAdapter
|
|
13
|
+
from matrx_orm.client import SupabaseClientConfig, SupabaseAuth
|
|
14
|
+
|
|
15
|
+
config = SupabaseClientConfig(url="https://abc.supabase.co", anon_key="eyJ...")
|
|
16
|
+
auth = SupabaseAuth(config)
|
|
17
|
+
AdapterRegistry.register("supabase_automation_matrix",
|
|
18
|
+
PostgRESTClientAdapter(config, auth))
|
|
19
|
+
|
|
20
|
+
After that, every ``Model.filter().all()``, ``BaseManager.load_items()``,
|
|
21
|
+
``create_item()``, ``update_item()``, etc. routes through PostgREST.
|
|
22
|
+
|
|
23
|
+
Limitations
|
|
24
|
+
-----------
|
|
25
|
+
- Raw SQL (``Model.raw()``, ``Model.raw_sql()``) is **not supported** — PostgREST
|
|
26
|
+
does not accept arbitrary SQL. Those methods will raise ``NotImplementedError``
|
|
27
|
+
when this adapter is active. Use ``SupabaseManager.rpc()`` for stored
|
|
28
|
+
procedures instead.
|
|
29
|
+
- M2M junction-table writes (``Model.ensure_m2m_tables()``) are also not
|
|
30
|
+
supported in client mode — schema DDL cannot be issued via PostgREST.
|
|
31
|
+
- Transactions (``get_connection()`` context manager) are not supported. Each
|
|
32
|
+
PostgREST request is its own implicit transaction.
|
|
33
|
+
- ``select_related`` (SQL JOINs) fall back to PostgREST embedded resource
|
|
34
|
+
notation when possible; complex JOINs will degrade gracefully to a
|
|
35
|
+
non-joined result.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
from contextlib import asynccontextmanager
|
|
42
|
+
from typing import Any, AsyncIterator
|
|
43
|
+
|
|
44
|
+
from matrx_orm.adapters.base_adapter import BaseAdapter
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger("matrx_orm.adapters.postgrest_client")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PostgRESTClientAdapter(BaseAdapter):
|
|
50
|
+
"""BaseAdapter implementation that speaks to Supabase via PostgREST HTTP.
|
|
51
|
+
|
|
52
|
+
Two construction patterns are supported:
|
|
53
|
+
|
|
54
|
+
**Pattern 1 — with SupabaseAuth (standalone client-side use)**::
|
|
55
|
+
|
|
56
|
+
from matrx_orm.client import SupabaseClientConfig, SupabaseAuth
|
|
57
|
+
from matrx_orm.adapters.postgrest_client_adapter import PostgRESTClientAdapter
|
|
58
|
+
|
|
59
|
+
config = SupabaseClientConfig(url="https://abc.supabase.co", anon_key="eyJ...")
|
|
60
|
+
auth = SupabaseAuth(config)
|
|
61
|
+
adapter = PostgRESTClientAdapter(config=config, auth=auth)
|
|
62
|
+
|
|
63
|
+
**Pattern 2 — with a get_jwt callable (matrx_ai client mode)**::
|
|
64
|
+
|
|
65
|
+
adapter = PostgRESTClientAdapter(
|
|
66
|
+
url="https://abc.supabase.co",
|
|
67
|
+
anon_key="eyJ...",
|
|
68
|
+
get_jwt=lambda: current_token, # sync OR async callable
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
config:
|
|
74
|
+
``SupabaseClientConfig`` **or** ``None``. When ``None``, ``url`` and
|
|
75
|
+
``anon_key`` must be provided and a minimal config is built internally.
|
|
76
|
+
auth:
|
|
77
|
+
``SupabaseAuth`` instance, **or** ``None`` when ``get_jwt`` is provided.
|
|
78
|
+
url:
|
|
79
|
+
Supabase project URL. Only used when ``config`` is ``None``.
|
|
80
|
+
anon_key:
|
|
81
|
+
Supabase publishable anon key. Only used when ``config`` is ``None``.
|
|
82
|
+
get_jwt:
|
|
83
|
+
Sync or async callable (no arguments) that returns the current user's
|
|
84
|
+
JWT string, or ``None`` / empty string when unauthenticated. When
|
|
85
|
+
provided, ``auth`` is ignored for token resolution. Falls back to
|
|
86
|
+
``anon_key`` when the callable returns ``None`` or raises.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
config: Any = None,
|
|
92
|
+
auth: Any = None,
|
|
93
|
+
*,
|
|
94
|
+
url: str = "",
|
|
95
|
+
anon_key: str = "",
|
|
96
|
+
get_jwt: Any = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
from matrx_orm.client.postgrest import PostgRESTClient
|
|
99
|
+
from matrx_orm.client.supabase_config import SupabaseClientConfig
|
|
100
|
+
|
|
101
|
+
if config is None:
|
|
102
|
+
if not url or not anon_key:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"Either a SupabaseClientConfig must be passed as 'config', "
|
|
105
|
+
"or 'url' and 'anon_key' must both be provided."
|
|
106
|
+
)
|
|
107
|
+
config = SupabaseClientConfig(url=url, anon_key=anon_key)
|
|
108
|
+
|
|
109
|
+
self._config = config
|
|
110
|
+
self._auth = auth
|
|
111
|
+
self._get_jwt = get_jwt # sync or async callable, or None
|
|
112
|
+
self._client = PostgRESTClient(config)
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# Token helper
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
async def _token(self) -> str:
|
|
119
|
+
"""Return a valid JWT.
|
|
120
|
+
|
|
121
|
+
Resolution order:
|
|
122
|
+
1. ``get_jwt`` callable if provided (sync or async).
|
|
123
|
+
2. ``SupabaseAuth.ensure_valid_session()`` if auth provided.
|
|
124
|
+
3. Anon key fallback (works for public / RLS-unrestricted tables).
|
|
125
|
+
"""
|
|
126
|
+
import asyncio
|
|
127
|
+
|
|
128
|
+
if self._get_jwt is not None:
|
|
129
|
+
try:
|
|
130
|
+
result = self._get_jwt()
|
|
131
|
+
if asyncio.iscoroutine(result):
|
|
132
|
+
result = await result
|
|
133
|
+
if result:
|
|
134
|
+
return result
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
if self._auth is not None:
|
|
139
|
+
try:
|
|
140
|
+
session = await self._auth.ensure_valid_session()
|
|
141
|
+
if session and session.access_token:
|
|
142
|
+
return session.access_token
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
return self._config.anon_key
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# execute_query_dict — primary SELECT hook for QueryExecutor
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
async def execute_query_dict(
|
|
153
|
+
self,
|
|
154
|
+
query_dict: dict[str, Any],
|
|
155
|
+
) -> list[dict[str, Any]] | type[NotImplemented]:
|
|
156
|
+
"""Translate a QueryBuilder query dict to a PostgREST SELECT call.
|
|
157
|
+
|
|
158
|
+
This is called by ``QueryExecutor._execute()`` **before** the SQL path.
|
|
159
|
+
Returning a list (even empty) signals that PostgREST handled the query
|
|
160
|
+
and the executor should skip SQL entirely.
|
|
161
|
+
"""
|
|
162
|
+
table: str = query_dict.get("table", "")
|
|
163
|
+
if not table:
|
|
164
|
+
return NotImplemented
|
|
165
|
+
|
|
166
|
+
token = await self._token()
|
|
167
|
+
filters = self._translate_filters(query_dict.get("filters", {}))
|
|
168
|
+
order = self._translate_order(query_dict.get("order_by", []))
|
|
169
|
+
limit: int | None = query_dict.get("limit")
|
|
170
|
+
offset: int | None = query_dict.get("offset")
|
|
171
|
+
select_cols: list[str] | None = query_dict.get("select")
|
|
172
|
+
columns = ", ".join(select_cols) if select_cols else "*"
|
|
173
|
+
|
|
174
|
+
logger.debug(
|
|
175
|
+
"[PostgRESTClientAdapter] SELECT %s filters=%s limit=%s",
|
|
176
|
+
table, filters, limit,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
result = await self._client.select(
|
|
181
|
+
table,
|
|
182
|
+
token,
|
|
183
|
+
columns=columns,
|
|
184
|
+
filters=filters if filters else None,
|
|
185
|
+
order=order if order else None,
|
|
186
|
+
limit=limit,
|
|
187
|
+
offset=offset,
|
|
188
|
+
)
|
|
189
|
+
if isinstance(result, list):
|
|
190
|
+
return result
|
|
191
|
+
# single-object response (shouldn't happen on list queries)
|
|
192
|
+
return [result] if result else []
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
logger.error(
|
|
195
|
+
"[PostgRESTClientAdapter] SELECT failed for %s: %s", table, exc
|
|
196
|
+
)
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
# execute_write_dict — write hook for QueryExecutor DML
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
async def execute_write_dict(
|
|
204
|
+
self,
|
|
205
|
+
query_dict: dict[str, Any],
|
|
206
|
+
) -> list[dict[str, Any]] | type[NotImplemented]:
|
|
207
|
+
"""Translate a write query dict to the appropriate PostgREST call.
|
|
208
|
+
|
|
209
|
+
Called by ``QueryExecutor`` for INSERT / UPDATE / DELETE / UPSERT
|
|
210
|
+
before it falls through to the SQL path.
|
|
211
|
+
|
|
212
|
+
query_dict keys used:
|
|
213
|
+
- ``operation``: "insert" | "update" | "delete" | "upsert" | "bulk_insert"
|
|
214
|
+
| "bulk_upsert"
|
|
215
|
+
- ``table``: table name
|
|
216
|
+
- ``data``: dict (single row) or list of dicts (bulk)
|
|
217
|
+
- ``filters``: ORM filter dict (for UPDATE / DELETE)
|
|
218
|
+
- ``conflict_fields``: list[str] (for UPSERT)
|
|
219
|
+
- ``update_fields``: list[str] (for UPSERT, optional)
|
|
220
|
+
"""
|
|
221
|
+
operation: str = query_dict.get("operation", "")
|
|
222
|
+
table: str = query_dict.get("table", "")
|
|
223
|
+
if not table or not operation:
|
|
224
|
+
return NotImplemented
|
|
225
|
+
|
|
226
|
+
token = await self._token()
|
|
227
|
+
filters = self._translate_filters(query_dict.get("filters", {}))
|
|
228
|
+
data: dict | list = query_dict.get("data", {})
|
|
229
|
+
|
|
230
|
+
logger.debug(
|
|
231
|
+
"[PostgRESTClientAdapter] %s %s", operation.upper(), table
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if operation == "insert":
|
|
236
|
+
result = await self._client.insert(table, token, data)
|
|
237
|
+
return [result] if isinstance(result, dict) else (result or [])
|
|
238
|
+
|
|
239
|
+
if operation == "bulk_insert":
|
|
240
|
+
result = await self._client.insert(table, token, data)
|
|
241
|
+
return result if isinstance(result, list) else ([result] if result else [])
|
|
242
|
+
|
|
243
|
+
if operation == "upsert":
|
|
244
|
+
conflict_fields: list[str] = query_dict.get("conflict_fields", [])
|
|
245
|
+
on_conflict = ",".join(conflict_fields) if conflict_fields else "id"
|
|
246
|
+
result = await self._client.insert(
|
|
247
|
+
table, token, data,
|
|
248
|
+
on_conflict=on_conflict,
|
|
249
|
+
upsert=True,
|
|
250
|
+
)
|
|
251
|
+
return [result] if isinstance(result, dict) else (result or [])
|
|
252
|
+
|
|
253
|
+
if operation == "bulk_upsert":
|
|
254
|
+
conflict_fields = query_dict.get("conflict_fields", [])
|
|
255
|
+
on_conflict = ",".join(conflict_fields) if conflict_fields else "id"
|
|
256
|
+
result = await self._client.insert(
|
|
257
|
+
table, token, data,
|
|
258
|
+
on_conflict=on_conflict,
|
|
259
|
+
upsert=True,
|
|
260
|
+
)
|
|
261
|
+
return result if isinstance(result, list) else ([result] if result else [])
|
|
262
|
+
|
|
263
|
+
if operation == "update":
|
|
264
|
+
result = await self._client.update(
|
|
265
|
+
table, token, data,
|
|
266
|
+
filters=filters if filters else None,
|
|
267
|
+
)
|
|
268
|
+
return result if isinstance(result, list) else ([result] if result else [])
|
|
269
|
+
|
|
270
|
+
if operation == "delete":
|
|
271
|
+
if not filters:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"DELETE via PostgREST requires at least one filter. "
|
|
274
|
+
"Use explicit filter kwargs to identify which rows to delete."
|
|
275
|
+
)
|
|
276
|
+
result = await self._client.delete(
|
|
277
|
+
table, token,
|
|
278
|
+
filters=filters,
|
|
279
|
+
)
|
|
280
|
+
return result if isinstance(result, list) else ([result] if result else [])
|
|
281
|
+
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
logger.error(
|
|
284
|
+
"[PostgRESTClientAdapter] %s failed for %s: %s",
|
|
285
|
+
operation.upper(), table, exc,
|
|
286
|
+
)
|
|
287
|
+
raise
|
|
288
|
+
|
|
289
|
+
return NotImplemented
|
|
290
|
+
|
|
291
|
+
# ------------------------------------------------------------------
|
|
292
|
+
# execute_query / execute_write — SQL fallbacks (not supported)
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
async def execute_query(
|
|
296
|
+
self,
|
|
297
|
+
sql: str,
|
|
298
|
+
*args: Any,
|
|
299
|
+
timeout: float = 10.0,
|
|
300
|
+
) -> list[dict[str, Any]]:
|
|
301
|
+
"""Raw SQL is not supported via PostgREST.
|
|
302
|
+
|
|
303
|
+
This is called only when ``execute_query_dict`` returns ``NotImplemented``
|
|
304
|
+
(which shouldn't happen for normal ORM queries). If you reach this
|
|
305
|
+
method, the query uses a feature that PostgREST cannot represent —
|
|
306
|
+
complex CTEs, raw aggregations, pgvector similarity, etc.
|
|
307
|
+
|
|
308
|
+
Consider using ``SupabaseManager.rpc()`` with a PostgreSQL function
|
|
309
|
+
for these cases, or switch to a direct asyncpg adapter for server-side
|
|
310
|
+
deployments.
|
|
311
|
+
"""
|
|
312
|
+
raise NotImplementedError(
|
|
313
|
+
"Raw SQL queries are not supported in PostgREST (client) mode.\n"
|
|
314
|
+
"This query requires a direct database connection (asyncpg).\n"
|
|
315
|
+
f"SQL attempted: {sql[:200]!r}"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def execute_write(
|
|
319
|
+
self,
|
|
320
|
+
sql: str,
|
|
321
|
+
*args: Any,
|
|
322
|
+
timeout: float = 10.0,
|
|
323
|
+
max_retries: int = 2,
|
|
324
|
+
) -> list[dict[str, Any]]:
|
|
325
|
+
"""Raw SQL writes are not supported via PostgREST."""
|
|
326
|
+
raise NotImplementedError(
|
|
327
|
+
"Raw SQL writes are not supported in PostgREST (client) mode.\n"
|
|
328
|
+
"This write requires a direct database connection (asyncpg).\n"
|
|
329
|
+
f"SQL attempted: {sql[:200]!r}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# ------------------------------------------------------------------
|
|
333
|
+
# Connection management — not supported in client mode
|
|
334
|
+
# ------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
@asynccontextmanager
|
|
337
|
+
async def get_connection(self, timeout: float = 10.0) -> AsyncIterator[Any]:
|
|
338
|
+
"""Transactions are not supported via PostgREST."""
|
|
339
|
+
raise NotImplementedError(
|
|
340
|
+
"Direct connections and transactions are not available in PostgREST "
|
|
341
|
+
"(client) mode. Each PostgREST request is its own implicit transaction."
|
|
342
|
+
)
|
|
343
|
+
yield # pragma: no cover
|
|
344
|
+
|
|
345
|
+
def get_active_connection(self) -> Any | None:
|
|
346
|
+
"""No persistent connection in PostgREST mode."""
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
# Lifecycle
|
|
351
|
+
# ------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
async def close(self) -> None:
|
|
354
|
+
"""Nothing to close — PostgREST uses stateless HTTP."""
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
# ------------------------------------------------------------------
|
|
358
|
+
# Filter / order translation helpers
|
|
359
|
+
# ------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
def _translate_filters(
|
|
362
|
+
self, filters: dict[str, Any] | None
|
|
363
|
+
) -> dict[str, Any]:
|
|
364
|
+
"""Pass ORM filter dicts straight through.
|
|
365
|
+
|
|
366
|
+
``PostgRESTClient._build_query_params()`` already understands the ORM
|
|
367
|
+
``field__operator`` convention, so no translation is needed here.
|
|
368
|
+
The executor hands us the same filter dict the query builder built.
|
|
369
|
+
"""
|
|
370
|
+
if not filters:
|
|
371
|
+
return {}
|
|
372
|
+
cleaned: dict[str, Any] = {}
|
|
373
|
+
for key, value in filters.items():
|
|
374
|
+
# Skip None values — they mean "no filter"
|
|
375
|
+
if value is not None:
|
|
376
|
+
cleaned[key] = value
|
|
377
|
+
return cleaned
|
|
378
|
+
|
|
379
|
+
def _translate_order(self, order_by: list[str] | None) -> list[str] | None:
|
|
380
|
+
"""Pass order_by list straight through — same format as PostgRESTClient."""
|
|
381
|
+
return order_by if order_by else None
|
|
382
|
+
|
|
383
|
+
# ------------------------------------------------------------------
|
|
384
|
+
# Repr
|
|
385
|
+
# ------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
def __repr__(self) -> str:
|
|
388
|
+
return (
|
|
389
|
+
f"PostgRESTClientAdapter("
|
|
390
|
+
f"url={getattr(self._config, 'url', '?')!r}, "
|
|
391
|
+
f"auth={'get_jwt' if self._get_jwt else ('SupabaseAuth' if self._auth else 'anon_key')})"
|
|
392
|
+
)
|
|
@@ -703,10 +703,12 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
703
703
|
Usage::
|
|
704
704
|
|
|
705
705
|
users = await User.raw("SELECT * FROM users WHERE age > $1", 18)
|
|
706
|
-
"""
|
|
707
|
-
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
708
706
|
|
|
709
|
-
|
|
707
|
+
Not supported in PostgREST (client) mode — raises ``NotImplementedError``.
|
|
708
|
+
"""
|
|
709
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
710
|
+
adapter = AdapterRegistry.get(cls.get_database_name())
|
|
711
|
+
results = await adapter.execute_query(sql, *params)
|
|
710
712
|
instances = []
|
|
711
713
|
for row in results:
|
|
712
714
|
try:
|
|
@@ -724,10 +726,12 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
724
726
|
Usage::
|
|
725
727
|
|
|
726
728
|
rows = await User.raw_sql("SELECT count(*) as cnt, role FROM users GROUP BY role")
|
|
727
|
-
"""
|
|
728
|
-
from matrx_orm.core.async_db_manager import AsyncDatabaseManager
|
|
729
729
|
|
|
730
|
-
|
|
730
|
+
Not supported in PostgREST (client) mode — raises ``NotImplementedError``.
|
|
731
|
+
"""
|
|
732
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
733
|
+
adapter = AdapterRegistry.get(cls.get_database_name())
|
|
734
|
+
results = await adapter.execute_query(sql, *params)
|
|
731
735
|
return [dict(row) for row in results]
|
|
732
736
|
|
|
733
737
|
@classmethod
|
|
@@ -1376,13 +1380,18 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1376
1380
|
|
|
1377
1381
|
@classmethod
|
|
1378
1382
|
async def ensure_m2m_tables(cls) -> None:
|
|
1379
|
-
"""Create any pending junction tables that don't yet exist in the database.
|
|
1383
|
+
"""Create any pending junction tables that don't yet exist in the database.
|
|
1384
|
+
|
|
1385
|
+
Not supported in PostgREST (client) mode — DDL cannot be issued via the
|
|
1386
|
+
REST API. Schema migrations must be applied server-side.
|
|
1387
|
+
"""
|
|
1380
1388
|
if not cls._pending_junction_tables:
|
|
1381
1389
|
return
|
|
1382
1390
|
|
|
1383
|
-
from matrx_orm.
|
|
1391
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
1384
1392
|
|
|
1385
1393
|
db_name = cls.get_database_name()
|
|
1394
|
+
adapter = AdapterRegistry.get(db_name)
|
|
1386
1395
|
for jt in cls._pending_junction_tables:
|
|
1387
1396
|
sql = (
|
|
1388
1397
|
f"CREATE TABLE IF NOT EXISTS {jt['junction_table']} ("
|
|
@@ -1391,7 +1400,7 @@ class Model(RuntimeMixin, metaclass=ModelMeta):
|
|
|
1391
1400
|
f"PRIMARY KEY ({jt['source_column']}, {jt['target_column']})"
|
|
1392
1401
|
f")"
|
|
1393
1402
|
)
|
|
1394
|
-
await
|
|
1403
|
+
await adapter.execute_query(sql)
|
|
1395
1404
|
|
|
1396
1405
|
@classmethod
|
|
1397
1406
|
async def get_many(cls, **kwargs: Any) -> list[Self]:
|
|
@@ -57,25 +57,42 @@ class BaseManager(Generic[ModelT]):
|
|
|
57
57
|
def _initialize_manager(self) -> None:
|
|
58
58
|
"""Initialize the manager and trigger auto-fetch if configured.
|
|
59
59
|
|
|
60
|
-
Auto-fetch
|
|
61
|
-
|
|
60
|
+
Auto-fetch is skipped when no database configuration or adapter has been
|
|
61
|
+
registered yet (e.g. in client/PostgREST mode where asyncpg is not used
|
|
62
|
+
and the adapter is registered asynchronously after module import).
|
|
62
63
|
"""
|
|
63
|
-
if int(self.fetch_on_init_limit)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
if int(self.fetch_on_init_limit) <= 0:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Skip auto-fetch if no adapter/database config is registered — avoids
|
|
68
|
+
# DatabaseConfigError in client mode where asyncpg is not configured.
|
|
69
|
+
try:
|
|
70
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
71
|
+
from matrx_orm.core.config import registry as _db_registry
|
|
72
|
+
db_name = self.model.get_database_name() if hasattr(self.model, "get_database_name") else None
|
|
73
|
+
if db_name:
|
|
74
|
+
adapter_exists = AdapterRegistry.get_or_none(db_name) is not None
|
|
75
|
+
config_exists = db_name in _db_registry._configs
|
|
76
|
+
if not adapter_exists and not config_exists:
|
|
77
|
+
return
|
|
78
|
+
except Exception:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
asyncio.get_running_loop()
|
|
83
|
+
asyncio.create_task(coro=self._auto_fetch_on_init_async())
|
|
84
|
+
vcprint(
|
|
85
|
+
f"[{self.model.__name__}] Auto-fetching on init (async)",
|
|
86
|
+
verbose=info,
|
|
87
|
+
color="yellow",
|
|
88
|
+
)
|
|
89
|
+
except RuntimeError:
|
|
90
|
+
vcprint(
|
|
91
|
+
f"[{self.model.__name__}] Auto-fetching on init (sync)",
|
|
92
|
+
verbose=info,
|
|
93
|
+
color="yellow",
|
|
94
|
+
)
|
|
95
|
+
self._auto_fetch_on_init_sync()
|
|
79
96
|
|
|
80
97
|
def _validate_input_data(
|
|
81
98
|
self,
|
|
@@ -335,7 +335,7 @@ class ManyToManyReference:
|
|
|
335
335
|
|
|
336
336
|
async def fetch_related(self, instance: Model) -> list[Model]:
|
|
337
337
|
"""Single-query JOIN: junction table + target model in one round-trip."""
|
|
338
|
-
from matrx_orm.
|
|
338
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
339
339
|
from matrx_orm.exceptions import ORMException
|
|
340
340
|
|
|
341
341
|
pk_field = instance._meta.primary_keys[0]
|
|
@@ -345,6 +345,7 @@ class ManyToManyReference:
|
|
|
345
345
|
|
|
346
346
|
try:
|
|
347
347
|
db_name = await self._get_db_name(instance)
|
|
348
|
+
adapter = AdapterRegistry.get(db_name)
|
|
348
349
|
junction = self.qualified_junction_table
|
|
349
350
|
target_model = self.resolved_model
|
|
350
351
|
target_table = target_model._meta.qualified_table_name
|
|
@@ -357,7 +358,7 @@ class ManyToManyReference:
|
|
|
357
358
|
f"JOIN {junction} j ON j.{self.target_column} = t.{target_pk} "
|
|
358
359
|
f"WHERE j.{self.source_column} = $1"
|
|
359
360
|
)
|
|
360
|
-
rows = await
|
|
361
|
+
rows = await adapter.execute_query(sql, source_value)
|
|
361
362
|
|
|
362
363
|
if not rows:
|
|
363
364
|
return []
|
|
@@ -372,7 +373,7 @@ class ManyToManyReference:
|
|
|
372
373
|
if not target_ids:
|
|
373
374
|
return 0
|
|
374
375
|
|
|
375
|
-
from matrx_orm.
|
|
376
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
376
377
|
from matrx_orm.exceptions import ORMException
|
|
377
378
|
|
|
378
379
|
pk_field = instance._meta.primary_keys[0]
|
|
@@ -380,6 +381,7 @@ class ManyToManyReference:
|
|
|
380
381
|
|
|
381
382
|
try:
|
|
382
383
|
db_name = await self._get_db_name(instance)
|
|
384
|
+
adapter = AdapterRegistry.get(db_name)
|
|
383
385
|
junction = self.qualified_junction_table
|
|
384
386
|
|
|
385
387
|
placeholders = ", ".join(
|
|
@@ -395,7 +397,7 @@ class ManyToManyReference:
|
|
|
395
397
|
for tid in target_ids:
|
|
396
398
|
params.extend([source_value, tid])
|
|
397
399
|
|
|
398
|
-
rows = await
|
|
400
|
+
rows = await adapter.execute_query(sql, *params)
|
|
399
401
|
return len(rows) if rows else len(target_ids)
|
|
400
402
|
except ORMException as e:
|
|
401
403
|
e.enrich(model=instance.__class__, operation="add_m2m", **self._enrich_context())
|
|
@@ -406,7 +408,7 @@ class ManyToManyReference:
|
|
|
406
408
|
if not target_ids:
|
|
407
409
|
return 0
|
|
408
410
|
|
|
409
|
-
from matrx_orm.
|
|
411
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
410
412
|
from matrx_orm.exceptions import ORMException
|
|
411
413
|
|
|
412
414
|
pk_field = instance._meta.primary_keys[0]
|
|
@@ -414,6 +416,7 @@ class ManyToManyReference:
|
|
|
414
416
|
|
|
415
417
|
try:
|
|
416
418
|
db_name = await self._get_db_name(instance)
|
|
419
|
+
adapter = AdapterRegistry.get(db_name)
|
|
417
420
|
junction = self.qualified_junction_table
|
|
418
421
|
|
|
419
422
|
sql = (
|
|
@@ -421,9 +424,7 @@ class ManyToManyReference:
|
|
|
421
424
|
f"WHERE {self.source_column} = $1 AND {self.target_column} = ANY($2) "
|
|
422
425
|
f"RETURNING *"
|
|
423
426
|
)
|
|
424
|
-
rows = await
|
|
425
|
-
db_name, sql, source_value, list(target_ids)
|
|
426
|
-
)
|
|
427
|
+
rows = await adapter.execute_query(sql, source_value, list(target_ids))
|
|
427
428
|
return len(rows) if rows else 0
|
|
428
429
|
except ORMException as e:
|
|
429
430
|
e.enrich(model=instance.__class__, operation="remove_m2m", **self._enrich_context())
|
|
@@ -437,7 +438,7 @@ class ManyToManyReference:
|
|
|
437
438
|
|
|
438
439
|
async def clear(self, instance: Model) -> int:
|
|
439
440
|
"""Remove all junction rows for this instance."""
|
|
440
|
-
from matrx_orm.
|
|
441
|
+
from matrx_orm.adapters import AdapterRegistry
|
|
441
442
|
from matrx_orm.exceptions import ORMException
|
|
442
443
|
|
|
443
444
|
pk_field = instance._meta.primary_keys[0]
|
|
@@ -445,10 +446,11 @@ class ManyToManyReference:
|
|
|
445
446
|
|
|
446
447
|
try:
|
|
447
448
|
db_name = await self._get_db_name(instance)
|
|
449
|
+
adapter = AdapterRegistry.get(db_name)
|
|
448
450
|
junction = self.qualified_junction_table
|
|
449
451
|
|
|
450
452
|
sql = f"DELETE FROM {junction} WHERE {self.source_column} = $1 RETURNING *"
|
|
451
|
-
rows = await
|
|
453
|
+
rows = await adapter.execute_query(sql, source_value)
|
|
452
454
|
return len(rows) if rows else 0
|
|
453
455
|
except ORMException as e:
|
|
454
456
|
e.enrich(model=instance.__class__, operation="clear_m2m", **self._enrich_context())
|