refactor(driver-sql)!: distinct 的第三参收成裸 FilterCondition (#6320) - #6517
Merged
Conversation
`SqlDriver.distinct` 不在 `IDataDriver` 上,所以 #5181 / #6075 的收窄没走到它, #6212 批 A+E(#6355)收的是 analyzeQuery / findWithWindowFunctions,也没覆盖它。 方法体一直说得很清楚 —— `applyFilters(builder, filters)` 拿的是实参本身,因此它 要的是 find() 放在 query.where 里的那个值,不是 query 信封;`filters?: any` 只是 没把这句话写进类型里。 实测(三行数据,逐个形状喂给 distinct): { status: 'completed' } RESOLVED ["Laptop","Mouse"] 省略 RESOLVED ["Laptop","Mouse","Ghost"] 'completed'(标量) RESOLVED ["Laptop","Mouse","Ghost"] <- 静默放宽 { object, where }(信封) THREW INVALID_FILTER / 400 ['status','=','completed'] THREW INVALID_FILTER / 400 (#5158) 第三行是本次消掉的那一格:applyFilters 对「真值但非对象、非数组」的 filter 不发射 任何谓词(尾注写着这件事),于是一个真心想按条件去重的调用编译通过并拿到全集。 有一格任何类型都关不上,如实写进注释而不是假装关上了:FilterCondition 的键就是 字段名,所以它是开放映射,`{ object, where }` 在结构上是合法 filter(约束两个分别 叫 object 和 where 的列)。#6320 提出的「让反向错配也编译不过」在这个参数上不可达, 实测确认;拿得到的保证是运行期响亮失败,已按行为 pin 钉住。driver-memory 那半边留在 #5499 冻结面内,本次不碰;aggregate 区(PR #6404 在飞)一行未动。 零运行时改动:非测试改动是一个类型注解加一段注释。 逐处复核了全部 14 个调用点(本单正文记 3 处,实测偏低):driver-sql 11、 driver-sqlite-wasm 3、driver-turso 0;真正传第三参的 4 处全部本来就写的裸 filter, 零报错、零 fixture 改动。 反向验证(先预判后跑,两次逐一相符): - 签名改回 any:driver-sql 3 红(1x TS2322 + 2x TS2578)、 driver-sqlite-wasm 2 红(1x TS2322 + 1x TS2578)。 - 往参数类型塞一个不可满足的成员并重建 driver-sql:driver-sql 6 红、 driver-sqlite-wasm 3 红 —— 证明 sqlite-wasm 确实读到了新构建的 dist/*.d.ts, 而不是陈旧副本。 Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx Co-authored-by: Claude <noreply@anthropic.com>
…-distinct-filter-condition
…-distinct-filter-condition
…ger (#6320) The #6148 gate asks a declared-breaking changeset what its ledger disposition is. Answered as `registered` rather than exempted, and the three mechanical categories are all genuinely closed to this changeset: - `unpublished` -- @objectstack/driver-sql and @objectstack/driver-sqlite-wasm are both published (private: false), so it fails outright. - `already-registered` -- no existing id covers this surface. - `no-migration-prescription` -- the changeset body carries a worked FROM -> TO migration section, so the gate refuses this category by construction. Using it would be a dodge, not an answer. The entry says up front that it records a TYPE being added rather than a surface being withdrawn, because that distinction decides who has to do anything. It renders into the upgrade guide's "Semantic (delegated to you)" section, carries no `migrate meta` implication and names no stored field -- the same disposition four existing code-surface entries already carry: data-driver-find-stream-retired (#4484), storage-service-list-retired (#5540), actor-user-roles-to-positions (#6011), driver-aggregate-undeclared-key-aliases-removed (#6321). The reason an entry is owed at all: the narrowing is compile-time only, so an untyped JS caller gets neither an error nor a behaviour change -- and that caller is exactly the one sitting on the silent-widening defect, before and after. The generated upgrade guide is the only channel that reaches them. Also moves both runtime pins in the driver-sql suite onto inline argument positions. A `const envelope: FilterCondition = ...` proves only that the alias admits the shape; the claim being pinned is that it reaches THIS PARAMETER uncast, which only an argument position can show (#5018 / #4984 dead-pin shape). Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-distinct-filter-condition # Conflicts: # packages/spec/src/migrations/registry.ts
…ger entry (#6320) `spec-changes.json` and `docs/protocol-upgrade-guide.md` are a pure projection of the registries, and `check:spec-changes` / `check:upgrade-guide` pin the synchrony. The ledger entry landed without them, so both gates were red: spec-changes.json is stale -- the ADR-0087 registries changed without regenerating the manifest. Regenerated, not hand-edited (`pnpm --filter @objectstack/spec gen:spec-changes` and `gen:upgrade-guide`). The entry renders where the disposition claims it does and nowhere else -- under "Semantic (delegated to you, with acceptance criteria)" at line 372, below the section head at 274, and NOT into the "Mechanical (applied for you)" table at 221-273. So it carries no `migrate meta` implication, which is the whole reason a code-surface entry is allowed to sit in this ledger at all. Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…as any` (#6320) Both new suites opened with `(driver as any).knex`, the idiom ~50 sibling files in these two packages already use. It is not a query/options erasure -- the `check:query-options-erasure` ratchet's vocabulary never matched it, and its count is unchanged either way -- but a file whose entire subject is "this parameter stopped being `any`" should not introduce `any` into its own harness. `knex` is `protected` on `SqlDriver`, so a fixture outside the class does need a cast. The #6204 spelling names the one member being reached instead of erasing every member of the driver to get at it: knexInstance = (driver as unknown as { knex: Knex }).knex; `knexInstance` is typed `Knex` rather than `any` as a result, which types the `createTable` callback as `Knex.CreateTableBuilder` for free. Sibling suites are deliberately NOT swept -- out of scope for this card, and worth its own pass. Re-verified after the change, not assumed: driver-sql and driver-sqlite-wasm both typecheck clean, 967 + 257 tests pass, eslint clean on both files, and the erasure ratchet still reports 263 test-surface sites in 49 files. Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 8, 2026 03:08
os-zhuang
enabled auto-merge
August 8, 2026 03:08
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.
Fixes #6320
这份 PR 是「接手 + 验证」,不是从零重做
上一位实施 agent 在提交后、推送前失联,PM 把分支原样抢救推上远端(未改一行、未经任何验证)。本 PR 在此基础上完成:合 main(落后 37 个提交)→ 逐条复核 → 全套实测 → 补齐它漏掉的门禁产物。
一、改了什么
SqlDriver.distinct的第三参filters?: any收成FilterCondition。非测试改动 100% 是一个类型注解加一段 JSDoc,零运行时改动 —— 没有一条语句移动。方法体一直说得很清楚:
applyFilters(builder, filters)拿的是实参本身,从来不是.where,所以它要的是find()放在query.where里的那个值,不是 query 信封。distinct不在IDataDriver上(已核:packages/spec/src/contracts/data-driver.ts全文无该方法),所以 #5181 / #6075 的收窄程序没走到它。收窄消掉的那一格是标量:
applyFilters对「真值但非对象、非数组」的 filter 不发射任何谓词,于是这个真心想按条件去重的调用编译通过,然后拿到全集。这就是 #6320 说的「方向是放宽」。现在它是 TS2345。⛔ 不碰:
driver-memory/driver-mongodb(#5499 冻结面)、aggregate区(#6404 批 B 刚落地)、applyLike/$icontains区(#5702 在飞)。二、调用点实测 —— 立单记 3 处,实测 14 处
自己数的,不是沿用:
传第三参的 4 处本来就写的裸 filter 对象,零报错、零 fixture 改动。加上本 PR 新增的两个 pin 文件(sql 7 + wasm 2)共 23 处。
三、反向验证 —— 方向先判后跑,其中一向推翻了派发模板的预判
A. 签名改回
any⇒ pin 必须红(预判:红)预判命中,逐条相符:
即 sql 3 红(1x TS2322 + 2x TS2578)、wasm 2 红(1x TS2322 + 1x TS2578)。
B.⚠️ 「query 信封交给 sql.distinct 必须编译不过」—— 这一向是反的,如实报告
派发模板预判此处应当编译失败。我的预判是它会成功编译,实测证实了这一点,理由不是实现漏了,而是任何类型都关不上这一格:
FilterCondition的键就是字段名,所以它是开放映射(packages/spec/src/data/filter.zod.ts:304,[key: string]: any)。{ object, where }在结构上是一个完全合法的 filter —— 约束两个分别叫object和where的列。没有任何注解能把它和正当 filter 分开。实测:给那一行加
@ts-expect-error,编译器反过来报「这里根本没有错误可压」:抢救到的代码在这一点上是诚实的:它没有假装关上,而是把注释写清楚,并改用运行期 pin 钉住「响亮失败」—— 信封的
where值是对象,而没有任何比较值可以是对象,于是assertCompilableComparand抛INVALID_FILTER/ 400。这条 pin 是绿的(测试通过)。数组形式(FilterArray,#5158)同理,同样只能靠运行期。结论:#6320 正文提的「让反向错配也编译不过」在这个参数上不可达;能拿到的保证是运行期响亮失败,已按行为钉住。这一格我按实测写,没有把模板的预判硬套上去。
C. driver-sqlite-wasm 确实读到新构建的
d.ts(预判:sql 6 红 / wasm 3 红)wasm 读的是 driver-sql 构建后的
dist/*.d.ts而不是源码,是已知门禁盲区,所以按 #6354 / PR #6396 的做法证明而不是假设:往参数类型临时塞一个调用方不可能满足的成员(FilterCondition & { __os6320_probe__: never }),重建 driver-sql,看调用点是否逐一变红。预判逐处列名后再跑,数量与位置全部相符:
sql-driver-advanced.test.ts393、403;...narrowing.test.ts97、140、166、182 —— 恰是全部 6 处传第三参的调用点。sqlite-wasm-driver-advanced.test.ts382、392;...narrowing.test.ts67 —— 恰是该包全部 3 处传第三参的调用点。探针只存在于 driver-sql 的源码里,wasm 自己一行没改,所以这 3 红只可能来自新构建的d.ts。构建产物同步核对:探针期
dist/index.d.ts:1442读到filters?: FilterCondition & {,还原重建后读回filters?: FilterCondition。@ts-expect-error那两处标量调用在探针期没有新增报错(指令仍被使用),与预判一致 —— 这一条是区分「真的逐点红」与「整片红」的判据。四、ADR-0087 门 —— 走
registered,并说明为什么另外三格都不诚实changeset 标
major,触发 #6148 的门。四格逐一判:not-required (unpublished):假。@objectstack/driver-sql与@objectstack/driver-sqlite-wasm都是private: false,这一格机械上就过不了。not-required (already-registered):假。没有既有条目覆盖这个面。not-required (no-migration-prescription):不诚实。本 changeset 正文带着一段实打实的 FROM 到 TO 迁移处方,门会按「自相矛盾」拒绝它 —— 用它就是把「被回答」变成「被绕过」,⛔ 不用。registered:这一格成立,落一条 step-17 semantic 台账条目driver-sql-distinct-bare-filter-typed。条目开篇就写明它记录的是「加类型」而不是「退役某个面」,因为这个区分决定了谁需要动手。它与
data-driver-find-stream-retired(#4484)、storage-service-list-retired(#5540)、actor-user-roles-to-positions(#6011)、driver-aggregate-undeclared-key-aliases-removed(#6321)同族:驱动调用参数是代码、不是 stack 元数据,所以没有 D2 链要改写,也刻意不立 schema 墓碑。为什么它确实欠一条条目:本次零运行时改动,所以无类型的 JS 调用方既拿不到编译错误、也没有行为变化 —— 而这类调用方恰恰是 tsc 永远够不到的那一批,「静默返回全集」这个缺陷他们改动前后都可能正坐在上面。生成的升级指南是唯一能触达他们的渠道。
渲染位置已核对(不是推断):条目落在
docs/protocol-upgrade-guide.md:372,位于### Semantic (delegated to you, with acceptance criteria)(第 274 行)之下,不在### Mechanical (applied for you)(221–273 行)的表里 —— 所以它不带任何migrate meta含义。spec-changes.json与升级指南没有重新生成,两道同步门当场是红的:已用
gen:spec-changes/gen:upgrade-guide重新生成(非手改),两门现已绿。五、我对抢救到的代码做的修改
const envelope: FilterCondition = ...再传进去,那只证明别名接受这个形状;要钉的是它未经强制转换地到达这个参数,只有实参位能证明。这正是 fix(lint): 删除 validateOrgAxisRedLines 里 spec 合法 stack 到不了的四条分支 (#5009) #5018 / validateOrgAxisRedLines 读的 sharing-rule 键是 spec 拒收的:ADR-0105 D6 ① 在 criteria 路径上从不触发 #4984 的死 pin 形状。(driver as any).knex。knex是protected,fixture 确实需要一次转换,但改成 fix(drivers): 聚合函数拒收带上 ADR-0112 信封,并把两类条件分开措辞 (#5907) #6204 体例的具名写法(driver as unknown as { knex: Knex }).knex,只点名要读的那一个成员,而不是把整个 driver 抹成any;knexInstance因此从any变成Knex。一个通篇主题是「这个参数不再是any」的文件,不该在自己的脚手架里引入any。⛔ 兄弟测试文件(同包约 50 个)不顺手扫 —— 超出本单范围。六、门禁与测试(全部本轮实跑)
driver-sqltypecheck / testdriver-sqlite-wasmtypecheck / testspectestturbo typecheck --filter='...@objectstack/driver-sql'+ speccheck:adr-0087-registrationregistered driver-sql-distinct-bare-filter-typed (new here: ...)check:spec-changes/check:upgrade-guide/check:generatedcheck:empty-changesetcheck:type-check-debtcheck:nul-bytes+ 改动文件自扫eslint --no-inline-configcheck:query-options-erasurecheck:query-options-erasure的绿不是本次改动挣来的护栏:它的词表只匹配 query/options 位置的as any,本次改动的两个文件从头到尾没进过它的计数(改动前后都是263 test-surface sites in 49 files,一模一样)。如实记录,不当成本单的成绩。check:type-check-debt一度报红,归因后确认不是本次改动(§9 陈旧产物陷阱):@objectstack/http-conformance记录 4、实测 10(+6)。逐条读错误内容后确认——10 条里 2 条在node_modules(该条目自己的 note 就预告了这一点),6 条来自metadata-protocol/objectql/runtime/service-cluster的陈旧dist/*.d.ts,指名的是SeedLoaderConfigInput/ExecutionContextInput/ClusterCapabilityConfigInput这些被 main 今天合入的 #6083(ADR-0122 phase 2)退役掉的名字,与distinct/FilterCondition/ driver-sql 无一相关。重建该闭包后重测:10 降到 3,反而比台账记的 4 还低一条。门现已整体绿。七、changeset 复核
级别
major正确:两个包都已发布,distinct是SqlDriver的公开方法,标量第三参从「能编译」变成「TS2345」,对 TS 使用者是编译期破坏。driver-sqlite-wasm同标major也正确 ——SqliteWasmDriver extends SqlDriver且不覆写distinct,它已发布的.d.ts里这个方法签名同样收窄。正文里的每一条读数我都独立复算过(14 个调用点、6/3 与 3/2 的两组反向读数、五种形状的运行期结果),与正文相符。迁移处方如实写了「这不是等价改写:修完之后返回值会变,而变化后的那个才是调用方本来想要的答案」,我认同这个写法。
Generated by Claude Code