matrx-orm 3.1.74__tar.gz → 3.1.75__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.74 → matrx_orm-3.1.75}/CLAUDE.md +2 -2
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/PKG-INFO +2 -2
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/exceptions.py +62 -2
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/update.py +128 -23
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/package_wiring.py +5 -2
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/README.md +6 -1
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/lifecycle.py +61 -22
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/pyproject.toml +2 -2
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_optimistic_concurrency.py +197 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_watched_lifecycle.py +43 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/.gitignore +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/AGENTS.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/MODEL_API.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/docs/migrations.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/catalog.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/FEATURE.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/pooler_role.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/FEATURE.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/scripts/release.sh +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/conftest.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/live_pooler_role.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema_builder/test_array_default_parsing.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_pooler_role_guard.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.74 → matrx_orm-3.1.75}/tests/test_sync_twins_guard.py +0 -0
|
@@ -23,7 +23,7 @@ Every table that becomes a Model needs row identity: a single-column primary key
|
|
|
23
23
|
- 🚨 **ONE CONNECTION, REQUIRED** (`matrx_orm.platform_db`): `register_platform_db(name, ...)` + `platform_connection_url()` — the ONLY sanctioned way any Matrx service or package turns env vars into a pool. One set of variables (`SUPABASE_MATRIX_*`), owned here; TLS is a deployment input (`SUPABASE_MATRIX_SSL`), never hardcoded; **RAISES** if it isn't given a database — no degraded mode. Every package's `bootstrap_db()` is one call to it. A package may absolutely run on its OWN Postgres — that is a change of VALUES, never a new variable name; what it may never have is a *second candidate* or a fallback chain. `register_database_from_env` is the low-level primitive underneath — **application and package code must not call it directly** (guard: `scripts/check_one_database.py`, blocking in release).
|
|
24
24
|
- **Core model layer**: `Model`, `BaseManager`, `BaseDTO`, `ModelView`, `model_registry`, plus 50+ field types (`CharField`, `IntegerField`, `ForeignKey`, `ManyToManyField`, `JSONField`, …).
|
|
25
25
|
- **`MatrxEntity`** (`matrx_orm/entity.py`) — **the platform entity contract as an ORM primitive.** A table registered in `platform.entity_types` generates as a `MatrxEntity` (everything else stays a plain `Model`) and gets the behavior instead of every consumer hand-wiring it: `await page.versions()` (not a query on `history.row_versions`), `await page.restore(3)` (not a `version_restore` RPC + a cache bust — `restore()` busts the cache itself), `Note.alive()` / `.dead()` / `.soft_delete()` / `.undelete()`, and `Page.capabilities()`. Capability flags (`_entity_token`, `_is_versioned`, `_has_soft_delete`, `_is_org_scoped`, `_rls_variant`) are **stamped by the generator from the LIVE DATABASE** (does the table really carry the `_version_capture` trigger? a `deleted_at` column?) — never from `entity_types`' own flags, which are wrong on 61 of its 242 rows. **The database is truth; a flag is a claim.** Using a capability an entity does not declare RAISES (`EntityCapabilityError`) and names the fix — it never returns silently-wrong data, and `is_deleted` refuses to answer from a column a partial fetch never loaded. Drift guard: `scripts/check_entity_drift.py` (loud, non-blocking, in `release.sh`).
|
|
26
|
-
- **Optimistic concurrency (`expected_version=`)** — opt-in compare-and-swap on the canonical `version` int column: `update_where` / `update_item` / instance `update()` take `expected_version=N` (`WHERE version = N`, `SET version = N + 1`; 0 rows → `DoesNotExist` if gone, `OptimisticLockError`
|
|
26
|
+
- **Optimistic concurrency (`expected_version=`)** — opt-in compare-and-swap on the canonical `version` int column: `update_where` / `update_item` / instance `update()` take `expected_version=N` (`WHERE version = N`, `SET version = N + 1`; 0 rows → `DoesNotExist` if gone, `OptimisticLockError` if changed — carrying the **decision package**: `current_version`, `current_row`, `contested_fields` (current values of ONLY the fields this write attempted), and `changed_by`/`changed_at` from `history.row_versions` where versioned); `bulk_update(..., check_version=True)` guards per row; `MatrxEntity.update_with_rebase` auto-rebases past DISJOINT concurrent writes using `history.row_versions` snapshots. **Never automatic** — an unguarded write stays last-write-wins (the `_touch_row` trigger bumps `version` on every update, so auto-guarding would raise spurious conflicts). Contract: `common-docs/systems/optimistic-concurrency/FEATURE.md`.
|
|
27
27
|
- **Change tracking** — every instance records which columns were **loaded** (a SELECTed row, or the caller's kwargs) and which were **changed**. `save()` writes only those, so `.only("id","email")` + `save()` can never NULL the columns it never fetched (it used to — silent data loss). Mutable containers (jsonb/arrays) are always rewritten when loaded, because `obj.metadata["k"]=1` cannot be seen by `__setattr__`; **reassigning** (`obj.metadata = {...}`) is tracked precisely and is the sanctioned way. `Model._hydrate()` is the ONE row→model entry point.
|
|
28
28
|
- **Upsert writes only what the caller supplied.** `upsert` / `bulk_upsert` / `upsert_with_conflict` materialize every field's Python default so the **INSERT** half is complete — but `ON CONFLICT DO UPDATE SET` covers **only the columns present in the caller's `data`** (an explicit `update_fields` still wins). Letting defaults reach the UPDATE half silently RESET every defaulted column on each re-upsert of an existing row: it once reset scraped-and-analyzed `research.rs_source` rows to `scrape_status='pending', server_attempts=0` (a watchdog then finalized them `'failed'`) and undid users' manual `is_included=false`. A caller-supplied `None` is excluded too — it is dropped from the INSERT, and `EXCLUDED.<col>` on an omitted column resolves to that column's **DEFAULT**. An upsert of only conflict keys means "ensure this row exists" and reconciles to a no-op self-assignment, never a raise (a bare `DO NOTHING` would break `RETURNING *`). Pinned by `tests/level1/test_upsert_default_leak.py`.
|
|
29
29
|
- **Read-only models** (`_read_only = True`): a model that can be queried but NEVER written through the ORM — every write path raises `ReadOnlyModelError` (enforced at `QueryExecutor.insert/bulk_insert/upsert/update/delete`, so it covers all of `create`/`save`/`update`/`delete`/`update_where`/`delete_where`/`upsert`/`bulk_*`). Used for DB-managed tables the app must not mutate. The canonical **`auth.users`** model is baked into the schema generator (`get_string_user_model`) as the full, curated Supabase auth schema (safe columns only — no password/token fields), `_read_only`, bound per-database. Read a user's profile/metadata there; mutate users only through Supabase Auth (GoTrue).
|
|
@@ -98,7 +98,7 @@ If the admin router ever needs auth beyond token-based, inject a `resolve_user`
|
|
|
98
98
|
|
|
99
99
|
- Full type hints — the ORM's contract with callers is very type-heavy and IDE discoverability matters.
|
|
100
100
|
- No docstrings except on public API (Model, QueryBuilder, field classes, migration entry points). One line.
|
|
101
|
-
- Hot paths (`QueryBuilder.execute`, adapter fetches, dict→dataclass transforms) must stay allocation-light. Favor
|
|
101
|
+
- Hot paths (`QueryBuilder.execute`, adapter fetches, dict→dataclass transforms) must stay allocation-light. Favor mandated dataclasses / `__slots__`.
|
|
102
102
|
- Explicit exception handling — never swallow `asyncpg` errors.
|
|
103
103
|
|
|
104
104
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.75
|
|
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.16
|
|
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
|
|
@@ -5,7 +5,10 @@ import ssl
|
|
|
5
5
|
import traceback as _tb
|
|
6
6
|
from collections.abc import Mapping
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
+
from datetime import date, datetime, time
|
|
9
|
+
from decimal import Decimal
|
|
8
10
|
from typing import Any
|
|
11
|
+
from uuid import UUID
|
|
9
12
|
|
|
10
13
|
from matrx_utils import vcprint
|
|
11
14
|
|
|
@@ -1899,11 +1902,49 @@ class UnknownDatabaseError(ORMException):
|
|
|
1899
1902
|
return f"{_RED}{body}{_RESET}"
|
|
1900
1903
|
|
|
1901
1904
|
|
|
1905
|
+
def jsonable(value: object) -> object:
|
|
1906
|
+
"""Coerce a DB value into something JSON-serializable, losslessly where we can.
|
|
1907
|
+
|
|
1908
|
+
Error payloads cross the wire (the platform 409 `version_conflict` body is
|
|
1909
|
+
typed `JsonValue`), so a datetime / UUID / Decimal in a conflict's contested
|
|
1910
|
+
fields must not be what turns a conflict into a 500. Unknown types degrade
|
|
1911
|
+
to `str()` — a display value is always better than a crash inside an error
|
|
1912
|
+
handler.
|
|
1913
|
+
"""
|
|
1914
|
+
if value is None or isinstance(value, bool | int | float | str):
|
|
1915
|
+
return value
|
|
1916
|
+
if isinstance(value, datetime | date | time):
|
|
1917
|
+
return value.isoformat()
|
|
1918
|
+
if isinstance(value, Decimal):
|
|
1919
|
+
return float(value)
|
|
1920
|
+
if isinstance(value, UUID):
|
|
1921
|
+
return str(value)
|
|
1922
|
+
if isinstance(value, Mapping):
|
|
1923
|
+
return {str(k): jsonable(v) for k, v in value.items()}
|
|
1924
|
+
if isinstance(value, list | tuple | set | frozenset):
|
|
1925
|
+
return [jsonable(v) for v in value]
|
|
1926
|
+
return str(value)
|
|
1927
|
+
|
|
1928
|
+
|
|
1902
1929
|
class OptimisticLockError(ORMException):
|
|
1903
1930
|
"""Raised when an optimistic-lock version conflict is detected.
|
|
1904
1931
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1932
|
+
THE LOSER GETS A DECISION, NOT AN ERROR (platform ruling 2026-08-10, item 3).
|
|
1933
|
+
A caller that lost a compare-and-swap already holds "proposed" (its own
|
|
1934
|
+
payload); this exception carries everything else it needs to RESOLVE rather
|
|
1935
|
+
than blindly retry:
|
|
1936
|
+
|
|
1937
|
+
``expected_version`` — the revision the caller wrote against
|
|
1938
|
+
``current_version`` — the revision that actually won
|
|
1939
|
+
``contested_fields`` — {field: current value} for ONLY the fields this
|
|
1940
|
+
write attempted (never the whole row)
|
|
1941
|
+
``changed_by`` / ``changed_at`` — who moved the row and when, from
|
|
1942
|
+
``history.row_versions``; None on an unversioned
|
|
1943
|
+
entity or when the lookup found nothing
|
|
1944
|
+
``current_row`` — the live instance, for a merge UI without a re-read
|
|
1945
|
+
|
|
1946
|
+
Resolution is just another write: keep-mine / keep-theirs / merged, resubmitted
|
|
1947
|
+
against ``current_version``. No queue, no resolution endpoint.
|
|
1907
1948
|
"""
|
|
1908
1949
|
|
|
1909
1950
|
def __init__(
|
|
@@ -1914,13 +1955,29 @@ class OptimisticLockError(ORMException):
|
|
|
1914
1955
|
message: str | None = None,
|
|
1915
1956
|
current_version: int | None = None,
|
|
1916
1957
|
current_row=None,
|
|
1958
|
+
contested_fields: Mapping[str, object] | None = None,
|
|
1959
|
+
changed_by: object = None,
|
|
1960
|
+
changed_at: object = None,
|
|
1917
1961
|
) -> None:
|
|
1918
1962
|
msg = message or (
|
|
1919
1963
|
f"Optimistic lock conflict on {getattr(model, '__name__', model)} "
|
|
1920
1964
|
f"(pk={pk}, expected version={expected_version}"
|
|
1921
1965
|
+ (f", current version={current_version}" if current_version is not None else "")
|
|
1966
|
+
+ (f", contested fields={sorted(contested_fields)}" if contested_fields else "")
|
|
1967
|
+
+ (f", changed by {changed_by}" if changed_by else "")
|
|
1922
1968
|
+ "). The record was modified by another process — re-fetch and retry."
|
|
1923
1969
|
)
|
|
1970
|
+
# Stored already-jsonable: this is a decision package for a human or a
|
|
1971
|
+
# wire payload, not a data-access path (that is `current_row`).
|
|
1972
|
+
self.contested_fields: dict[str, object] | None = (
|
|
1973
|
+
{str(k): jsonable(v) for k, v in contested_fields.items()}
|
|
1974
|
+
if contested_fields is not None
|
|
1975
|
+
else None
|
|
1976
|
+
)
|
|
1977
|
+
self.changed_by: str | None = str(changed_by) if changed_by is not None else None
|
|
1978
|
+
self.changed_at: str | None = (
|
|
1979
|
+
jsonable(changed_at) if changed_at is not None else None # type: ignore[assignment]
|
|
1980
|
+
)
|
|
1924
1981
|
super().__init__(
|
|
1925
1982
|
message=msg,
|
|
1926
1983
|
model=model,
|
|
@@ -1928,6 +1985,9 @@ class OptimisticLockError(ORMException):
|
|
|
1928
1985
|
"pk": pk,
|
|
1929
1986
|
"expected_version": expected_version,
|
|
1930
1987
|
"current_version": current_version,
|
|
1988
|
+
"contested_fields": self.contested_fields,
|
|
1989
|
+
"changed_by": self.changed_by,
|
|
1990
|
+
"changed_at": self.changed_at,
|
|
1931
1991
|
},
|
|
1932
1992
|
)
|
|
1933
1993
|
self.expected_version = expected_version
|
|
@@ -37,36 +37,129 @@ def _require_version_column(model_cls: type[Model], version_column: str) -> None
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def _contested_values(
|
|
41
|
+
model_cls: type[Model],
|
|
42
|
+
current: Model,
|
|
43
|
+
attempted_fields: Iterable[str] | None,
|
|
44
|
+
version_column: str,
|
|
45
|
+
) -> dict[str, Any] | None:
|
|
46
|
+
"""The CURRENT values of just the fields the losing write attempted.
|
|
47
|
+
|
|
48
|
+
Only the contested fields — never a dump of the whole row (a conflict body
|
|
49
|
+
is not a read endpoint, and the row may carry columns the caller is not
|
|
50
|
+
entitled to see). The version column and any primary key are excluded: the
|
|
51
|
+
revisions travel as their own fields and the pk was never in contest.
|
|
52
|
+
"""
|
|
53
|
+
if not attempted_fields:
|
|
54
|
+
return None
|
|
55
|
+
pks = set(model_cls._meta.primary_keys or [])
|
|
56
|
+
return {
|
|
57
|
+
name: getattr(current, name, None)
|
|
58
|
+
for name in attempted_fields
|
|
59
|
+
if name != version_column and name not in pks and name in model_cls._fields
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def _conflict_actor(
|
|
64
|
+
model_cls: type[Model],
|
|
65
|
+
pk_filter: dict[str, Any],
|
|
66
|
+
current_version: int | None,
|
|
67
|
+
) -> tuple[Any, Any]:
|
|
68
|
+
"""(changed_by, changed_at) for the winning revision, from history.row_versions.
|
|
69
|
+
|
|
70
|
+
Best-effort by construction: absent (None, None) on an unversioned entity, a
|
|
71
|
+
composite-PK table (the version log keys on a single ``row_id``), a missing
|
|
72
|
+
snapshot, or any failure reading the log. A conflict must NEVER be turned
|
|
73
|
+
into a different error because the courtesy lookup did not work out.
|
|
74
|
+
"""
|
|
75
|
+
from matrx_orm.entity import MatrxEntity
|
|
76
|
+
|
|
77
|
+
if not isinstance(model_cls, type) or not issubclass(model_cls, MatrxEntity):
|
|
78
|
+
return (None, None)
|
|
79
|
+
if not (model_cls._is_versioned and model_cls._entity_token) or current_version is None:
|
|
80
|
+
return (None, None)
|
|
81
|
+
if len(pk_filter) != 1:
|
|
82
|
+
return (None, None)
|
|
83
|
+
row_id = next(iter(pk_filter.values()))
|
|
84
|
+
try:
|
|
85
|
+
log = model_cls._version_log()
|
|
86
|
+
snapshots = (
|
|
87
|
+
await log.filter(
|
|
88
|
+
entity_type=model_cls._entity_token,
|
|
89
|
+
row_id=str(row_id),
|
|
90
|
+
version=int(current_version),
|
|
91
|
+
)
|
|
92
|
+
.order_by("-id")
|
|
93
|
+
.limit(1)
|
|
94
|
+
.all()
|
|
95
|
+
)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
vcprint(
|
|
98
|
+
f"[matrx-orm] conflict actor lookup failed for {model_cls.__name__} "
|
|
99
|
+
f"{row_id} v{current_version} ({type(exc).__name__}: {exc}) — the "
|
|
100
|
+
f"conflict still carries its versions and contested fields.",
|
|
101
|
+
color="yellow",
|
|
102
|
+
log_level="WARNING",
|
|
103
|
+
)
|
|
104
|
+
return (None, None)
|
|
105
|
+
if not snapshots:
|
|
106
|
+
return (None, None)
|
|
107
|
+
snapshot = snapshots[0]
|
|
108
|
+
return (getattr(snapshot, "actor_id", None), getattr(snapshot, "occurred_at", None))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def _build_conflict(
|
|
112
|
+
model_cls: type[Model],
|
|
113
|
+
pk_filter: dict[str, Any],
|
|
114
|
+
version_column: str,
|
|
115
|
+
expected_version: int | None,
|
|
116
|
+
current: Model,
|
|
117
|
+
attempted_fields: Iterable[str] | None,
|
|
118
|
+
) -> Any:
|
|
119
|
+
"""Assemble the full decision package for a lost compare-and-swap."""
|
|
120
|
+
from matrx_orm.exceptions import OptimisticLockError
|
|
121
|
+
|
|
122
|
+
current_version = getattr(current, version_column, None)
|
|
123
|
+
changed_by, changed_at = await _conflict_actor(model_cls, pk_filter, current_version)
|
|
124
|
+
return OptimisticLockError(
|
|
125
|
+
model=model_cls,
|
|
126
|
+
pk=pk_filter if len(pk_filter) > 1 else next(iter(pk_filter.values())),
|
|
127
|
+
expected_version=expected_version,
|
|
128
|
+
current_version=current_version,
|
|
129
|
+
current_row=current,
|
|
130
|
+
contested_fields=_contested_values(model_cls, current, attempted_fields, version_column),
|
|
131
|
+
changed_by=changed_by,
|
|
132
|
+
changed_at=changed_at,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
40
136
|
async def _raise_stale_write(
|
|
41
137
|
model_cls: type[Model],
|
|
42
138
|
pk_filter: dict[str, Any],
|
|
43
139
|
version_column: str,
|
|
44
140
|
expected_version: int | None,
|
|
141
|
+
attempted_fields: Iterable[str] | None = None,
|
|
45
142
|
) -> None:
|
|
46
143
|
"""Classify a guarded UPDATE that hit 0 rows: gone vs. changed.
|
|
47
144
|
|
|
48
145
|
Re-reads by primary key alone (no version filter, no cache). Row absent →
|
|
49
146
|
DoesNotExist (deleted concurrently, or never existed). Row present → its
|
|
50
|
-
version no longer matches → OptimisticLockError carrying the live row
|
|
147
|
+
version no longer matches → OptimisticLockError carrying the live row and
|
|
148
|
+
the decision package (contested field values + who changed it, when).
|
|
51
149
|
"""
|
|
52
|
-
from matrx_orm.exceptions import DoesNotExist
|
|
150
|
+
from matrx_orm.exceptions import DoesNotExist
|
|
53
151
|
|
|
54
152
|
# Note: inside an open Session the re-read merges PENDING (uncommitted)
|
|
55
153
|
# writes for this row — a CAS should run on the direct write paths, not
|
|
56
154
|
# Coordinator-adjacent code (see the contract's session-plane exclusion).
|
|
57
155
|
current = await QueryBuilder(model_cls).filter(**pk_filter).get_or_none()
|
|
58
|
-
pk_value = pk_filter if len(pk_filter) > 1 else next(iter(pk_filter.values()))
|
|
59
156
|
if current is None or getattr(current, "deleted_at", None) is not None:
|
|
60
157
|
# Hard-gone or soft-deleted: for conflict classification a soft-deleted
|
|
61
158
|
# row IS gone — reporting it as "changed by someone else, refresh and
|
|
62
159
|
# re-apply" would send the user chasing a record that no longer exists.
|
|
63
160
|
raise DoesNotExist(model=model_cls, filters=pk_filter)
|
|
64
|
-
raise
|
|
65
|
-
|
|
66
|
-
pk=pk_value,
|
|
67
|
-
expected_version=expected_version,
|
|
68
|
-
current_version=getattr(current, version_column, None),
|
|
69
|
-
current_row=current,
|
|
161
|
+
raise await _build_conflict(
|
|
162
|
+
model_cls, pk_filter, version_column, expected_version, current, attempted_fields
|
|
70
163
|
)
|
|
71
164
|
|
|
72
165
|
|
|
@@ -91,17 +184,21 @@ async def guarded_update(
|
|
|
91
184
|
(e.g. `version=F("version") + 1`), as matrx-graph's DefinitionStore does.
|
|
92
185
|
|
|
93
186
|
0 rows affected raises: DoesNotExist when the row is gone,
|
|
94
|
-
OptimisticLockError (with current_version
|
|
95
|
-
|
|
187
|
+
OptimisticLockError (with current_version, current_row, the contested
|
|
188
|
+
fields' current values and who changed them) when it changed. The
|
|
189
|
+
gone-vs-changed re-read uses the PRIMARY-KEY columns only, so an extra
|
|
96
190
|
narrowing filter that stopped matching still classifies as a conflict.
|
|
97
191
|
"""
|
|
98
192
|
_require_version_column(model_cls, version_column)
|
|
99
193
|
data = dict(update_data)
|
|
194
|
+
attempted_fields = list(data)
|
|
100
195
|
data.setdefault(version_column, expected_version + 1)
|
|
101
196
|
result = await update(model_cls, {**filters, version_column: expected_version}, **data)
|
|
102
197
|
if result.rows_affected == 0:
|
|
103
198
|
pk_filter = {pk: filters[pk] for pk in model_cls._meta.primary_keys}
|
|
104
|
-
await _raise_stale_write(
|
|
199
|
+
await _raise_stale_write(
|
|
200
|
+
model_cls, pk_filter, version_column, expected_version, attempted_fields
|
|
201
|
+
)
|
|
105
202
|
return result
|
|
106
203
|
|
|
107
204
|
|
|
@@ -183,7 +280,11 @@ async def bulk_update(
|
|
|
183
280
|
result = await QueryBuilder(model_cls).filter(**row_filter).update(**update_data)
|
|
184
281
|
if check_version and result.rows_affected == 0:
|
|
185
282
|
await _raise_stale_write(
|
|
186
|
-
model_cls,
|
|
283
|
+
model_cls,
|
|
284
|
+
pk_filter,
|
|
285
|
+
VERSION_COLUMN,
|
|
286
|
+
getattr(obj, VERSION_COLUMN, None),
|
|
287
|
+
fields,
|
|
187
288
|
)
|
|
188
289
|
if result.rows_affected > 0:
|
|
189
290
|
rows_affected += 1
|
|
@@ -271,8 +372,6 @@ async def guarded_delete(
|
|
|
271
372
|
version → OptimisticLockError (someone rewrote what the caller decided to
|
|
272
373
|
delete; deleting it anyway would discard their work unseen).
|
|
273
374
|
"""
|
|
274
|
-
from matrx_orm.exceptions import OptimisticLockError
|
|
275
|
-
|
|
276
375
|
_require_version_column(model_cls, version_column)
|
|
277
376
|
deleted = (
|
|
278
377
|
await QueryBuilder(model_cls)
|
|
@@ -284,12 +383,10 @@ async def guarded_delete(
|
|
|
284
383
|
current = await QueryBuilder(model_cls).filter(**pk_filter).get_or_none()
|
|
285
384
|
# Soft-deleted counts as already gone — idempotent 0, not a conflict.
|
|
286
385
|
if current is not None and getattr(current, "deleted_at", None) is None:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
expected_version
|
|
291
|
-
current_version=getattr(current, version_column, None),
|
|
292
|
-
current_row=current,
|
|
386
|
+
# A delete contests no individual field (it contests the whole row),
|
|
387
|
+
# so contested_fields stays absent; the actor half still applies.
|
|
388
|
+
raise await _build_conflict(
|
|
389
|
+
model_cls, pk_filter, version_column, expected_version, current, None
|
|
293
390
|
)
|
|
294
391
|
return deleted
|
|
295
392
|
|
|
@@ -377,8 +474,16 @@ async def update_instance(
|
|
|
377
474
|
|
|
378
475
|
if result.rows_affected == 0:
|
|
379
476
|
if version_field_name is not None and current_version is not None:
|
|
380
|
-
# Guarded write: classify gone-vs-changed and raise accordingly.
|
|
381
|
-
|
|
477
|
+
# Guarded write: classify gone-vs-changed and raise accordingly. The
|
|
478
|
+
# contested fields are exactly what this write attempted, minus the
|
|
479
|
+
# version bump the guard itself added.
|
|
480
|
+
await _raise_stale_write(
|
|
481
|
+
model_cls,
|
|
482
|
+
pk_filter,
|
|
483
|
+
version_field_name,
|
|
484
|
+
current_version,
|
|
485
|
+
[f for f in update_data if f != version_field_name],
|
|
486
|
+
)
|
|
382
487
|
raise ValueError(f"No rows were updated for {model_cls.__name__} with {pk_filter}")
|
|
383
488
|
|
|
384
489
|
if result.updated_rows:
|
|
@@ -329,8 +329,7 @@ _HEADER = '''\
|
|
|
329
329
|
# because it imports db.models.* / db.managers.* — forbidden inside packages/.
|
|
330
330
|
"""AUTO-GENERATED host->package DB wiring. Do not edit."""
|
|
331
331
|
|
|
332
|
-
from __future__ import annotations
|
|
333
|
-
'''
|
|
332
|
+
from __future__ import annotations'''
|
|
334
333
|
|
|
335
334
|
|
|
336
335
|
def _bucket_order(bucket: str) -> int:
|
|
@@ -360,6 +359,10 @@ def render_wiring_module(output_module: str, packages: list[ResolvedPackage]) ->
|
|
|
360
359
|
for (module, symbol), local in sorted(local_for.items(), key=lambda kv: int(kv[1][2:])):
|
|
361
360
|
lines.append(f" from {module} import {symbol} as {local}")
|
|
362
361
|
lines.append(f" import {pkg.configure_import} as _target")
|
|
362
|
+
# Blank line after the import block: keeps the emitted file byte-identical
|
|
363
|
+
# to `ruff format` output, so the wiring drift gate and the formatter can
|
|
364
|
+
# never fight over this file.
|
|
365
|
+
lines.append("")
|
|
363
366
|
|
|
364
367
|
# Build the kwargs.
|
|
365
368
|
buckets: dict[str, list[Assignment]] = {}
|
|
@@ -110,7 +110,12 @@ and:
|
|
|
110
110
|
2. Registers a `WatchedLifecycleConfig` with `WatchedLifecycleRegistry`.
|
|
111
111
|
|
|
112
112
|
The sweeper (started by the host app at startup) iterates the registry
|
|
113
|
-
on its periodic tick.
|
|
113
|
+
on its periodic tick. A watchdog that automatically transitions rows reports
|
|
114
|
+
every sweep in which it finds work. An alert-only watchdog reports immediately,
|
|
115
|
+
again whenever its stuck-row count changes, after recovery if the condition
|
|
116
|
+
returns, and once per hour while an unchanged condition persists. This keeps a
|
|
117
|
+
durable operational problem visible without emitting the same ERROR every
|
|
118
|
+
minute forever.
|
|
114
119
|
|
|
115
120
|
## Pending-aware reads
|
|
116
121
|
|
|
@@ -37,6 +37,7 @@ import asyncio
|
|
|
37
37
|
import inspect
|
|
38
38
|
import logging
|
|
39
39
|
import re
|
|
40
|
+
import time
|
|
40
41
|
import traceback as _tb
|
|
41
42
|
from collections.abc import Sequence
|
|
42
43
|
from dataclasses import dataclass, replace
|
|
@@ -284,6 +285,13 @@ WatchedLifecycleRegistry = _Registry()
|
|
|
284
285
|
|
|
285
286
|
SWEEP_INTERVAL_SECS_DEFAULT = 60.0
|
|
286
287
|
SWEEP_LIMIT_DEFAULT = 100
|
|
288
|
+
ALERT_ONLY_REMINDER_SECS = 3600.0
|
|
289
|
+
|
|
290
|
+
# Alert-only watches intentionally leave rows untouched, so the same persistent
|
|
291
|
+
# condition can remain visible for days. Remember the last emitted state in this
|
|
292
|
+
# process: alert immediately, alert again when the count changes, and otherwise
|
|
293
|
+
# emit one hourly reminder instead of one ERROR per sweep tick.
|
|
294
|
+
_alert_only_emissions: dict[str, tuple[int, float]] = {}
|
|
287
295
|
|
|
288
296
|
|
|
289
297
|
def _alert_color(channel: AlertChannel) -> str:
|
|
@@ -313,6 +321,35 @@ def _emit_stuck_rows_summary(summary: str, *, color: str) -> None:
|
|
|
313
321
|
cool_print(text=f"\n{summary}", color=color)
|
|
314
322
|
|
|
315
323
|
|
|
324
|
+
def _should_emit_stuck_rows_alert(
|
|
325
|
+
cfg: WatchedLifecycleConfig,
|
|
326
|
+
*,
|
|
327
|
+
stuck_count: int,
|
|
328
|
+
monotonic_now: float | None = None,
|
|
329
|
+
) -> bool:
|
|
330
|
+
"""Return whether this sweep should emit the human + structured alert."""
|
|
331
|
+
if cfg.transition_to_on_timeout is not None:
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
key = f"{cfg.schema}.{cfg.table}"
|
|
335
|
+
if stuck_count == 0:
|
|
336
|
+
_alert_only_emissions.pop(key, None)
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
now_value = time.monotonic() if monotonic_now is None else monotonic_now
|
|
340
|
+
previous = _alert_only_emissions.get(key)
|
|
341
|
+
if previous is not None:
|
|
342
|
+
previous_count, previous_emitted_at = previous
|
|
343
|
+
if (
|
|
344
|
+
previous_count == stuck_count
|
|
345
|
+
and now_value - previous_emitted_at < ALERT_ONLY_REMINDER_SECS
|
|
346
|
+
):
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
_alert_only_emissions[key] = (stuck_count, now_value)
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
|
|
316
353
|
def _build_select_query(cfg: WatchedLifecycleConfig, limit: int) -> tuple[str, list[Any]]:
|
|
317
354
|
params: list[Any] = []
|
|
318
355
|
predicate_sql = cfg.predicate.as_sql(params)
|
|
@@ -547,6 +584,7 @@ async def sweep_once(
|
|
|
547
584
|
stuck_count = len(rows)
|
|
548
585
|
out[f"{cfg.schema}.{cfg.table}"] = stuck_count
|
|
549
586
|
if stuck_count == 0:
|
|
587
|
+
_should_emit_stuck_rows_alert(cfg, stuck_count=0)
|
|
550
588
|
continue
|
|
551
589
|
|
|
552
590
|
color = _alert_color(cfg.alert_channel)
|
|
@@ -561,29 +599,30 @@ async def sweep_once(
|
|
|
561
599
|
pass
|
|
562
600
|
max_age_seen = max(ages_seconds) if ages_seconds else 0
|
|
563
601
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
"
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
602
|
+
if _should_emit_stuck_rows_alert(cfg, stuck_count=stuck_count):
|
|
603
|
+
_emit_stuck_rows_summary(
|
|
604
|
+
_format_stuck_rows_summary(
|
|
605
|
+
cfg,
|
|
606
|
+
stuck_count=stuck_count,
|
|
607
|
+
max_age_seen=max_age_seen,
|
|
608
|
+
),
|
|
609
|
+
color=color,
|
|
610
|
+
)
|
|
611
|
+
logger.error(
|
|
612
|
+
"watchdog_stuck_rows",
|
|
613
|
+
extra={
|
|
614
|
+
"matrx_watchdog": {
|
|
615
|
+
"table": cfg.table,
|
|
616
|
+
"schema": cfg.schema,
|
|
617
|
+
"stuck_count": stuck_count,
|
|
618
|
+
"predicate": repr(cfg.predicate),
|
|
619
|
+
"max_age_secs": cfg.max_age_secs,
|
|
620
|
+
"alert_channel": cfg.alert_channel,
|
|
621
|
+
"transition_to": cfg.transition_to_on_timeout,
|
|
622
|
+
"extra_fields_on_timeout": dict(cfg.extra_fields_on_timeout),
|
|
623
|
+
}
|
|
584
624
|
}
|
|
585
|
-
|
|
586
|
-
)
|
|
625
|
+
)
|
|
587
626
|
|
|
588
627
|
for row in rows:
|
|
589
628
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "matrx-orm"
|
|
3
|
-
version = "3.1.
|
|
3
|
+
version = "3.1.75"
|
|
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.16",
|
|
45
45
|
"pyyaml>=6.0.3",
|
|
46
46
|
"python-dotenv>=1.0",
|
|
47
47
|
"cryptography>=43.0",
|