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; // ← 全未知:退回全集
这就是 #4226 在 select 上修掉的那两段逻辑的逐行同构:先丢弃未知项,再在结果为空时退回全集。
resolveSearchFields 直接测:
allowed default : ["title","notes","status"]
requested=["title"] : ["title"]
requested=["no_such"] : ["title","notes","status"] ← 要一列,给了全部
requested=["title","no_such"] : ["title"] ← 未知的那半无声无息
端到端(search=alpha,title:'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/export(rest-server.ts:4757-4763,export-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 / aggregations 是 QUERY_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 走的是原生 aggregate,GROUP BY no_such_field 会被数据库拒绝 —— 具体是抛出还是被吞成 [] 未实测(SqlDriver 对 no such column 有识别与重试逻辑)。如果两条路径给出不同答案,那就是 #4226 里点名的「两条路由对同一份输入给出相反答案」在聚合轴上的重演,值得一并实测确认。
建议
按 #3948 原则(An unapplied filter must not look like a satisfied one)在同一个 ingress 收口,复用 #4226 落地的 resolveQueryFields(protocol.ts,已经是四条轴共用的一次解析):
关联
TL;DR
#4226(PR #4240)把
sort/select/expand三条轴收口成了「要么生效,要么抛错」,加上 filter 轴的 #4134 / #4164 / #4181 / #4121,读路径上四条点名字段的轴已经收满。同一台机器在另外三条上还在漏气,而且失败方向和已修的那些一模一样:
实测
真
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:这就是 #4226 在
select上修掉的那两段逻辑的逐行同构:先丢弃未知项,再在结果为空时退回全集。resolveSearchFields直接测:端到端(
search=alpha,title:'Alpha'在 t1,notes:'alpha in notes'在 t2):这一条比
select那条更值得修,因为它改变结果集(select只改列)。searchFields是 ADR-0061 的 override,唯一用途就是收窄搜索范围,失败方向却是扩大 —— 同一个反直觉方向。而且它落在刚修好的那个 bug 的隔壁。
searchFields目前在框架内的唯一调用方是GET /data/:object/export(rest-server.ts:4757-4763,export-honors-search-term刚合入)。那个 changeset 自己写的动机是:searchFields=<拼错>现在做的正是这件事:导出一个比请求更宽的集合,文件看起来同样权威。同一条路由,同一个失败方向,刚被堵上的洞的旁边。2.
groupBy—— N 组塌成 1 组,结果看起来完全正常groupBy/aggregations是QUERY_AST_KEYS的成员,所以不会掉进隐式 filter 桶,但也没有任何字段校验:findData原样转给engine.aggregate()(protocol.ts:3746-3752)。in-memory 路径上
projectGroupValue(row, 'no_such_field')对每一行都得undefined,于是所有行进同一个桶:n: 3是真实的行数,结构也完全合法 —— 和「这个对象的该列真的只有一个取值」一模一样。一个图表拿到这个会画出一根柱子,没有任何东西提示分组没生效。3.
aggregations——sum得 0,与真实的 0 无法区分财务报表里的「本季度营收 0」和「字段名拼错了」返回同一个东西。
avg/min/max同理。4. 顺带:两个后端可能给出相反答案
上面是 in-memory 回退路径的实测。SQL driver 走的是原生
aggregate,GROUP BY no_such_field会被数据库拒绝 —— 具体是抛出还是被吞成[]未实测(SqlDriver对no such column有识别与重试逻辑)。如果两条路径给出不同答案,那就是 #4226 里点名的「两条路由对同一份输入给出相反答案」在聚合轴上的重演,值得一并实测确认。建议
按 #3948 原则(An unapplied filter must not look like a satisfied one)在同一个 ingress 收口,复用 #4226 落地的
resolveQueryFields(protocol.ts,已经是四条轴共用的一次解析):searchFields→ 未知字段400 INVALID_FIELD。两支都拒(全未知 / 部分未知),理由与 REST 列表:sort/select/expand指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 对select的裁决一致:一个没生效的收窄不该看起来像生效了。注意resolveSearchFields的allowed是可搜索字段子集而非全部字段,所以「这个字段存在但不可搜索」需要单独一条消息(与 REST 列表:sort/select/expand指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 给expand的三段式消息同构)。groupBy/aggregations→ 未知字段400 INVALID_FIELD。aggregations[].field与groupBy[]都要校验;count(*)这类无 field 的形态要放行。sort/select/expand指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226:registry + 字段表在 → 权威;无 registry / 无字段表 / legacy 数组字段表 → 跳过。引擎内部调用方不受影响。关联
sort/select/expand指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 / PR fix(data):sort/select/expand指向不存在的字段时被拒绝,而不是静默丢弃 (#4226) #4240(sort / select / expand,已合并)—— 本 issue 是它明确划在范围外的剩余部分?pageSize=5返回 200 + 空列表 #4134 / REST 列表:显式filter在场时,同时传的字段级参数被静默丢弃(#4134 的邻居) #4164 / REST 列表:无法解析的filterJSON 被静默忽略 —— 返回未过滤整页(#4134/#4164 家族第三员) #4181 / A filter array that isn't a valid AST reaches the driver as an opaquewhere— reject it at the protocol instead #4121(filter 轴四案,均已合并)searchFieldsoverride 的来源)export-honors-search-term(刚给 export 路由加上search/searchFields的那个改动)