Skip to content

fix(metadata-protocol): loadMetaFromDb 判「表未建」改问 isMissingTableError,退休手抄的 /no such table/i (#5841) - #5889

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-5841-load-meta-benign-predicate
Aug 6, 2026
Merged

fix(metadata-protocol): loadMetaFromDb 判「表未建」改问 isMissingTableError,退休手抄的 /no such table/i (#5841)#5889
baozhoutao merged 2 commits into
mainfrom
claude/issue-5841-load-meta-benign-predicate

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5841

前提核验(origin/main,实读)

派发前先按 Prime Directive #6 核前提,成立:

  • packages/metadata-protocol/src/protocol.ts/no such table/i 正则仍在(分诊时锚在 :10352,今天 5 次合并后漂到 :10443-10444,以内容定位);
  • 同文件 :15import { isMissingTableError } from '@objectstack/metadata/errors',:3162rethrowUnlessMetadataStoreUnprovisioned 正在用它,另有 6 处调用点。⇒ 「同文件已有集中谓词,这里却手抄第二份」属实,且零 import 成本

改了什么(issue 事实 1)

loadMetaFromDb 外层 catch 从「拿 e.message 跑自己的正则」改为「问 isMissingTableError(e)」。

手抄的一份两个方向都错,而且只有一个方向是响的:

驱动 首启「表还没建」的原话 旧正则 结果
SQLite / libsql no such table: sys_metadata 命中 良性,静默(碰巧对)
PostgreSQL relation "sys_metadata" does not exist(SQLSTATE 42P01) 不命中 一次健康首启打出 [Protocol] DB hydration skipped: …
MySQL / MariaDB Table 'app.sys_metadata' doesn't exist(errno 1146) 不命中 同上
任意驱动 别的失败也写成 "no such table" 命中 误判良性、无声吞掉

isMissingTableError 按 code / errno / message / 一层 cause 认定,DatabaseLoader(#5108)、本包 SysMetadataRepository(#4867)、同文件的 rethrowUnlessMetadataStoreUnprovisioned(#5532 / PR #5705)问的都是它 —— 一个驱动怪癖只教平台一遍。与 #5808「message 正则名单退休」同一类。

顺带一处随之而来的修正:warn 行改用 e instanceof Error ? e.message : String(e)(catch (e: any)catch (e: unknown))。旧版对非 Error 的 rejection 读 e.message 得到 undefined,打出的正是 DB hydration skipped: undefined

反向验证(方向先预测,后运行)

恢复正则版本,预测 5 红 5 绿 —— 不是整片红,红绿的分界本身就是结论:

  • 预测:Postgres 措辞、42P01(纯 code,正则根本看不见)、errno 1146、cause 链包裹,以及非 Error rejection;
  • 预测绿:sqlite 措辞(旧正则本来就是照它写的)、ECONNREFUSED(本来就告警,现在仍告警)、「不认识的措辞仍要响」守卫、fact-2 等价性、工作库对照。全绿说明改动没有溢出到分类之外。

实测逐条一致:

✓ … for sqlite message
× … for postgres message
× … for postgres SQLSTATE 42P01
× … for mysql errno 1146
× … for wrapped in a cause chain
✓ … an unrecognised wording stays loud
✓ … warns for a connection refusal
× … reports a non-Error rejection instead of printing "undefined"
✓ … records the fact-2 indistinguishability
✓ … a working store … hydrates rows and prints no skipped line
Tests  5 failed | 5 passed (10)

那 5 例在修复后全绿。注意第 5 红(非 Error rejection)红在 message 格式化那一半、不在谓词那一半 —— 如实记,不并进「谓词证据」里充数。

事实 2:只测量、只报告,不在本 PR 实现

issue 正文自己要求先量消费方、别把两件事绑成一次改动。测量结果:

返回值 { loaded, errors, invalid } 的生产消费方只有一个 —— packages/objectql/src/plugin.tsrestoreMetadataFromDb(:1140),读法:

const { loaded, errors, invalid = 0 } = await protocol.loadMetaFromDb();
if (loaded > 0 || errors > 0) {
  ctx.logger.info('Metadata restored from database to SchemaRegistry', { loaded, errors, invalid });
} else {
  ctx.logger.debug('No persisted metadata found in database');
}

没有任何调用方对 loaded: 0 做分支处置 —— 唯一的分支是选哪条日志。于是一次读不到存储的 boot,在 kernel 自己的日志里被写成 No persisted metadata found in database,且是 debug 级;协议层那条 console.warn 是唯一的相反信号,两条互相矛盾且不在同一日志通道上。ADR-0110 D3 的同一条规矩,落在 boot 侧。

下游实际行为(plugin.ts start() 的 Phase 2 前后,已实读):

  1. hydration 失败 → 注册表里没有那些 overlay 对象;
  2. Phase 3 的 syncRegisteredSchemas 按注册表建表 —— 没读到的对象自然不建;
  3. bridgeObjectsToMetadataService 同理,不会桥接;
  4. restoreMetadataFromDb 自身 catchreturn,不抛,kernel 照常 ready。

代价在 Phase 2 自己的注释里已经写着(原本说的是「项目内核跳过 hydration」那一支,但对一次失败的读逐字成立):「otherwise registry.getObject() returns nothing for them and every registry consumer (the unknown-$select guard, hooks, relationships) silently degrades」。也就是说:存储读不到 → 一个只有 artifact 的世界被当作真相服务出去,而 kernel 报告健康。

为什么不在本 PR 修:任何能表达「这次根本没读到存储」的修法都要改本方法的返回契约,并同步改 objectql/src/plugin.ts:23 那个 ProtocolWithDbRestore 接口声明与其消费分支 —— 面比本单大,且修向有分叉(加诊断字段 / 抛出 / 只改消费方)。已在报告里列进 open questions,交分诊。

必答项:对相邻单的影响

测试

新建 packages/metadata-protocol/src/protocol.load-meta-hydration-benign.test.ts(10 例,⛔ 未触碰在飞 #5619 的 13 个既有测试文件,也未触碰 scripts/engine-double-contract.baseline.json):

  • 5 种「表未建」形状(sqlite 措辞 / Postgres 措辞 / 42P01 / errno 1146 / cause 链)→ 均静默、{ loaded: 0, errors: 0, invalid: 0 } 正常返回。这 5 种形状是从 isMissingTableError 已声明的签名转录的,没有另起第三份措辞清单;
  • 过宽守卫:role "app_ro" does not exist 仍告警(该谓词刻意不匹配裸 does not exist);
  • ECONNREFUSED → 仍走 warn 分支、不抛(现行为保持);
  • 非 Error rejection → warn 里打出内容而不是 undefined;
  • fact-2 等价性一例:把「outage 的返回」与「空库的返回」toEqual 比对,记录测量、不是背书,注释写明修事实 2 时这条应当翻转、改成断言差异而非删除;
  • 工作正常的库对照:1 行 → loaded: 1,不打 skipped 行。

假引擎只声明 find(loadMetaFromDb 只调它),不声明任何写动词 —— 本包不能 import @objectstack/objectql(反向依赖成环,host-engine.ts 有注释在案),因此也不手抄谓词;check:engine-double-contract 按 verb 逐片扫描,成员缺席即无可 pin。

命令与结果(全部前台阻塞、持容器级 flock 锁):

# 本包全量(merge origin/main 后重跑)
packages/metadata-protocol $ npx vitest run --maxWorkers=2
  Test Files  48 passed (48)
       Tests  471 passed (471)

# 消费半径:objectql 侧读 loadMetaFromDb 的全部测试文件
packages/objectql $ npx vitest run --maxWorkers=2 \
    src/protocol-meta.test.ts src/plugin.integration.test.ts \
    src/protocol-boot-hydration-scoped.test.ts src/plugin.step2.test.ts \
    src/plugin-authored-actions.test.ts src/plugin-authored-hooks.test.ts \
    src/registry-tenant-authored-artifact.test.ts src/save-meta-response-conformance.test.ts
  Test Files  8 passed (8)
       Tests  166 passed (166)

typecheck 说明(照实写,不套模板):@objectstack/metadata-protocol 没有 typecheck 脚本 —— 它在 scripts/check-type-check-coverage.mjs 里是一条已计量的 DEBT 条目,所以 pnpm --filter … typecheck 无从跑起。改为直接在包内跑 npx tsc --noEmit 做增量对照:

BASELINE(origin/main 的 protocol.ts,且不含新测试文件):63 errors
AFTER   (本 PR):                                        63 errors
错误码分布 diff:IDENTICAL
新测试文件中的错误:none

(过程中确实抓到过一个:new Error(msg, { cause }) 的 ES2022 两参重载超出本包 tsconfig 的 lib,已改为 Object.assigncause —— 谓词走的本来就是 cause 属性。)

门禁:check-nul-bytes OK(5701 文件)、check-engine-double-contract OK(38 pinned / 165 DEBT / 2 exempt,无新增)、check-adr-anchors OK。合并 origin/main(⛔ 未 rebase)后 packages/spec 有移动,已按 AGENTS §10 重建 spec 并跑 check:generated —— 10 个生成物全部最新。

changeset

.changeset/load-meta-hydration-benign-predicate.md,@objectstack/metadata-protocol patch

论证「是否 user-visible」:是。运维读到的日志行两个方向都变了 —— Postgres/MySQL 首启不再打那条无从下手的 DB hydration skipped,而一个措辞恰好像「表未建」的真实故障不再被无声吞掉。日志是自托管运维唯一的 boot 期信号,所以按可见处理、带 changeset,不建议skip-changeset


Generated by Claude Code

claude added 2 commits August 6, 2026 10:53
…休手抄的 /no such table/i (#5841)

`loadMetaFromDb` 的外层 catch 靠自己的 `/no such table/i` 正则判断一次失败的
`sys_metadata` 读是不是「首启表还没建」这个良性原因。这是同一个文件里的第二份
手抄词表:本文件 `:15` 已经 import 了 `isMissingTableError`,
`rethrowUnlessMetadataStoreUnprovisioned`(#5532 / PR #5705)、本包
`SysMetadataRepository`(#4867)、`DatabaseLoader`(#5108)问的都是它。

手抄的一份两个方向都错,而且只有一个方向是响的:

- SQLite 说 `no such table: sys_metadata`,正则碰巧匹配;
- PostgreSQL 说 `relation "sys_metadata" does not exist`(SQLSTATE 42P01),
  MySQL 说 `Table 'app.sys_metadata' doesn't exist`(errno 1146),两者都不匹配
  —— 于是一次完全健康的首启会打出 `[Protocol] DB hydration skipped: …`,
  一条运维无从下手的告警;
- 反向:任何驱动把别的失败也写成 "no such table" 时被误判为良性、无声吞掉。

改为按错误类型问 `isMissingTableError`(code / errno / message / 一层 cause),
一个驱动怪癖只教平台一遍。顺带:warn 行改用
`e instanceof Error ? e.message : String(e)`,非 Error 的 rejection 不再打印
`undefined`。

本单只做事实 1。事实 2(非良性失败仍是 `console.warn` + `{loaded:0}` 返回,
调用方无法把「库里没有 overlay」与「根本没读到库」分开,ADR-0110 D3 落在 boot 侧)
只测量、只报告:改它要动本方法的返回契约与 `ObjectQLPlugin.restoreMetadataFromDb`,
不与本改动绑成一次。

新测试文件 `protocol.load-meta-hydration-benign.test.ts`(10 例)覆盖 sqlite /
Postgres 措辞 / 42P01 / errno 1146 / cause 链五种「表未建」形状均判良性无告警,
ECONNREFUSED 与非 Error rejection 仍走告警分支,以及工作正常的库不受影响。
其中的假引擎只声明 `find`(`loadMetaFromDb` 只调它),不声明任何写动词 ——
本包不能 import `@objectstack/objectql`(反向依赖成环),也就不手抄谓词。

反向验证(先预测后运行):恢复正则版本 → 5 红 5 绿,红的恰是 Postgres 措辞、
42P01、errno 1146、cause 链与非 Error rejection 四加一例,绿的是 sqlite 措辞、
ECONNREFUSED、良性过宽守卫、fact-2 等价性与工作库对照 —— 与预测逐条一致。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
@vercel

vercel Bot commented Aug 6, 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 6, 2026 11:02am

Request Review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

3 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-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

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.

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 6, 2026 11:12
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 444a07c Aug 6, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5841-load-meta-benign-predicate branch August 6, 2026 11:26
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/m tests tooling

Projects

None yet

2 participants