Skip to content

analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437

Description

@baozhoutao

Found while verifying the 17.0.0-rc.1 checklist on #3909 (F5). Verified on main @ 1ee48bc60, showcase under os serve --dev.

The declared-aggregation half of the analytics honesty axis works (total_sum emits SUM(total), #4184/#4153 hold), and the DATA route already refuses a missing field on searchFields/groupBy/aggregations with a 400 naming the field (#4315/#4254). But the ANALYTICS route lets a measure naming a nonexistent field fall through to SQL:

POST /api/v1/analytics/query  {"cube":"showcase_invoice","measures":["ghost_sum"]}
→ 500 {"success":false,"error":{"code":"SQLITE_ERROR","message":"Internal server error","httpStatus":500}}

inferMeasure('ghost_sum') happily builds SUM(ghost), the driver throws no such column, and the caller gets an opaque SQLITE_ERROR 500 — 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 the error.code for a caller-shaped mistake (ADR-0112).

Part of the #3909 rc.1 verification (section F5).

Activity

  1. baozhoutao commented on Aug 1, 2026

    @baozhoutao
    ContributorAuthor

    Tracked under the v17 verification tracker #3909 (section F, rc.1 run 1).

  2. baozhoutao commented on Aug 1, 2026

    @baozhoutao
    ContributorAuthor

    Rolled up in #4482 (all 17 defects from the v17 verification, grouped by severity with a suggested RC-exit triage).

  3. self-assigned this
    on Aug 1, 2026
  4. os-zhuang commented on Aug 1, 2026

    @os-zhuang
    Contributor

    交接说明(进行中)

    分支:claude/v17-verification-defects-gnf9e6-analytics(framework 仓库,已推送)

    已完成

    1. 复现成功,与 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", ...}     # 点号写法走同一条路
    
    1. 已提交修复并现场验证通过(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 字段的正确性,但读起来会误导。

    下一步具体该做什么

    1. 修上面那个瑕疵:推断路径下把校验不通过的 measure 从建议列表里剔除(或者在推断路径改成列"对象可用字段"而不是"已声明 measure")。
    2. 加回归测试,建议直接照 packages/services/service-analytics/src/__tests__/cube-inference-gate.test.ts 的写法新开一个文件:覆盖 ghost_sum、点号 total.sum、count 正常放行、探针缺失时 stand down、以及"被拒绝不污染 registry"。
    3. 闸门:pnpm --filter @objectstack/service-analytics test、pnpm typecheck。
    4. 开 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions