Uh oh!
There was an error while loading. Please reload this page.
fix(metadata): sys_metadata 的 DDL 失败必须响亮,只静默「表已存在」一种 (#4728) - #4823
Conversation
`DatabaseLoader.ensureSchema()` 过去用一个空 `catch` 吞掉全部 DDL 失败,并且照样
把 `schemaReady` 置为 `true` —— 注释里的免责理由("e.g. table already exists")
只覆盖了最良性的一种原因,却为**所有**原因开脱。权限不足 / 数据源未连上 / 列类型
冲突之后,表或新列压根不存在,而进程状态与成功路径逐字节相同,日志里一行痕迹也
没有。这正是 #4420 的形态,#4632 已把它定成规则并落地了机械检查。
改为按错误类型判别:
- 新增内部工具 `isSchemaAlreadyExistsError()`,按驱动错误码(Postgres SQLSTATE
42P07/42701/42710、MySQL ER_TABLE_EXISTS_ERROR/ER_DUP_FIELDNAME/ER_DUP_KEYNAME
及 errno、SQLite 只能靠消息)判别,并跟随 `cause` 链;凡是没有被正面识别为
「已存在」的,一律当作真实失败。
- 良性「已存在」:表确实已就绪,静默通过,并照常执行后续迁移与 ADR-0005 索引。
- 其余失败:`console.error` 上报后果(表/列未创建,后续元数据写入不持久,而服务器
仍报告健康)与修复动作(修掉驱动/数据源错误后重启),且只说一次。
- 真实失败后 `schemaReady` 不再置 `true`:启动依旧不被阻断(方法不抛),但 loader
不再声称它并不具备的就绪状态,下一次操作会重试,瞬时故障可自愈(恢复补一条 info)。
- `ensureHistorySchema()` 按同一规则对齐,两处不再一边过度静默、一边过度报错。
删除 `scripts/durability-degradation.baseline.json` 中指向本单的条目(shrink-only)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny…abase-loader-ddl-loud
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
commented
Aug 3, 2026
范围外发现(已单开,未在本 PR 修改):#4825 —— 同文件的 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Fixes#4728
缺陷
DatabaseLoader.ensureSchema()用一个空catch吞掉全部 DDL 失败,并且照样把schemaReady置为true:注释里的免责理由只覆盖了失败原因中最良性的一种,却用它为所有原因开脱 —— 这才是缺陷本身。真实失败(权限不足 / 数据源根本没连上 / 列类型冲突)之后,表或新列压根不存在,而进程状态与成功路径逐字节相同,启动日志里一行痕迹都没有:声称已持久化、实际没落盘、系统看起来完全健康,正是 #4420 的形态。#4632 已把规则(AGENTS.md → "Degradation log levels")与机械检查立好,本处挂在 baseline 里指向本单。
改法 —— 按错误类型判别,而不是按注释里的乐观假设
新增内部工具
packages/metadata/src/utils/schema-sync-errors.ts(未从包入口导出):code42P07/42701/42710;MySQLER_TABLE_EXISTS_ERROR/ER_DUP_FIELDNAME/ER_DUP_KEYNAMEerrno1050/1060/1061code恒为无差别的SQLITE_ERROR,只能靠table … already exists/duplicate column name;Postgres 的relation "x" already exists同样命中cause链方向是刻意保守的:凡是没有被正面识别为「已存在」的,一律当作真实失败。误判为「良性」的代价是静默丢数据,误判为「真实」的代价只是多一行 error。
三条要求的落点:
catch以error上报,文案同时给出后果(sys_metadata的表/列未创建,后续每一次元数据写入都会报错、或在宽松驱动上悄悄丢列,而服务器仍报告健康)与修复动作(修掉下面那条驱动/数据源错误后重启)。按 AGENTS.md「说一次,不是每次失败写入都说」,由schemaFailureReported保证只说一次。schemaReady不再置true。 启动依旧不被阻断(该方法不抛,调用方继续走,真缺表时会在驱动那层响亮地失败),但 loader 不再声称一个它并不具备的就绪状态;下一次元数据操作会重试,所以「数据源当时还在连接」这类瞬时故障可以自愈,恢复时补一条info。这与同文件ensureHistorySchema()的形状一致 —— 这也是把「不阻断启动」变成一个响亮的、被记录的决定,而不是与成功路径同形。project_id → environment_id迁移与 ADR-0005 索引(此前良性路径会连迁移一起跳过)。顺带把
ensureHistorySchema()对齐同一规则:良性「已存在」不再每次写入都打一条error(过度使用error是镜像失败,会训练所有人跳读 error),真实失败同样只响亮一次并保持重试。两处从此一致。Baseline
scripts/durability-degradation.baseline.json中指向本单的条目已删除(该文件 shrink-only,残留会让 gate 变红),现在entries: []。验证
新增测试把两种情况都固化(只测真实失败不够 —— 那样
() => true的分类器也能通过):src/utils/schema-sync-errors.test.ts:良性 7 例(SQLite / Postgres / MySQL /cause链 / 裸字符串)+ 非良性 6 例(权限不足、ECONNREFUSED、列类型不兼容、只读库、无信号值、超深cause)。src/loaders/database-loader.test.ts→DatabaseLoader schema-sync failure reporting (#4728):真实失败响亮且文案含后果与修复动作、不置 ready 因而下次重试(此前是 1 次 syncSchema,现在 3 次)、只说一次、瞬时故障自愈并报告恢复;良性失败静默、置 ready 不重试、迁移照常执行;外加一条DISTINGUISHES the two: same call site, opposite verdicts直接钉住两者可区分;历史表两条同构用例。packages/metadata在check-type-check-coverage.mjs里是 DEBT 条目(无typecheck脚本),仍手工跑了tsc --noEmit:新增文件 0 error,改动未引入新 error(92,均为既有的 TS2835/TS7006 等)。eslint 对四个文件干净。范围严格限定在
packages/metadata+ 那条 baseline 条目;packages/spec/**零改动。🤖 Generated with Claude Code
https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
Generated by Claude Code