# ADR-0039:PostgreSQL Schema Capability 与 Runtime Readiness - 状态:Proposed - 日期:2026-07-19 - 关联:QL-RFC-0001、ADR-0037、ADR-0038 ## 上下文 数据库连接成功只能证明网络、认证和一个 session 可用,不能证明当前 cluster-control 副本可以安全处理 Run。以下状态都必须阻止 ready: - PostgreSQL 版本不在支持范围; - 数据库 history 来自更新代码、存在 gap、checksum 或 dialect 漂移; - migration 已部分执行,但 schema capability 尚未推进; - 表、列或关键索引缺失; - 同名对象被手工修改或第三方对象混入 QingLong 核心 schema; - runtime role 是表 owner、拥有 schema CREATE/DDL,或缺少业务所需 DML; - runtime role 可以修改 migration history、schema capability 或 immutable RunEvent。 如果 readiness 只检查 `SELECT 1`,旧 pod 可能在滚动升级期间处理新 schema,未迁移 pod 可能开始 claim,过度授权 runtime 凭据也会长期进入常驻服务。 ## 决策 ### 1. Readiness 是有顺序的 fail-closed capability gate cluster-control 在创建 Repository、启动 dispatcher/reconciler 或开放 admission 前,必须依次只读验证: 1. PostgreSQL `server_version_num` 和稳定 runtime User; 2. 完整 `postgresql-main` history; 3. `control-core` schema capability; 4. `ql3` schema 的表、列和关键索引; 5. runtime role 的 schema/table privilege 与 ownership。 任一步查询失败、结果行数不确定、类型不符合或内容漂移都保持 not-ready。auditor 不执行修复、DDL、GRANT、migration 或外部副作用。 ### 2. History 必须是当前代码的完整前缀 readiness 与 migration runner 复用同一个纯 history auditor: - ID 必须属于当前 `pg-*` stream; - 已应用集合必须是代码 migration 列表的连续前缀; - stream、dialect、ID、64 位 SHA-256 和非负 safe-integer 时间全部精确; - 未知 ahead ID、gap、重复记录或 checksum mismatch 都不允许 ready。 不允许只比较最后一个 migration ID,也不允许因数据库“比代码新”而继续运行旧 pod。 ### 3. Capability 只代表完整 vertical slice 历史首个 capability v1 固定为: ```json { "contract_name": "control-core", "contract_version": 1, "migration_id": "pg-0002-run-core", "capabilities": { "run_core": 1 } } ``` capability 对 migration history 使用 deferred FK。`pg-0002` 的全部 DDL、capability update 和 history insert 在同一事务成功后,commit 才能通过。字段缺失、额外 capability key、版本或 migration ID 不一致均不允许 ready。 ADR-0041 的 `pg-0003-run-retry-policy` 补齐完整 RunRepository 所需的 RetryPolicy 后,当前 capability 精确推进为 v2: ```json { "contract_name": "control-core", "contract_version": 2, "migration_id": "pg-0003-run-retry-policy", "capabilities": { "run_core": 1, "run_retry_policy": 1 } } ``` `pg-0003` 只接受精确 v1 前置状态;推进失败时新表、索引、capability 和 history 同事务回滚。 ### 4. `ql3` 是严格拥有的核心 schema 当前 contract version 2 精确拥有: - `schema_migrations` - `schema_capabilities` - `runs` - `run_attempts` - `run_events` - `run_retry_policies` 这些表的 required columns 和关键索引由 typed schema contract 固定。缺失或额外表/列/索引均视为 drift。插件、运维工具和未来独立模块必须使用自己的 schema 或经新 migration/contract version 纳入,不能向 `ql3` 静默加对象。 ### 5. Runtime role 使用精确的最小权限 runtime role 必须: - 对 `ql3` 有 `USAGE`、没有 `CREATE`; - 不是任何核心表 owner; - 对 history/capability 只有 `SELECT`; - 对 Run/Attempt 有 `SELECT/INSERT/UPDATE`,没有 `DELETE`; - 对 RunRetryPolicy 有 `SELECT/INSERT/UPDATE`,没有 `DELETE`; - 对 immutable RunEvent 有 `SELECT/INSERT`,没有 `UPDATE/DELETE`。 权限比要求少会导致运行时中途失败;权限比要求多也会 readiness 失败。migration role 由独立 Job 使用,不作为常驻 runtime 凭据。 ### 6. 失败事实必须低敏且稳定 readiness error 只暴露稳定 code 和 bounded finding,例如: - `server_version_unsupported` - `migration_history_invalid` - `capability_invalid` - `schema_contract_invalid` - `runtime_role_invalid` 不得输出连接 URI、口令、SQL 参数、Run payload 或 Secret。数据库连接/查询错误由外层映射为 unavailable,不从错误推导 migration 或副作用结果。 ## 当前孵化状态 `next` 已实现 driver-neutral `assertPostgresSchemaReady()` 和 contract v2: - 支持 PostgreSQL 16–18; - 复用 migration core 的 ahead/gap/checksum 审计; - 精确验证 capability、6 张表、columns、18 个索引、39 个命名 CHECK 和 7 个命名 FK; - 精确验证 schema CREATE、table owner 和六张表的 DML 矩阵; - SQL contract 与 reviewed `pg-0001/0002/0003` migration 有静态 lockstep test; - fake-query tests 覆盖成功、版本过低、capability 漂移、未知对象/约束和过度授权 role。 auditor 已绑定 `@qinglong/cluster-postgres` 的真实 lazy `pg.Pool` integration,并在独立 migration/runtime role 下执行完整 history、catalog 与权限 readiness;该测试已进入 PostgreSQL 16/18 × x64/arm64 CI matrix。ADR-0045 的 HTTP application host 已把 `/readyz` 绑定到本次 activation:catalog/recovery/lifecycle 未全部完成时固定返回 503,完成后才与 `/api/v3` admission 一起切换。远端 PostgreSQL 矩阵成功证据、认证业务 router 和 Kubernetes 部署实测尚未完成,因此 cluster-control 仍不可发布为 ready。 runtime readiness 现在使用 metadata-only migration manifest 审计 ID/checksum,不加载 `pg-*` DDL/up 模块;manifest 与 executable stream 由精确 lockstep test 约束。该拆分隔离常驻 runtime 与 migration role 的代码入口,但不改变完整 history fail-closed 语义。 ## 影响 正面影响: - 滚动升级和 downgrade 不会让不兼容 pod 接受流量; - migration 完成与业务 capability 可分别证明; - 最小权限成为可执行发布门禁,不只是一段运维文档; - 插件和第三方对象不会静默污染核心 schema。 代价与风险: - 每次 contract version 变更都要同步 migration、manifest、GRANT 和 readiness test; - 对额外对象严格失败要求插件使用独立 schema; - 云数据库权限模型差异需要在支持矩阵内逐个验证; - readiness 查询必须有 timeout 和缓存/节流,不能在每个 HTTP 请求重复执行 catalog 扫描。 ## 未选择的方案 1. **只执行 `SELECT 1`**:不能证明 schema 或权限,拒绝。 2. **只比较最后 migration ID**:无法发现 gap、checksum 和 dialect 漂移,拒绝。 3. **发现 drift 自动运行 migration**:runtime/migration 角色混权,拒绝。 4. **忽略未知 `ql3` 对象**:无法区分插件对象、手工 drift 和 ahead schema,拒绝。 5. **runtime 使用 owner/superuser**:破坏最小权限和 blast-radius 边界,拒绝。 6. **readiness 失败后回退 SQLite**:会形成双事实源,拒绝。 ## 验证要求 - PostgreSQL 16/18 上 success report 与 catalog 事实一致; - 15/19/未知未来 major 均 fail closed; - ahead、gap、checksum、stream、dialect 和 history 类型损坏均 not-ready; - capability/history 任一事务回滚后 contract version 不推进; - 任一 required table/column/index 缺失或额外对象出现均 not-ready; - runtime role 多一个或少一个 privilege 都 not-ready; - migration role 可以迁移但不能被 runtime entrypoint 接受; - auditor query timeout、连接失败和 failover 均保持 not-ready,恢复后可重新收敛; - readiness 成功前没有 Repository admission、dispatcher timer、claim 或用户副作用。