matrx-orm 3.1.48__tar.gz → 3.1.49__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.48 → matrx_orm-3.1.49}/.gitignore +2 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/CLAUDE.md +2 -1
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/PKG-INFO +1 -1
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/__init__.py +14 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/async_db_manager.py +43 -2
- matrx_orm-3.1.49/matrx_orm/core/one_database.py +210 -0
- matrx_orm-3.1.49/matrx_orm/platform_db.py +138 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/query/FEATURE.md +8 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/query/executor.py +6 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/secrets_battery/crypto.py +1 -1
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/pyproject.toml +1 -1
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_join_and_aggregate.py +8 -0
- matrx_orm-3.1.49/tests/test_one_database.py +105 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/AGENTS.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/MODEL_API.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/docs/migrations.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/FEATURE.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/scripts/release.sh +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/conftest.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.48 → matrx_orm-3.1.49}/tests/test_sync_twins_guard.py +0 -0
|
@@ -38,6 +38,8 @@ lib64/
|
|
|
38
38
|
# Re-allow them explicitly so frontend builds don't ship without their lib layer.
|
|
39
39
|
!apps/dashboard/src/lib/
|
|
40
40
|
!apps/dashboard/src/lib/**
|
|
41
|
+
!apps/dashboard/src/features/crawler/lib/
|
|
42
|
+
!apps/dashboard/src/features/crawler/lib/**
|
|
41
43
|
!apps/workflow-studio/src/lib/
|
|
42
44
|
!apps/workflow-studio/src/lib/**
|
|
43
45
|
parts/
|
|
@@ -20,6 +20,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
|
|
|
20
20
|
## What this package provides
|
|
21
21
|
|
|
22
22
|
- **Config / project registry** (`matrx_orm.core.config`): `DatabaseProjectConfig`, `register_database`, `register_database_from_env`, `get_database_config`, `get_manager_config`, `DatabaseRegistry`. A single process can run multiple named database projects concurrently.
|
|
23
|
+
- 🚨 **ONE CONNECTION, REQUIRED** (`matrx_orm.platform_db`): `register_platform_db(name, ...)` + `platform_connection_url()` — the ONLY sanctioned way any Matrx service or package turns env vars into a pool. One set of variables (`SUPABASE_MATRIX_*`), owned here; TLS is a deployment input (`SUPABASE_MATRIX_SSL`), never hardcoded; **RAISES** if it isn't given a database — no degraded mode. Every package's `bootstrap_db()` is one call to it. A package may absolutely run on its OWN Postgres — that is a change of VALUES, never a new variable name; what it may never have is a *second candidate* or a fallback chain. `register_database_from_env` is the low-level primitive underneath — **application and package code must not call it directly** (guard: `scripts/check_one_database.py`, blocking in release).
|
|
23
24
|
- **Core model layer**: `Model`, `BaseManager`, `BaseDTO`, `ModelView`, `model_registry`, plus 50+ field types (`CharField`, `IntegerField`, `ForeignKey`, `ManyToManyField`, `JSONField`, …).
|
|
24
25
|
- **`MatrxEntity`** (`matrx_orm/entity.py`) — **the platform entity contract as an ORM primitive.** A table registered in `platform.entity_types` generates as a `MatrxEntity` (everything else stays a plain `Model`) and gets the behavior instead of every consumer hand-wiring it: `await page.versions()` (not a query on `history.row_versions`), `await page.restore(3)` (not a `version_restore` RPC + a cache bust — `restore()` busts the cache itself), `Note.alive()` / `.dead()` / `.soft_delete()` / `.undelete()`, and `Page.capabilities()`. Capability flags (`_entity_token`, `_is_versioned`, `_has_soft_delete`, `_is_org_scoped`, `_rls_variant`) are **stamped by the generator from the LIVE DATABASE** (does the table really carry the `_version_capture` trigger? a `deleted_at` column?) — never from `entity_types`' own flags, which are wrong on 61 of its 242 rows. **The database is truth; a flag is a claim.** Using a capability an entity does not declare RAISES (`EntityCapabilityError`) and names the fix — it never returns silently-wrong data, and `is_deleted` refuses to answer from a column a partial fetch never loaded. Drift guard: `scripts/check_entity_drift.py` (loud, non-blocking, in `release.sh`).
|
|
25
26
|
- **Change tracking** — every instance records which columns were **loaded** (a SELECTed row, or the caller's kwargs) and which were **changed**. `save()` writes only those, so `.only("id","email")` + `save()` can never NULL the columns it never fetched (it used to — silent data loss). Mutable containers (jsonb/arrays) are always rewritten when loaded, because `obj.metadata["k"]=1` cannot be seen by `__setattr__`; **reassigning** (`obj.metadata = {...}`) is tracked precisely and is the sanctioned way. `Model._hydrate()` is the ONE row→model entry point.
|
|
@@ -72,7 +73,7 @@ register_database_from_env(
|
|
|
72
73
|
|
|
73
74
|
Inside this package:
|
|
74
75
|
|
|
75
|
-
- **Never** call `os.environ[...]` outside `config.py` and the `schema_builder` CLI. All env-var reads
|
|
76
|
+
- **Never** call `os.environ[...]` outside `config.py`, `platform_db.py` and the `schema_builder` CLI. All env-var reads go through `register_database_from_env` with an explicit prefix — and for a Matrx service that means `register_platform_database`, never a hand-picked prefix. **Never invent a new env var meaning "the database"** (`*_DATABASE_URL`, `MATRX_*_POSTGRES_*`): the name already exists and is `SUPABASE_MATRIX_*`.
|
|
76
77
|
- The schema-builder tools (`ADMIN_PYTHON_ROOT`, `ADMIN_TS_ROOT`, etc.) MUST accept the same values as function kwargs; env vars are only the fallback for CLI convenience.
|
|
77
78
|
- Transaction default (`MATRX_DEFAULT_DATABASE`) is acceptable but must be overrideable per-call.
|
|
78
79
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.49
|
|
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
|
|
@@ -35,8 +35,15 @@ from .core.connections import (
|
|
|
35
35
|
open_listener_connection,
|
|
36
36
|
)
|
|
37
37
|
from .entity import EntityCapabilityError, MatrxEntity
|
|
38
|
+
from .platform_db import (
|
|
39
|
+
PLATFORM_DB_ENV_OVERRIDES,
|
|
40
|
+
PLATFORM_DB_ENV_PREFIX,
|
|
41
|
+
platform_connection_url,
|
|
42
|
+
register_platform_db,
|
|
43
|
+
)
|
|
38
44
|
from .core.diagnostics import collect_pool_diagnostics, log_pool_diagnostics
|
|
39
45
|
from .core.introspection import introspect_columns, introspect_rls_policies, schema_exists
|
|
46
|
+
from .core.one_database import OneDatabaseReport, PoolIdentity, audit_one_database
|
|
40
47
|
from .core.listen import notify, ping
|
|
41
48
|
from .core.tls import resolve_asyncpg_ssl, resolve_asyncpg_ssl_from_dsn
|
|
42
49
|
from .operations.bulk_update_values import bulk_update_by_pk
|
|
@@ -333,6 +340,10 @@ __all__ = [
|
|
|
333
340
|
"DatabaseProjectConfig",
|
|
334
341
|
"register_database",
|
|
335
342
|
"register_database_from_env",
|
|
343
|
+
"register_platform_db",
|
|
344
|
+
"platform_connection_url",
|
|
345
|
+
"PLATFORM_DB_ENV_PREFIX",
|
|
346
|
+
"PLATFORM_DB_ENV_OVERRIDES",
|
|
336
347
|
"register_database_alias",
|
|
337
348
|
"resolve_database_name",
|
|
338
349
|
"is_database_registered",
|
|
@@ -358,6 +369,9 @@ __all__ = [
|
|
|
358
369
|
"EntityCapabilityError",
|
|
359
370
|
"ArrayArg",
|
|
360
371
|
# ── pg_catalog introspection (read-only) ───────────────────────────────────
|
|
372
|
+
"audit_one_database",
|
|
373
|
+
"OneDatabaseReport",
|
|
374
|
+
"PoolIdentity",
|
|
361
375
|
"introspect_columns",
|
|
362
376
|
"introspect_rls_policies",
|
|
363
377
|
"schema_exists",
|
|
@@ -285,11 +285,37 @@ _init_vector_codec = _init_connection_default
|
|
|
285
285
|
|
|
286
286
|
@dataclass(frozen=True, slots=True)
|
|
287
287
|
class ConnectionIdentity:
|
|
288
|
-
"""The role/database a pool is actually connected as.
|
|
288
|
+
"""The role/database a pool is actually connected as — asked, never assumed.
|
|
289
|
+
|
|
290
|
+
``server_addr`` / ``server_port`` / ``cluster_id`` answer the *physical*
|
|
291
|
+
question "am I talking to the same Postgres as everyone else?", which env
|
|
292
|
+
vars cannot answer (a var set on one server and not another is invisible to
|
|
293
|
+
any static check). ``physical_key`` is the comparable form: two pools with
|
|
294
|
+
different keys are two different databases, full stop.
|
|
295
|
+
|
|
296
|
+
The physical fields are best-effort — a Unix socket has no
|
|
297
|
+
``inet_server_addr()`` and ``pg_control_system()`` needs ``pg_monitor`` — so
|
|
298
|
+
they are optional and the key degrades gracefully rather than lying.
|
|
299
|
+
"""
|
|
289
300
|
|
|
290
301
|
current_user: str
|
|
291
302
|
session_user: str
|
|
292
303
|
database: str
|
|
304
|
+
server_addr: str | None = None
|
|
305
|
+
server_port: int | None = None
|
|
306
|
+
cluster_id: str | None = None
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def physical_key(self) -> str:
|
|
310
|
+
"""The comparable identity of the physical database behind this pool.
|
|
311
|
+
|
|
312
|
+
Prefers the cluster's ``system_identifier`` (unique per Postgres data
|
|
313
|
+
directory) and falls back to the server address/port. The database name
|
|
314
|
+
is always part of the key: two logical databases in one cluster are still
|
|
315
|
+
two databases.
|
|
316
|
+
"""
|
|
317
|
+
cluster = self.cluster_id or f"{self.server_addr or '?'}:{self.server_port or '?'}"
|
|
318
|
+
return f"{cluster}/{self.database}"
|
|
293
319
|
|
|
294
320
|
|
|
295
321
|
class AsyncDatabaseManager:
|
|
@@ -518,12 +544,27 @@ class AsyncDatabaseManager:
|
|
|
518
544
|
async with pool.acquire() as conn:
|
|
519
545
|
row = await conn.fetchrow(
|
|
520
546
|
"SELECT current_user AS current_user, session_user AS session_user, "
|
|
521
|
-
"current_database() AS database"
|
|
547
|
+
"current_database() AS database, "
|
|
548
|
+
"host(inet_server_addr()) AS server_addr, "
|
|
549
|
+
"inet_server_port() AS server_port"
|
|
522
550
|
)
|
|
551
|
+
# The cluster's system_identifier is the strongest physical-identity
|
|
552
|
+
# signal (unique per data directory) but needs pg_monitor, so it is
|
|
553
|
+
# best-effort: a NULL here just degrades physical_key to addr:port.
|
|
554
|
+
cluster_id: str | None = None
|
|
555
|
+
try:
|
|
556
|
+
cluster_id = await conn.fetchval(
|
|
557
|
+
"SELECT system_identifier::text FROM pg_control_system()"
|
|
558
|
+
)
|
|
559
|
+
except Exception: # noqa: BLE001 — insufficient privilege is normal
|
|
560
|
+
cluster_id = None
|
|
523
561
|
return ConnectionIdentity(
|
|
524
562
|
current_user=row["current_user"],
|
|
525
563
|
session_user=row["session_user"],
|
|
526
564
|
database=row["database"],
|
|
565
|
+
server_addr=row["server_addr"],
|
|
566
|
+
server_port=row["server_port"],
|
|
567
|
+
cluster_id=cluster_id,
|
|
527
568
|
)
|
|
528
569
|
|
|
529
570
|
@classmethod
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""ONE DATABASE guard (runtime layer) — do the registered pools actually agree?
|
|
2
|
+
|
|
3
|
+
There is ONE database. The static twin (``scripts/check_one_database.py`` in the
|
|
4
|
+
host repo) proves the CODE resolves only ``SUPABASE_MATRIX_*``; it cannot see an
|
|
5
|
+
env var set on a server, a pool registered by a caller, or a hostname that points
|
|
6
|
+
somewhere unexpected. This layer asks the connections themselves and compares
|
|
7
|
+
their PHYSICAL identity — cluster ``system_identifier`` (or server address/port)
|
|
8
|
+
plus database name.
|
|
9
|
+
|
|
10
|
+
WHY (verified 2026-08-09): matrx-scraper's standalone service bound the LIVE,
|
|
11
|
+
shared ``scraper.*`` tables to a Coolify-local Postgres while ``web.*`` went to
|
|
12
|
+
Matrx Main — 14 rows sat in the second database, invisible to the platform, and
|
|
13
|
+
boot logged it as "optional legacy database registered". Two pools in one process
|
|
14
|
+
pointing at two different Postgres instances is the observable signature of that
|
|
15
|
+
bug, and this is the check that would have screamed on the first boot.
|
|
16
|
+
|
|
17
|
+
Posture (root CLAUDE.md): **scream, never crash.** A divergence is a red banner
|
|
18
|
+
and a durable ops record for the host to file; killing a running service over it
|
|
19
|
+
would trade a silent data split for an outage. Callers that genuinely need a
|
|
20
|
+
second database declare it (``sanctioned_separate``) — aidream's separate CMS
|
|
21
|
+
product database is the only one today.
|
|
22
|
+
|
|
23
|
+
Usage (host startup, or any standalone package service boot)::
|
|
24
|
+
|
|
25
|
+
from matrx_orm.core.one_database import audit_one_database
|
|
26
|
+
|
|
27
|
+
report = await audit_one_database(
|
|
28
|
+
platform_config_name="supabase_automation_matrix",
|
|
29
|
+
sanctioned_separate=("cms",),
|
|
30
|
+
)
|
|
31
|
+
if not report.ok:
|
|
32
|
+
... # persist report.findings() to your ops ledger
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import asyncio
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
|
|
40
|
+
from matrx_utils import vcprint
|
|
41
|
+
|
|
42
|
+
from matrx_orm.core.async_db_manager import AsyncDatabaseManager, ConnectionIdentity
|
|
43
|
+
|
|
44
|
+
# How long a single identity probe may take before it is recorded as a failure.
|
|
45
|
+
# A pool that cannot answer "who are you" at boot is itself worth reporting.
|
|
46
|
+
PROBE_TIMEOUT_SECONDS = 20.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class PoolIdentity:
|
|
51
|
+
"""One registered matrx-orm config and the physical database it reached."""
|
|
52
|
+
|
|
53
|
+
config_name: str
|
|
54
|
+
identity: ConnectionIdentity | None = None
|
|
55
|
+
error: str | None = None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def physical_key(self) -> str | None:
|
|
59
|
+
return self.identity.physical_key if self.identity is not None else None
|
|
60
|
+
|
|
61
|
+
def describe(self) -> str:
|
|
62
|
+
if self.identity is None:
|
|
63
|
+
return f"{self.config_name}: PROBE FAILED — {self.error}"
|
|
64
|
+
ident = self.identity
|
|
65
|
+
where = f"{ident.server_addr or '?'}:{ident.server_port or '?'}"
|
|
66
|
+
cluster = f" cluster={ident.cluster_id}" if ident.cluster_id else ""
|
|
67
|
+
return f"{self.config_name}: {ident.database} @ {where}{cluster} as {ident.current_user}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class OneDatabaseReport:
|
|
72
|
+
"""The result of comparing every registered pool against the platform pool."""
|
|
73
|
+
|
|
74
|
+
platform_config: str
|
|
75
|
+
platform: PoolIdentity | None
|
|
76
|
+
pools: list[PoolIdentity] = field(default_factory=list)
|
|
77
|
+
divergent: list[PoolIdentity] = field(default_factory=list)
|
|
78
|
+
probe_failures: list[PoolIdentity] = field(default_factory=list)
|
|
79
|
+
sanctioned_separate: tuple[str, ...] = ()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def ok(self) -> bool:
|
|
83
|
+
"""True when every non-sanctioned pool reached the same physical database."""
|
|
84
|
+
return (
|
|
85
|
+
not self.divergent and self.platform is not None and self.platform.identity is not None
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def findings(self) -> list[dict[str, str]]:
|
|
89
|
+
"""Flat, persistable records — one per divergent pool, for an ops ledger."""
|
|
90
|
+
platform_key = self.platform.physical_key if self.platform else None
|
|
91
|
+
out: list[dict[str, str]] = []
|
|
92
|
+
for pool in self.divergent:
|
|
93
|
+
out.append(
|
|
94
|
+
{
|
|
95
|
+
"config_name": pool.config_name,
|
|
96
|
+
"physical_key": pool.physical_key or "<unknown>",
|
|
97
|
+
"platform_config": self.platform_config,
|
|
98
|
+
"platform_physical_key": platform_key or "<unknown>",
|
|
99
|
+
"detail": pool.describe(),
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
def render_banner(self) -> str:
|
|
105
|
+
"""The red banner text. Names every pool so the split is unmissable."""
|
|
106
|
+
lines = "\n".join(f"## {pool.describe()}" for pool in self.pools)
|
|
107
|
+
platform_key = (self.platform.physical_key if self.platform else None) or "<unknown>"
|
|
108
|
+
return (
|
|
109
|
+
"\n"
|
|
110
|
+
"############################################################################\n"
|
|
111
|
+
f"## TWO DATABASES IN ONE PROCESS — {len(self.divergent)} pool(s) do NOT point at\n"
|
|
112
|
+
f"## the platform database ({self.platform_config} = {platform_key}).\n"
|
|
113
|
+
"## There is ONE database. A pool bound elsewhere means live tables are\n"
|
|
114
|
+
"## being written where the platform will never see them (2026-08-09:\n"
|
|
115
|
+
"## matrx-scraper wrote 14 rows to a Coolify-local Postgres this way).\n"
|
|
116
|
+
"## FIX: resolve every pool from SUPABASE_MATRIX_* — see\n"
|
|
117
|
+
"## packages/matrx-scraper/matrx_scraper/db/__init__.py::register_platform_db\n"
|
|
118
|
+
"############################################################################\n"
|
|
119
|
+
f"{lines}\n"
|
|
120
|
+
"############################################################################"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def _probe(config_name: str) -> PoolIdentity:
|
|
125
|
+
try:
|
|
126
|
+
identity = await asyncio.wait_for(
|
|
127
|
+
AsyncDatabaseManager.connection_identity(config_name),
|
|
128
|
+
timeout=PROBE_TIMEOUT_SECONDS,
|
|
129
|
+
)
|
|
130
|
+
except Exception as exc: # noqa: BLE001 — a probe never breaks boot
|
|
131
|
+
return PoolIdentity(config_name=config_name, error=f"{type(exc).__name__}: {exc}")
|
|
132
|
+
return PoolIdentity(config_name=config_name, identity=identity)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def audit_one_database(
|
|
136
|
+
*,
|
|
137
|
+
platform_config_name: str,
|
|
138
|
+
sanctioned_separate: tuple[str, ...] | list[str] = (),
|
|
139
|
+
scream: bool = True,
|
|
140
|
+
) -> OneDatabaseReport:
|
|
141
|
+
"""Compare every registered pool's physical database against the platform's.
|
|
142
|
+
|
|
143
|
+
Asks each connection who it is (never re-reads env — env is the thing that
|
|
144
|
+
lied). Returns the report; screams a red banner when pools disagree or the
|
|
145
|
+
platform pool itself cannot be reached. Never raises.
|
|
146
|
+
|
|
147
|
+
``sanctioned_separate`` names configs that are deliberately a different
|
|
148
|
+
physical database (aidream's ``cms`` product database). Anything else that
|
|
149
|
+
diverges is the bug.
|
|
150
|
+
"""
|
|
151
|
+
from matrx_orm.core.config import get_all_database_project_names, resolve_database_name
|
|
152
|
+
|
|
153
|
+
separate = {resolve_database_name(name) for name in sanctioned_separate}
|
|
154
|
+
platform = resolve_database_name(platform_config_name)
|
|
155
|
+
names = sorted(set(get_all_database_project_names()))
|
|
156
|
+
|
|
157
|
+
probes = await asyncio.gather(*(_probe(name) for name in names))
|
|
158
|
+
by_name = {probe.config_name: probe for probe in probes}
|
|
159
|
+
platform_probe = by_name.get(platform)
|
|
160
|
+
|
|
161
|
+
platform_key = platform_probe.physical_key if platform_probe else None
|
|
162
|
+
divergent = [
|
|
163
|
+
probe
|
|
164
|
+
for probe in probes
|
|
165
|
+
if probe.config_name != platform
|
|
166
|
+
and probe.config_name not in separate
|
|
167
|
+
and probe.identity is not None
|
|
168
|
+
and platform_key is not None
|
|
169
|
+
and probe.physical_key != platform_key
|
|
170
|
+
]
|
|
171
|
+
failures = [probe for probe in probes if probe.identity is None]
|
|
172
|
+
|
|
173
|
+
report = OneDatabaseReport(
|
|
174
|
+
platform_config=platform,
|
|
175
|
+
platform=platform_probe,
|
|
176
|
+
pools=list(probes),
|
|
177
|
+
divergent=divergent,
|
|
178
|
+
probe_failures=failures,
|
|
179
|
+
sanctioned_separate=tuple(sorted(separate)),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if not scream:
|
|
183
|
+
return report
|
|
184
|
+
|
|
185
|
+
# A pool that cannot answer is an unverified pool — say so every time, not
|
|
186
|
+
# only on the happy path.
|
|
187
|
+
for probe in failures:
|
|
188
|
+
vcprint(f"[one-database] pool probe failed: {probe.describe()}", color="yellow")
|
|
189
|
+
|
|
190
|
+
if divergent:
|
|
191
|
+
vcprint(report.render_banner(), color="red")
|
|
192
|
+
elif platform_probe is None or platform_probe.identity is None:
|
|
193
|
+
detail = platform_probe.error if platform_probe else "not registered"
|
|
194
|
+
vcprint(
|
|
195
|
+
f"[one-database] cannot verify pool agreement — the platform config "
|
|
196
|
+
f"{platform!r} did not answer ({detail}). The check is NOT trustworthy "
|
|
197
|
+
"until it does.",
|
|
198
|
+
color="red",
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
extra = f" (+{len(separate)} declared-separate)" if separate else ""
|
|
202
|
+
vcprint(
|
|
203
|
+
f"[one-database] {len(probes) - len(separate)} pool(s) all resolve the ONE "
|
|
204
|
+
f"database: {platform_key}{extra}.",
|
|
205
|
+
color="green",
|
|
206
|
+
)
|
|
207
|
+
return report
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
__all__ = ["OneDatabaseReport", "PoolIdentity", "audit_one_database"]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""THE ONE DATABASE — the single place any Matrx package resolves Postgres.
|
|
2
|
+
|
|
3
|
+
**Read this before you conclude anything about "which database" a package uses.**
|
|
4
|
+
|
|
5
|
+
There is exactly ONE database per deployment. Every Matrx package — matrx-rag,
|
|
6
|
+
matrx-files, matrx-scraper, matrx-seo, matrx-runtime, and every future one —
|
|
7
|
+
puts its schema in that same Postgres. `rag.*`, `files.*`, `scraper.*`, `web.*`,
|
|
8
|
+
`seo.*`, `runtime.*` are schemas in ONE database, not databases.
|
|
9
|
+
|
|
10
|
+
WHERE that database lives is a DEPLOYMENT input, not a code decision. Today the
|
|
11
|
+
hosted product points at Supabase (project `txzxabzwovsujtloxrus`); a client who
|
|
12
|
+
wants to own their data points the SAME five variables at THEIR Postgres and the
|
|
13
|
+
entire platform moves with them. That is the whole design: one connection, set
|
|
14
|
+
once, per deployment.
|
|
15
|
+
|
|
16
|
+
{PLATFORM_DB_ENV_PREFIX}_HOST
|
|
17
|
+
{PLATFORM_DB_ENV_PREFIX}_PORT
|
|
18
|
+
{PLATFORM_DB_ENV_PREFIX}_DATABASE_NAME
|
|
19
|
+
{PLATFORM_DB_ENV_PREFIX}_USER
|
|
20
|
+
{PLATFORM_DB_ENV_PREFIX}_PASSWORD
|
|
21
|
+
|
|
22
|
+
**ONE connection, REQUIRED — never a chain of candidates.** A package takes
|
|
23
|
+
exactly one database connection, from exactly one set of variables. If resolution
|
|
24
|
+
fails, this module RAISES: no degraded mode, no "optional" binding, no quiet
|
|
25
|
+
reach for `DATABASE_URL`, a `<PKG>_DATABASE_URL`, or a `<PKG>_POSTGRES_*` set.
|
|
26
|
+
|
|
27
|
+
This does NOT mean a package may not own a database. **Every matrx-* package
|
|
28
|
+
ships its own schemas and migrations and can create and run them on a Postgres of
|
|
29
|
+
its own, standalone — that independence is a requirement, never remove it.** What
|
|
30
|
+
a package may not have is a SECOND CANDIDATE for where it connects.
|
|
31
|
+
|
|
32
|
+
That prohibition is not stylistic — it is the fix for a real incident. Each
|
|
33
|
+
package used to resolve `<PKG>_DATABASE_URL` → `DATABASE_URL` → `<PKG>_POSTGRES_*`.
|
|
34
|
+
Coolify injects a `DATABASE_URL` into every service automatically, so the
|
|
35
|
+
standalone scraper silently bound the LIVE, SHARED `scraper.*` tables to a
|
|
36
|
+
service-local Postgres and wrote 14 rows the platform never saw. A fallback
|
|
37
|
+
ladder does not add resilience; it hands the choice of database to whatever the
|
|
38
|
+
host happens to have set. The defect is the CHAIN, not the destination.
|
|
39
|
+
|
|
40
|
+
Guard: `scripts/check_one_database.py` fails the build if any package
|
|
41
|
+
reintroduces such a variable. If you are tempted to add one, you want a
|
|
42
|
+
different VALUE for these five variables in that deployment — not a new variable.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import os
|
|
48
|
+
from urllib.parse import quote
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"PLATFORM_DB_ENV_PREFIX",
|
|
52
|
+
"PLATFORM_DB_ENV_OVERRIDES",
|
|
53
|
+
"platform_connection_url",
|
|
54
|
+
"register_platform_db",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# The ONE database's env prefix. Historical name — it predates the platform
|
|
58
|
+
# being Postgres-agnostic and says nothing about the engine. Any Postgres works.
|
|
59
|
+
PLATFORM_DB_ENV_PREFIX = "SUPABASE_MATRIX"
|
|
60
|
+
|
|
61
|
+
# `..._DATABASE_NAME` rather than the default `..._NAME`, because `..._NAME` is
|
|
62
|
+
# already used elsewhere to mean "name of a registered matrx-orm config".
|
|
63
|
+
PLATFORM_DB_ENV_OVERRIDES = {"NAME": "SUPABASE_MATRIX_DATABASE_NAME"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _missing_env_message(package: str) -> str:
|
|
67
|
+
who = f"{package}: " if package else ""
|
|
68
|
+
return (
|
|
69
|
+
f"{who}cannot resolve the platform database — set "
|
|
70
|
+
f"{PLATFORM_DB_ENV_PREFIX}_HOST/_PORT/_DATABASE_NAME/_USER/_PASSWORD. "
|
|
71
|
+
"These five variables ARE the database: point them at Supabase, at a "
|
|
72
|
+
"client's Postgres, or at a local instance. There is exactly one "
|
|
73
|
+
"database connection per deployment — one set of variables, required. "
|
|
74
|
+
"There is no second candidate to try, and adding one would let the host "
|
|
75
|
+
"silently decide which database this package lands in."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def platform_connection_url(*, package: str = "") -> str:
|
|
80
|
+
"""Build the ONE database's connection URL for ad-hoc / migration use.
|
|
81
|
+
|
|
82
|
+
Raises ``RuntimeError`` if the five variables are not fully set. There is
|
|
83
|
+
deliberately no fallback — see the module docstring.
|
|
84
|
+
"""
|
|
85
|
+
host = os.environ.get(f"{PLATFORM_DB_ENV_PREFIX}_HOST")
|
|
86
|
+
user = os.environ.get(f"{PLATFORM_DB_ENV_PREFIX}_USER")
|
|
87
|
+
password = os.environ.get(f"{PLATFORM_DB_ENV_PREFIX}_PASSWORD")
|
|
88
|
+
database = os.environ.get(PLATFORM_DB_ENV_OVERRIDES["NAME"]) or "postgres"
|
|
89
|
+
port = os.environ.get(f"{PLATFORM_DB_ENV_PREFIX}_PORT") or "5432"
|
|
90
|
+
|
|
91
|
+
if not (host and user and password):
|
|
92
|
+
raise RuntimeError(_missing_env_message(package))
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
f"postgresql://{quote(user, safe='')}:{quote(password, safe='')}"
|
|
96
|
+
f"@{host}:{port}/{database}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def register_platform_db(
|
|
101
|
+
name: str,
|
|
102
|
+
*,
|
|
103
|
+
additional_schemas: list[str],
|
|
104
|
+
pool_min: int = 1,
|
|
105
|
+
pool_max: int = 5,
|
|
106
|
+
ssl: object | None = None,
|
|
107
|
+
package: str = "",
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Register matrx-orm config ``name`` against the ONE database.
|
|
110
|
+
|
|
111
|
+
``name`` is a matrx-orm *config name* (e.g. ``"matrx_rag"``), NOT a
|
|
112
|
+
database — every package's models bake a config name so a host can alias it
|
|
113
|
+
onto an already-open pool. This function is what standalone boot calls when
|
|
114
|
+
no host did that.
|
|
115
|
+
|
|
116
|
+
``ssl`` defaults to ``None``, which lets ``{PREFIX}_SSL`` decide (matrx-orm
|
|
117
|
+
itself defaults to ``"require"``). That is what makes a plain local or
|
|
118
|
+
client-owned Postgres reachable: set ``SUPABASE_MATRIX_SSL=disable``. Do not
|
|
119
|
+
hardcode a TLS posture here.
|
|
120
|
+
|
|
121
|
+
Idempotent. Raises ``RuntimeError`` if the environment is incomplete.
|
|
122
|
+
"""
|
|
123
|
+
from matrx_orm import is_database_registered, register_database_from_env
|
|
124
|
+
|
|
125
|
+
if is_database_registered(name):
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
ok = register_database_from_env(
|
|
129
|
+
name=name,
|
|
130
|
+
env_prefix=PLATFORM_DB_ENV_PREFIX,
|
|
131
|
+
additional_schemas=additional_schemas,
|
|
132
|
+
env_var_overrides=dict(PLATFORM_DB_ENV_OVERRIDES),
|
|
133
|
+
pool_min=pool_min,
|
|
134
|
+
pool_max=pool_max,
|
|
135
|
+
ssl=ssl,
|
|
136
|
+
)
|
|
137
|
+
if not ok:
|
|
138
|
+
raise RuntimeError(_missing_env_message(package))
|
|
@@ -15,3 +15,11 @@ process identity cache.
|
|
|
15
15
|
`True` for compatibility. The cache bypass controls only cache population; it
|
|
16
16
|
does not weaken database authorization, filters, hydration, or transaction
|
|
17
17
|
semantics.
|
|
18
|
+
|
|
19
|
+
## Aggregate projections
|
|
20
|
+
|
|
21
|
+
A scalar `.aggregate(...)` projects only its aggregate expressions. It never
|
|
22
|
+
inherits the model query's default `*` projection. A grouped aggregate with no
|
|
23
|
+
explicit `.values(...)` projects the grouped columns plus its aggregate
|
|
24
|
+
expressions. These shapes keep every generated aggregate valid in PostgreSQL
|
|
25
|
+
without forcing callers to add a redundant projection.
|
|
@@ -583,6 +583,12 @@ class QueryExecutor:
|
|
|
583
583
|
reason="group_by() entries require a column name or typed expression",
|
|
584
584
|
details={"entry": repr(f)},
|
|
585
585
|
)
|
|
586
|
+
elif aggregations:
|
|
587
|
+
# A scalar aggregate owns the entire projection. Keeping the
|
|
588
|
+
# default ``*`` would emit ``SELECT *, COUNT(...)`` and make
|
|
589
|
+
# Postgres require every model column in a nonexistent GROUP
|
|
590
|
+
# BY clause.
|
|
591
|
+
pass
|
|
586
592
|
elif select_related or explicit_joins:
|
|
587
593
|
# Need explicit table prefix to avoid ambiguity with joined columns
|
|
588
594
|
select_parts.insert(0, f"{self._base_ref()}.*")
|
|
@@ -4,7 +4,7 @@ Fernet, keyed by ``CREDENTIALS_ENCRYPTION_KEY``. Every package and every
|
|
|
4
4
|
deployment that reads or writes encrypted credentials holds the SAME key and
|
|
5
5
|
uses THIS module — there is no privileged decryptor anywhere, and no second
|
|
6
6
|
Fernet instance may ever be created (`aidream.services.user_secrets.encryption`
|
|
7
|
-
and `aidream.services.
|
|
7
|
+
and `aidream.services.web_credentials.credentials` delegate here).
|
|
8
8
|
|
|
9
9
|
Key resolution (identical to the historical aidream implementation so every
|
|
10
10
|
existing ciphertext keeps decrypting):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.49"
|
|
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" }
|
|
@@ -307,12 +307,20 @@ class TestBaseManagerAggregate:
|
|
|
307
307
|
# before terminating. (aggregate() on the builder also accepts kw.)
|
|
308
308
|
qb = qb.annotate(total=Sum("amount"), n=Count("id"))
|
|
309
309
|
sql, params = QueryExecutor(qb._build_query())._build_query()
|
|
310
|
+
assert "SELECT *" not in sql.split(" FROM ")[0]
|
|
310
311
|
assert "SUM(amount) AS total" in sql
|
|
311
312
|
assert "COUNT(id) AS n" in sql
|
|
312
313
|
assert "FROM orders" in sql
|
|
313
314
|
assert "WHERE status = $1" in sql
|
|
314
315
|
assert params == ["paid"]
|
|
315
316
|
|
|
317
|
+
def test_aggregate_composite_distinct_has_no_model_projection(self):
|
|
318
|
+
CE = _make_chunk_entity_model()
|
|
319
|
+
qb = CE.filter().annotate(pairs=Count("entity_id", "chunk_id", distinct=True))
|
|
320
|
+
sql, _ = QueryExecutor(qb._build_query())._build_query()
|
|
321
|
+
select_clause = sql.split(" FROM ")[0]
|
|
322
|
+
assert select_clause == "SELECT COUNT(DISTINCT (entity_id, chunk_id)) AS pairs"
|
|
323
|
+
|
|
316
324
|
|
|
317
325
|
class TestValuesDeduplicatesAnnotateAliases:
|
|
318
326
|
"""Regression: ``.values("col", "alias")`` after ``.annotate(alias=Count())``
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""THE ONE DATABASE — pin the single resolver every Matrx package shares.
|
|
2
|
+
|
|
3
|
+
These tests fail if someone reintroduces a way for one package's schema to land
|
|
4
|
+
in a different Postgres than the rest of the platform.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from matrx_orm import platform_db
|
|
16
|
+
|
|
17
|
+
REPO = Path(__file__).resolve().parents[3]
|
|
18
|
+
|
|
19
|
+
# Every retired per-package connection variable. None of these may ever again
|
|
20
|
+
# influence where a schema lands.
|
|
21
|
+
RETIRED_VARS = [
|
|
22
|
+
"DATABASE_URL",
|
|
23
|
+
"RAG_DATABASE_URL",
|
|
24
|
+
"FILES_DATABASE_URL",
|
|
25
|
+
"RUNTIME_DATABASE_URL",
|
|
26
|
+
"SCRAPER_DATABASE_URL",
|
|
27
|
+
"MATRX_RAG_POSTGRES_HOST",
|
|
28
|
+
"MATRX_FILES_POSTGRES_HOST",
|
|
29
|
+
"MATRX_RUNTIME_POSTGRES_HOST",
|
|
30
|
+
"MATRX_SCRAPER_POSTGRES_HOST",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _clear_platform_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
35
|
+
for suffix in ("HOST", "PORT", "USER", "PASSWORD", "DATABASE_NAME"):
|
|
36
|
+
monkeypatch.delenv(f"SUPABASE_MATRIX_{suffix}", raising=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_connection_url_ignores_every_retired_second_database_var(
|
|
40
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""A service-local Postgres URL must NOT be able to fork a live schema."""
|
|
43
|
+
_clear_platform_env(monkeypatch)
|
|
44
|
+
for var in RETIRED_VARS:
|
|
45
|
+
monkeypatch.setenv(var, "postgresql://a:b@rogue-postgres.internal:5434/rogue")
|
|
46
|
+
|
|
47
|
+
with pytest.raises(RuntimeError) as exc:
|
|
48
|
+
platform_db.platform_connection_url(package="matrx-test")
|
|
49
|
+
|
|
50
|
+
# It raises rather than silently binding to the rogue host, and the message
|
|
51
|
+
# tells the reader what the ONE database actually is.
|
|
52
|
+
assert "rogue-postgres" not in str(exc.value)
|
|
53
|
+
assert "SUPABASE_MATRIX_HOST" in str(exc.value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_connection_url_builds_from_the_five_platform_vars(
|
|
57
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
58
|
+
) -> None:
|
|
59
|
+
monkeypatch.setenv("SUPABASE_MATRIX_HOST", "db.example.com")
|
|
60
|
+
monkeypatch.setenv("SUPABASE_MATRIX_PORT", "6543")
|
|
61
|
+
monkeypatch.setenv("SUPABASE_MATRIX_USER", "svc")
|
|
62
|
+
monkeypatch.setenv("SUPABASE_MATRIX_PASSWORD", "p@ss/word")
|
|
63
|
+
monkeypatch.setenv("SUPABASE_MATRIX_DATABASE_NAME", "postgres")
|
|
64
|
+
|
|
65
|
+
url = platform_db.platform_connection_url()
|
|
66
|
+
|
|
67
|
+
# Password is URL-quoted, not naively interpolated.
|
|
68
|
+
assert url == "postgresql://svc:p%40ss%2Fword@db.example.com:6543/postgres"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_a_client_owned_postgres_is_just_different_values(
|
|
72
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""The whole point: relocating the platform is a VALUE change, not a new var."""
|
|
75
|
+
monkeypatch.setenv("SUPABASE_MATRIX_HOST", "localhost")
|
|
76
|
+
monkeypatch.setenv("SUPABASE_MATRIX_PORT", "5432")
|
|
77
|
+
monkeypatch.setenv("SUPABASE_MATRIX_USER", "postgres")
|
|
78
|
+
monkeypatch.setenv("SUPABASE_MATRIX_PASSWORD", "postgres")
|
|
79
|
+
monkeypatch.setenv("SUPABASE_MATRIX_DATABASE_NAME", "matrx")
|
|
80
|
+
|
|
81
|
+
assert (
|
|
82
|
+
platform_db.platform_connection_url()
|
|
83
|
+
== "postgresql://postgres:postgres@localhost:5432/matrx"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_register_platform_db_leaves_tls_to_the_deployment() -> None:
|
|
88
|
+
"""ssl defaults to None so SUPABASE_MATRIX_SSL governs — a local Postgres
|
|
89
|
+
with TLS off must remain reachable."""
|
|
90
|
+
import inspect
|
|
91
|
+
|
|
92
|
+
sig = inspect.signature(platform_db.register_platform_db)
|
|
93
|
+
assert sig.parameters["ssl"].default is None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_repo_guard_reports_clean() -> None:
|
|
97
|
+
"""The build-blocking guard must pass on the current tree."""
|
|
98
|
+
script = REPO / "scripts" / "check_one_database.py"
|
|
99
|
+
if not script.exists(): # package used outside the monorepo
|
|
100
|
+
pytest.skip("monorepo guard script not present")
|
|
101
|
+
|
|
102
|
+
result = subprocess.run(
|
|
103
|
+
[sys.executable, str(script)], capture_output=True, text=True, cwd=REPO
|
|
104
|
+
)
|
|
105
|
+
assert result.returncode == 0, result.stdout + result.stderr
|
|
File without changes
|