Skip to content

fix(service-analytics): postgres 的「缺列」措辞不再被判为「缺源」(#6035) - #6346

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6035-column-phrase-missing-source
Aug 7, 2026
Merged

fix(service-analytics): postgres 的「缺列」措辞不再被判为「缺源」(#6035)#6346
hotlong merged 1 commit into
mainfrom
claude/issue-6035-column-phrase-missing-source

Conversation

@hotlong

@hotlonghotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes#6035

前提复核(结论:成立;但分诊评论的「事实修正」本身已过期,以下为实读)

⚠️ 分诊评论核的是 9e3709a,而 #5717 的实现 PR #6045 在那之后才合并(705efebf2)。所以分诊那条事实修正(「:96 的宽判据 msg.includes('relation') && msg.includes('does not exist') 仍在服役」)在今天的 origin/main 上已不成立——#6045 确实把 isMissingSourceError 里的 postgres 支也收紧成了锚定正则。逐行实读(基于 2bc187641,含 #6292):

立单/分诊说法origin/main 现状结论
isMissingSourceError docblock 写明「not column/syntax errors, which stay hard failures」在(函数上方 docblock,逐字仍在)成立
:96 仍是 includes('relation') && includes('does not exist') 宽判据——已是锚定正则 /relation\s+["']?[A-Za-z0-9_$.]+["']?\s+does not exist/i分诊修正已过期
缺列措辞被判为「缺源」成立,实测 HIT成立
missingSourceRelation 对同一措辞也给出缺源答案成立,实测答 sys_team成立
今天无害(只读面不产生该措辞)成立(读路径措辞 column "bogus" does not exist 实测不命中)成立

核心前提成立,premise_still_valid: true 只是「要动 :96 的宽判据」这个落点描述过期了——真正的落点不是「宽判据 vs 正则」之争,而是判定顺序:缺列措辞里逐字包含一整段合法的缺表措辞,所以无论那一支收紧到什么程度都排除不掉它,只有先问更具体的问题才可以。#6045 自己的 docblock 已经把这条残留指名留在原地(写着 filed as #6035),本 PR 接上。

改了什么(一个文件,一处新常量 + 两处前置摘除)

新增 MISSING_COLUMN_OF_RELATION,在 isMissingSourceErrormissingSourceRelation 两处判定之前摘除:

column "label" of relation "acct" does not exist (SQLSTATE 42703)

正则逐字抄rest-server.tsmapDataError(它的 unknownColumn 探针自 #5352 起就先摘这条措辞、再进 unknown-object 分支,于是 REST 面回答 400 INVALID_FIELD 而不是 404;用例钉在 rest.test.ts)。刻意用同一条正则而非它的第二种方言——两个面不该对「postgres 什么时候在说 column」给出不同答案。仓内还有一条独立佐证:rest-unknown-object-heuristic.test.ts 有一例就叫「the column-level branches still win over the relation limbs」,理由写的是「Postgres spells an unknown column with relation … does not exist inside it」。REST 面早就有这条纪律,本 PR 是把 analytics 面拉齐。

要求两侧都带引号,是安全的犯错方向:postgres 的 errmsg 模板本身就是 column "%s" of relation "%s" does not exist;漏匹配只是维持今天的判定,而过度匹配会把真正缺表的错误变成硬失败,那是反向扩大、会回退 #5033 的刻意宽容。

两半后果都修了(哪一半触发只是 fixture 的偶然)

命中时关系名是旧行为新行为
被 JOIN 的表(如 sys_team)响亮但虚假的跨数据源拓扑错误,把拼写错误说成数据源布局问题原样上抛驱动错误
数据集自己的对象降级空网格 + 一条 warn,拼错的列名不告诉任何人原样上抛驱动错误

只修一半的话,从另一半看过去仍然是绿的,所以两半都钉。

missingSourceRelation 是否同样前置摘除:取舍(立单要求写明)

做了,理由是一致性而不是第二个 bug。 实测:改动前它对这条措辞回答 sys_team。若只修「是不是缺了什么」,这对函数就会互相矛盾——一个说什么都没缺,另一个指着一张表说它缺了。而那正是 #5717 刚在这一支上消除的分歧(它的 docblock 原话:so "is something missing" and "what is missing" can no longer disagree on this limb),再打开等于把同一颗雷重新接上电。

诚实的代价说明:这处 guard 今天不可达,因为该函数只在 isMissingSourceError 为真之后才被调用。但「不可达」是一个调用点的调用顺序的性质,不是这个函数的性质——它一行编辑之外就会变。所以我加了 guard,并且不为它单独写测试:没有公开路径能到达它,硬写一个只会得到一个「因为什么都没产生所以通过」的空用例(#5046 的教训)。这一点写在代码注释里,也写在这里,而不是用一个看起来验证过的断言糊过去。

测试

新增 packages/services/service-analytics/src/__tests__/missing-column-phrase-hard-failure.test.ts(18 例)。

关键设计:#5717 量的 13 条语料,这里queryDataset 的可观测结果(空网格 / 拓扑拒收 / 原样上抛)重新钉,而不是按私有判据的布尔值——这样被守住的是调用方真能观察到的行为,而且 isMissingSourceError + missingSourceRelation 这一是被联合行使的。每条措辞都裸抛(剥掉信封):其中好几条真实 producer 自带 ADR-0112 信封,会靠 #5717 的防线 B 上抛,不剥的话它们作为嗅探器的 pin 就是空的。

13 条语料量化(实测,收紧前 / 后)

措辞
01 sqlite/libsql no such table: temptyempty
02 sqlite 经 knex(SQL 前缀)emptyempty
03 postgres relation "t" does not existemptyempty
04 postgres schema 限定topologytopology
05 postgres 缺列(写路径)topologypropagated
06 postgres 缺列(读路径)propagatedpropagated
07 mysql Table '…' doesn't existemptyempty
08 objectql Datasource … is not registered.topologytopology
09 rest Object 'x' is not registeredtopologytopology
10 analytics CUBE_NOT_FOUND(#3867)emptyempty
11 dataset-compiler 关系拒收propagatedpropagated
12 read-scope nested/relation value …propagatedpropagated
13 analytics measure 闸门(#4437)propagatedpropagated

13 条里只有 1 条改判,就是缺列那条;没有任何真实驱动措辞改判——这就是「收紧而非改变 #5033 行为」的证据形态。

反向验证(方向在跑之前就写死在测试文件头)

预测:本改动只减掉一条措辞,所以回退必须只让关于那条措辞的用例变红,别的一条都不许动。实测(把两处 guard 同时停用):

预测实测
缺列措辞(JOIN 名)RED,变成虚假拓扑拒收✅ RED
缺列措辞(自身对象名)RED,变成空网格✅ RED
语料表仅 05 行 RED✅ 仅 05 行
其余 12 行 + dataset-degradation-envelope.test.ts全绿✅ 全绿(11/11)

本文件 3 红 / 15 绿。我第一版在文件头把绿数写成了 12(18 例算错),更正留在原处而不是悄悄改掉——但要说清楚:算错的是绿的条数,预测的红集合完全命中,而红集合才是这个反向验证承载论证的部分。

两条回退失败的实际输出值得引用,因为它们就是缺陷本身而不只是红标记:

Expected: "column "label" of relation "sys_team" does not exist"
Received: "[Analytics] dataset "sales" cannot be executed as one statement: table
"sys_team" is not on the default datasource, … (driver said: column
"label" of relation "sys_team" does not exist)"
AssertionError: a mistyped column became a confident empty chart:
expected undefined to be an instance of Error

第一条是「对着一个拼写错误讲数据源拓扑的故事」,第二条是什么都没抛、控件拿到了空网格。

消费半径清扫

isMissingSourceError / missingSourceRelation 都是 analytics-service.ts 的模块私有函数,全仓引用实测各只有 1 个调用点(queryDataset 的 catch),无跨包 fixture 喂它们。仍按规矩全仓扫了 of relation 的所有拼写:命中的是 packages/rest 的既有先例与用例(不受影响,且已单独回归跑过)、packages/metadata 的一处注释,以及本 PR 自己的文件——无需改写任何既有 fixture。

验证记录(全部前台阻塞执行,持容器级 flock /tmp/os-heavy-verify.lock,--max-old-space-size=4096)

  • pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=264 files / 1238 tests passed(改动前后各跑一次,均全绿)
  • 新增文件 + [finding] queryDataset 里还有第二个 message 嗅探器 isMissingSourceError,命中即静默返回空结果 —— dataset-compiler 的一条拒收措辞已经命中它,只因抛点在 try 之外才没出事 #5717 文件定向:2 files / 29 tests passed
  • 消费半径 packages/rest(analytics 相关 + rest-unknown-object-heuristic)→ 8 files / 105 tests passed
  • pnpm --filter @objectstack/service-analytics build → 成功;tsc --noEmit 直测该包 → 10 个错误,与账本冻结数一致,新增文件贡献 0 个
  • 根门:check:type-check-coveragecheck:nul-bytescheck:doc-authoringcheck:adr-anchorscheck:route-envelopecheck:error-code-casingcheck:wildcard-fallthroughcheck:durability-log-levelcheck:startup-registry-verdictcheck:engine-double-contractcheck:slot-lookupcheck:query-options-erasurecheck:release-notescheck:empty-changesetpnpm lint(全仓 ESLint)→ 全 PASS
  • check:type-check-debt:首跑红,复核为未建依赖假阳性。该门在 lint.yml 里跑在 turbo run build --filter='./packages/*' --filter='./packages/*/*' 之后(脚本注释写明 tsc 要经各依赖已构建的 dist/*.d.ts 解析)。补跑同一条构建后复测 → PASS(34 个账本条目,none above its recorded number)。首跑报红的 12 个包里没有一个是 service-analytics,与本改动无关。
  • 控制字节自扫:grep -naP 扫三个改动文件的控制字节区间 → 干净

未触:packages/spec、drivers、REST 路由本体、#5717 的信封防线 B。

Changeset

.changeset/analytics-missing-column-hard-failure.md@objectstack/service-analytics: patch(降级路径的判定行为对调用方可见)。

出界发现

已立 #6347(finding,未认领,未带 pm:queue),不在本 PR 顺手改:packages/metadata/src/utils/schema-sync-errors.tsMISSING_TABLE 签名是同族子串洞——写路径缺列措辞实测命中它的 message 支(读路径措辞不命中),而 matchesDriverError 里 code / errno / message 是并列的或,所以「缺列 SQLSTATE 42703 不在 codes 集里」挡不住它。该判据自己的 docblock 恰好点名了 42703、说它「must stay loud」,并说明误判的代价是「start numbering at 1 would be the wrong answer against a table that may be full of rows」——而那正是消费点 database-loader.ts:307-312 在做的事。同样因只读面不产生该措辞而休眠,故按 observation 类记录,严重度留给分诊轮定级。

…d failure (#6035)
`isMissingSourceError`'s docblock promises the dataset degradation path is
scoped to missing SOURCE and that "column/syntax errors stay hard failures so
real query bugs still surface". One postgres wording broke that by
construction:
column "label" of relation "acct" does not exist (SQLSTATE 42703)
carries `relation "acct" does not exist` inside it verbatim. #5717 anchored the
postgres limb to postgres's real missing-table wording and this string matched
anyway -- it had to, because it literally contains that wording. No tightening
of "does this say a relation is missing" can exclude it; only asking the more
specific question first can. The fix is therefore an ORDERING, not a better
regex: subtract the column phrase, then classify.
Both consequences were wrong, and which one fired was an accident of whether
the named relation happened to be the dataset's own object: a joined name
produced a loud but FALSE cross-datasource topology refusal, while the
dataset's own name degraded the widget to an empty grid with the mistyped
column mentioned to nobody. Both halves are pinned.
The subtraction is `rest-server.ts`'s `mapDataError` regex verbatim (its
`unknownColumn` probe has extracted this same phrase ahead of the
unknown-object branch since #5352, so the REST face answers 400 INVALID_FIELD
rather than 404) -- the same pattern rather than a second dialect of it, so the
two faces cannot disagree about what counts as postgres saying "column".
`missingSourceRelation` subtracts it too: measured on origin/main it answered
`sys_team` for this wording, so guarding only the sniffer would leave "is
something missing" and "what is missing" contradicting each other -- the exact
disagreement #5717 closed on this limb.
This aligns a predicate with its own documentation rather than repairing an
incident: analytics is a read face and postgres spells an unknown column in a
SELECT as `column "bogus" does not exist`, with no `relation` in it. The value
is that the disagreement no longer depends on that dormancy holding.
#5717's 13 measured in-repo wordings are re-pinned as `queryDataset` OUTCOMES
(empty grid / topology refusal / propagated) rather than as private-predicate
booleans, so the pair is exercised jointly through the public API: exactly ONE
verdict moves, the column phrase; the other 12 are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015a5qkLzpGXhLL2F5gvJ7dD
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 7, 2026 2:30pm

Request Review

@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics.

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

  • content/docs/api/data-api.mdx(via @objectstack/service-analytics)
  • content/docs/api/index.mdx(via @objectstack/service-analytics)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/service-analytics)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/service-analytics)
  • content/docs/plugins/packages.mdx(via @objectstack/service-analytics)
  • content/docs/releases/implementation-status.mdx(via @objectstack/service-analytics)
  • content/docs/releases/v17.mdx(via @objectstack/service-analytics)
  • content/docs/releases/v9.mdx(via @objectstack/service-analytics)

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.

@hotlong
hotlong marked this pull request as ready for review August 7, 2026 14:51
@hotlong
hotlong added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit 1d0faa7Aug 7, 2026
25 checks passed
@hotlong
hotlong deleted the claude/issue-6035-column-phrase-missing-source branch August 7, 2026 15:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@hotlong@claude