matrx-orm 3.1.77__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.77 → matrx_orm-3.1.78}/CLAUDE.md +3 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/PKG-INFO +4 -2
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/README.md +2 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/__init__.py +7 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/admin/FEATURE.md +12 -1
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/admin/router.py +55 -21
- 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.77 → matrx_orm-3.1.78}/matrx_orm/operations/conflict_writes.py +9 -2
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/pyproject.toml +2 -2
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_admin_db_columns.py +64 -2
- {matrx_orm-3.1.77 → 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.77 → matrx_orm-3.1.78}/.gitignore +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/AGENTS.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/MODEL_API.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/docs/migrations.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/FEATURE.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/pooler_role.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/scripts/release.sh +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/conftest.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_admin_response_rendering.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.77 → matrx_orm-3.1.78}/tests/test_sync_twins_guard.py +0 -0
|
@@ -38,6 +38,9 @@ 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
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`.
|
|
@@ -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,7 +25,7 @@ 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
|
|
@@ -57,6 +57,8 @@ Python 3.13+ required. Needs a PostgreSQL server (or Supabase / any Postgres-com
|
|
|
57
57
|
- **Admin router** (FastAPI): `admin_router` exposes a ready-to-mount set of read/write endpoints for every registered model.
|
|
58
58
|
- **API layer** (optional `[api]` extra): `APIServer`, `APIConfig`, `TokenAuth`.
|
|
59
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.
|
|
60
62
|
- **Signals**: `pre_create`, `post_create`, `pre_save`, `post_save`, `pre_delete`, `post_delete`.
|
|
61
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.
|
|
62
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",
|
|
@@ -14,10 +14,21 @@ FastAPI admin surface.
|
|
|
14
14
|
- **The page cap is 2,000 rows.** Keep `ADMIN_LIST_MAX_PAGE_SIZE` aligned with the
|
|
15
15
|
dashboard constant in `orm-proxy.ts`.
|
|
16
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
|
+
|
|
17
26
|
Verified against `router.py` and
|
|
18
|
-
`tests/level1/
|
|
27
|
+
`tests/level1/test_admin_db_columns.py` on 2026-08-17.
|
|
19
28
|
|
|
20
29
|
## Change Log
|
|
21
30
|
|
|
22
31
|
- 2026-08-17 — Moved bulk row serialization and JSON encoding off the event
|
|
23
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.
|
|
@@ -58,22 +58,42 @@ _COMPOSITE_ROW_ID_PREFIX = "cmp1_"
|
|
|
58
58
|
# ---------------------------------------------------------------------------
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def _model_by_table(
|
|
62
|
-
|
|
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
|
+
)
|
|
63
71
|
|
|
64
72
|
|
|
65
|
-
def _require_model(
|
|
66
|
-
|
|
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
|
|
67
82
|
if model is None:
|
|
68
83
|
effective_schema = schema or "public"
|
|
84
|
+
database_suffix = f" in database '{database}'" if database else ""
|
|
69
85
|
log.warning(
|
|
70
|
-
"Table '%s.%s' not found in model registry",
|
|
86
|
+
"Table '%s.%s' not found in model registry%s",
|
|
71
87
|
effective_schema,
|
|
72
88
|
table_name,
|
|
89
|
+
database_suffix,
|
|
73
90
|
)
|
|
74
91
|
raise HTTPException(
|
|
75
92
|
status_code=404,
|
|
76
|
-
detail=
|
|
93
|
+
detail=(
|
|
94
|
+
f"Table '{effective_schema}.{table_name}' not found in model registry"
|
|
95
|
+
f"{database_suffix}"
|
|
96
|
+
),
|
|
77
97
|
)
|
|
78
98
|
return model
|
|
79
99
|
|
|
@@ -428,16 +448,19 @@ async def _fetch_check_choices(model: type) -> dict[str, list[str]]:
|
|
|
428
448
|
async def list_tables() -> list[dict[str, object]]:
|
|
429
449
|
models = model_registry.all_models()
|
|
430
450
|
log.info("list_tables: %d models in registry", len(models))
|
|
431
|
-
|
|
432
|
-
db_counts = await _fetch_db_column_counts(sample_model) if sample_model else {}
|
|
451
|
+
counts_by_database: dict[str, dict[tuple[str, str], int]] = {}
|
|
433
452
|
tables = []
|
|
434
453
|
for model in models.values():
|
|
435
454
|
meta = model._meta
|
|
436
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)
|
|
437
459
|
orm_count = len(meta.fields)
|
|
438
|
-
db_count =
|
|
460
|
+
db_count = counts_by_database[database].get((schema, meta.table_name))
|
|
439
461
|
tables.append(
|
|
440
462
|
{
|
|
463
|
+
"database_name": database,
|
|
441
464
|
"table_name": meta.table_name,
|
|
442
465
|
"table_schema": schema,
|
|
443
466
|
"name": meta.table_name,
|
|
@@ -445,7 +468,7 @@ async def list_tables() -> list[dict[str, object]]:
|
|
|
445
468
|
"column_count": db_count if db_count is not None else orm_count,
|
|
446
469
|
}
|
|
447
470
|
)
|
|
448
|
-
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"]))
|
|
449
472
|
return tables
|
|
450
473
|
|
|
451
474
|
|
|
@@ -453,8 +476,9 @@ async def list_tables() -> list[dict[str, object]]:
|
|
|
453
476
|
async def get_columns(
|
|
454
477
|
table_name: str,
|
|
455
478
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
479
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
456
480
|
) -> list[dict[str, object]]:
|
|
457
|
-
model = _require_model(table_name, schema)
|
|
481
|
+
model = _require_model(table_name, schema, database)
|
|
458
482
|
columns = await _admin_column_descriptors(model)
|
|
459
483
|
|
|
460
484
|
# Enrich with CHECK constraint choices from the database
|
|
@@ -647,8 +671,9 @@ async def list_rows(
|
|
|
647
671
|
sort_dir: str = Query("asc", pattern="^(asc|desc)$"),
|
|
648
672
|
search: str | None = Query(None, description="Column filters as 'col1:val1,col2:val2'"),
|
|
649
673
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
674
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
650
675
|
) -> Response:
|
|
651
|
-
model = _require_model(table_name, schema)
|
|
676
|
+
model = _require_model(table_name, schema, database)
|
|
652
677
|
log.info(
|
|
653
678
|
"list_rows: table=%s page=%d size=%d sort_by=%s sort_dir=%s search=%s",
|
|
654
679
|
table_name,
|
|
@@ -707,8 +732,9 @@ async def get_row(
|
|
|
707
732
|
table_name: str,
|
|
708
733
|
row_id: str,
|
|
709
734
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
735
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
710
736
|
) -> JSONResponse:
|
|
711
|
-
model = _require_model(table_name, schema)
|
|
737
|
+
model = _require_model(table_name, schema, database)
|
|
712
738
|
log.info("get_row: table=%s id=%s", table_name, row_id)
|
|
713
739
|
|
|
714
740
|
try:
|
|
@@ -724,8 +750,9 @@ async def create_row(
|
|
|
724
750
|
table_name: str,
|
|
725
751
|
request: Request,
|
|
726
752
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
753
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
727
754
|
) -> JSONResponse:
|
|
728
|
-
model = _require_model(table_name, schema)
|
|
755
|
+
model = _require_model(table_name, schema, database)
|
|
729
756
|
body: dict[str, Any] = await request.json()
|
|
730
757
|
log.info("create_row: table=%s fields=%s", table_name, list(body.keys()))
|
|
731
758
|
|
|
@@ -744,8 +771,9 @@ async def update_row(
|
|
|
744
771
|
row_id: str,
|
|
745
772
|
request: Request,
|
|
746
773
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
774
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
747
775
|
) -> JSONResponse:
|
|
748
|
-
model = _require_model(table_name, schema)
|
|
776
|
+
model = _require_model(table_name, schema, database)
|
|
749
777
|
body: dict[str, Any] = await request.json()
|
|
750
778
|
log.info("update_row: table=%s id=%s fields=%s", table_name, row_id, list(body.keys()))
|
|
751
779
|
|
|
@@ -764,8 +792,9 @@ async def delete_row(
|
|
|
764
792
|
table_name: str,
|
|
765
793
|
row_id: str,
|
|
766
794
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
795
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
767
796
|
) -> JSONResponse:
|
|
768
|
-
model = _require_model(table_name, schema)
|
|
797
|
+
model = _require_model(table_name, schema, database)
|
|
769
798
|
log.info("delete_row: table=%s id=%s", table_name, row_id)
|
|
770
799
|
|
|
771
800
|
try:
|
|
@@ -783,10 +812,11 @@ async def bulk_delete_rows(
|
|
|
783
812
|
table_name: str,
|
|
784
813
|
request: Request,
|
|
785
814
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
815
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
786
816
|
) -> JSONResponse:
|
|
787
817
|
"""Delete many rows in one call. Never fails the whole batch on one bad id —
|
|
788
818
|
returns a partial-result envelope so the caller can report what landed."""
|
|
789
|
-
model = _require_model(table_name, schema)
|
|
819
|
+
model = _require_model(table_name, schema, database)
|
|
790
820
|
body: dict[str, Any] = await request.json()
|
|
791
821
|
ids = body.get("ids")
|
|
792
822
|
if not isinstance(ids, list) or not ids:
|
|
@@ -830,11 +860,12 @@ async def bulk_update_rows(
|
|
|
830
860
|
table_name: str,
|
|
831
861
|
request: Request,
|
|
832
862
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
863
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
833
864
|
) -> JSONResponse:
|
|
834
865
|
"""Apply the same field changes to many rows in one call. Never fails the
|
|
835
866
|
whole batch on one bad id — returns a partial-result envelope (same shape
|
|
836
867
|
as bulk-delete) so the caller can report what landed."""
|
|
837
|
-
model = _require_model(table_name, schema)
|
|
868
|
+
model = _require_model(table_name, schema, database)
|
|
838
869
|
body: dict[str, Any] = await request.json()
|
|
839
870
|
ids = body.get("ids")
|
|
840
871
|
data = body.get("data")
|
|
@@ -901,12 +932,13 @@ async def cache_stats() -> list[dict[str, object]]:
|
|
|
901
932
|
results.append(
|
|
902
933
|
{
|
|
903
934
|
"model_name": model.__name__,
|
|
935
|
+
"database_name": model.get_database_name(),
|
|
904
936
|
"table_name": model._meta.table_name,
|
|
905
937
|
"table_schema": _model_schema(model),
|
|
906
938
|
"cached_count": count,
|
|
907
939
|
}
|
|
908
940
|
)
|
|
909
|
-
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"]))
|
|
910
942
|
return results
|
|
911
943
|
|
|
912
944
|
|
|
@@ -914,9 +946,10 @@ async def cache_stats() -> list[dict[str, object]]:
|
|
|
914
946
|
async def clear_table_cache(
|
|
915
947
|
table_name: str,
|
|
916
948
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
949
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
917
950
|
) -> JSONResponse:
|
|
918
951
|
"""Flush the entire in-memory cache for a table."""
|
|
919
|
-
model = _require_model(table_name, schema)
|
|
952
|
+
model = _require_model(table_name, schema, database)
|
|
920
953
|
log.info("clear_table_cache: %s", table_name)
|
|
921
954
|
|
|
922
955
|
try:
|
|
@@ -932,9 +965,10 @@ async def evict_row_cache(
|
|
|
932
965
|
table_name: str,
|
|
933
966
|
row_id: str,
|
|
934
967
|
schema: str | None = Query(None, description="PostgreSQL schema name"),
|
|
968
|
+
database: str | None = Query(None, description="Registered database project name"),
|
|
935
969
|
) -> JSONResponse:
|
|
936
970
|
"""Evict a single record from the in-memory cache."""
|
|
937
|
-
model = _require_model(table_name, schema)
|
|
971
|
+
model = _require_model(table_name, schema, database)
|
|
938
972
|
log.info("evict_row_cache: %s/%s", table_name, row_id)
|
|
939
973
|
|
|
940
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",
|
|
@@ -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:
|
|
@@ -13,6 +13,9 @@ caller-connection seam are all exercised without a registered database.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import asyncio
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
16
19
|
from typing import Any
|
|
17
20
|
|
|
18
21
|
import pytest
|
|
@@ -27,6 +30,7 @@ from matrx_orm.operations.conflict_writes import (
|
|
|
27
30
|
bulk_insert_ignore,
|
|
28
31
|
bulk_upsert_increment,
|
|
29
32
|
)
|
|
33
|
+
import matrx_orm.operations.conflict_writes as conflict_writes
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
class _Meta:
|
|
@@ -143,6 +147,76 @@ async def test_insert_ignore_empty_is_noop_without_touching_connection() -> None
|
|
|
143
147
|
assert conn.calls == []
|
|
144
148
|
|
|
145
149
|
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_insert_ignore_prepares_bulk_rows_off_event_loop(
|
|
152
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
153
|
+
) -> None:
|
|
154
|
+
model = _make_model("t", {"id": TextField()})
|
|
155
|
+
conn = _CaptureConn()
|
|
156
|
+
loop_thread = threading.get_ident()
|
|
157
|
+
prep_thread: int | None = None
|
|
158
|
+
original_prep_rows = conflict_writes._prep_rows
|
|
159
|
+
|
|
160
|
+
def slow_prep_rows(
|
|
161
|
+
model_cls: type,
|
|
162
|
+
objects_data: list[dict[str, Any]],
|
|
163
|
+
) -> list[dict[str, Any]]:
|
|
164
|
+
nonlocal prep_thread
|
|
165
|
+
prep_thread = threading.get_ident()
|
|
166
|
+
time.sleep(0.05)
|
|
167
|
+
return original_prep_rows(model_cls, objects_data)
|
|
168
|
+
|
|
169
|
+
monkeypatch.setattr(conflict_writes, "_prep_rows", slow_prep_rows)
|
|
170
|
+
write_task = asyncio.create_task(
|
|
171
|
+
bulk_insert_ignore(model, [{"id": "x"}], on_conflict=["id"], connection=conn)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
await asyncio.sleep(0.01)
|
|
175
|
+
|
|
176
|
+
assert not write_task.done()
|
|
177
|
+
await write_task
|
|
178
|
+
assert prep_thread is not None
|
|
179
|
+
assert prep_thread != loop_thread
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.asyncio
|
|
183
|
+
async def test_increment_prepares_bulk_rows_off_event_loop(
|
|
184
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
185
|
+
) -> None:
|
|
186
|
+
model = _make_model("t", {"id": TextField(), "count": IntegerField()})
|
|
187
|
+
conn = _CaptureConn()
|
|
188
|
+
loop_thread = threading.get_ident()
|
|
189
|
+
prep_thread: int | None = None
|
|
190
|
+
original_prep_rows = conflict_writes._prep_rows
|
|
191
|
+
|
|
192
|
+
def slow_prep_rows(
|
|
193
|
+
model_cls: type,
|
|
194
|
+
objects_data: list[dict[str, Any]],
|
|
195
|
+
) -> list[dict[str, Any]]:
|
|
196
|
+
nonlocal prep_thread
|
|
197
|
+
prep_thread = threading.get_ident()
|
|
198
|
+
time.sleep(0.05)
|
|
199
|
+
return original_prep_rows(model_cls, objects_data)
|
|
200
|
+
|
|
201
|
+
monkeypatch.setattr(conflict_writes, "_prep_rows", slow_prep_rows)
|
|
202
|
+
write_task = asyncio.create_task(
|
|
203
|
+
bulk_upsert_increment(
|
|
204
|
+
model,
|
|
205
|
+
[{"id": "x", "count": 1}],
|
|
206
|
+
on_conflict=["id"],
|
|
207
|
+
increments={"count": 1},
|
|
208
|
+
connection=conn,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
await asyncio.sleep(0.01)
|
|
213
|
+
|
|
214
|
+
assert not write_task.done()
|
|
215
|
+
await write_task
|
|
216
|
+
assert prep_thread is not None
|
|
217
|
+
assert prep_thread != loop_thread
|
|
218
|
+
|
|
219
|
+
|
|
146
220
|
@pytest.mark.asyncio
|
|
147
221
|
async def test_insert_ignore_returning_column_gives_landed_ids_only() -> None:
|
|
148
222
|
"""The matrx-rag derived-chunk write shape: ON CONFLICT DO NOTHING, then
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from matrx_orm import read_local_sqlite_rows
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _fixture_database(path: Path) -> None:
|
|
12
|
+
connection = sqlite3.connect(path)
|
|
13
|
+
try:
|
|
14
|
+
connection.execute("CREATE TABLE events (id INTEGER PRIMARY KEY, label TEXT, payload BLOB)")
|
|
15
|
+
connection.executemany(
|
|
16
|
+
"INSERT INTO events (id, label, payload) VALUES (?, ?, ?)",
|
|
17
|
+
[(2, "later", b"two"), (1, "first", b"one")],
|
|
18
|
+
)
|
|
19
|
+
connection.commit()
|
|
20
|
+
finally:
|
|
21
|
+
connection.close()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_read_local_sqlite_rows_projects_and_orders(tmp_path: Path) -> None:
|
|
25
|
+
database = tmp_path / "artifact.db"
|
|
26
|
+
_fixture_database(database)
|
|
27
|
+
|
|
28
|
+
rows = read_local_sqlite_rows(
|
|
29
|
+
database,
|
|
30
|
+
table="events",
|
|
31
|
+
columns=("label", "payload"),
|
|
32
|
+
order_by=("id",),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert rows == [("first", b"one"), ("later", b"two")]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_read_local_sqlite_rows_rejects_statement_shaped_table_name(tmp_path: Path) -> None:
|
|
39
|
+
database = tmp_path / "artifact.db"
|
|
40
|
+
_fixture_database(database)
|
|
41
|
+
|
|
42
|
+
with pytest.raises(ValueError, match="invalid SQLite identifier"):
|
|
43
|
+
read_local_sqlite_rows(
|
|
44
|
+
database,
|
|
45
|
+
table='events"; DELETE FROM events; --',
|
|
46
|
+
columns=("id",),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize("columns", [(), ("label DESC",)])
|
|
51
|
+
def test_read_local_sqlite_rows_rejects_invalid_projection(
|
|
52
|
+
tmp_path: Path, columns: tuple[str, ...]
|
|
53
|
+
) -> None:
|
|
54
|
+
database = tmp_path / "artifact.db"
|
|
55
|
+
_fixture_database(database)
|
|
56
|
+
|
|
57
|
+
with pytest.raises(ValueError):
|
|
58
|
+
read_local_sqlite_rows(database, table="events", columns=columns)
|
|
File without changes
|
|
File without changes
|