Skip to content

fix(data): sort 的点号路径被拒绝(400 INVALID_SORT),不再静默降级为不排序 (#4256) - #4303

Merged
os-zhuang merged 3 commits into
mainfrom
claude/sort-dot-path-fallback-fc72e9
Jul 31, 2026
Merged

fix(data): sort 的点号路径被拒绝(400 INVALID_SORT),不再静默降级为不排序 (#4256)#4303
os-zhuang merged 3 commits into
mainfrom
claude/sort-dot-path-fallback-fc72e9

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4256

TL;DR

#4226 收口后唯一漏网的 sort 形态补上了:?sort=account.company_name(点号路径)现在在共享 normalizer 处被拒 —— 400 INVALID_SORT,消息说明它想跨的关系、以及正确做法(用公式/汇总字段把值反范式化到本对象再排)。此前它是 200 + 未排序行:头段是真实字段所以过闸,然后 SqlDriver 渲染出 "account"."company_name" 撞上从未 join 的表,#3821 兜底丢掉排序重跑;Mongo / memory driver 则把路径解析到行内,而外键是标量 id,排序照样无效。sort + top 的「最新 N 条」于是变成任意 N 条,响应里没有任何信号。

采用 issue 的方案 1(ingress 拒绝),决策依据

Issue 要求先量影响面再定。已对 framework / objectui / cloud 三仓做了穷尽调查:

  • 真实调用方发出点号 sort 的数量:0。 视图/报表/仪表盘元数据、examples、seeds、sys_saved_report 种子里全部是平面字段名。
  • objectui 结构上产生不了点号 sort key:lookup 列按自己的平面字段名作为列 key(accessorKey: fieldName),关联数据走独立的 $expand 参数嵌在同一平面 key 下;SortBuilder 的字段选项来自 objectDef.fields 的平面枚举,无自由输入。通用表格的列头排序是纯客户端的,不出网。
  • cloud 全部 ~25 处 sort 均为平面单字段。
  • 测试无一依赖点号 sort(无论断言丢弃还是依赖功能),文档也没有任何一页在教它 —— 仅有的两处提及都是警告。

因此不需要 deprecation 轮次,直接拒绝的爆炸半径就是手写请求 —— 恰好是被静默降级误导的那批调用方。

残余风险(grep 覆盖不到的):客户库里的存量视图元数据。ObjectView 会把工具栏排序持久化进保存视图,sys_saved_report 存用户排序;但由于没有任何 UI 路径和种子能写出点号值,存量里存在点号 sort 的唯一途径是管理员在 Studio 元数据管理里手改 JSON(SchemaForm 的 repeater 是裸文本输入)。若要绝对确定,可在生产库对保存视图/报表做一次一次性查询。

实现

assertSortFieldsExistpackages/metadata-protocol/src/protocol.ts)在原有 unknown-field 检查之后加点号检查,消息按头段区分两种误解:

  • 头段 reference 字段(project_id.name)→ 指名它想跨的关系,处方是反范式化(公式/汇总字段);
  • 头段不是title.length)→ 陈述契约:sort 只作用于整列,不进入列值内部。

未知头段(no_such.title)保持 #4226 的 typo 答案且优先报告 —— 与 expand gate 的 unknown > not-a-reference 优先级一致。仅注册表有字段表时生效(与所有 #4226 gate 相同的分层);直接调 engine.find() 的内部调用方不受影响。expand 子查询的嵌套 orderBy 不经过顶层 gate,无误伤。

顺带查证:QueryAST 的显式 joins(点号排序唯一可能合法的场景)已在 #4294 被 tombstone —— 「join 别名排序」这个合法形态不存在。

实测(真引擎 + 真 registry,showcase fresh 实例,与 issue 实测表同法)

?sort=project.name      -> 400 INVALID_SORT  「follows the relationship 'project'…Denormalise…」  ✅(修复前:200 + 未排序)
?sort=no_such.title     -> 400 INVALID_SORT  「not a field on object…」                           ✅(不变)
?sort=-title            -> 200,正确排序                                                          ✅(控制组)
?sort=project           -> 200(裸外键列仍可排)                                                   ✅(控制组)
?sort=title.length      -> 400 INVALID_SORT  「whole columns, not values inside them」            ✅

测试与文档

  • query-expression-conformance.test.ts 新增 12 个用例:七种 wire 拼写的点号拒绝、关系/非关系两种消息、typo 优先级、裸外键控制组、top footgun 收口。
  • 受影响包全绿:metadata-protocol 122 / objectql 1357 / rest 521。全仓套件在本机 turbo 并行下有资源型超时抖动(五次失败为五个不同包,含 auth 注册测试 10s 超时,transform 124s —— 均单独复跑全绿),以远端 CI 为准。
  • 文档:query-syntax.mdx §3 告警改写(「仍静默降级」的前提已不成立)、queries.mdx info callout 与 data-api.mdx 参数表/示例表补点号形态。
  • changeset:@objectstack/metadata-protocol patch。

关联

#4256 / #4226(PR #4240)/ #3821 / #3948 / #4294(joins tombstone)

🤖 Generated with Claude Code

os-zhuang and others added 2 commits July 31, 2026 12:40
The one sort shape #4226 deliberately left open. `?sort=account.company_name`
passed the gate on its head segment (a real field) and was then unusable by
every driver: SqlDriver rendered `"account"."company_name"` against a table
that was never joined and the #3821 backstop retried WITHOUT the sort; Mongo
and the memory driver resolved the path against the row, where a foreign key
is a scalar id. 200, every row present, arbitrary order — which `top` then
sliced into an arbitrary "latest N".

Now `400 INVALID_SORT` at the shared normalizer, with the message split by
what the head segment is: a relationship head names the relation it tried to
cross and prescribes denormalising the value onto the queried object (formula
/ rollup field); a non-reference head states the contract (sort reaches whole
columns, not values inside them). Unknown heads keep the #4226 typo answer,
reported first — the same precedence the expand gate uses.

A survey of framework, objectui and cloud found zero callers emitting a
dotted sort; objectui's column-header sort structurally cannot produce one
(lookup columns are keyed by their flat field name, relations load via
$expand). Blast radius is hand-authored requests — exactly the callers the
silent degradation was misleading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 5:25am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit a62bd9e into main Jul 31, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/sort-dot-path-fallback-fc72e9 branch July 31, 2026 05:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sort 的点号路径(?sort=account.company_name)仍然静默降级为「不排序」——#4226 收口后唯一漏网的 sort 形态

1 participant