matrx-orm 3.1.76__tar.gz → 3.1.78__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.76 → matrx_orm-3.1.78}/CLAUDE.md +4 -1
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/PKG-INFO +5 -2
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/README.md +2 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/__init__.py +7 -0
- matrx_orm-3.1.78/matrx_orm/admin/FEATURE.md +34 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/admin/router.py +105 -36
- matrx_orm-3.1.78/matrx_orm/local_sqlite.py +40 -0
- matrx_orm-3.1.78/matrx_orm/operations/FEATURE.md +23 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/conflict_writes.py +9 -2
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/pyproject.toml +3 -2
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_admin_db_columns.py +64 -2
- matrx_orm-3.1.78/tests/level1/test_admin_response_rendering.py +61 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_conflict_writes.py +74 -0
- matrx_orm-3.1.78/tests/level1/test_local_sqlite.py +58 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/.gitignore +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/AGENTS.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/MODEL_API.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/docs/migrations.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/FEATURE.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/pooler_role.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/scripts/release.sh +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/conftest.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.76 → matrx_orm-3.1.78}/tests/test_sync_twins_guard.py +0 -0
|
@@ -38,8 +38,11 @@ Every table that becomes a Model needs row identity: a single-column primary key
|
|
|
38
38
|
Large reconciliation/export scans call `.all(use_cache=False)` so returned
|
|
39
39
|
models do not quadratically populate the process identity cache. Contract:
|
|
40
40
|
[`matrx_orm/query/FEATURE.md`](matrx_orm/query/FEATURE.md).
|
|
41
|
+
- **Conflict writes**: bulk insert-ignore and counter-upsert row coercion runs
|
|
42
|
+
off the event loop before statement chunking. Contract:
|
|
43
|
+
[`matrx_orm/operations/FEATURE.md`](matrx_orm/operations/FEATURE.md).
|
|
41
44
|
- **Migrations**: `MigrationDB`, `MigrationLoader`, `MigrationExecutor`, `makemigrations`, `migrate` — migrations declare explicit `dependencies` and apply in topological order (no multi-head/merge support yet).
|
|
42
|
-
- **Admin router** (FastAPI): `admin_router` exposes read/write endpoints for any registered model.
|
|
45
|
+
- **Admin router** (FastAPI): `admin_router` exposes read/write endpoints for any registered model; bulk response rendering stays off the event loop. Contract: [`matrx_orm/admin/FEATURE.md`](matrx_orm/admin/FEATURE.md).
|
|
43
46
|
- **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
|
|
44
47
|
- **Adapters**: `AsyncPostgreSQLAdapter`, `SupabaseAdapter`, `PostgRESTClientAdapter`.
|
|
45
48
|
- **Signals**: `pre_create`, `post_create`, `pre_save`, `post_save`, `pre_delete`, `post_delete`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.78
|
|
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
|
|
@@ -25,13 +25,14 @@ Requires-Python: >=3.13
|
|
|
25
25
|
Requires-Dist: asyncpg>=0.31.0
|
|
26
26
|
Requires-Dist: cryptography>=43.0
|
|
27
27
|
Requires-Dist: gitpython>=3.1.40
|
|
28
|
-
Requires-Dist: matrx-utils>=2.0.
|
|
28
|
+
Requires-Dist: matrx-utils>=2.0.19
|
|
29
29
|
Requires-Dist: psycopg-pool>=3.2.5
|
|
30
30
|
Requires-Dist: psycopg[binary]>=3.2.5
|
|
31
31
|
Requires-Dist: python-dotenv>=1.0
|
|
32
32
|
Requires-Dist: pyyaml>=6.0.3
|
|
33
33
|
Provides-Extra: admin
|
|
34
34
|
Requires-Dist: fastapi>=0.115; extra == 'admin'
|
|
35
|
+
Requires-Dist: orjson>=3.10; extra == 'admin'
|
|
35
36
|
Provides-Extra: api
|
|
36
37
|
Requires-Dist: aiohttp>=3.9.0; extra == 'api'
|
|
37
38
|
Description-Content-Type: text/markdown
|
|
@@ -56,6 +57,8 @@ Python 3.13+ required. Needs a PostgreSQL server (or Supabase / any Postgres-com
|
|
|
56
57
|
- **Admin router** (FastAPI): `admin_router` exposes a ready-to-mount set of read/write endpoints for every registered model.
|
|
57
58
|
- **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
|
|
58
59
|
- **Adapters**: `AsyncPostgreSQLAdapter`, `SupabaseAdapter`, `PostgRESTClientAdapter`.
|
|
60
|
+
- **Local artifact reads**: `read_local_sqlite_rows` provides a validated, read-only
|
|
61
|
+
projection boundary for SQLite files owned by external tools such as Chromium.
|
|
59
62
|
- **Signals**: `pre_create`, `post_create`, `pre_save`, `post_save`, `pre_delete`, `post_delete`.
|
|
60
63
|
- **Schema builder** (`matrx_orm.schema_builder`): code generation for Python + TypeScript type definitions from the live DB schema — useful for keeping a frontend's row types in sync.
|
|
61
64
|
|
|
@@ -18,6 +18,8 @@ Python 3.13+ required. Needs a PostgreSQL server (or Supabase / any Postgres-com
|
|
|
18
18
|
- **Admin router** (FastAPI): `admin_router` exposes a ready-to-mount set of read/write endpoints for every registered model.
|
|
19
19
|
- **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
|
|
20
20
|
- **Adapters**: `AsyncPostgreSQLAdapter`, `SupabaseAdapter`, `PostgRESTClientAdapter`.
|
|
21
|
+
- **Local artifact reads**: `read_local_sqlite_rows` provides a validated, read-only
|
|
22
|
+
projection boundary for SQLite files owned by external tools such as Chromium.
|
|
21
23
|
- **Signals**: `pre_create`, `post_create`, `pre_save`, `post_save`, `pre_delete`, `post_delete`.
|
|
22
24
|
- **Schema builder** (`matrx_orm.schema_builder`): code generation for Python + TypeScript type definitions from the live DB schema — useful for keeping a frontend's row types in sync.
|
|
23
25
|
|
|
@@ -43,6 +43,7 @@ from .platform_db import (
|
|
|
43
43
|
platform_connection_url,
|
|
44
44
|
register_platform_db,
|
|
45
45
|
)
|
|
46
|
+
from .local_sqlite import SQLiteValue, read_local_sqlite_rows
|
|
46
47
|
from .core.diagnostics import collect_pool_diagnostics, log_pool_diagnostics
|
|
47
48
|
from .core.introspection import introspect_columns, introspect_rls_policies, schema_exists
|
|
48
49
|
from .core.one_database import OneDatabaseReport, PoolIdentity, audit_one_database
|
|
@@ -633,6 +634,12 @@ __all__ = [
|
|
|
633
634
|
"AsyncPostgreSQLAdapter",
|
|
634
635
|
"SupabaseAdapter",
|
|
635
636
|
"PostgRESTClientAdapter",
|
|
637
|
+
"create_external_pool",
|
|
638
|
+
"get_registered_pool",
|
|
639
|
+
"open_listener_connection",
|
|
640
|
+
# ── Read-only local SQLite artifacts ──────────────────────────────────────
|
|
641
|
+
"SQLiteValue",
|
|
642
|
+
"read_local_sqlite_rows",
|
|
636
643
|
# ── Cache ──────────────────────────────────────────────────────────────────
|
|
637
644
|
"CachePolicy",
|
|
638
645
|
"StateManager",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Admin router
|
|
2
|
+
|
|
3
|
+
`router.py` exposes model-registry CRUD and cache operations through the optional
|
|
4
|
+
FastAPI admin surface.
|
|
5
|
+
|
|
6
|
+
## Response rendering
|
|
7
|
+
|
|
8
|
+
- **Bulk row rendering never runs on the event loop.** `list_rows` passes row
|
|
9
|
+
serialization and JSON encoding through `_render_list_rows_response`, which
|
|
10
|
+
offloads the CPU-bound work with `asyncio.to_thread`; `orjson` bounds the
|
|
11
|
+
GIL-held encoding section.
|
|
12
|
+
- **Keep both list aliases.** `data` and `rows` contain the same serialized rows;
|
|
13
|
+
admin clients use both shapes during compatibility transitions.
|
|
14
|
+
- **The page cap is 2,000 rows.** Keep `ADMIN_LIST_MAX_PAGE_SIZE` aligned with the
|
|
15
|
+
dashboard constant in `orm-proxy.ts`.
|
|
16
|
+
|
|
17
|
+
## Database identity
|
|
18
|
+
|
|
19
|
+
- **A table is `database.schema.table`.** `GET /tables` and cache stats expose
|
|
20
|
+
`database_name`; every columns, row, mutation, bulk, and cache endpoint accepts
|
|
21
|
+
`database=` and passes it to the multi-database model registry.
|
|
22
|
+
- **Ambiguity is a controlled conflict.** An omitted database that matches more
|
|
23
|
+
than one registered model returns HTTP 409 with the candidates. It never guesses
|
|
24
|
+
and never escapes as a request crash.
|
|
25
|
+
|
|
26
|
+
Verified against `router.py` and
|
|
27
|
+
`tests/level1/test_admin_db_columns.py` on 2026-08-17.
|
|
28
|
+
|
|
29
|
+
## Change Log
|
|
30
|
+
|
|
31
|
+
- 2026-08-17 — Moved bulk row serialization and JSON encoding off the event
|
|
32
|
+
loop after a 1.1-second production stall.
|
|
33
|
+
- 2026-08-17 — Carried database identity through every admin relation endpoint
|
|
34
|
+
and converted unqualified cross-database ambiguity into HTTP 409.
|
|
@@ -24,6 +24,7 @@ Effective routes (relative to the mount prefix):
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
+
import asyncio
|
|
27
28
|
import base64
|
|
28
29
|
import binascii
|
|
29
30
|
import json
|
|
@@ -32,7 +33,8 @@ import math
|
|
|
32
33
|
import re
|
|
33
34
|
from typing import Any
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
import orjson
|
|
37
|
+
from fastapi import APIRouter, HTTPException, Query, Request, Response
|
|
36
38
|
from fastapi.responses import JSONResponse
|
|
37
39
|
|
|
38
40
|
from matrx_orm.core.expressions import Q
|
|
@@ -56,22 +58,42 @@ _COMPOSITE_ROW_ID_PREFIX = "cmp1_"
|
|
|
56
58
|
# ---------------------------------------------------------------------------
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
def _model_by_table(
|
|
60
|
-
|
|
61
|
+
def _model_by_table(
|
|
62
|
+
table_name: str,
|
|
63
|
+
schema: str | None = None,
|
|
64
|
+
database: str | None = None,
|
|
65
|
+
) -> type | None:
|
|
66
|
+
return model_registry.get_model_by_table(
|
|
67
|
+
schema or "public",
|
|
68
|
+
table_name,
|
|
69
|
+
database=database,
|
|
70
|
+
)
|
|
61
71
|
|
|
62
72
|
|
|
63
|
-
def _require_model(
|
|
64
|
-
|
|
73
|
+
def _require_model(
|
|
74
|
+
table_name: str,
|
|
75
|
+
schema: str | None = None,
|
|
76
|
+
database: str | None = None,
|
|
77
|
+
) -> type:
|
|
78
|
+
try:
|
|
79
|
+
model = _model_by_table(table_name, schema, database)
|
|
80
|
+
except ValueError as exc:
|
|
81
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
65
82
|
if model is None:
|
|
66
83
|
effective_schema = schema or "public"
|
|
84
|
+
database_suffix = f" in database '{database}'" if database else ""
|
|
67
85
|
log.warning(
|
|
68
|
-
"Table '%s.%s' not found in model registry",
|
|
86
|
+
"Table '%s.%s' not found in model registry%s",
|
|
69
87
|
effective_schema,
|
|
70
88
|
table_name,
|
|
89
|
+
database_suffix,
|
|
71
90
|
)
|
|
72
91
|
raise HTTPException(
|
|
73
92
|
status_code=404,
|
|
74
|
-
detail=
|
|
93
|
+
detail=(
|
|
94
|
+
f"Table '{effective_schema}.{table_name}' not found in model registry"
|
|
95
|
+
f"{database_suffix}"
|
|
96
|
+
),
|
|
75
97
|
)
|
|
76
98
|
return model
|
|
77
99
|
|
|
@@ -113,6 +135,46 @@ def _serialize(instance: Any) -> dict[str, Any]:
|
|
|
113
135
|
return complete
|
|
114
136
|
|
|
115
137
|
|
|
138
|
+
def _build_list_rows_response(
|
|
139
|
+
items: list[Any],
|
|
140
|
+
*,
|
|
141
|
+
total_count: int,
|
|
142
|
+
page: int,
|
|
143
|
+
page_size: int,
|
|
144
|
+
) -> Response:
|
|
145
|
+
data = [_serialize(row) for row in items]
|
|
146
|
+
total_pages = math.ceil(total_count / page_size) if page_size > 0 else 0
|
|
147
|
+
return Response(
|
|
148
|
+
content=orjson.dumps(
|
|
149
|
+
{
|
|
150
|
+
"data": data,
|
|
151
|
+
"rows": data,
|
|
152
|
+
"total": total_count,
|
|
153
|
+
"page": page,
|
|
154
|
+
"page_size": page_size,
|
|
155
|
+
"total_pages": total_pages,
|
|
156
|
+
}
|
|
157
|
+
),
|
|
158
|
+
media_type="application/json",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def _render_list_rows_response(
|
|
163
|
+
items: list[Any],
|
|
164
|
+
*,
|
|
165
|
+
total_count: int,
|
|
166
|
+
page: int,
|
|
167
|
+
page_size: int,
|
|
168
|
+
) -> Response:
|
|
169
|
+
return await asyncio.to_thread(
|
|
170
|
+
_build_list_rows_response,
|
|
171
|
+
items,
|
|
172
|
+
total_count=total_count,
|
|
173
|
+
page=page,
|
|
174
|
+
page_size=page_size,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
116
178
|
def _decode_row_id(model: type, row_id: str) -> str | dict[str, Any]:
|
|
117
179
|
primary_keys = list(model._meta.primary_keys or ())
|
|
118
180
|
if len(primary_keys) <= 1:
|
|
@@ -386,16 +448,19 @@ async def _fetch_check_choices(model: type) -> dict[str, list[str]]:
|
|
|
386
448
|
async def list_tables() -> list[dict[str, object]]:
|
|
387
449
|
models = model_registry.all_models()
|
|
388
450
|
log.info("list_tables: %d models in registry", len(models))
|
|
389
|
-
|
|
390
|
-
db_counts = await _fetch_db_column_counts(sample_model) if sample_model else {}
|
|
451
|
+
counts_by_database: dict[str, dict[tuple[str, str], int]] = {}
|
|
391
452
|
tables = []
|
|
392
453
|
for model in models.values():
|
|
393
454
|
meta = model._meta
|
|
394
455
|
schema = _model_schema(model)
|
|
456
|
+
database = model.get_database_name()
|
|
457
|
+
if database not in counts_by_database:
|
|
458
|
+
counts_by_database[database] = await _fetch_db_column_counts(model)
|
|
395
459
|
orm_count = len(meta.fields)
|
|
396
|
-
db_count =
|
|
460
|
+
db_count = counts_by_database[database].get((schema, meta.table_name))
|
|
397
461
|
tables.append(
|
|
398
462
|
{
|
|
463
|
+
"database_name": database,
|
|
399
464
|
"table_name": meta.table_name,
|
|
400
465
|
"table_schema": schema,
|
|
401
466
|
"name": meta.table_name,
|
|
@@ -403,7 +468,7 @@ async def list_tables() -> list[dict[str, object]]:
|
|
|
403
468
|
"column_count": db_count if db_count is not None else orm_count,
|
|
404
469
|
}
|
|
405
470
|
)
|
|
406
|
-
tables.sort(key=lambda t: (t["table_schema"], t["table_name"]))
|
|
471
|
+
tables.sort(key=lambda t: (t["database_name"], t["table_schema"], t["table_name"]))
|
|
407
472
|
return tables
|
|
408
473
|
|
|
409
474
|
|
|
@@ -411,8 +476,9 @@ async def list_tables() -> list[dict[str, object]]:
|
|
|
411
476
|
async def get_columns(
|
|
412
477
|
table_name: str,
|
|
413
478
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
479
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
414
480
|
) -> list[dict[str, object]]:
|
|
415
|
-
model = _require_model(table_name, schema)
|
|
481
|
+
model = _require_model(table_name, schema, database)
|
|
416
482
|
columns = await _admin_column_descriptors(model)
|
|
417
483
|
|
|
418
484
|
# Enrich with CHECK constraint choices from the database
|
|
@@ -605,8 +671,9 @@ async def list_rows(
|
|
|
605
671
|
sort_dir: str = Query("asc", pattern="^(asc|desc)$"),
|
|
606
672
|
search: str | None = Query(None, description="Column filters as 'col1:val1,col2:val2'"),
|
|
607
673
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
608
|
-
|
|
609
|
-
|
|
674
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
675
|
+
) -> Response:
|
|
676
|
+
model = _require_model(table_name, schema, database)
|
|
610
677
|
log.info(
|
|
611
678
|
"list_rows: table=%s page=%d size=%d sort_by=%s sort_dir=%s search=%s",
|
|
612
679
|
table_name,
|
|
@@ -629,25 +696,18 @@ async def list_rows(
|
|
|
629
696
|
paginator = Paginator(qb, per_page=page_size)
|
|
630
697
|
# page() is 1-based; our API uses 0-based pages
|
|
631
698
|
page_obj = await paginator.page(page + 1)
|
|
632
|
-
data = [_serialize(row) for row in page_obj.items]
|
|
633
|
-
total_pages = math.ceil(page_obj.total_count / page_size) if page_size > 0 else 0
|
|
634
|
-
|
|
635
699
|
log.info(
|
|
636
700
|
"list_rows: %s returned %d rows (total %d)",
|
|
637
701
|
table_name,
|
|
638
|
-
len(
|
|
702
|
+
len(page_obj.items),
|
|
639
703
|
page_obj.total_count,
|
|
640
704
|
)
|
|
641
705
|
|
|
642
|
-
return
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
"page": page,
|
|
648
|
-
"page_size": page_size,
|
|
649
|
-
"total_pages": total_pages,
|
|
650
|
-
}
|
|
706
|
+
return await _render_list_rows_response(
|
|
707
|
+
page_obj.items,
|
|
708
|
+
total_count=page_obj.total_count,
|
|
709
|
+
page=page,
|
|
710
|
+
page_size=page_size,
|
|
651
711
|
)
|
|
652
712
|
|
|
653
713
|
except ValueError:
|
|
@@ -672,8 +732,9 @@ async def get_row(
|
|
|
672
732
|
table_name: str,
|
|
673
733
|
row_id: str,
|
|
674
734
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
735
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
675
736
|
) -> JSONResponse:
|
|
676
|
-
model = _require_model(table_name, schema)
|
|
737
|
+
model = _require_model(table_name, schema, database)
|
|
677
738
|
log.info("get_row: table=%s id=%s", table_name, row_id)
|
|
678
739
|
|
|
679
740
|
try:
|
|
@@ -689,8 +750,9 @@ async def create_row(
|
|
|
689
750
|
table_name: str,
|
|
690
751
|
request: Request,
|
|
691
752
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
753
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
692
754
|
) -> JSONResponse:
|
|
693
|
-
model = _require_model(table_name, schema)
|
|
755
|
+
model = _require_model(table_name, schema, database)
|
|
694
756
|
body: dict[str, Any] = await request.json()
|
|
695
757
|
log.info("create_row: table=%s fields=%s", table_name, list(body.keys()))
|
|
696
758
|
|
|
@@ -709,8 +771,9 @@ async def update_row(
|
|
|
709
771
|
row_id: str,
|
|
710
772
|
request: Request,
|
|
711
773
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
774
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
712
775
|
) -> JSONResponse:
|
|
713
|
-
model = _require_model(table_name, schema)
|
|
776
|
+
model = _require_model(table_name, schema, database)
|
|
714
777
|
body: dict[str, Any] = await request.json()
|
|
715
778
|
log.info("update_row: table=%s id=%s fields=%s", table_name, row_id, list(body.keys()))
|
|
716
779
|
|
|
@@ -729,8 +792,9 @@ async def delete_row(
|
|
|
729
792
|
table_name: str,
|
|
730
793
|
row_id: str,
|
|
731
794
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
795
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
732
796
|
) -> JSONResponse:
|
|
733
|
-
model = _require_model(table_name, schema)
|
|
797
|
+
model = _require_model(table_name, schema, database)
|
|
734
798
|
log.info("delete_row: table=%s id=%s", table_name, row_id)
|
|
735
799
|
|
|
736
800
|
try:
|
|
@@ -748,10 +812,11 @@ async def bulk_delete_rows(
|
|
|
748
812
|
table_name: str,
|
|
749
813
|
request: Request,
|
|
750
814
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
815
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
751
816
|
) -> JSONResponse:
|
|
752
817
|
"""Delete many rows in one call. Never fails the whole batch on one bad id —
|
|
753
818
|
returns a partial-result envelope so the caller can report what landed."""
|
|
754
|
-
model = _require_model(table_name, schema)
|
|
819
|
+
model = _require_model(table_name, schema, database)
|
|
755
820
|
body: dict[str, Any] = await request.json()
|
|
756
821
|
ids = body.get("ids")
|
|
757
822
|
if not isinstance(ids, list) or not ids:
|
|
@@ -795,11 +860,12 @@ async def bulk_update_rows(
|
|
|
795
860
|
table_name: str,
|
|
796
861
|
request: Request,
|
|
797
862
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
863
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
798
864
|
) -> JSONResponse:
|
|
799
865
|
"""Apply the same field changes to many rows in one call. Never fails the
|
|
800
866
|
whole batch on one bad id — returns a partial-result envelope (same shape
|
|
801
867
|
as bulk-delete) so the caller can report what landed."""
|
|
802
|
-
model = _require_model(table_name, schema)
|
|
868
|
+
model = _require_model(table_name, schema, database)
|
|
803
869
|
body: dict[str, Any] = await request.json()
|
|
804
870
|
ids = body.get("ids")
|
|
805
871
|
data = body.get("data")
|
|
@@ -866,12 +932,13 @@ async def cache_stats() -> list[dict[str, object]]:
|
|
|
866
932
|
results.append(
|
|
867
933
|
{
|
|
868
934
|
"model_name": model.__name__,
|
|
935
|
+
"database_name": model.get_database_name(),
|
|
869
936
|
"table_name": model._meta.table_name,
|
|
870
937
|
"table_schema": _model_schema(model),
|
|
871
938
|
"cached_count": count,
|
|
872
939
|
}
|
|
873
940
|
)
|
|
874
|
-
results.sort(key=lambda r: (r["table_schema"], r["table_name"]))
|
|
941
|
+
results.sort(key=lambda r: (r["database_name"], r["table_schema"], r["table_name"]))
|
|
875
942
|
return results
|
|
876
943
|
|
|
877
944
|
|
|
@@ -879,9 +946,10 @@ async def cache_stats() -> list[dict[str, object]]:
|
|
|
879
946
|
async def clear_table_cache(
|
|
880
947
|
table_name: str,
|
|
881
948
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
949
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
882
950
|
) -> JSONResponse:
|
|
883
951
|
"""Flush the entire in-memory cache for a table."""
|
|
884
|
-
model = _require_model(table_name, schema)
|
|
952
|
+
model = _require_model(table_name, schema, database)
|
|
885
953
|
log.info("clear_table_cache: %s", table_name)
|
|
886
954
|
|
|
887
955
|
try:
|
|
@@ -897,9 +965,10 @@ async def evict_row_cache(
|
|
|
897
965
|
table_name: str,
|
|
898
966
|
row_id: str,
|
|
899
967
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
968
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
900
969
|
) -> JSONResponse:
|
|
901
970
|
"""Evict a single record from the in-memory cache."""
|
|
902
|
-
model = _require_model(table_name, schema)
|
|
971
|
+
model = _require_model(table_name, schema, database)
|
|
903
972
|
log.info("evict_row_cache: %s/%s", table_name, row_id)
|
|
904
973
|
|
|
905
974
|
try:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sqlite3
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
type SQLiteValue = str | int | float | bytes | None
|
|
9
|
+
|
|
10
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _identifier(value: str) -> str:
|
|
14
|
+
if not _IDENTIFIER.fullmatch(value):
|
|
15
|
+
raise ValueError(f"invalid SQLite identifier: {value!r}")
|
|
16
|
+
return f'"{value}"'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_local_sqlite_rows(
|
|
20
|
+
database: str | Path,
|
|
21
|
+
*,
|
|
22
|
+
table: str,
|
|
23
|
+
columns: Sequence[str],
|
|
24
|
+
order_by: Sequence[str] = (),
|
|
25
|
+
) -> list[tuple[SQLiteValue, ...]]:
|
|
26
|
+
"""Read a projected table from a local SQLite artifact in read-only mode."""
|
|
27
|
+
if not columns:
|
|
28
|
+
raise ValueError("columns must contain at least one identifier")
|
|
29
|
+
|
|
30
|
+
projection = ", ".join(_identifier(column) for column in columns)
|
|
31
|
+
statement = f"SELECT {projection} FROM {_identifier(table)}"
|
|
32
|
+
if order_by:
|
|
33
|
+
statement += " ORDER BY " + ", ".join(_identifier(column) for column in order_by)
|
|
34
|
+
|
|
35
|
+
path = Path(database).expanduser().resolve()
|
|
36
|
+
connection = sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)
|
|
37
|
+
try:
|
|
38
|
+
return [tuple(row) for row in connection.execute(statement)]
|
|
39
|
+
finally:
|
|
40
|
+
connection.close()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Conflict-write operations
|
|
2
|
+
|
|
3
|
+
`conflict_writes.py` provides typed insert-ignore and conflict-update primitives
|
|
4
|
+
for append-only rows, partial unique indexes, and atomic counters.
|
|
5
|
+
|
|
6
|
+
## Event-loop safety
|
|
7
|
+
|
|
8
|
+
- **Bulk row coercion never runs on the event loop.** `bulk_insert_ignore` and
|
|
9
|
+
`bulk_upsert_increment` pass model construction, defaults, and field DB-value
|
|
10
|
+
conversion through `_prep_bulk_rows`, which uses `asyncio.to_thread`.
|
|
11
|
+
- **Single-row writes stay inline.** `insert_ignore` and
|
|
12
|
+
`upsert_with_conflict` avoid thread-dispatch overhead for one row.
|
|
13
|
+
- **Statement chunking still enforces the bind-parameter ceiling.** Offloading
|
|
14
|
+
row preparation does not change `_MAX_BIND_PARAMS` or caller-connection
|
|
15
|
+
transaction semantics.
|
|
16
|
+
|
|
17
|
+
Verified against `conflict_writes.py` and
|
|
18
|
+
`tests/level1/test_conflict_writes.py` on 2026-08-17.
|
|
19
|
+
|
|
20
|
+
## Change Log
|
|
21
|
+
|
|
22
|
+
- 2026-08-17 — Moved bulk model materialization off the event loop after a
|
|
23
|
+
scheduled search-performance import froze unrelated requests for 2.8 seconds.
|
|
@@ -33,6 +33,7 @@ primitive instead of hand-rolled f-strings.
|
|
|
33
33
|
|
|
34
34
|
from __future__ import annotations
|
|
35
35
|
|
|
36
|
+
import asyncio
|
|
36
37
|
import re
|
|
37
38
|
from typing import TYPE_CHECKING, Any
|
|
38
39
|
|
|
@@ -115,6 +116,12 @@ def _prep_rows(model_cls: type[Model], objects_data: list[dict[str, Any]]) -> li
|
|
|
115
116
|
return data_list
|
|
116
117
|
|
|
117
118
|
|
|
119
|
+
async def _prep_bulk_rows(
|
|
120
|
+
model_cls: type[Model], objects_data: list[dict[str, Any]]
|
|
121
|
+
) -> list[dict[str, Any]]:
|
|
122
|
+
return await asyncio.to_thread(_prep_rows, model_cls, objects_data)
|
|
123
|
+
|
|
124
|
+
|
|
118
125
|
async def _run(
|
|
119
126
|
model_cls: type[Model],
|
|
120
127
|
sql: str,
|
|
@@ -171,7 +178,7 @@ async def bulk_insert_ignore(
|
|
|
171
178
|
if isinstance(returning, str) and not _IDENT_RE.match(returning):
|
|
172
179
|
raise ValueError(f"returning column must be a plain identifier; got {returning!r}")
|
|
173
180
|
returning_clause = "*" if returning is True else returning if returning else "1"
|
|
174
|
-
data_list =
|
|
181
|
+
data_list = await _prep_bulk_rows(model_cls, objects_data)
|
|
175
182
|
target_clause = _render_conflict_target(on_conflict)
|
|
176
183
|
table = model_cls._meta.qualified_table_name
|
|
177
184
|
col_count = len({k for row in data_list for k in row})
|
|
@@ -362,7 +369,7 @@ async def bulk_upsert_increment(
|
|
|
362
369
|
raise ValueError(f"set_fields column must be a plain identifier; got {col!r}")
|
|
363
370
|
if col in increments:
|
|
364
371
|
raise ValueError(f"{col!r} is in both increments and set_fields — pick one")
|
|
365
|
-
data_list =
|
|
372
|
+
data_list = await _prep_bulk_rows(model_cls, objects_data)
|
|
366
373
|
target_clause = _render_conflict_target(on_conflict)
|
|
367
374
|
table = model_cls._meta.qualified_table_name
|
|
368
375
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.78"
|
|
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" }
|
|
@@ -41,7 +41,7 @@ dependencies = [
|
|
|
41
41
|
"gitpython>=3.1.40",
|
|
42
42
|
"psycopg[binary]>=3.2.5",
|
|
43
43
|
"psycopg-pool>=3.2.5",
|
|
44
|
-
"matrx-utils>=2.0.
|
|
44
|
+
"matrx-utils>=2.0.19",
|
|
45
45
|
"pyyaml>=6.0.3",
|
|
46
46
|
"python-dotenv>=1.0",
|
|
47
47
|
"cryptography>=43.0",
|
|
@@ -59,6 +59,7 @@ api = [
|
|
|
59
59
|
]
|
|
60
60
|
admin = [
|
|
61
61
|
"fastapi>=0.115",
|
|
62
|
+
"orjson>=3.10",
|
|
62
63
|
]
|
|
63
64
|
|
|
64
65
|
[project.scripts]
|
|
@@ -4,13 +4,16 @@ from types import SimpleNamespace
|
|
|
4
4
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
+
from fastapi import HTTPException
|
|
7
8
|
from matrx_orm.admin.router import (
|
|
8
9
|
COMPOSITE_ROW_ID_FIELD,
|
|
9
10
|
_admin_column_descriptors,
|
|
10
11
|
_db_row_to_descriptor,
|
|
11
12
|
_decode_row_id,
|
|
12
13
|
_model_by_table,
|
|
14
|
+
_require_model,
|
|
13
15
|
_serialize,
|
|
16
|
+
list_tables,
|
|
14
17
|
)
|
|
15
18
|
|
|
16
19
|
|
|
@@ -35,7 +38,7 @@ def test_admin_model_lookup_defaults_to_exact_public_key() -> None:
|
|
|
35
38
|
return_value=sentinel,
|
|
36
39
|
) as lookup:
|
|
37
40
|
assert _model_by_table("definition") is sentinel
|
|
38
|
-
lookup.assert_called_once_with("public", "definition")
|
|
41
|
+
lookup.assert_called_once_with("public", "definition", database=None)
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
def test_admin_model_lookup_uses_requested_schema() -> None:
|
|
@@ -44,7 +47,66 @@ def test_admin_model_lookup_uses_requested_schema() -> None:
|
|
|
44
47
|
return_value=None,
|
|
45
48
|
) as lookup:
|
|
46
49
|
assert _model_by_table("definition", "agent") is None
|
|
47
|
-
lookup.assert_called_once_with("agent", "definition")
|
|
50
|
+
lookup.assert_called_once_with("agent", "definition", database=None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_admin_model_lookup_uses_requested_database() -> None:
|
|
54
|
+
sentinel = object()
|
|
55
|
+
with patch(
|
|
56
|
+
"matrx_orm.admin.router.model_registry.get_model_by_table",
|
|
57
|
+
return_value=sentinel,
|
|
58
|
+
) as lookup:
|
|
59
|
+
assert _model_by_table("users", "auth", "main") is sentinel
|
|
60
|
+
lookup.assert_called_once_with("auth", "users", database="main")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_admin_model_lookup_returns_conflict_for_ambiguous_relation() -> None:
|
|
64
|
+
with (
|
|
65
|
+
patch(
|
|
66
|
+
"matrx_orm.admin.router.model_registry.get_model_by_table",
|
|
67
|
+
side_effect=ValueError("Table 'auth.users' is ambiguous across databases"),
|
|
68
|
+
),
|
|
69
|
+
pytest.raises(HTTPException) as exc_info,
|
|
70
|
+
):
|
|
71
|
+
_require_model("users", "auth")
|
|
72
|
+
|
|
73
|
+
assert exc_info.value.status_code == 409
|
|
74
|
+
assert "ambiguous" in str(exc_info.value.detail)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_admin_table_inventory_exposes_database_identity() -> None:
|
|
79
|
+
model = SimpleNamespace(
|
|
80
|
+
__name__="Users",
|
|
81
|
+
_meta=SimpleNamespace(
|
|
82
|
+
table_name="users",
|
|
83
|
+
db_schema="auth",
|
|
84
|
+
fields={"id": object()},
|
|
85
|
+
),
|
|
86
|
+
get_database_name=lambda: "supabase_automation_matrix",
|
|
87
|
+
)
|
|
88
|
+
with (
|
|
89
|
+
patch(
|
|
90
|
+
"matrx_orm.admin.router.model_registry.all_models",
|
|
91
|
+
return_value={("supabase_automation_matrix", "auth", "Users"): model},
|
|
92
|
+
),
|
|
93
|
+
patch(
|
|
94
|
+
"matrx_orm.admin.router._fetch_db_column_counts",
|
|
95
|
+
new=AsyncMock(return_value={("auth", "users"): 24}),
|
|
96
|
+
),
|
|
97
|
+
):
|
|
98
|
+
tables = await list_tables()
|
|
99
|
+
|
|
100
|
+
assert tables == [
|
|
101
|
+
{
|
|
102
|
+
"database_name": "supabase_automation_matrix",
|
|
103
|
+
"table_name": "users",
|
|
104
|
+
"table_schema": "auth",
|
|
105
|
+
"name": "users",
|
|
106
|
+
"model_name": "Users",
|
|
107
|
+
"column_count": 24,
|
|
108
|
+
}
|
|
109
|
+
]
|
|
48
110
|
|
|
49
111
|
|
|
50
112
|
def test_composite_row_identity_round_trips() -> None:
|