matrx-orm 3.1.72__tar.gz → 3.1.74__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.72 → matrx_orm-3.1.74}/.gitignore +3 -1
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/PKG-INFO +1 -1
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/__init__.py +11 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/catalog.py +46 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/FEATURE.md +4 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/async_db_manager.py +44 -0
- matrx_orm-3.1.74/matrx_orm/core/pooler_role.py +290 -0
- matrx_orm-3.1.74/matrx_orm/schema_builder/FEATURE.md +12 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/columns.py +106 -3
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/helpers/base_generators.py +15 -14
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/pyproject.toml +1 -1
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_catalog.py +17 -0
- matrx_orm-3.1.74/tests/live_pooler_role.py +104 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_base_generation.py +33 -0
- matrx_orm-3.1.74/tests/schema_builder/test_array_default_parsing.py +56 -0
- matrx_orm-3.1.74/tests/test_pooler_role_guard.py +142 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/AGENTS.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/CLAUDE.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/MODEL_API.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/docs/migrations.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/catalog_sql.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/pool_watch.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/FEATURE.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/attachments.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/crypto.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/items.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/model.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/scripts/release.sh +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/conftest.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_bulk_hydration_offload.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_pool_holder_census.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_rls_session_scope.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_table_relationships_query.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_secrets_battery.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.72 → matrx_orm-3.1.74}/tests/test_sync_twins_guard.py +0 -0
|
@@ -218,7 +218,7 @@ frontend/
|
|
|
218
218
|
# Armani
|
|
219
219
|
.history*
|
|
220
220
|
.history/
|
|
221
|
-
local_data/
|
|
221
|
+
/local_data/
|
|
222
222
|
local_reports_data/
|
|
223
223
|
webscraper/quick_scrapes/temp/
|
|
224
224
|
automation_matrix/ai_apis/fireworks/_dev/*
|
|
@@ -248,6 +248,7 @@ requirements_filtered.txt
|
|
|
248
248
|
|
|
249
249
|
# Claude Code local worktrees and per-user settings
|
|
250
250
|
.claude/worktrees/
|
|
251
|
+
.worktrees/
|
|
251
252
|
.claude/settings.local.json
|
|
252
253
|
|
|
253
254
|
# Append-only snapshots from matrx_utils.update_history (unbounded; do not commit)
|
|
@@ -283,3 +284,4 @@ db/evidence/
|
|
|
283
284
|
|
|
284
285
|
# Pooler contamination evidence — machine-local forensic capture, not source
|
|
285
286
|
db/evidence/
|
|
287
|
+
db/mirror/.env.mirror
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.74
|
|
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
|
|
@@ -203,6 +203,12 @@ from .core.pool_watch import (
|
|
|
203
203
|
connection_holder_census,
|
|
204
204
|
register_pool_saturation_sink,
|
|
205
205
|
)
|
|
206
|
+
from .core.pooler_role import (
|
|
207
|
+
EmptyReadContaminationEvent,
|
|
208
|
+
PoolerRoleContamination,
|
|
209
|
+
SessionRoleChangeRefused,
|
|
210
|
+
register_contamination_sink,
|
|
211
|
+
)
|
|
206
212
|
from .core.pydantic_bridge import (
|
|
207
213
|
validate_input as validate_model_input,
|
|
208
214
|
)
|
|
@@ -573,6 +579,11 @@ __all__ = [
|
|
|
573
579
|
"PoolSaturationEvent",
|
|
574
580
|
"connection_holder_census",
|
|
575
581
|
"register_pool_saturation_sink",
|
|
582
|
+
# ── Pooler role contamination ──────────────────────────────────────────────
|
|
583
|
+
"EmptyReadContaminationEvent",
|
|
584
|
+
"PoolerRoleContamination",
|
|
585
|
+
"SessionRoleChangeRefused",
|
|
586
|
+
"register_contamination_sink",
|
|
576
587
|
# ── Signals ────────────────────────────────────────────────────────────────
|
|
577
588
|
"Signal",
|
|
578
589
|
"pre_create",
|
|
@@ -26,6 +26,7 @@ __all__ = [
|
|
|
26
26
|
"softdelete_unique_conflicts",
|
|
27
27
|
"qualified_relation_names",
|
|
28
28
|
"trigger_function_usage",
|
|
29
|
+
"trigger_column_coverage",
|
|
29
30
|
"column_privileges",
|
|
30
31
|
"entity_capability_state",
|
|
31
32
|
]
|
|
@@ -487,6 +488,51 @@ async def trigger_function_usage(
|
|
|
487
488
|
)
|
|
488
489
|
|
|
489
490
|
|
|
491
|
+
async def trigger_column_coverage(
|
|
492
|
+
database: str,
|
|
493
|
+
*,
|
|
494
|
+
function_name: str,
|
|
495
|
+
column_names: list[str],
|
|
496
|
+
) -> list[dict[str, Any]]:
|
|
497
|
+
"""Return relations carrying any named column or the named trigger function."""
|
|
498
|
+
return await _query(
|
|
499
|
+
database,
|
|
500
|
+
"""
|
|
501
|
+
WITH column_relations AS (
|
|
502
|
+
SELECT DISTINCT c.oid,
|
|
503
|
+
n.nspname AS schema,
|
|
504
|
+
c.relname AS table_name
|
|
505
|
+
FROM pg_attribute a
|
|
506
|
+
JOIN pg_class c ON c.oid = a.attrelid
|
|
507
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
508
|
+
WHERE a.attname = ANY($2::text[])
|
|
509
|
+
AND a.attnum > 0
|
|
510
|
+
AND NOT a.attisdropped
|
|
511
|
+
AND c.relkind = 'r'
|
|
512
|
+
), trigger_relations AS (
|
|
513
|
+
SELECT DISTINCT t.tgrelid AS oid,
|
|
514
|
+
n.nspname AS schema,
|
|
515
|
+
c.relname AS table_name
|
|
516
|
+
FROM pg_trigger t
|
|
517
|
+
JOIN pg_class c ON c.oid = t.tgrelid
|
|
518
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
519
|
+
JOIN pg_proc p ON p.oid = t.tgfoid
|
|
520
|
+
WHERE NOT t.tgisinternal
|
|
521
|
+
AND p.proname = $1
|
|
522
|
+
)
|
|
523
|
+
SELECT COALESCE(cols.schema, triggers.schema) AS schema,
|
|
524
|
+
COALESCE(cols.table_name, triggers.table_name) AS table_name,
|
|
525
|
+
(cols.oid IS NOT NULL) AS has_columns,
|
|
526
|
+
(triggers.oid IS NOT NULL) AS has_trigger
|
|
527
|
+
FROM column_relations cols
|
|
528
|
+
FULL OUTER JOIN trigger_relations triggers ON cols.oid = triggers.oid
|
|
529
|
+
ORDER BY 1, 2
|
|
530
|
+
""",
|
|
531
|
+
function_name,
|
|
532
|
+
column_names,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
490
536
|
async def foreign_keys_to_relation(
|
|
491
537
|
database: str,
|
|
492
538
|
*,
|
|
@@ -89,6 +89,10 @@ Verified against code: 2026-08-15.
|
|
|
89
89
|
|
|
90
90
|
## Change log
|
|
91
91
|
|
|
92
|
+
- 2026-08-16: Kept the destructive transaction-pool contamination proof out of
|
|
93
|
+
the installable runtime package and isolated it under package tests; the
|
|
94
|
+
operator wrapper is SQL-free and runtime prose no longer resembles an
|
|
95
|
+
executable non-local role mutation.
|
|
92
96
|
- 2026-08-15: Pinned transaction-local RLS role cleanup across success,
|
|
93
97
|
exception, and early-return paths.
|
|
94
98
|
- 2026-08-15: Added first-timeout pool holder census, host alarm seam, tracked raw-pool helper, and bounded cancellation-safe release.
|
|
@@ -18,6 +18,11 @@ from matrx_orm.core.config import (
|
|
|
18
18
|
resolve_pool_mode,
|
|
19
19
|
)
|
|
20
20
|
from matrx_orm.error_handling import handle_orm_operation
|
|
21
|
+
from matrx_orm.core.pooler_role import (
|
|
22
|
+
VERIFY_EMPTY_READS,
|
|
23
|
+
assert_no_session_role_change,
|
|
24
|
+
reprove_empty_read,
|
|
25
|
+
)
|
|
21
26
|
from matrx_orm.core.tls import resolve_asyncpg_ssl
|
|
22
27
|
from matrx_orm.exceptions import (
|
|
23
28
|
AdapterError,
|
|
@@ -327,6 +332,25 @@ class AsyncDatabaseManager:
|
|
|
327
332
|
_keepalive_tasks: dict[str, asyncio.Task] = {} # per-config pool warmer
|
|
328
333
|
_growth_locks: dict[str, asyncio.Lock] = {}
|
|
329
334
|
|
|
335
|
+
# Config names already resolved as "does an empty read need proving here?".
|
|
336
|
+
# Only a transaction-mode pool can be handed a server connection another
|
|
337
|
+
# client left wearing a role, so a direct Postgres pays nothing.
|
|
338
|
+
_empty_read_verification: dict[str, bool] = {}
|
|
339
|
+
|
|
340
|
+
@classmethod
|
|
341
|
+
def _verifies_empty_reads(cls, config_name: str) -> bool:
|
|
342
|
+
if not VERIFY_EMPTY_READS:
|
|
343
|
+
return False
|
|
344
|
+
cached = cls._empty_read_verification.get(config_name)
|
|
345
|
+
if cached is not None:
|
|
346
|
+
return cached
|
|
347
|
+
try:
|
|
348
|
+
verifies = resolve_pool_mode(get_database_config(config_name)) == "transaction"
|
|
349
|
+
except Exception:
|
|
350
|
+
verifies = False
|
|
351
|
+
cls._empty_read_verification[config_name] = verifies
|
|
352
|
+
return verifies
|
|
353
|
+
|
|
330
354
|
@classmethod
|
|
331
355
|
async def _replace_zero_connection_pool(cls, config_name: str, failed_pool: Any) -> bool:
|
|
332
356
|
"""Atomically discard a pool that has lost every physical connection."""
|
|
@@ -1025,6 +1049,11 @@ class AsyncDatabaseManager:
|
|
|
1025
1049
|
``.timeout()`` seam for known-long reads. ``None`` keeps the pool
|
|
1026
1050
|
default.
|
|
1027
1051
|
"""
|
|
1052
|
+
# Layer 1 of the pooler-role defence: SQL that would leave a borrowed role
|
|
1053
|
+
# on a shared server connection never reaches Postgres. See
|
|
1054
|
+
# matrx_orm/core/pooler_role.py for what that contamination does to reads.
|
|
1055
|
+
assert_no_session_role_change(query)
|
|
1056
|
+
|
|
1028
1057
|
# Reuse the active transaction connection when available
|
|
1029
1058
|
try:
|
|
1030
1059
|
from matrx_orm.core.transaction import get_active_connection
|
|
@@ -1124,6 +1153,21 @@ class AsyncDatabaseManager:
|
|
|
1124
1153
|
read_attempt + 1,
|
|
1125
1154
|
query[:120],
|
|
1126
1155
|
)
|
|
1156
|
+
if not results and read_only and cls._verifies_empty_reads(config_name):
|
|
1157
|
+
# Layer 2: an empty result from a pooled connection is
|
|
1158
|
+
# unproven until a session that can state its own role
|
|
1159
|
+
# says so. A borrowed `authenticated` role returns ZERO
|
|
1160
|
+
# ROWS on an RLS table instead of raising.
|
|
1161
|
+
true_rows = await reprove_empty_read(
|
|
1162
|
+
conn,
|
|
1163
|
+
config_name=config_name,
|
|
1164
|
+
query=query,
|
|
1165
|
+
args=args,
|
|
1166
|
+
statement_timeout=statement_timeout,
|
|
1167
|
+
normalize=_normalize_record,
|
|
1168
|
+
)
|
|
1169
|
+
if true_rows is not None:
|
|
1170
|
+
return true_rows
|
|
1127
1171
|
return [_normalize_record(r) for r in results]
|
|
1128
1172
|
except asyncpg.exceptions.PostgresSyntaxError as e:
|
|
1129
1173
|
raise QueryError(
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Pooler role contamination — the ORM's two layers against a SILENT empty read.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS (reproduced 2026-08-15, FOUND_DEFECTS "An ORM read returned ZERO
|
|
4
|
+
ROWS against a table holding 4,060"):
|
|
5
|
+
|
|
6
|
+
On a transaction-mode pooler (Supavisor / PgBouncer) routing is *per transaction*,
|
|
7
|
+
so consecutive statements from ONE client connection execute on DIFFERENT server
|
|
8
|
+
connections. A server connection that any client on the tenant left with a
|
|
9
|
+
non-LOCAL ``SET ROLE`` comes back out of the shared pool still wearing that role.
|
|
10
|
+
Our statement then runs as ``authenticated`` instead of ``postgres``.
|
|
11
|
+
|
|
12
|
+
**On an RLS table that GRANTs SELECT to ``authenticated``, that does not raise —
|
|
13
|
+
it returns ZERO ROWS.** Measured live against the platform pooler: a bare
|
|
14
|
+
``SET ROLE authenticated`` issued on 8 connections made 22 of 40 subsequent ORM
|
|
15
|
+
reads of ``seo.backlink_observation`` (4,060 rows) come back empty, with no
|
|
16
|
+
exception, while an identity probe interleaved on the same pool reported
|
|
17
|
+
``current_user=authenticated, session_user=postgres`` on 21 of 40 samples.
|
|
18
|
+
|
|
19
|
+
Silent emptiness is indistinguishable from "there is no data" for every sweep,
|
|
20
|
+
reconciler, digest and monitor in the platform: a broken read reports a quiet week
|
|
21
|
+
forever and every scheduled run stays green. ``db/pooler_session.py`` already says
|
|
22
|
+
it for the migration tools — **a privilege error must never be recorded as
|
|
23
|
+
absence** — and this module extends that to every ORM read, because a per-caller
|
|
24
|
+
defence does not scale.
|
|
25
|
+
|
|
26
|
+
TWO INDEPENDENT LAYERS, each sufficient alone, each screaming when it fires:
|
|
27
|
+
|
|
28
|
+
1. **Prevention — the ORM refuses to emit a session-scoped role change.**
|
|
29
|
+
``assert_no_session_role_change`` rejects a bare ``SET ROLE`` /
|
|
30
|
+
``SET SESSION AUTHORIZATION`` / ``set_config('role', …, false)`` on every query
|
|
31
|
+
that goes through the ORM. This closes the one source class the 2026-08-14
|
|
32
|
+
investigation could never catch statically: SQL authored at RUNTIME — the
|
|
33
|
+
admin ``sql`` tool, ``db_admin``, registered ``SQLQuery`` objects, agent
|
|
34
|
+
scratch scripts. ``scripts/check_pooler_role_scope.py`` guards committed code;
|
|
35
|
+
this guards everything else. The sanctioned ways to change role remain
|
|
36
|
+
``matrx_orm.rls_session`` and ``db/pooler_session.py::impersonate``.
|
|
37
|
+
|
|
38
|
+
2. **Reconciliation — an empty read is re-proved on a session that can prove its
|
|
39
|
+
role.** A zero-row result from a read-only query on a transaction-pool config
|
|
40
|
+
is re-run inside an explicit transaction (pinned to ONE server connection)
|
|
41
|
+
that starts with ``RESET ROLE``. If the rows are really there, the caller gets
|
|
42
|
+
the TRUE rows and the lie is reported through every registered sink. If the
|
|
43
|
+
session cannot be made clean, it RAISES rather than handing back an absence.
|
|
44
|
+
|
|
45
|
+
Layer 2 follows the repo's guard doctrine: a guard that CAN reconcile MUST
|
|
46
|
+
reconcile — we hold the correct answer, so killing the caller's request would be
|
|
47
|
+
strictly worse than returning it loudly. It costs nothing on a non-empty read and
|
|
48
|
+
nothing at all on a direct (session-mode) Postgres connection, where cross-client
|
|
49
|
+
contamination cannot happen.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import logging
|
|
55
|
+
import re
|
|
56
|
+
from collections.abc import Callable
|
|
57
|
+
from datetime import UTC, datetime
|
|
58
|
+
from typing import Any
|
|
59
|
+
|
|
60
|
+
from pydantic import BaseModel, ConfigDict
|
|
61
|
+
|
|
62
|
+
from matrx_utils import vcprint
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger("matrx_orm.pooler_role")
|
|
65
|
+
|
|
66
|
+
# Turning this off re-arms a silent-data-loss class platform-wide. It exists so a
|
|
67
|
+
# deployment on a genuinely unshared Postgres can skip the work, and so the cost
|
|
68
|
+
# is a code change (reviewable) rather than an env var (invisible).
|
|
69
|
+
VERIFY_EMPTY_READS = True
|
|
70
|
+
|
|
71
|
+
# How many times a re-proof may try to obtain a session running as our own login
|
|
72
|
+
# role before it gives up and raises. A contaminated server connection is repaired
|
|
73
|
+
# in-transaction, so one retry is almost always enough.
|
|
74
|
+
REPROVE_ATTEMPTS = 3
|
|
75
|
+
|
|
76
|
+
# A role change that is NOT scoped to a transaction. ``SET LOCAL ROLE`` and
|
|
77
|
+
# ``set_config('role', …, true)`` are safe and deliberately not matched.
|
|
78
|
+
_NON_LOCAL_SET_ROLE = re.compile(r"(?im)(?:^|;)\s*SET\s+(?!LOCAL\b)ROLE\b")
|
|
79
|
+
_NON_LOCAL_SET_AUTHORIZATION = re.compile(
|
|
80
|
+
r"(?im)(?:^|;)\s*SET\s+(?!LOCAL\b)SESSION\s+AUTHORIZATION\b"
|
|
81
|
+
)
|
|
82
|
+
_NON_LOCAL_SET_CONFIG_ROLE = re.compile(
|
|
83
|
+
r"(?is)set_config\s*\(\s*['\"]role['\"]\s*,.*?,\s*(?:false|'false'|\"false\")\s*\)"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Cheap pre-filter so the overwhelming majority of queries pay a substring scan
|
|
87
|
+
# rather than three regexes. Deliberately loose — a false hint costs microseconds,
|
|
88
|
+
# a missed one costs the platform a silent empty read.
|
|
89
|
+
_ROLE_HINTS = ("role", "authorization", "set_config")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SessionRoleChangeRefused(RuntimeError):
|
|
93
|
+
"""A query tried to change the Postgres role for the whole session."""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class PoolerRoleContamination(RuntimeError):
|
|
97
|
+
"""A statement could not be pinned to a session running as our own login role."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class EmptyReadContaminationEvent(BaseModel):
|
|
101
|
+
"""A read returned zero rows because the session was wearing a borrowed role."""
|
|
102
|
+
|
|
103
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
104
|
+
|
|
105
|
+
config_name: str
|
|
106
|
+
query: str
|
|
107
|
+
contaminated_role: str
|
|
108
|
+
login_role: str
|
|
109
|
+
true_row_count: int
|
|
110
|
+
occurred_at: datetime
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
ContaminationSink = Callable[[EmptyReadContaminationEvent], None]
|
|
114
|
+
|
|
115
|
+
_sinks: list[ContaminationSink] = []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def register_contamination_sink(sink: ContaminationSink) -> None:
|
|
119
|
+
"""Register a durable observer for proven silent-empty reads."""
|
|
120
|
+
if sink not in _sinks:
|
|
121
|
+
_sinks.append(sink)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def reset_contamination_sinks() -> None:
|
|
125
|
+
"""Clear process state for isolated tests."""
|
|
126
|
+
_sinks.clear()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def assert_no_session_role_change(query: str) -> None:
|
|
130
|
+
"""Refuse SQL that would leave a borrowed role on a pooled server connection.
|
|
131
|
+
|
|
132
|
+
Layer 1. Raises ``SessionRoleChangeRefused`` and names the sanctioned path;
|
|
133
|
+
it never rewrites the caller's SQL, because guessing which role a caller meant
|
|
134
|
+
is exactly the ambiguity a guard must not resolve on its own.
|
|
135
|
+
"""
|
|
136
|
+
if not query:
|
|
137
|
+
return
|
|
138
|
+
lowered = query.lower()
|
|
139
|
+
if not any(hint in lowered for hint in _ROLE_HINTS):
|
|
140
|
+
return
|
|
141
|
+
if (
|
|
142
|
+
_NON_LOCAL_SET_ROLE.search(query)
|
|
143
|
+
or _NON_LOCAL_SET_AUTHORIZATION.search(query)
|
|
144
|
+
or _NON_LOCAL_SET_CONFIG_ROLE.search(query)
|
|
145
|
+
):
|
|
146
|
+
vcprint(
|
|
147
|
+
"REFUSED: a session-scoped role change reached the ORM.\n"
|
|
148
|
+
f" query: {query[:300]}\n"
|
|
149
|
+
" On a transaction pooler this role rides the SHARED server connection "
|
|
150
|
+
"back into the pool, and the next client — any service on this tenant — "
|
|
151
|
+
"silently runs as that role. On an RLS table it does not error: it "
|
|
152
|
+
"returns ZERO ROWS, and absence gets recorded as fact.\n"
|
|
153
|
+
" Use `async with rls_session(claims, role=...)` (ORM) or "
|
|
154
|
+
"`impersonate(conn, role)` (db/pooler_session.py). Both use SET LOCAL "
|
|
155
|
+
"ROLE inside a transaction and cannot leak.",
|
|
156
|
+
color="red",
|
|
157
|
+
style="bold",
|
|
158
|
+
)
|
|
159
|
+
raise SessionRoleChangeRefused(
|
|
160
|
+
"A session-scoped role change (SET ROLE / SET SESSION AUTHORIZATION / "
|
|
161
|
+
"a non-local set_config role change) may never be executed through the ORM — it "
|
|
162
|
+
"contaminates the shared pooler and turns other services' reads into "
|
|
163
|
+
"silent empty results. Use rls_session(...) / impersonate(...), which "
|
|
164
|
+
"issue SET LOCAL ROLE inside a transaction."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def emit_contamination(event: EmptyReadContaminationEvent) -> None:
|
|
169
|
+
"""Scream about a proven silent-empty read, durably record it, fan out to sinks."""
|
|
170
|
+
vcprint(
|
|
171
|
+
"POOLER ROLE CONTAMINATION — an ORM read returned ZERO ROWS that were "
|
|
172
|
+
"really there.\n"
|
|
173
|
+
f" config: {event.config_name}\n"
|
|
174
|
+
f" session ran as: {event.contaminated_role} (login role "
|
|
175
|
+
f"{event.login_role})\n"
|
|
176
|
+
f" true row count on a clean session: {event.true_row_count}\n"
|
|
177
|
+
f" query: {event.query[:300]}\n"
|
|
178
|
+
" The caller has been given the TRUE rows. Something on this Postgres "
|
|
179
|
+
"tenant issued a non-LOCAL SET ROLE; every other client is exposed until "
|
|
180
|
+
"those server connections cycle.",
|
|
181
|
+
color="red",
|
|
182
|
+
style="bold",
|
|
183
|
+
)
|
|
184
|
+
logger.error(
|
|
185
|
+
"[pooler-role] silent empty read repaired on %s (ran as %s, %d true rows): %s",
|
|
186
|
+
event.config_name,
|
|
187
|
+
event.contaminated_role,
|
|
188
|
+
event.true_row_count,
|
|
189
|
+
event.query[:200],
|
|
190
|
+
)
|
|
191
|
+
# Durable by default — a host should not have to wire anything for the
|
|
192
|
+
# platform's loudest data-integrity alarm to survive the process.
|
|
193
|
+
try:
|
|
194
|
+
from matrx_orm.session.fallback import record_error
|
|
195
|
+
|
|
196
|
+
await record_error(
|
|
197
|
+
PoolerRoleContamination(
|
|
198
|
+
f"read returned 0 rows as '{event.contaminated_role}'; "
|
|
199
|
+
f"{event.true_row_count} rows were really there"
|
|
200
|
+
),
|
|
201
|
+
kind="pooler_role_contamination",
|
|
202
|
+
source_app="matrx-orm",
|
|
203
|
+
route=f"orm:{event.config_name}",
|
|
204
|
+
payload={
|
|
205
|
+
"config_name": event.config_name,
|
|
206
|
+
"query": event.query[:2000],
|
|
207
|
+
"contaminated_role": event.contaminated_role,
|
|
208
|
+
"login_role": event.login_role,
|
|
209
|
+
"true_row_count": event.true_row_count,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
except Exception:
|
|
213
|
+
logger.exception("[pooler-role] could not durably record the contamination event")
|
|
214
|
+
|
|
215
|
+
for sink in list(_sinks):
|
|
216
|
+
try:
|
|
217
|
+
sink(event)
|
|
218
|
+
except Exception:
|
|
219
|
+
logger.exception("[pooler-role] contamination sink raised")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def reprove_empty_read(
|
|
223
|
+
conn: Any,
|
|
224
|
+
*,
|
|
225
|
+
config_name: str,
|
|
226
|
+
query: str,
|
|
227
|
+
args: tuple[Any, ...],
|
|
228
|
+
statement_timeout: float | None,
|
|
229
|
+
normalize: Callable[[Any], dict[str, Any]],
|
|
230
|
+
) -> list[dict[str, Any]] | None:
|
|
231
|
+
"""Re-run an empty read on a session proven to be running as US.
|
|
232
|
+
|
|
233
|
+
Layer 2. Returns the TRUE rows when the empty result was a lie, ``None`` when
|
|
234
|
+
the read was genuinely empty. Raises ``PoolerRoleContamination`` when a clean
|
|
235
|
+
session cannot be obtained at all — an unprovable absence is never a result.
|
|
236
|
+
|
|
237
|
+
``conn`` must be a connection not currently inside a transaction; the explicit
|
|
238
|
+
transaction opened here is what pins every statement below to ONE server
|
|
239
|
+
connection, which is the only thing that makes ``RESET ROLE`` meaningful.
|
|
240
|
+
"""
|
|
241
|
+
last_role: tuple[str, str] | None = None
|
|
242
|
+
for _ in range(REPROVE_ATTEMPTS):
|
|
243
|
+
async with conn.transaction():
|
|
244
|
+
await conn.execute("RESET ROLE")
|
|
245
|
+
identity = await conn.fetchrow("SELECT current_user, session_user")
|
|
246
|
+
current = identity["current_user"]
|
|
247
|
+
login = identity["session_user"]
|
|
248
|
+
if current != login:
|
|
249
|
+
last_role = (current, login)
|
|
250
|
+
continue
|
|
251
|
+
rows = await conn.fetch(query, *args, timeout=statement_timeout)
|
|
252
|
+
if not rows:
|
|
253
|
+
return None
|
|
254
|
+
true_rows = [normalize(r) for r in rows]
|
|
255
|
+
|
|
256
|
+
await emit_contamination(
|
|
257
|
+
EmptyReadContaminationEvent(
|
|
258
|
+
config_name=config_name,
|
|
259
|
+
query=query,
|
|
260
|
+
# The first read ran on a session we could not observe directly;
|
|
261
|
+
# what we can state is the role it was NOT running as.
|
|
262
|
+
contaminated_role="<not the login role>" if last_role is None else last_role[0],
|
|
263
|
+
login_role=login,
|
|
264
|
+
true_row_count=len(true_rows),
|
|
265
|
+
occurred_at=datetime.now(UTC),
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
return true_rows
|
|
269
|
+
|
|
270
|
+
current, login = last_role if last_role is not None else ("<unknown>", "<unknown>")
|
|
271
|
+
raise PoolerRoleContamination(
|
|
272
|
+
f"an empty read could not be proved: every attempt to pin a clean session "
|
|
273
|
+
f"landed as '{current}' (login role '{login}'). Anything this session could "
|
|
274
|
+
f"not see is a PRIVILEGE failure, not absence. Query: {query[:200]}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
__all__ = [
|
|
279
|
+
"ContaminationSink",
|
|
280
|
+
"EmptyReadContaminationEvent",
|
|
281
|
+
"PoolerRoleContamination",
|
|
282
|
+
"REPROVE_ATTEMPTS",
|
|
283
|
+
"SessionRoleChangeRefused",
|
|
284
|
+
"VERIFY_EMPTY_READS",
|
|
285
|
+
"assert_no_session_role_change",
|
|
286
|
+
"emit_contamination",
|
|
287
|
+
"register_contamination_sink",
|
|
288
|
+
"reprove_empty_read",
|
|
289
|
+
"reset_contamination_sinks",
|
|
290
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Schema Builder
|
|
2
|
+
|
|
3
|
+
The schema builder generates importable models and manager wrappers from live database metadata.
|
|
4
|
+
|
|
5
|
+
## Manager method names
|
|
6
|
+
|
|
7
|
+
- A generated manager must never contain duplicate method names; later definitions silently replace earlier behavior in Python.
|
|
8
|
+
- When a table's singular and plural tokens are identical (for example, `entity_grants`), item methods keep the table token and collection methods use the `all_` prefix: `load_entity_grants(...)` and `load_all_entity_grants(...)`.
|
|
9
|
+
- Relation collection wrappers follow the same rule, such as `get_all_entity_grants_with_iam_organizations()`.
|
|
10
|
+
- Generated code must pass Ruff without unused imports or redefined methods.
|
|
11
|
+
|
|
12
|
+
Guard: `packages/matrx-orm/tests/schema/test_base_generation.py`.
|
|
@@ -27,6 +27,86 @@ _SQL_DYNAMIC_DEFAULT_KEYWORDS = frozenset(
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def _parse_array_default(value: str) -> list[object]:
|
|
31
|
+
"""Turn a non-empty PostgreSQL array default into a real Python list.
|
|
32
|
+
|
|
33
|
+
Handles the two spellings Postgres reports for the same thing:
|
|
34
|
+
|
|
35
|
+
ARRAY['react'::text, 'lucide-react'::text] → ['react', 'lucide-react']
|
|
36
|
+
'{chat_gpt,claude,gemini}'::text[] → ['chat_gpt', 'claude', 'gemini']
|
|
37
|
+
|
|
38
|
+
Returns ``[]`` for anything it cannot confidently parse — a nested array, a
|
|
39
|
+
quoted element containing a comma, a function call inside the literal. An
|
|
40
|
+
empty list is the same conservative answer the generator gave before, and it
|
|
41
|
+
is deliberately preferred over a half-parsed guess: a WRONG default is worse
|
|
42
|
+
than an absent one, because it looks authoritative.
|
|
43
|
+
"""
|
|
44
|
+
text = value.strip()
|
|
45
|
+
|
|
46
|
+
if text.startswith("ARRAY["):
|
|
47
|
+
inner = text[len("ARRAY[") :]
|
|
48
|
+
close = inner.rfind("]")
|
|
49
|
+
if close == -1:
|
|
50
|
+
return []
|
|
51
|
+
elements = [e.strip() for e in inner[:close].split(",")]
|
|
52
|
+
# In ARRAY[...] every element is a SQL EXPRESSION, so a bare token is an
|
|
53
|
+
# identifier or a function call, not a string — refuse the whole default
|
|
54
|
+
# rather than stringify something the database would evaluate.
|
|
55
|
+
bare_is_a_string = False
|
|
56
|
+
else:
|
|
57
|
+
# '{a,b,c}'::text[] — take what is inside the FIRST quoted run, which is
|
|
58
|
+
# the array literal itself; everything after it is the cast.
|
|
59
|
+
open_brace = text.find("{")
|
|
60
|
+
close_brace = text.rfind("}")
|
|
61
|
+
if open_brace == -1 or close_brace <= open_brace:
|
|
62
|
+
return []
|
|
63
|
+
body = text[open_brace + 1 : close_brace]
|
|
64
|
+
if not body.strip():
|
|
65
|
+
return []
|
|
66
|
+
# A quoted element may legally contain a comma; splitting naively would
|
|
67
|
+
# invent elements. Refuse rather than guess.
|
|
68
|
+
if '"' in body:
|
|
69
|
+
return []
|
|
70
|
+
elements = [e.strip() for e in body.split(",")]
|
|
71
|
+
# Inside a curly ARRAY LITERAL an unquoted element IS its literal text —
|
|
72
|
+
# '{chat_gpt,claude}' means the two strings, not two identifiers. A
|
|
73
|
+
# numeric-typed array still parses its elements as numbers below.
|
|
74
|
+
bare_is_a_string = not _array_cast_is_numeric(text)
|
|
75
|
+
|
|
76
|
+
parsed: list[object] = []
|
|
77
|
+
for element in elements:
|
|
78
|
+
if not element:
|
|
79
|
+
return []
|
|
80
|
+
# Strip a per-element cast: 'react'::text → 'react'
|
|
81
|
+
item = re.sub(r"::[A-Za-z_][A-Za-z0-9_ .]*$", "", element).strip()
|
|
82
|
+
if item.startswith("'") and item.endswith("'") and len(item) >= 2:
|
|
83
|
+
parsed.append(item[1:-1].replace("''", "'"))
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
literal = ast.literal_eval(item)
|
|
87
|
+
except (ValueError, SyntaxError):
|
|
88
|
+
if bare_is_a_string:
|
|
89
|
+
parsed.append(item)
|
|
90
|
+
continue
|
|
91
|
+
return []
|
|
92
|
+
if not isinstance(literal, str | int | float | bool):
|
|
93
|
+
return []
|
|
94
|
+
parsed.append(literal)
|
|
95
|
+
return parsed
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_NUMERIC_ARRAY_CAST_RE = re.compile(
|
|
99
|
+
r"::\s*(?:small|big)?int(?:eger)?\d*\s*\[\]$|"
|
|
100
|
+
r"::\s*(?:numeric|decimal|real|double precision|float\d*)\s*\[\]$",
|
|
101
|
+
re.IGNORECASE,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _array_cast_is_numeric(value: str) -> bool:
|
|
106
|
+
"""Whether ``'{...}'::<type>[]`` casts to a numeric element type."""
|
|
107
|
+
return bool(_NUMERIC_ARRAY_CAST_RE.search(value.strip()))
|
|
108
|
+
|
|
109
|
+
|
|
30
110
|
def is_sql_expression_default(value: object) -> bool:
|
|
31
111
|
"""Return whether a PostgreSQL default must be evaluated by the database."""
|
|
32
112
|
if not isinstance(value, str):
|
|
@@ -1068,9 +1148,32 @@ class Column:
|
|
|
1068
1148
|
elif value.startswith("ARRAY[]::") and value.endswith("[]"):
|
|
1069
1149
|
return {"blank": "[]", "generator": ""}
|
|
1070
1150
|
|
|
1071
|
-
#
|
|
1072
|
-
|
|
1073
|
-
|
|
1151
|
+
# 🚨 NON-EMPTY array defaults, BOTH Postgres spellings. Two separate
|
|
1152
|
+
# bugs lived here and both were silent:
|
|
1153
|
+
#
|
|
1154
|
+
# * ``'{a,b}'::text[]`` (the curly-literal spelling) matched NO
|
|
1155
|
+
# branch at all and fell through to the scalar handlers, which
|
|
1156
|
+
# wrapped the whole thing in quotes and emitted
|
|
1157
|
+
# ``default=''{a,b}'::text[]'`` — a Python SYNTAX ERROR that
|
|
1158
|
+
# makes the generated models file unimportable for the entire
|
|
1159
|
+
# repo. It was hand-patched in the output once (2026-08-16,
|
|
1160
|
+
# seo.ai_visibility_panel.engines) and the very next
|
|
1161
|
+
# regeneration re-broke it, because the generator was never
|
|
1162
|
+
# fixed. Patching generated output is not a fix.
|
|
1163
|
+
# * ``ARRAY['a'::text, 'b'::text]`` DID match, and silently
|
|
1164
|
+
# returned ``[]`` — so a column whose database default is 13
|
|
1165
|
+
# values got a Python default of zero values, and a model
|
|
1166
|
+
# instantiated in code disagreed with every row the DB wrote.
|
|
1167
|
+
# No error, no log, just a wrong default.
|
|
1168
|
+
#
|
|
1169
|
+
# Both are the same question — "what list does this default mean?" —
|
|
1170
|
+
# so both go through one parser that answers it. Anything it cannot
|
|
1171
|
+
# confidently parse returns an EMPTY list rather than a guess, which
|
|
1172
|
+
# is the pre-existing (conservative) behaviour, never a syntax error.
|
|
1173
|
+
elif value.startswith("ARRAY[") or (
|
|
1174
|
+
value.startswith("'{") and "}'::" in value and value.endswith("[]")
|
|
1175
|
+
):
|
|
1176
|
+
return {"blank": _parse_array_default(value), "generator": ""}
|
|
1074
1177
|
|
|
1075
1178
|
# Handle simple numeric defaults (no quotes, no casting)
|
|
1076
1179
|
# This includes integers, decimals, negative numbers, scientific notation
|