Repository navigation
analytics 自动推断路径:measures 上的关系穿越点号 member 仍被剥成基表列 —— owner.region_count_distinct 静默聚合基表 region(#5739 裁决未覆盖的第四个铸造点) #5918
Description
Activity
分诊:
needs-user-decision(即席分析路径的公共查询语义)+ 维持domain:services。⚠️ 本单此前处于**「半分诊」态**(只有domain:services,无 pm-state 标签)⇒ 对队列视图与决策箱皆不可见。本轮补状态标签。过时前提检查(
origin/main9e3709a)—— 成立:analytics-service.ts的inferCubeFromQuery在(:1141调用点);lookupMember的 relation-traversal 那一档仍是 dimension-only(:151-196区段的注释与kind === 'dimension'分支);:1480/:1516两处注释仍记着 #5739 只覆盖 dimension 形状的三个铸造点。⇒ 「measures 是第四个铸造点、且未被 #5739 覆盖」的事实面未变。#5739 已 closed:completed,这恰恰确认本单是它刻意留在范围外的残项,不是它的重复。为什么进决策箱而不是入队(判据:正文自己列的三问,每一问都不是实现细节)
- 即席路径要不要支持关系穿越 measure(
SUM("owner"."amount")+ LEFT JOIN)—— 这是给公开查询面新增一项能力;两个策略的能力还不对等(NativeSQL 有,ObjectQL 的planCrossObject对跨对象 measure 是响亮拒收),即答「支持」就同时要答「两个策略行为不一致怎么办」; - 若支持,
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只给名字,不给类型/关系目标)⇒ 这可能才是真正的成本所在,也可能意味着「支持」这条路需要先补元数据能力; - 若不支持,是否响亮拒收 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
- 即席路径要不要支持关系穿越 measure(
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 liketotal.sumfrom #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
认领: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
ACCEPT → PR #6292(draft 转 ready,auto-merge 挂上,慢检查绿后自动入队)。
- 裁决(选项 3)照实落地:
mintableMeasureKey只剥真正的 cube 限定符,其余点号一律经既有invalidMemberError拒收——INVALID_FIELD/ 400 /member= 调用方原拼写 /param:'measures',analytics: a measure naming a missing field 500s with SQLITE_ERROR instead of a 400 naming the field #4437 家族形状,零新码表成员(STOP 条件未触发)。 - 一处主动申报的扩面,复核采信:issue 指的是
inferCubeFromQuery的铸造循环,dev 实测即席路径会注册推断结果——同一查询自第二次请求起走ensureCube后缀增补循环复现同一静默错列(暖库复现在 PR)。只堵冷路径会让每台长活服务器从第 2 个请求起照旧错,故规则由两个铸造点共享;增补循环先逐字认 cube 声明的 measure 键,逆向实验证明该检查承重(blanket strip 下声明的带点 measure 被误杀转红)。这是裁决意图在同一面两扇门上的完整落地,不是漂移。 - 验收主证:改前基线复现两策略的错误 SQL/聚合;逆向验证方向先写后跑,预判 10 红/4 绿逐条命中且空
sqls断言非空转;两条既有钉按新语义改写(现断言原拼写 + gatefield缺席);包内 1220 绿、rest 861 + runtime 1506 消费半径绿;tsc 与 DEBT 台账逐条吻合零新增(初稿曾 +8,重写归零,如实记录)。 - changeset 以「Observable behaviour change.」开头声明收紧:今天静默错答的查询将 400——这正是裁决要点。
- 衍生:docs:
/analytics/query的两处示例用measures: ['revenue.sum']—— 首段不是 cube 名,这条查询今天就答 400 #6291(content/docs 两处示例自身用了revenue.sum拼写,今天已 400、本 PR 后读作被明确拒绝的拼写)立案交分诊,未在本 PR 越面修 docs。 - [finding]
isMissingSourceError仍把 Postgres 的「缺列」措辞column "c" of relation "t" does not exist判为「缺源」——文档明说不该,今天靠读路径不产生该措辞而无害 #6035 排程:同文件串行,待本 PR MERGED 后立即派发(其 worktree 需含本改动)。
Generated by Claude Code
- 裁决(选项 3)照实落地:
- added a commit that references this issue
on Aug 8, 2026 - added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Sep 29, 2026 - added a commit that references this issue
on Oct 1, 2026
实现 #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'],两个策略都静默通过、无任何拒收:这与 #5739 正文里的 ④ 是同一个形状:响应列名标着关系属性
owner.region_count_distinct,值却来自基表region,调用方无法从结果里看出来。#5739 已把dimensions/where/timeDimensions三个键上的这个形状消掉,measures是剩下的第四个。分档同 #5739:
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 的:所以 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 的判定复制过来。
裁定时需要回答的
SUM("owner"."amount")+ LEFT JOIN)?NativeSQL 有能力(qualifyAndRegisterJoin对 measure 的sql一视同仁);ObjectQL 没有(planCrossObject对跨对象 measure 是响亮拒收),这与 dimension 的情形一致。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只给字段名列表,不给类型/关系目标),这可能才是本单真正的成本所在。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 键。