refactor(drivers)!: aggregate 的 query 参数收窄到 DriverQuery,并退役 aggregate / func 两个未声明别名 (#6212 批 B, #6321) - #6404
Merged
Conversation
…liases retire (#6212 batch B, #6321) Three doors moved off `query: any` onto `DriverQuery`: `SqlDriver.aggregate`, `TursoDriver.aggregate` (the forwarding layer) and `RemoteTransport.aggregate`. `any` on a query parameter is not "the object name goes unchecked", it is every check off on exactly the members these bodies read. Three steps, in the order that makes the reds explicable: 1. Fixtures re-spelled to the declared keys (`aggregate:` -> `aggregations:`, `func:` -> `function:`) in driver-sql's and driver-sqlite-wasm's advanced + queryast suites. Non-test writers were zero, so this took the writer count of both undeclared keys to zero. 2. `query.aggregations || query.aggregate` and `agg.function || agg.func` deleted on both faces (ADR-0049 enforce-or-remove, #6321). The trailing `|| ''` on the remote face went with them: it only fired when neither key was written, and it made the two faces quote different spellings back. 3. Signatures narrowed. One real behaviour change, not a type-only one. `groupBy` is a `GroupByNode` union and the remote transport read it as `string[]`. The plain structured form (`{ field: 'region' }`) is spec-valid AND pushed down by objectql today, and it died here as `"[object Object]"` while the local face of the same driver compiled it — a live remote/local fork. It now reads `.field`. A date-bucketed entry genuinely cannot be compiled remotely and is refused with the ADR-0112 envelope (NOT_IMPLEMENTED/501). `SqlDriver`'s bare `throw` for the identical condition took the same envelope in the same commit, so the two faces keep one wire identity (#5240/#5907); a cross-package parity case compares their runtime messages. `alias` is deliberately not read, in step with `SqlDriver.aggregate`. Closes #6321
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…0087 ledger (#6321) The #6148 gate asks a declared-breaking changeset what the ledger disposition is. Measured, rather than assumed: a candidate entry renders into the upgrade guide's "Semantic (delegated to you, with acceptance criteria)" section as a true, executable human instruction, and NOT into the "Mechanical (applied for you)" table -- so it carries no `migrate meta` implication and names no stored field that does not exist. That is the same disposition three existing entries already carry for code surfaces: data-driver-find-stream-retired (#4484), storage-service-list-retired (#5540), actor-user-roles-to-positions (#6011). An untyped JS caller has no enforced channel at all once the alias is gone -- tsc reaches typed call sites only -- which is precisely the case the ledger exists to reach.
This was referenced Aug 7, 2026
os-zhuang
marked this pull request as ready for review
August 7, 2026 17:36
os-zhuang
enabled auto-merge
August 7, 2026 17:36
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #6212 (批 B)
Closes #6321
aggregate的 query 参数从any收到DriverQuery,三处落点:SqlDriver.aggregate、TursoDriver.aggregate(转发层)、RemoteTransport.aggregate。any在 query 参数上不是「对象名没检查」,而是检查全关——where的 filter 方言、groupBy的节点联合、aggregations的节点形状,恰恰就是这三个方法体读的全部内容。严格按派工的硬性顺序做:① 重拼 fixture → ② 删两个未声明别名 → ③ 收窄签名。倒过来会拿到一堆解释不清的红;按这个顺序走,收窄那一步在 driver-sql / driver-turso / driver-sqlite-wasm 上零报错。
① fixture 重拼(4 个文件,16 处)
aggregate:→aggregations:、func:→function:。逐处核对过语义:sql-driver-advanced.test.ts7 处、sqlite-wasm-driver-advanced.test.ts7 处 —— 纯重拼:这些用例考的是 sum/count/avg/min/max/groupBy 的行为,别名只是拼法。field: '*'保留不动,AggregationNodeSchema.field是z.string().optional(),而驱动对'*'有专门分支。sql-driver-queryast.test.ts/sqlite-wasm-driver-queryast.test.ts各 1 处 —— 整条替换,不是重拼。它们的标题是should still support legacy aggregate format,钉的恰恰是本 PR 删掉的那条肢;「legacy format」在这次之后不存在了,标题说着一个已消失的东西的用例比没有用例更糟。改为 declared 拼法的同一查询(这个文件本来就是讲标准 QueryAST 键的),旧拼法的拒收另立 pin 文件。② 退役两个未声明别名(#6321,ADR-0049)
QueryASTSchema声明aggregations,AggregationNodeSchema声明function;aggregate/func在packages/spec里一个字都没有。这是 #4984 家族:fixture 拼着别名,宽容分支就永远绿着活下去,没有任何测试能在删掉它时转红。①做完写者归零,PD #12 + ADR-0049 于是把两条||删掉。顺带删掉
|| '':它只在两个键都没写时生效,那时远端面把名字回引成""、本地面回引成"undefined"—— 同一份越界输入两种措辞(#5240)。别名在时这条岔路够不着,删别名恰恰让它够得着,所以同一次关掉。driver-mongodb/src/mongodb-driver.ts:472也读query.aggregate,但已被as any罩住、且 mongodb 是批 C 的面 ⇒ 本批不碰,是已知残余,随批 C 处置。③ 一处真实行为改动:
RemoteTransport现在会编GroupByNode联合GroupByNodeSchema是z.union([z.string(), z.object({ field, dateGranularity?, alias? })]),这一层却按string[]读。收窄后tsc直接把这条假设摆上台面(TS2322)。处置选 (a):运行期收窄 + 残余显式拒收,无任何as any/ 类型断言。派工书说「今天不炸的原因是
turso-driver.ts在 remote 模式把queryDateGranularity声明为空表」。那只挡住了一半。 objectqlengine.ts的 aggregate 派发里逐字写着:即无 granularity 的结构化条目根本不受那个能力位约束,引擎照样下推。而
groupBy: [{ field: 'db_password' }]这个形状是活体 ——packages/objectql/src/secret-fields.test.ts:341就是它。于是今天:groupBy: [{ field: 'region' }]SqlDriver.aggregate读g.field)GROUP BY "region",正常返回RemoteTransport)"[object Object]",死在assertSafeIdentifier,报「不安全标识符」一条查询两种答案、由连接串决定,而且是活体不是休眠 —— 正是 #6203 那个形状。这把本批从「顺手收窄类型」抬成了「关掉一条真实分叉」,也是 (a) 之所以是对的选择的全部理由。
处置
.field,两面收敛。dateGranularity的条目 → 远端确实编不出,且这一半是已声明能力位管的(remote 发布queryDateGranularity: {},引擎全部落内存分桶、不下推)。缺的只是「绕过能力位直连驱动」的调用方该得到什么 —— 现在得到 ADR-0112 信封NOT_IMPLEMENTED/ 501(照 drivers(sql,turso):Unsupported aggregate function两面都是裸Error(code/status 皆 undefined),且三个 spec 已声明的聚合函数无任何 SQL 后端编译 #5907 / PR fix(drivers): 聚合函数拒收带上 ADR-0112 信封,并把两类条件分开措辞 (#5907) #6204 体例),而不是一句 SQL 注入告警。alias不读:SqlDriver.aggregate也不读它,只在这一面读会是新分叉而不是修复。「pushdown 忽略 / in-memory 认g.alias ?? g.field」这条既有分歧另立 finding(见下),本 PR 不修、也不加剧。SqlDriver那一面的同一条件也换上了信封(PM 已裁定留在本 PR)SqlDriver.aggregate对「本方言编不出这个 granularity」原本抛裸Error(code/status皆undefined⇒mapDataError落默认分支,一个具名能力缺口以不透明 500 到达调用方)。两处触发条件逐字同一(supports.queryDateGranularity[g] !== true),只给远端加信封正是 #5907 花一整个 issue 关掉的「一条件两身份」。它们不是两个可分的改动,是一个条件的两种拼写,因此同一次落地:两面首句逐字一致,尾句各报本面编得出的 granularity(这一处差异本身就是能力位的全部内容)。由一条跨包 parity 用例比对两个运行时消息钉住 —— 两边都不读共享常量,⚠️ 每条拒收用例都断
REFUSAL_SENTENCE是第三份独立副本,所以「将来只改一面」和「两面一起改错」都会红。code和status,不只断「抛了」(#6144 / PR #6233:本面对这些输入原本也抛,只断 toThrow 会恒绿)。反向验证(方向先判后跑,三组全部命中)
三组预判都写在各测试文件的 docblock 里,跑之前写的;实测数字随后补录。
A. 把两条别名分支放回去(两面),签名不动
pnpm test:driver-sql 2 红(expected 30 to be undefined;expected the driver to refuse the query, but it resolved)、driver-turso 3 红 / 4 绿(两条运行期 + parity)。pnpm typecheck:绿,不动 —— excess-property 检查不关心方法体读什么。a || b放回去仍然先读a。所以 fixture 重拼必须在前,且不能兼作 pin —— 那四个文件此后已无法分辨别名分支在不在。B. 把 remote 的 groupBy 读法改回
string[](只改远端面)unsafe identifier rejected: "[object Object]";5 条 granularity 拒收红在expected undefined to be 'NOT_IMPLEMENTED';parity 同上;'month'非对称用例只红在远端半边;注入用例第一条断言绿、第二条红(旧代码抛的是同一句话、原因却相反 —— 所以断的是越界文本而不是那句话)。C. 把签名改回
anypnpm typecheck红在四条@ts-expect-error上:TS2578: Unused '@ts-expect-error' directive.(driver-sql 116/124 行、driver-turso 106/112 行),外加remote-transport.ts一条TS7006。pnpm test不受影响 —— 所以这两个 key pin 的通道是tsc,不是 vitest,docblock 里逐条按通道写清楚了。验证与门禁
基线:merge
origin/main@e15bf7ef7(含批 A+E PR #6355 与批 C PR #6356,两者都动过sql-driver.ts,合并无冲突)后全仓pnpm build再验。driver-sqltypecheck / testdriver-tursotypecheck / testdriver-sqlite-wasmtypecheck / testverifytest(消费面)spectypecheck / test(因新增台账条目)pnpm check:query-options-erasuretestSurface.sites263,无变动pnpm check:type-check-debt(全仓 re-measure)none above its recorded numberpnpm check:nul-bytes+ 手工控制字符自扫check:adr-0087-registration/check:generatedquery-options/no-any-erasure)dist/*.d.ts(派工点名的门禁盲区):全程按「先 build driver-sql → 再 typecheck/test driver-sqlite-wasm」做,它在验证面内。它自身源码零改动(改的只有 fixture 与一条断言),因此按批 A+E 的先例未列入 changeset。两条与派工书预判不同、如实记:
--update。 派工书预判「删多余as any会让 267 跌」。实测:三个驱动包里.aggregate(… as any)站点为 0,那 49 处全在别的包、走的是engine.aggregate,属超范围。基线一行未动(263 是 main 上批 C 已经棘轮下来的数)。check:type-check-debt出现过一次 §9 假红并已证伪。 新 worktree 只建了依赖闭包时,该门报 root DEBT +5 / plugin-approvals TEST_DEBT +1。把本分支改动git stash掉、在干净 main 上重测得到同样的 +5 / +1 ⇒ 与本改动无关;跑完全仓pnpm build后该门转绿。即 §9 的漏建假红也会打到这道门上([skill] 新 worktree 里第一次验证之前必须先 build 依赖闭包 —— AGENTS.md §9 的陈旧产物陷阱当日连咬三个 dev(假红 + 假绿两个方向) #6371 记)。ADR-0087 台账处置:
registered(先测后选,实测决定的)#6148 新门(PR #6342,今天合入,仓内零先例)要求声明为 breaking 的 changeset 写明台账处置。四个标记逐个在真 commit 上实测:
not-required (unpublished)private: true,不是「这个键从未发布」。原话:@objectstack/driver-sql, @objectstack/driver-turso are PUBLISHEDnot-required (already-registered)not-required (no-migration-prescription)FROM → TO,而本 changeset 的迁移段是中文。正文确实给了改写处方 ⇒ 用它就是踩盲区过关,不写registered+ 条目 id决定性测量:在本地写候选台账条目 →
gen:spec-changes+gen:upgrade-guide→ 看它实际渲染成什么。判据只有一条:那行字对仓外升级者是不是真话、能不能照着做。结果:条目落进
### Semantic (delegated to you, with acceptance criteria),不在### Mechanical (applied for you)那张带「migrate metaonly」列的表里(全文命中 1 次)。渲染出来是可执行的人类指令:⇒ 没有任何
migrate meta暗示,没有指向不存在的入库字段。我先前判registered为「实质错」是错的:我把台账等同于migrate meta,而台账的semantic数组恰恰是代码面的非机械残余。仓里已有三条同形先例,且它们在自己的reason里逐字写着这个道理:data-driver-find-stream-retired(#4484)、storage-service-list-retired(#5540)、actor-user-roles-to-positions(#6011 —— 正是这道门的立门案例)。其中 #6011 那条写着:「for an untyped or sandboxed body there is no enforced channel at all, which is exactly why this ledger entry has to exist」 —— 本例同理:收窄后tsc只够得着有类型的调用点,无类型 JS 调用方唯一的通知渠道就是这条台账。于是新增一条⚠️ 条目里显式写了一句别读串的警告:
packages/spec/src/migrations/registry.ts的 step-17semantic条目(只加这一条,PM 就此放开批 B 的「⛔ 不碰 packages/spec」范围约定),并重新生成spec-changes.json与docs/protocol-upgrade-guide.md。dashboard/page的 measure 那边aggregate是规范键、func是已声明的响亮别名(DatasetMeasureSchema)—— 那套词表本 PR 不碰,而它很可能正是仓外调用方当初把这两个键写到 query 上的原因。门禁:
node scripts/check-adr-0087-registration.mjs→✓ 1 declared-breaking changeset(s) … registered driver-aggregate-undeclared-key-aliases-removed (new here: …);pnpm --filter @objectstack/spec check:generated→✓ All 10 generated artifacts are up to date(含check:spec-changes、check:upgrade-guide);spec 自身typecheck绿、Tests 8644 passed (8644);改台账后三个驱动包 + verify 全部重跑仍绿(959 / 828 / 254 / 17)。一条与本 PR 无关、但影响面更大的连带发现(已记 #6419):
hasMigrationPrescription只认英文大写FROM → TO,而 AGENTS.md 要求解释性文字用中文 ⇒ 本仓用中文写的迁移处方一律照不到,该格的实际约束力低于设计意图。边界
未碰:
analyzeQuery/findWithWindowFunctions(批 A+E)、memory / mongodb(批 C)、distinct(#6320)、#6204 的信封与降级表、#6203 的大小写判定、#6210 的其它签名。packages/spec只动了 ADR-0087 台账那一条新条目及其两个生成物(PM 就此放开范围约定,理由见上一节),schema / 类型 / 导出面零改动。TursoDriver.aggregate的options?: any也刻意不动 —— 那是第二条轴,与它上面四个 override 逐字相同,五改其一会被读成对另外四个的判断(#6210 在count上同理)。超范围发现(另立单,本 PR 不修)
GroupByNode.alias只有内存分桶认,三个 SQL 面一律忽略 —— 同一个 aggregate 走下推与走回退拿到不同的列键 #6401GroupByNode.alias只有 in-memory 分桶认(g.alias ?? g.field),三个 SQL 面(driver-sql / sqlite-wasm / turso 两面)一律忽略 ⇒ 同一个 aggregate 走 pushdown 与走内存回退拿到不同的列键。本 PR 与本地面保持一致(同样不读),不加剧。TursoDriver五个 override 的options?: any—— #5181 收窄的是 query,options 这条轴还没人碰过 #6402TursoDriver五个 override 的options?: any(update/upsert/delete/count/aggregate)—— 观察类。no-migration-prescription矛盾检查匹配的是占位符FROM/TO,不是真实处方 —— 写得越好的处方越照不到 #6419 ADR-0087 处置词表缺「该键从未是已声明表面」这一格 + 中文迁移处方照不到探测器。本 PR 已由实测走通registered,故不再阻塞本 PR;但那两条对全仓仍成立,留给domain:spec-tooling座位裁。