Skip to content

analytics 自动推断路径:measures 上的关系穿越点号 member 仍被剥成基表列 —— owner.region_count_distinct 静默聚合基表 region(#5739 裁决未覆盖的第四个铸造点) #5918

Description

@hotlong

实现 #5739(裁 B:即席推断路径支持关系穿越)时实测发现并刻意留在范围外。本单不是 #5739 的 PR 引入的,也不被它修复。

位置

packages/services/service-analytics/src/analytics-service.ts,inferCubeFromQuery 的 measures 铸造循环。

#5739 把该函数里三个 dimension 形状的铸造点(dimensions / where 的字段键 / timeDimensions)从「剥掉任何点号首段」改成「只剥真正的 cube. 限定符」,于是 owner.region 原样铸造并走 JOIN 穿越。measures 循环保持原样(仍是无差别剥首段),理由写在该循环的注释里 —— 见下面「为什么 #5739 没有顺手改」。

实测(在含 #5739 改动的分支上;crm_account 字段 id/name/industry/region/owner,注意 region 是基表自己的列)

查询 measures: ['owner.region_count_distinct'],两个策略都静默通过、无任何拒收:

NativeSQL
→ SELECT COUNT(DISTINCT region) AS "owner.region_count_distinct" FROM "crm_account"
                        ↑ 基表列                ↑ 别名标着关系属性        ↑ 没有 JOIN

ObjectQL
→ executeAggregate 收到 aggregations:
  [{ field: "region", method: "count_distinct", alias: "owner.region_count_distinct" }]

两者铸出的 measure 键都是 `region_count_distinct`(getMeta: `crm_account.region_count_distinct`)

这与 #5739 正文里的 ④ 是同一个形状:响应列名标着关系属性 owner.region_count_distinct,值却来自基表 region,调用方无法从结果里看出来。#5739 已把 dimensions / where / timeDimensions 三个键上的这个形状消掉,measures 是剩下的第四个。

分档同 #5739:

  • 基表恰好有同名列 → 静默聚合错列(上面的实测),行数/图表都是错的,没有任何错误可读;
  • 基表没有同名列 → 落到 analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437 的 400 INVALID_FIELD,点名剥出来的尾段。实测 measures: ['owner.score_sum'] 答 Measure 'owner.score_sum' … aggregates field 'score', which object 'crm_account' does not have —— 消息本身诚实(进 SQL 的确实是 score),但调用方写的是 owner.score_sum。

为什么 #5739 没有顺手改(实测,不是猜测)

lookupMember 的 synthetic relation-traversal 那一档是 dimension-only 的:

if (kind === 'dimension') return { sql: member, type: 'string' };

所以 dotted measure 没有一个「已经正确的穿越答案」可以收敛过去 —— #5739 裁 B 的依据(数组 where 写法今天已编出正确 JOIN,拒收会拒掉与已正确行为等价的查询)在 measures 上不成立。

而且实测:若把 measures 也改成原样铸造,measures: ['total.sum'](一个 total_sum 的手滑拼写,不是穿越)会从 #4437 的 400 INVALID_FIELD 退化成 ObjectQL 的 cannot evaluate a cross-object measure —— 一个不带 code/status 的 5xx 级错误,而且诊断是错的(它不是穿越)。measure-source-field-gate.test.ts 的用例 refuses the dotted spelling the same way, naming what it stripped to 正是钉这条的,#5739 落地时它红了一次,遂把 measures 排除在改动外并在代码里写明原因。

所以这一支需要自己的裁定,而不是把 #5739 的判定复制过来。

裁定时需要回答的

  1. 即席路径要不要支持关系穿越 measure(SUM("owner"."amount") + LEFT JOIN)?NativeSQL 有能力(qualifyAndRegisterJoin 对 measure 的 sql 一视同仁);ObjectQL 没有(planCrossObject 对跨对象 measure 是响亮拒收),这与 dimension 的情形一致。
  2. 如果支持,total.sum 这类非穿越的手滑拼写要怎样保住 analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437 的 400 INVALID_FIELD?二者今天在词法上不可区分(都是「点号 + 尾段」)。可能的判定:首段是否为基表的 lookup 字段名 —— 但即席路径上没有字段元数据可查(getObjectFieldNames 只给字段名列表,不给类型/关系目标),这可能才是本单真正的成本所在。
  3. 如果不支持,是否应当响亮拒收 dotted measure(带 code/status 的 400,点名调用方写的 owner.region_count_distinct),以消掉静默错列?这一档不需要新的元数据能力,代价最小。

查重

搜过三仓 open issue / open PR:inferCubeFromQuery / stripPrefix / analytics dotted measure / cross-object measure —— 无同题单。#5739(本单的兄弟,dimension 侧)已落地;#5353 / #5669 / #5740 均不覆盖 measures 键。

Activity

  1. claude commented on Aug 6, 2026

    @claude
    Contributor

    分诊:needs-user-decision(即席分析路径的公共查询语义)+ 维持 domain:services。

    ⚠️ 本单此前处于**「半分诊」态**(只有 domain:services,无 pm-state 标签)⇒ 对队列视图与决策箱皆不可见。本轮补状态标签。

    过时前提检查(origin/main 9e3709a)—— 成立:analytics-service.ts 的 inferCubeFromQuery 在(:1141 调用点);lookupMember 的 relation-traversal 那一档仍是 dimension-only(:151-196 区段的注释与 kind === 'dimension' 分支);:1480 / :1516 两处注释仍记着 #5739 只覆盖 dimension 形状的三个铸造点。⇒ 「measures 是第四个铸造点、且未被 #5739 覆盖」的事实面未变。#5739 已 closed:completed,这恰恰确认本单是它刻意留在范围外的残项,不是它的重复。

    为什么进决策箱而不是入队(判据:正文自己列的三问,每一问都不是实现细节)

    1. 即席路径要不要支持关系穿越 measure(SUM("owner"."amount") + LEFT JOIN)—— 这是给公开查询面新增一项能力;两个策略的能力还不对等(NativeSQL 有,ObjectQL 的 planCrossObject 对跨对象 measure 是响亮拒收),即答「支持」就同时要答「两个策略行为不一致怎么办」;
    2. 若支持,total.sum 这类非穿越的手滑拼写如何保住 analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437 的 400 INVALID_FIELD —— 立单人已实测二者在词法上不可区分,且即席路径上没有字段元数据可查(getObjectFieldNames 只给名字,不给类型/关系目标)⇒ 这可能才是真正的成本所在,也可能意味着「支持」这条路需要先补元数据能力;
    3. 若不支持,是否响亮拒收 dotted measure —— 这一档不需要新元数据能力、代价最小,但它会拒掉今天静默通过的查询,即一次对外行为收紧。

    ⇒ 三条出路各自改变对外可写的查询形状,PM 不代拍。⚠️ 且 #5739 已有先例:那一单裁 B 的依据是「拒收会拒掉与已正确行为等价的查询」,而立单人实测该依据在 measures 上不成立(dotted measure 今天没有一个「已经正确的穿越答案」可收敛)⇒ 不得把 #5739 的判定直接复制过来,这一支需要自己的裁定。

    今天的可达伤害(供定优先级,已实测):基表恰好有同名列时 → owner.region_count_distinct 静默聚合基表 region,无 JOIN、无任何错误,响应列名标着关系属性而值来自基表,调用方无法从结果里看出来(行数/图表都是错的)。这是静默错答,不是拒收 —— 决策箱里属应尽早拍的一类。

    域锚定:落点 packages/services/service-analytics/src/analytics-service.ts ⇒ 域表 packages/services/* 那一行 ⇒ domain:services(维持)。

    串行提示(裁决落地后适用):在飞 PR #6045(#57xx,ready)实读触同一文件 analytics-service.ts ⇒ 届时排其后。

    查重:立单人已搜过三仓(inferCubeFromQuery / stripPrefix / dotted measure / cross-object measure),本座位复搜确认 —— #5739(兄弟单,dimension 侧,已收官)、#5353 / #5669 / #5740 均不覆盖 measures 键;objectui / cloud 无影子单。本单是该处唯一入口。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 7, 2026

    @claude
    Contributor

    Ruling (maintainer, 2026-08-07): option 3 — refuse dotted measures loudly (400, naming the caller's original spelling).

    Note this is explicitly not an extension of #5739's ruling. There, refusing would have rejected queries that already compiled correctly, so casting as-is was the convergent answer. On the measures face the dev's measurement shows the opposite: there is no correct traversal answer to converge on (lookupMember's traversal path is dimension-only), so casting as-is would turn a typo like total.sum from #4437's clean 400 into a code-less 5xx. The two faces genuinely differ; the母单's rationale does not carry over.

    A 400 naming the full spelling (owner.region_count_distinct) answers both the typo and the genuine-traversal case honestly, without needing the field metadata that options 1 and 2 would have to build first. If a real traversal-measure requirement ever appears, that is a capability issue with its own justification.

    This tightens externally visible behaviour: queries that silently pass today will start returning 400. That is the point — today they silently aggregate the wrong column. Options 1/2 are not being deferred for cost alone; they are unbuildable without a metadata capability nobody has asked for.

    Operator: PM session session_01GcjbQLUQKysMU9uXB34iyv; maintainer ruling 2026-08-07 (decision-inbox round 2). Veto window open — comment or reopen to overturn.


    Generated by Claude Code

  3. self-assigned this
    on Aug 7, 2026
  4. hotlong commented on Aug 7, 2026

    @hotlong
    ContributorAuthor

    认领:PM 循环第 7 轮
    会话:session_015a5qkLzpGXhLL2F5gvJ7dD
    分支:claude/issue-5918-dotted-measure-reject
    Worktree:objectstack-5918
    域:domain:services
    文件面:packages/services/service-analytics/src/analytics-service.ts(inferCubeFromQuery 的 measures 铸造循环)+ measure-source-field-gate.test.ts + changeset

    按 2026-08-07 03:19Z 维护者裁决实施:选项 3——响亮拒收 dotted measure(带 code/status 的 400,点名调用方原拼写如 owner.region_count_distinct);⛔ 明确不是 #5739 的延伸(其裁 B 依据在 measures 上被实测推翻),不做穿越支持(选项 1/2 需先建元数据能力,无人拉动)。对外行为收紧是裁决要点:今天静默聚合错列的查询将开始返回 400。串行前提已清:#6045/#6003/#6213 均落地,#6035 同文件排本单之后。


    Generated by Claude Code

  5. hotlong commented on Aug 7, 2026

    @hotlong
    ContributorAuthor

    ACCEPT → PR #6292(draft 转 ready,auto-merge 挂上,慢检查绿后自动入队)。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions