matrx-orm 3.1.22__tar.gz → 3.1.24__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.22 → matrx_orm-3.1.24}/CLAUDE.md +10 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/PKG-INFO +2 -1
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/conflict.py +54 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/query/executor.py +15 -12
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/runner.py +89 -9
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/pyproject.toml +2 -1
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_conflict_target.py +20 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_expression_primitives.py +19 -1
- matrx_orm-3.1.24/tests/schema_builder/test_focused_generation_plan.py +58 -0
- matrx_orm-3.1.24/tests/test_secrets_battery.py +135 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/.gitignore +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/AGENTS.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/MODEL_API.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/docs/migrations.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/scripts/release.sh +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/conftest.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_execute_admin_sql.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_filter_raw.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.22 → matrx_orm-3.1.24}/tests/test_sync_twins_guard.py +0 -0
|
@@ -32,6 +32,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
|
|
|
32
32
|
- **Adapters**: `AsyncPostgreSQLAdapter`, `SupabaseAdapter`, `PostgRESTClientAdapter`.
|
|
33
33
|
- **Signals**: `pre_create`, `post_create`, `pre_save`, `post_save`, `pre_delete`, `post_delete`.
|
|
34
34
|
- **Schema builder** (`matrx_orm.schema_builder`): code generation for Python + TypeScript type definitions from the DB schema. Postgres **VIEWs** are emitted as **read-only `Model` classes** into the same schema models file (on by default; per-entry `output.include_views: false` opts a schema out, `output.exclude_views: [...]` skips individual views). View models get no manager/DTO/base-class/auto_config (views are not writable), synthesize `_primary_keys` from an `id` column (else the first column, for cache-key/`load_by_id` only), and emit plain typed fields with **no `ForeignKey`** (views carry no FK metadata — nothing cross-schema to resolve). Never hand-write a view model again.
|
|
35
|
+
- **Secrets battery** (`matrx_orm.secrets`): the ONE canonical user-credential vault — `users.user_secrets` CRUD + the platform's single Fernet primitive, keyed by `CREDENTIALS_ENCRYPTION_KEY`. Any package: `configure_secrets(db_config_name=...)` once, then `await require_user_secret(user_id, key)`. No package ever calls another service to resolve a credential. Contract: [`matrx_orm/secrets/FEATURE.md`](matrx_orm/secrets/FEATURE.md).
|
|
35
36
|
- **Polymorphic associations** (`matrx_orm.associations`): `EntityRegistry` (token→Model) + `Associations` — a registry-driven, **ownership-gated** edge engine over a host's polymorphic edge table. Every `source→target` hop is org-scoped *and* the resolved target's `org_id` is re-verified (fail-closed, screams on mismatch), so a hop can't widen access past the tenant boundary. Hardcodes no table names — the host wires its own edge + token-registry models.
|
|
36
37
|
|
|
37
38
|
Public API is defined explicitly in `matrx_orm/__init__.py` (100+ exports). Treat changes there as semver-breaking.
|
|
@@ -98,6 +99,15 @@ uv run pytest packages/matrx-orm/tests
|
|
|
98
99
|
|
|
99
100
|
Tests must run against a Postgres instance the developer provides (or a testcontainers-based fixture). No sibling matrx package is needed. If a test needs `matrx-connect`, that's a sign the test is in the wrong package.
|
|
100
101
|
|
|
102
|
+
Host generators may expose a focused schema pass by forwarding
|
|
103
|
+
`only_schemas={...}` to `run_schema_generation`; aidream's focused package-model
|
|
104
|
+
command is `uv run python db/generate.py --schema <schema> --models-only`.
|
|
105
|
+
`models_only=True` skips unrelated schema/type/helper outputs but still performs
|
|
106
|
+
live introspection and writes the canonical generated Model file. The unfiltered
|
|
107
|
+
command remains the release-wide regeneration path. A package target may declare
|
|
108
|
+
`dependency_schemas` in `db/matrx_orm.yaml`; a focused pass then introspects only
|
|
109
|
+
the target and those explicitly named cross-schema FK dependencies.
|
|
110
|
+
|
|
101
111
|
---
|
|
102
112
|
|
|
103
113
|
## Known issues
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.24
|
|
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
|
|
@@ -23,6 +23,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
24
|
Requires-Python: >=3.13
|
|
25
25
|
Requires-Dist: asyncpg>=0.31.0
|
|
26
|
+
Requires-Dist: cryptography>=43.0
|
|
26
27
|
Requires-Dist: gitpython>=3.1.40
|
|
27
28
|
Requires-Dist: matrx-utils>=1.0.20
|
|
28
29
|
Requires-Dist: psycopg-pool>=3.2.5
|
|
@@ -40,6 +40,29 @@ Example::
|
|
|
40
40
|
from __future__ import annotations
|
|
41
41
|
|
|
42
42
|
from dataclasses import dataclass
|
|
43
|
+
import re
|
|
44
|
+
from typing import Any
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_IDENTIFIER_RE = re.compile(r"^[a-z_][a-z0-9_]*$")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _identifier(value: str) -> str:
|
|
51
|
+
if not _IDENTIFIER_RE.fullmatch(value):
|
|
52
|
+
raise ValueError(f"ConflictTarget column must be a simple identifier: {value!r}")
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _literal(value: Any) -> str:
|
|
57
|
+
if value is None:
|
|
58
|
+
return "NULL"
|
|
59
|
+
if isinstance(value, bool):
|
|
60
|
+
return "TRUE" if value else "FALSE"
|
|
61
|
+
if isinstance(value, int | float):
|
|
62
|
+
return str(value)
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
return "'" + value.replace("'", "''") + "'"
|
|
65
|
+
raise TypeError(f"Unsupported ConflictTarget literal: {type(value).__name__}")
|
|
43
66
|
|
|
44
67
|
|
|
45
68
|
@dataclass(frozen=True)
|
|
@@ -83,5 +106,36 @@ class ConflictTarget:
|
|
|
83
106
|
"ConflictTarget(where=...) must be a non-empty string when provided"
|
|
84
107
|
)
|
|
85
108
|
|
|
109
|
+
@classmethod
|
|
110
|
+
def partial(
|
|
111
|
+
cls,
|
|
112
|
+
columns: list[str] | tuple[str, ...],
|
|
113
|
+
*,
|
|
114
|
+
coalesce: dict[str, Any] | None = None,
|
|
115
|
+
where_is_null: tuple[str, ...] = (),
|
|
116
|
+
where_is_not_null: tuple[str, ...] = (),
|
|
117
|
+
where_equals: dict[str, Any] | None = None,
|
|
118
|
+
) -> ConflictTarget:
|
|
119
|
+
"""Build an expression/partial-index target without SQL at call sites."""
|
|
120
|
+
coalesce = coalesce or {}
|
|
121
|
+
unknown = set(coalesce) - set(columns)
|
|
122
|
+
if unknown:
|
|
123
|
+
raise ValueError(f"coalesce columns are not in conflict target: {sorted(unknown)}")
|
|
124
|
+
expressions = [
|
|
125
|
+
f"COALESCE({_identifier(column)}, {_literal(coalesce[column])})"
|
|
126
|
+
if column in coalesce
|
|
127
|
+
else _identifier(column)
|
|
128
|
+
for column in columns
|
|
129
|
+
]
|
|
130
|
+
predicates = [f"{_identifier(column)} IS NULL" for column in where_is_null]
|
|
131
|
+
predicates.extend(
|
|
132
|
+
f"{_identifier(column)} IS NOT NULL" for column in where_is_not_null
|
|
133
|
+
)
|
|
134
|
+
predicates.extend(
|
|
135
|
+
f"{_identifier(column)} = {_literal(value)}"
|
|
136
|
+
for column, value in (where_equals or {}).items()
|
|
137
|
+
)
|
|
138
|
+
return cls(expressions=expressions, where=" AND ".join(predicates) or None)
|
|
139
|
+
|
|
86
140
|
|
|
87
141
|
__all__ = ["ConflictTarget"]
|
|
@@ -168,6 +168,15 @@ def _shift_param_refs(sql: str, delta: int) -> str:
|
|
|
168
168
|
return "".join(out)
|
|
169
169
|
|
|
170
170
|
|
|
171
|
+
def _render_update_expression(value: Any, params: list[Any]) -> str:
|
|
172
|
+
"""Render an expression after existing SET params without reusing $1..$N."""
|
|
173
|
+
sub_params: list[Any] = []
|
|
174
|
+
sql = value.as_sql(sub_params)
|
|
175
|
+
sql = _shift_param_refs(sql, len(params))
|
|
176
|
+
params.extend(sub_params)
|
|
177
|
+
return sql
|
|
178
|
+
|
|
179
|
+
|
|
171
180
|
# Postgres' wire protocol caps a single statement at 65535 bind parameters and
|
|
172
181
|
# asyncpg enforces a 32767 (int16) limit. We chunk bulk writes well under that
|
|
173
182
|
# so the per-row column count never pushes a batch over the edge.
|
|
@@ -417,7 +426,7 @@ class QueryExecutor:
|
|
|
417
426
|
return f"{field} <@ {placeholder}::jsonb", v
|
|
418
427
|
if operator == "json_path":
|
|
419
428
|
# field #>> '{key1,key2}' = $1 (value is (path_tuple, expected))
|
|
420
|
-
if isinstance(value,
|
|
429
|
+
if isinstance(value, list | tuple):
|
|
421
430
|
path = "{" + ",".join(str(p) for p in value) + "}"
|
|
422
431
|
return f"{field} #>> {placeholder}", path
|
|
423
432
|
return f"{field} #>> {placeholder}", str(value)
|
|
@@ -581,7 +590,7 @@ class QueryExecutor:
|
|
|
581
590
|
for agg in aggregations:
|
|
582
591
|
name = agg["name"]
|
|
583
592
|
func = agg["function"]
|
|
584
|
-
if isinstance(func,
|
|
593
|
+
if isinstance(func, _Subquery | _Exists):
|
|
585
594
|
# Correlated scalar subquery in the SELECT list — its as_sql needs
|
|
586
595
|
# the OUTER table so OuterRef("x") resolves to <table>.x. Params land
|
|
587
596
|
# here (SELECT position, before WHERE) — correct textual order.
|
|
@@ -656,7 +665,7 @@ class QueryExecutor:
|
|
|
656
665
|
from matrx_orm.core.expressions import Exists as ExistsExpr
|
|
657
666
|
from matrx_orm.core.expressions import TsMatch as TsMatchExpr
|
|
658
667
|
|
|
659
|
-
if isinstance(item,
|
|
668
|
+
if isinstance(item, ExistsExpr | TsMatchExpr):
|
|
660
669
|
where_conditions.append(item.as_sql(table, params))
|
|
661
670
|
elif isinstance(item, ColumnComparison):
|
|
662
671
|
where_conditions.append(item.as_sql(params))
|
|
@@ -695,7 +704,7 @@ class QueryExecutor:
|
|
|
695
704
|
field_name, operator = self._parse_lookup(key)
|
|
696
705
|
agg_func = agg_by_alias.get(field_name)
|
|
697
706
|
if agg_func is not None:
|
|
698
|
-
if isinstance(agg_func,
|
|
707
|
+
if isinstance(agg_func, _Subquery | _Exists):
|
|
699
708
|
field_expr = agg_func.as_sql(table, params)
|
|
700
709
|
elif hasattr(agg_func, "as_sql"):
|
|
701
710
|
field_expr = agg_func.as_sql(params)
|
|
@@ -1331,10 +1340,7 @@ class QueryExecutor:
|
|
|
1331
1340
|
set_clause.append(f"{field_name} = {expr_sql}")
|
|
1332
1341
|
param_index = len(params) + 1
|
|
1333
1342
|
elif isinstance(value, Expression):
|
|
1334
|
-
|
|
1335
|
-
expr_sql = value.as_sql(sub_params)
|
|
1336
|
-
for sp in sub_params:
|
|
1337
|
-
params.append(sp)
|
|
1343
|
+
expr_sql = _render_update_expression(value, params)
|
|
1338
1344
|
set_clause.append(f"{field_name} = {expr_sql}")
|
|
1339
1345
|
param_index = len(params) + 1
|
|
1340
1346
|
elif hasattr(value, "as_sql"):
|
|
@@ -1343,10 +1349,7 @@ class QueryExecutor:
|
|
|
1343
1349
|
# duck-typed convention as _render_func_arg's fallback, so
|
|
1344
1350
|
# SET col = CASE WHEN ... END works in .update()/
|
|
1345
1351
|
# .update_where() without hardcoding every expression class.
|
|
1346
|
-
|
|
1347
|
-
expr_sql = value.as_sql(sub_params)
|
|
1348
|
-
for sp in sub_params:
|
|
1349
|
-
params.append(sp)
|
|
1352
|
+
expr_sql = _render_update_expression(value, params)
|
|
1350
1353
|
set_clause.append(f"{field_name} = {expr_sql}")
|
|
1351
1354
|
param_index = len(params) + 1
|
|
1352
1355
|
else:
|
|
@@ -51,8 +51,68 @@ def _load_yaml(path: Path) -> dict[str, Any]:
|
|
|
51
51
|
return yaml.safe_load(f) or {}
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def _select_generation_targets(
|
|
55
|
+
generate_entries: list[dict[str, Any]],
|
|
56
|
+
only_schemas: set[str] | None,
|
|
57
|
+
) -> tuple[list[dict[str, Any]], dict[str, set[str] | None]]:
|
|
58
|
+
selected = list(generate_entries)
|
|
59
|
+
scoped_by_database: dict[str, set[str] | None] = {}
|
|
60
|
+
if not only_schemas:
|
|
61
|
+
return selected, scoped_by_database
|
|
62
|
+
selected = [
|
|
63
|
+
entry for entry in selected if entry.get("schema", "public") in only_schemas
|
|
64
|
+
]
|
|
65
|
+
found = {entry.get("schema", "public") for entry in selected}
|
|
66
|
+
missing = only_schemas - found
|
|
67
|
+
if missing:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"matrx-orm generation requested unknown schema target(s): "
|
|
70
|
+
+ ", ".join(sorted(missing))
|
|
71
|
+
)
|
|
72
|
+
for entry in selected:
|
|
73
|
+
database_name = entry.get("database")
|
|
74
|
+
if not database_name:
|
|
75
|
+
continue
|
|
76
|
+
if "dependency_schemas" not in entry:
|
|
77
|
+
scoped_by_database[database_name] = None
|
|
78
|
+
continue
|
|
79
|
+
existing = scoped_by_database.get(database_name, set())
|
|
80
|
+
if existing is None:
|
|
81
|
+
continue
|
|
82
|
+
existing.add(entry.get("schema", "public"))
|
|
83
|
+
existing.update(entry.get("dependency_schemas") or [])
|
|
84
|
+
scoped_by_database[database_name] = existing
|
|
85
|
+
return selected, scoped_by_database
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _database_registration_plan(
|
|
89
|
+
databases: list[dict[str, Any]],
|
|
90
|
+
scoped_by_database: dict[str, set[str] | None],
|
|
91
|
+
*,
|
|
92
|
+
focused: bool,
|
|
93
|
+
models_only: bool,
|
|
94
|
+
) -> list[dict[str, Any]]:
|
|
95
|
+
plan: list[dict[str, Any]] = []
|
|
96
|
+
for database in databases:
|
|
97
|
+
name = database.get("name")
|
|
98
|
+
if focused and name not in scoped_by_database:
|
|
99
|
+
continue
|
|
100
|
+
item = dict(database)
|
|
101
|
+
scoped = scoped_by_database.get(name)
|
|
102
|
+
if focused and scoped is not None:
|
|
103
|
+
item["additional_schemas"] = sorted(scoped)
|
|
104
|
+
item["pool_min"] = 1 if models_only else None
|
|
105
|
+
item["pool_max"] = 4 if models_only else None
|
|
106
|
+
plan.append(item)
|
|
107
|
+
return plan
|
|
108
|
+
|
|
109
|
+
|
|
54
110
|
def run_schema_generation(
|
|
55
|
-
config_path: str | Path = "matrx_orm.yaml",
|
|
111
|
+
config_path: str | Path = "matrx_orm.yaml",
|
|
112
|
+
skip_git_check: bool = False,
|
|
113
|
+
*,
|
|
114
|
+
only_schemas: set[str] | None = None,
|
|
115
|
+
models_only: bool = False,
|
|
56
116
|
) -> None:
|
|
57
117
|
"""
|
|
58
118
|
Read a matrx_orm.yaml config file, register all databases, apply debug
|
|
@@ -192,6 +252,13 @@ def run_schema_generation(
|
|
|
192
252
|
ignore_path_prefixes=_ignore_prefixes,
|
|
193
253
|
)
|
|
194
254
|
|
|
255
|
+
# Resolve targets before database registration. A focused package-model
|
|
256
|
+
# pass should not connect to unrelated databases, and targets may declare
|
|
257
|
+
# the schemas needed to resolve their cross-schema foreign keys.
|
|
258
|
+
generate_list, scoped_schemas_by_database = _select_generation_targets(
|
|
259
|
+
cfg.get("generate", []), only_schemas
|
|
260
|
+
)
|
|
261
|
+
|
|
195
262
|
# -------------------------------------------------------------------------
|
|
196
263
|
# Register databases
|
|
197
264
|
# -------------------------------------------------------------------------
|
|
@@ -202,7 +269,13 @@ def run_schema_generation(
|
|
|
202
269
|
)
|
|
203
270
|
return
|
|
204
271
|
|
|
205
|
-
|
|
272
|
+
registration_plan = _database_registration_plan(
|
|
273
|
+
databases,
|
|
274
|
+
scoped_schemas_by_database,
|
|
275
|
+
focused=bool(only_schemas),
|
|
276
|
+
models_only=models_only,
|
|
277
|
+
)
|
|
278
|
+
for db in registration_plan:
|
|
206
279
|
name = db.get("name")
|
|
207
280
|
prefix = db.get("env_prefix")
|
|
208
281
|
if not name or not prefix:
|
|
@@ -221,19 +294,19 @@ def run_schema_generation(
|
|
|
221
294
|
field_overrides=db.get("field_overrides") or {},
|
|
222
295
|
manager_config_overrides=db.get("manager_config_overrides") or {},
|
|
223
296
|
env_var_overrides=db.get("env_var_overrides") or {},
|
|
297
|
+
pool_min=db["pool_min"],
|
|
298
|
+
pool_max=db["pool_max"],
|
|
224
299
|
)
|
|
225
300
|
|
|
226
301
|
# -------------------------------------------------------------------------
|
|
227
302
|
# Run generation for each configured target
|
|
228
303
|
# -------------------------------------------------------------------------
|
|
229
|
-
generate_list = cfg.get("generate", [])
|
|
230
304
|
if not generate_list:
|
|
231
305
|
vcprint(
|
|
232
306
|
"[MATRX ORM] No entries in 'generate' — databases registered but nothing generated.",
|
|
233
307
|
color="yellow",
|
|
234
308
|
)
|
|
235
309
|
return
|
|
236
|
-
|
|
237
310
|
# model_name_prefix is RETIRED. The schema-aware model_registry (keyed on
|
|
238
311
|
# (schema, class_name)) makes same-named tables across schemas coexist with
|
|
239
312
|
# clean, prefix-free class names. A lingering prefix would silently re-create
|
|
@@ -309,7 +382,8 @@ def run_schema_generation(
|
|
|
309
382
|
output_overrides=output_overrides,
|
|
310
383
|
)
|
|
311
384
|
manager.initialize()
|
|
312
|
-
|
|
385
|
+
if not models_only:
|
|
386
|
+
manager.schema.generate_schema_files()
|
|
313
387
|
manager.schema.generate_models()
|
|
314
388
|
vcprint(
|
|
315
389
|
f"[MATRX ORM] Done — database='{db_name}' schema='{schema}'\n",
|
|
@@ -337,14 +411,20 @@ def run_schema_generation(
|
|
|
337
411
|
_close_pools()
|
|
338
412
|
raise
|
|
339
413
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
414
|
+
if not models_only:
|
|
415
|
+
_write_host_models_aggregator(
|
|
416
|
+
generate_list, python_root, output_config, host_schema_model_names
|
|
417
|
+
)
|
|
343
418
|
|
|
344
419
|
# Declarative host→package DB wiring: resolve each incorporated package's
|
|
345
420
|
# DB_REQUIREMENTS manifest against the inventory just written above and emit
|
|
346
421
|
# the aidream-side wiring module. Only when writing Python directly to disk.
|
|
347
|
-
if
|
|
422
|
+
if (
|
|
423
|
+
not models_only
|
|
424
|
+
and cfg.get("package_wiring")
|
|
425
|
+
and output_config.python
|
|
426
|
+
and output_config.save_direct
|
|
427
|
+
):
|
|
348
428
|
try:
|
|
349
429
|
from matrx_orm.schema_builder.package_wiring import generate_package_wiring
|
|
350
430
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.24"
|
|
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" }
|
|
@@ -44,6 +44,7 @@ dependencies = [
|
|
|
44
44
|
"matrx-utils>=1.0.20",
|
|
45
45
|
"pyyaml>=6.0.3",
|
|
46
46
|
"python-dotenv>=1.0",
|
|
47
|
+
"cryptography>=43.0",
|
|
47
48
|
]
|
|
48
49
|
|
|
49
50
|
[project.urls]
|
|
@@ -74,6 +74,26 @@ def test_conflict_target_normalises_list_to_tuple() -> None:
|
|
|
74
74
|
assert t.expressions == ("lower(name)", "kind")
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def test_typed_partial_target_renders_coalesce_and_predicates() -> None:
|
|
78
|
+
target = ConflictTarget.partial(
|
|
79
|
+
["project_id", "region", "status"],
|
|
80
|
+
coalesce={"region": ""},
|
|
81
|
+
where_is_null=("deleted_at",),
|
|
82
|
+
where_is_not_null=("host_resource_id",),
|
|
83
|
+
where_equals={"status": "active"},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert target.expressions == ("project_id", "COALESCE(region, '')", "status")
|
|
87
|
+
assert target.where == (
|
|
88
|
+
"deleted_at IS NULL AND host_resource_id IS NOT NULL AND status = 'active'"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_typed_partial_target_rejects_non_identifier_columns() -> None:
|
|
93
|
+
with pytest.raises(ValueError, match="simple identifier"):
|
|
94
|
+
ConflictTarget.partial(["project_id; DROP TABLE x"])
|
|
95
|
+
|
|
96
|
+
|
|
77
97
|
# ── Executor SQL shape ───────────────────────────────────────────────────────
|
|
78
98
|
|
|
79
99
|
|
|
@@ -24,7 +24,7 @@ from matrx_orm.core.expressions import (
|
|
|
24
24
|
_iter_subquery_conditions,
|
|
25
25
|
)
|
|
26
26
|
from matrx_orm.core.expressions import Q
|
|
27
|
-
from matrx_orm.query.executor import QueryExecutor
|
|
27
|
+
from matrx_orm.query.executor import QueryExecutor, _render_update_expression
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
# ── Array functions ─────────────────────────────────────────────────────────
|
|
@@ -262,3 +262,21 @@ def test_binary_expr_duck_types_as_sql_for_the_executor_fallback():
|
|
|
262
262
|
assert hasattr(expr, "as_sql")
|
|
263
263
|
params: list = []
|
|
264
264
|
assert expr.as_sql(params) == "(COALESCE(weight, $1) + $2)"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_multiple_update_expressions_shift_parameters_across_set_fields():
|
|
268
|
+
from matrx_orm import Coalesce, F
|
|
269
|
+
|
|
270
|
+
params: list = ["fetched-at", "task-id"]
|
|
271
|
+
request_sql = _render_update_expression(F("request_count") + 1, params)
|
|
272
|
+
provider_sql = _render_update_expression(
|
|
273
|
+
Coalesce(F("provider_cost"), 0) + 0.12, params
|
|
274
|
+
)
|
|
275
|
+
estimated_sql = _render_update_expression(
|
|
276
|
+
Coalesce(F("estimated_cost"), 0) + 0.10, params
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
assert request_sql == "request_count + $3"
|
|
280
|
+
assert provider_sql == "(COALESCE(provider_cost, $4) + $5)"
|
|
281
|
+
assert estimated_sql == "(COALESCE(estimated_cost, $6) + $7)"
|
|
282
|
+
assert params == ["fetched-at", "task-id", 1, 0, 0.12, 0, 0.10]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from matrx_orm.schema_builder.runner import (
|
|
6
|
+
_database_registration_plan,
|
|
7
|
+
_select_generation_targets,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_focused_target_scopes_dependencies_and_skips_unrelated_database() -> None:
|
|
12
|
+
entries = [
|
|
13
|
+
{
|
|
14
|
+
"database": "main",
|
|
15
|
+
"schema": "seo",
|
|
16
|
+
"dependency_schemas": ["auth", "iam", "platform"],
|
|
17
|
+
},
|
|
18
|
+
{"database": "cms", "schema": "public"},
|
|
19
|
+
]
|
|
20
|
+
selected, scoped = _select_generation_targets(entries, {"seo"})
|
|
21
|
+
plan = _database_registration_plan(
|
|
22
|
+
[
|
|
23
|
+
{"name": "main", "additional_schemas": ["seo", "web", "auth"]},
|
|
24
|
+
{"name": "cms", "additional_schemas": ["history"]},
|
|
25
|
+
],
|
|
26
|
+
scoped,
|
|
27
|
+
focused=True,
|
|
28
|
+
models_only=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assert selected == [entries[0]]
|
|
32
|
+
assert scoped == {"main": {"seo", "auth", "iam", "platform"}}
|
|
33
|
+
assert [item["name"] for item in plan] == ["main"]
|
|
34
|
+
assert plan[0]["additional_schemas"] == ["auth", "iam", "platform", "seo"]
|
|
35
|
+
assert (plan[0]["pool_min"], plan[0]["pool_max"]) == (1, 4)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_focused_target_without_dependencies_preserves_full_database_scope() -> None:
|
|
39
|
+
entries = [{"database": "main", "schema": "web"}]
|
|
40
|
+
_, scoped = _select_generation_targets(entries, {"web"})
|
|
41
|
+
plan = _database_registration_plan(
|
|
42
|
+
[{"name": "main", "additional_schemas": ["web", "auth", "iam"]}],
|
|
43
|
+
scoped,
|
|
44
|
+
focused=True,
|
|
45
|
+
models_only=False,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert scoped == {"main": None}
|
|
49
|
+
assert plan[0]["additional_schemas"] == ["web", "auth", "iam"]
|
|
50
|
+
assert (plan[0]["pool_min"], plan[0]["pool_max"]) == (None, None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_unknown_focused_schema_fails_before_registration() -> None:
|
|
54
|
+
with pytest.raises(ValueError, match="unknown schema target.*missing"):
|
|
55
|
+
_select_generation_targets(
|
|
56
|
+
[{"database": "main", "schema": "seo"}],
|
|
57
|
+
{"missing"},
|
|
58
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Secrets battery — crypto + contract tests (no DB required).
|
|
2
|
+
|
|
3
|
+
The DB-facing CRUD is exercised through aidream's existing user_secrets
|
|
4
|
+
tests; here we pin the parts that must never drift: key resolution,
|
|
5
|
+
round-trips, mask shape, env parsing, and the historical-compatibility
|
|
6
|
+
guarantee that a ciphertext written by the old aidream implementation
|
|
7
|
+
decrypts with the battery.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import hashlib
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from matrx_orm.secrets import crypto
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture(autouse=True)
|
|
21
|
+
def _clean_crypto_state(monkeypatch):
|
|
22
|
+
crypto.reset_encryption_for_tests()
|
|
23
|
+
monkeypatch.delenv("CREDENTIALS_ENCRYPTION_KEY", raising=False)
|
|
24
|
+
monkeypatch.delenv("SUPABASE_SECRET_KEY", raising=False)
|
|
25
|
+
yield
|
|
26
|
+
crypto.reset_encryption_for_tests()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fernet_key() -> str:
|
|
30
|
+
from cryptography.fernet import Fernet
|
|
31
|
+
|
|
32
|
+
return Fernet.generate_key().decode()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_no_key_is_loud():
|
|
36
|
+
with pytest.raises(RuntimeError, match="no encryption key configured"):
|
|
37
|
+
crypto.get_fernet()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_string_round_trip_env_key(monkeypatch):
|
|
41
|
+
monkeypatch.setenv("CREDENTIALS_ENCRYPTION_KEY", _fernet_key())
|
|
42
|
+
ct = crypto.encrypt_value("s3cret-value")
|
|
43
|
+
assert isinstance(ct, bytes)
|
|
44
|
+
assert crypto.decrypt_value(ct) == "s3cret-value"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_empty_string_round_trips(monkeypatch):
|
|
48
|
+
monkeypatch.setenv("CREDENTIALS_ENCRYPTION_KEY", _fernet_key())
|
|
49
|
+
assert crypto.decrypt_value(crypto.encrypt_value("")) == ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_payload_round_trip(monkeypatch):
|
|
53
|
+
monkeypatch.setenv("CREDENTIALS_ENCRYPTION_KEY", _fernet_key())
|
|
54
|
+
payload = {"api_key": "abc", "nested": {"n": 1}}
|
|
55
|
+
assert crypto.decrypt_payload(crypto.encrypt_payload(payload)) == payload
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_configure_encryption_overrides_env(monkeypatch):
|
|
59
|
+
monkeypatch.setenv("CREDENTIALS_ENCRYPTION_KEY", _fernet_key())
|
|
60
|
+
injected = _fernet_key()
|
|
61
|
+
crypto.configure_encryption(injected)
|
|
62
|
+
ct = crypto.encrypt_value("via-injected-key")
|
|
63
|
+
# Same key injected fresh must decrypt; env key must NOT have been used.
|
|
64
|
+
crypto.reset_encryption_for_tests()
|
|
65
|
+
crypto.configure_encryption(injected)
|
|
66
|
+
assert crypto.decrypt_value(ct) == "via-injected-key"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_non_fernet_key_is_sha256_derived_like_legacy(monkeypatch):
|
|
70
|
+
"""A non-44-char CREDENTIALS_ENCRYPTION_KEY derives via SHA-256 —
|
|
71
|
+
byte-identical to the historical aidream implementation, so every
|
|
72
|
+
existing ciphertext keeps decrypting."""
|
|
73
|
+
from cryptography.fernet import Fernet
|
|
74
|
+
|
|
75
|
+
monkeypatch.setenv("CREDENTIALS_ENCRYPTION_KEY", "some-passphrase-style-key")
|
|
76
|
+
legacy = Fernet(base64.urlsafe_b64encode(hashlib.sha256(b"some-passphrase-style-key").digest()))
|
|
77
|
+
legacy_ct = legacy.encrypt(b"written-by-old-code")
|
|
78
|
+
assert crypto.get_fernet().decrypt(legacy_ct) == b"written-by-old-code"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_supabase_seed_fallback_derivation(monkeypatch):
|
|
82
|
+
from cryptography.fernet import Fernet
|
|
83
|
+
|
|
84
|
+
monkeypatch.setenv("SUPABASE_SECRET_KEY", "seed-material")
|
|
85
|
+
derived = Fernet(base64.urlsafe_b64encode(hashlib.sha256(b"seed-material").digest()))
|
|
86
|
+
assert crypto.get_fernet().decrypt(derived.encrypt(b"x")) == b"x"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_mask_value_shapes():
|
|
90
|
+
assert crypto.mask_value("") == ""
|
|
91
|
+
assert crypto.mask_value("ghp_1234567890abcd") == "ghp_…abcd"
|
|
92
|
+
short = crypto.mask_value("abcd")
|
|
93
|
+
assert "…" in short and len(short) <= 5
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_type_guards(monkeypatch):
|
|
97
|
+
monkeypatch.setenv("CREDENTIALS_ENCRYPTION_KEY", _fernet_key())
|
|
98
|
+
with pytest.raises(TypeError):
|
|
99
|
+
crypto.encrypt_value(123) # type: ignore[arg-type]
|
|
100
|
+
with pytest.raises(TypeError):
|
|
101
|
+
crypto.decrypt_value("not-bytes") # type: ignore[arg-type]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_parse_env_text():
|
|
105
|
+
from matrx_orm.secrets import parse_env_text
|
|
106
|
+
|
|
107
|
+
text = """
|
|
108
|
+
# comment
|
|
109
|
+
export FOO=bar
|
|
110
|
+
QUOTED="with spaces"
|
|
111
|
+
SINGLE='sq'
|
|
112
|
+
EMPTY=
|
|
113
|
+
novalue
|
|
114
|
+
"""
|
|
115
|
+
pairs = dict(parse_env_text(text))
|
|
116
|
+
assert pairs == {"FOO": "bar", "QUOTED": "with spaces", "SINGLE": "sq", "EMPTY": ""}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_public_api_surface():
|
|
120
|
+
import matrx_orm.secrets as s
|
|
121
|
+
|
|
122
|
+
for name in (
|
|
123
|
+
"configure_secrets",
|
|
124
|
+
"require_user_secret",
|
|
125
|
+
"get_user_secret",
|
|
126
|
+
"create_user_secret",
|
|
127
|
+
"update_user_secret",
|
|
128
|
+
"delete_user_secret",
|
|
129
|
+
"list_user_secrets",
|
|
130
|
+
"bulk_upsert_from_env_text",
|
|
131
|
+
"encrypt_payload",
|
|
132
|
+
"decrypt_payload",
|
|
133
|
+
"UserSecret",
|
|
134
|
+
):
|
|
135
|
+
assert hasattr(s, name), name
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|