Skip to content

REST 读路径:searchFields / groupBy / aggregations 指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254

Description

@os-zhuang

TL;DR

#4226(PR #4240)把 sort / select / expand 三条轴收口成了「要么生效,要么抛错」,加上 filter 轴的 #4134 / #4164 / #4181 / #4121,读路径上四条点名字段的轴已经收满。

同一台机器在另外三条上还在漏气,而且失败方向和已修的那些一模一样:

── searchFields ──────────────────────────────────────
searchFields=title           -> 200  命中 1 行        ✅
searchFields=no_such_field   -> 200  命中 2 行        ❌ 要收窄,结果放宽了
searchFields=title,no_such   -> 200  用 [title]       ⚠️ 未知的那半静默丢弃

── groupBy ───────────────────────────────────────────
groupBy=[status]             -> [{status:'open',n:2},{status:'done',n:1}]  ✅
groupBy=[no_such_field]      -> [{no_such_field:null, n:3}]                ❌ 3 组塌成 1 组

── aggregations ──────────────────────────────────────
sum(amount)                  -> 真实合计                                   ✅
sum(no_such_field)           -> [{status:'open',s:0},{status:'done',s:0}]  ❌ 求和得 0

实测

ObjectQL 引擎 + 真 registry,3 行(open×2 / done×1),走引擎的 in-memory 聚合回退路径(driver-memory / driver-rest / 部分 SQL driver 走的就是这条)。

1. searchFields —— 与 select 完全同构,且端到端可见

根因在 packages/objectql/src/search-filter.ts:111-116

if (requested && requested.length > 0) {
  const allowSet = new Set(allowed);
  const validated = requested.filter((f) => allowSet.has(f));   // ← 部分未知:静默丢弃
  if (validated.length > 0) return validated;
}
return allowed;                                                  // ← 全未知:退回全集

这就是 #4226select 上修掉的那两段逻辑的逐行同构:先丢弃未知项,再在结果为空时退回全集

resolveSearchFields 直接测:

allowed default          : ["title","notes","status"]
requested=["title"]      : ["title"]
requested=["no_such"]    : ["title","notes","status"]   ← 要一列,给了全部
requested=["title","no_such"] : ["title"]                ← 未知的那半无声无息

端到端(search=alphatitle:'Alpha' 在 t1,notes:'alpha in notes' 在 t2):

?search=alpha&searchFields=title           -> ids ["t1"]
?search=alpha&searchFields=no_such_field   -> ids ["t1","t2"]    多返回一行

这一条比 select 那条更值得修,因为它改变结果集select 只改列)。searchFields 是 ADR-0061 的 override,唯一用途就是收窄搜索范围,失败方向却是扩大 —— 同一个反直觉方向。

而且它落在刚修好的那个 bug 的隔壁。 searchFields 目前在框架内的唯一调用方是 GET /data/:object/exportrest-server.ts:4757-4763export-honors-search-term 刚合入)。那个 changeset 自己写的动机是:

exporting after a search downloaded the unsearched superset — more rows than the screen showed, in a file that looks authoritative, with nothing indicating the difference.

searchFields=<拼错> 现在做的正是这件事:导出一个比请求更宽的集合,文件看起来同样权威。同一条路由,同一个失败方向,刚被堵上的洞的旁边。

2. groupBy —— N 组塌成 1 组,结果看起来完全正常

groupBy / aggregationsQUERY_AST_KEYS 的成员,所以不会掉进隐式 filter 桶,但也没有任何字段校验findData 原样转给 engine.aggregate()protocol.ts:3746-3752)。

in-memory 路径上 projectGroupValue(row, 'no_such_field') 对每一行都得 undefined,于是所有行进同一个桶:

groupBy=[no_such_field] -> [{ no_such_field: null, n: 3 }]

n: 3真实的行数,结构也完全合法 —— 和「这个对象的该列真的只有一个取值」一模一样。一个图表拿到这个会画出一根柱子,没有任何东西提示分组没生效。

3. aggregations —— sum 得 0,与真实的 0 无法区分

sum(no_such_field) -> [{status:'open', s:0}, {status:'done', s:0}]

财务报表里的「本季度营收 0」和「字段名拼错了」返回同一个东西。avg / min / max 同理。

4. 顺带:两个后端可能给出相反答案

上面是 in-memory 回退路径的实测。SQL driver 走的是原生 aggregateGROUP BY no_such_field 会被数据库拒绝 —— 具体是抛出还是被吞成 [] 未实测(SqlDriverno such column 有识别与重试逻辑)。如果两条路径给出不同答案,那就是 #4226 里点名的「两条路由对同一份输入给出相反答案」在聚合轴上的重演,值得一并实测确认。

建议

#3948 原则(An unapplied filter must not look like a satisfied one)在同一个 ingress 收口,复用 #4226 落地的 resolveQueryFieldsprotocol.ts,已经是四条轴共用的一次解析):

关联

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions