Skip to content

fix(metadata): sys_metadata 的 DDL 失败必须响亮,只静默「表已存在」一种 (#4728) - #4823

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-4728-database-loader-ddl-loud
Aug 3, 2026
Merged

fix(metadata): sys_metadata 的 DDL 失败必须响亮,只静默「表已存在」一种 (#4728)#4823
os-zhuang merged 2 commits into
mainfrom
claude/issue-4728-database-loader-ddl-loud

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4728

缺陷

DatabaseLoader.ensureSchema() 用一个空 catch 吞掉全部 DDL 失败,并且照样把 schemaReady 置为 true:

} catch {
  // If syncSchema fails (e.g. table already exists), mark ready and continue
  this.schemaReady = true;
}

注释里的免责理由只覆盖了失败原因中最良性的一种,却用它为所有原因开脱 —— 这才是缺陷本身。真实失败(权限不足 / 数据源根本没连上 / 列类型冲突)之后,表或新列压根不存在,而进程状态与成功路径逐字节相同,启动日志里一行痕迹都没有:声称已持久化、实际没落盘、系统看起来完全健康,正是 #4420 的形态。#4632 已把规则(AGENTS.md → "Degradation log levels")与机械检查立好,本处挂在 baseline 里指向本单。

改法 —— 按错误类型判别,而不是按注释里的乐观假设

新增内部工具 packages/metadata/src/utils/schema-sync-errors.ts(未从包入口导出):

判据 覆盖
驱动错误码 code Postgres SQLSTATE 42P07 / 42701 / 42710;MySQL ER_TABLE_EXISTS_ERROR / ER_DUP_FIELDNAME / ER_DUP_KEYNAME
errno MySQL 1050 / 1060 / 1061
消息兜底 SQLite 的 code 恒为无差别的 SQLITE_ERROR,只能靠 table … already exists / duplicate column name;Postgres 的 relation "x" already exists 同样命中
cause 跟随最多 4 层(驱动常包一层再抛)

方向是刻意保守的:凡是没有被正面识别为「已存在」的,一律当作真实失败。误判为「良性」的代价是静默丢数据,误判为「真实」的代价只是多一行 error。

三条要求的落点:

  1. catcherror 上报,文案同时给出后果(sys_metadata 的表/列未创建,后续每一次元数据写入都会报错、或在宽松驱动上悄悄丢列,而服务器仍报告健康)与修复动作(修掉下面那条驱动/数据源错误后重启)。按 AGENTS.md「说一次,不是每次失败写入都说」,由 schemaFailureReported 保证只说一次。
  2. 真实失败后 schemaReady 不再置 true 启动依旧不被阻断(该方法不抛,调用方继续走,真缺表时会在驱动那层响亮地失败),但 loader 不再声称一个它并不具备的就绪状态;下一次元数据操作会重试,所以「数据源当时还在连接」这类瞬时故障可以自愈,恢复时补一条 info。这与同文件 ensureHistorySchema() 的形状一致 —— 这也是把「不阻断启动」变成一个响亮的、被记录的决定,而不是与成功路径同形。
  3. 只静默「表已存在」这一种。 良性时表确实已就绪,当作 no-op 通过,并照常执行后续的 project_id → environment_id 迁移与 ADR-0005 索引(此前良性路径会连迁移一起跳过)。

顺带把 ensureHistorySchema() 对齐同一规则:良性「已存在」不再每次写入都打一条 error(过度使用 error 是镜像失败,会训练所有人跳读 error),真实失败同样只响亮一次并保持重试。两处从此一致。

Baseline

scripts/durability-degradation.baseline.json 中指向本单的条目已删除(该文件 shrink-only,残留会让 gate 变红),现在 entries: []

验证

$ pnpm --filter @objectstack/metadata test
 Test Files  15 passed (15)
      Tests  313 passed (313)

$ pnpm check:durability-log-level
✓ self-test: 10 case(s) passed
✓ durability-degradation log levels: 8 durability-critical catch seam(s), all loud or rethrowing.

新增测试把两种情况都固化(只测真实失败不够 —— 那样 () => true 的分类器也能通过):

  • src/utils/schema-sync-errors.test.ts:良性 7 例(SQLite / Postgres / MySQL / cause 链 / 裸字符串)+ 非良性 6 例(权限不足、ECONNREFUSED、列类型不兼容、只读库、无信号值、超深 cause)。
  • src/loaders/database-loader.test.tsDatabaseLoader schema-sync failure reporting (#4728):真实失败响亮且文案含后果与修复动作、不置 ready 因而下次重试(此前是 1 次 syncSchema,现在 3 次)、只说一次、瞬时故障自愈并报告恢复;良性失败静默、置 ready 不重试、迁移照常执行;外加一条 DISTINGUISHES the two: same call site, opposite verdicts 直接钉住两者可区分;历史表两条同构用例。

packages/metadatacheck-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

claude added 2 commits August 3, 2026 08:40
`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
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 8:44am

Request Review

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata.

7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata)
  • content/docs/kernel/cluster.mdx (via packages/metadata)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata)
  • content/docs/plugins/packages.mdx (via @objectstack/metadata)
  • content/docs/protocol/kernel/metadata-service.mdx (via @objectstack/metadata)
  • content/docs/releases/v12.mdx (via @objectstack/metadata)
  • content/docs/releases/v9.mdx (via @objectstack/metadata)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Copy link
Copy Markdown
Contributor Author

范围外发现(已单开,在本 PR 修改):#4825 —— 同文件的 nextEventSeq() 用同一个 return 1 对待「表还没建」(良性)和「驱动读失败」(不良性)。历史表已有 N 行时的一次瞬时读失败会让下一条历史记录从 event_seq = 1 重新发号、与既有行撞号,而写入成功、日志无痕。是本单缺陷的同形,只是在下一层,且 _find 不在 #4632DURABILITY_CRITICAL_CALLEES 词表里,机械检查看不到它。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 3, 2026 08:51
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 3, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit c4ab50b Aug 3, 2026
21 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4728-database-loader-ddl-loud branch August 3, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[metadata] database-loader 吞掉 sys_metadata 的 DDL 失败后仍置 schemaReady=true —— 第二类降级(#4632 规则),本轮因包冻结未修

2 participants