Repository navigation
analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437
Copy link
Copy link
Closed
Description
Activity
Tracked under the v17 verification tracker #3909 (section F, rc.1 run 1).
Rolled up in #4482 (all 17 defects from the v17 verification, grouped by severity with a suggested RC-exit triage).
交接说明(进行中)
分支:
claude/v17-verification-defects-gnf9e6-analytics(framework 仓库,已推送)已完成
- 复现成功,与 issue 完全一致(showcase dev server,
--fresh):
POST /analytics/query {"cube":"showcase_invoice","measures":["ghost_sum"]} → 500 {"code":"SQLITE_ERROR","message":"Internal server error","httpStatus":500} POST /analytics/query {"cube":"showcase_invoice","measures":["total.sum"]} → 500 {"code":"SQLITE_ERROR", ...} # 点号写法走同一条路- 已提交修复并现场验证通过(commit
4372aef)。AnalyticsService.ensureCube现在在任何 SQL 被构造之前,把每个 measure 解析出来的源字段拿去和承载对象的字段名核对,拒绝时用与 data route 完全相同的信封(400 INVALID_FIELD+field/object/param,fix(data):searchFields/groupBy/aggregations指向不存在的字段时被拒绝,而不是静默降级 (#4254) #4315/REST 读路径:searchFields/groupBy/aggregations指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254 的那一套):
POST /analytics/query {"cube":"showcase_invoice","measures":["ghost_sum"]} → 400 {"code":"INVALID_FIELD","message":"Measure 'ghost_sum' on cube 'showcase_invoice' aggregates field 'ghost', which object 'showcase_invoice' does not have. Valid measures: … "} POST /analytics/query {"cube":"showcase_invoice","measures":["total.sum"]} → 400 INVALID_FIELD,message 里指名 'sum' # 对照组仍然正常 {"measures":["total_sum"]} → 200 SELECT SUM(total) … = 2567.9 {"measures":["count"]} → 200 SELECT COUNT(*) … = 12门槛沿用 #3867 那个推断闸门的分级:只在 cube 的
sql是裸对象名时检查、只在新增的getObjectFieldNames探针能回答时检查、只检查源是裸列名的 measure(count(*)和account.industry这类跨对象点号引用一律放行)。校验发生在 cube 注册之前,所以被拒绝的查询不会在 registry 里留下痕迹(否则重试会撞上"已注册"的脏 cube 直接冲进 SQL)。当前卡在哪
不算卡住。有一个待打磨的小瑕疵:错误信息里的 "Valid measures:" 在自动推断 cube 这条路上会把出错的那个 measure 自己也列进去(实测输出
Valid measures: count, ghost_sum),因为inferCubeFromQuery已经把它塞进cube.measures了。不影响状态码和field字段的正确性,但读起来会误导。下一步具体该做什么
- 修上面那个瑕疵:推断路径下把校验不通过的 measure 从建议列表里剔除(或者在推断路径改成列"对象可用字段"而不是"已声明 measure")。
- 加回归测试,建议直接照
packages/services/service-analytics/src/__tests__/cube-inference-gate.test.ts的写法新开一个文件:覆盖ghost_sum、点号total.sum、count正常放行、探针缺失时 stand down、以及"被拒绝不污染 registry"。 - 闸门:
pnpm --filter @objectstack/service-analytics test、pnpm typecheck。 - 开 draft PR(同分支上还带着 analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467、Setup System Overview: every KPI tile reads 0 — the dataset path lowers an all-time range to WHERE created_at = $1 #4475)。
Generated by Claude Code
- 复现成功,与 issue 完全一致(showcase dev server,
- added 3 commits that reference this issue
on Aug 2, 2026
Metadata
Metadata
Assignees
Labels
No labels
Found while verifying the 17.0.0-rc.1 checklist on #3909 (F5). Verified on
main@1ee48bc60, showcase underos serve --dev.The declared-aggregation half of the analytics honesty axis works (
total_sumemitsSUM(total), #4184/#4153 hold), and the DATA route already refuses a missing field onsearchFields/groupBy/aggregationswith a 400 naming the field (#4315/#4254). But the ANALYTICS route lets a measure naming a nonexistent field fall through to SQL:inferMeasure('ghost_sum')happily buildsSUM(ghost), the driver throwsno such column, and the caller gets an opaqueSQLITE_ERROR500 — a driver name on the wire and nothing actionable. A dotted spelling takes the same path ("measures":["total.sum"]→ prefix-strip →inferMeasure('sum')→ 500 SQLITE_ERROR).Expected: measure resolution validates the inferred source field against the cube/object schema BEFORE building SQL, and refuses with a 400 naming the field and the valid measures — the same shape the data route's #4315 refusal already has (and the
dateRange-family validation on this same endpoint already achieves for its own inputs). A driver error class should never be theerror.codefor a caller-shaped mistake (ADR-0112).Part of the #3909 rc.1 verification (section F5).