Skip to content

refactor(drivers)!: 五个驱动的 query 参数跟进 DriverQuery,休眠的类型谎言没有藏身处 (#6075) - #6210

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-6075-driver-query-signatures
Aug 7, 2026
Merged

refactor(drivers)!: 五个驱动的 query 参数跟进 DriverQuery,休眠的类型谎言没有藏身处 (#6075)#6210
os-zhuang merged 3 commits into
mainfrom
claude/issue-6075-driver-query-signatures

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6075

这单是什么

#5181(PR #6076)把 IDataDriver 的六个 query 方法收窄为 DriverQueryOmit< QueryAST, 'object' >)。那条 changeset 自己写明了收尾未做:「把驱动签名一并迁到 DriverQuery 是后续的机械收尾」。这就是那次收尾。

前提复核(实测 origin/main @ 80f7dc6,行号以实测为准)

issue 正文的行号已漂移,以下为实测:

  • 契约侧成立packages/spec/src/contracts/data-driver.ts:40 导出 DriverQuery:141 :167 :195 :211 :214 :345 六个方法都已声明它。
  • 驱动侧成立:memory 5 处、mongodb 6 处(另 2 处私有辅助)、sql 6 处(其中 explainany)、turso 5 处 anysqlite-wasm 自身 0 处 —— SqliteWasmDriver extends SqlDriver,签名随基类走,本 PR 不需要动它的源码。
  • 休眠性成立git grep 'query\.object' origin/main -- 'packages/drivers/*/src' 零命中。issue 要求复核的这一条仍然成立 ⇒ 今天没有人踩,这是休眠的类型谎言,不是活体缺陷。

反向验证 —— 方向预判:(预判写在动手之前)

本单的全部价值就是让「驱动读 query.object」变成编译错误,所以预判是标准的「把删掉的肢体装回去 ⇒ 变红」。两个方向都真跑了:

after(收窄后),在 SqlDriver.find 里临时写一行 query.object

src/sql-driver.ts(2297,54): error TS2339: Property 'object' does not exist on type 'DriverQuery'.

before(对照:把同一个签名改回 QueryAST,探针保留),TS2339 消失——谎言在收窄前编译得干干净净。该轮剩下的 134 条全部是 TS2345,来自已重拼的 fixture 被改回去的宽签名拒绝,与探针无关。

探针已删除(git diff 中零残留)。

新增的 pin 文件 sql-driver-query-signature.test.ts 也做了同样的非幻影验证:把 find 改回 QueryAST 后它四条断言全红,且第一条按方法逐个报名:

src/sql-driver-query-signature.test.ts(53,10): error TS2322: Type 'string' is not assignable to type 'never'.
src/sql-driver-query-signature.test.ts(63,7): error TS2578: Unused '@ts-expect-error' directive.
src/sql-driver-query-signature.test.ts(72,5): error TS2578: Unused '@ts-expect-error' directive.
src/sql-driver-query-signature.test.ts(79,11): error TS2322: Type 'DriverQuery' is not assignable to type 'QueryAST'.

改了什么

非测试改动 100% 是类型注解,无逻辑、无行为、无 emit 差异(git diff 里 4 个源文件共 33 增 32 删,逐行都是签名)。

六个契约方法:find / findOne / count / updateMany / deleteMany / explain。turso 的 query: any 一并收紧。

跟着必须动的私有辅助方法(都只转发或读 where / orderBy / groupBy,本来就不读 object;不动它们则公有方法传参处直接类型不通):

辅助方法 说明
mongodb buildFindOptions / buildSortSpec 只读 fields / orderBy
sql findRows / orderKeysFor 只读 orderBy / limit / offset
turso toRemoteQuery / toRemoteReadQuery 只重写 where、拼 orderBy
memory performAggregation 见下

sql-driver.tsQueryAST import 收窄后已无使用者(余下两处是散文里的 QueryASTSchema),一并删除。

收窄不只是消除谎言:实测降了一批类型债

check:type-check-debt 的全仓 re-measure 在本分支报出一批下降项。门禁明说下降不必付记账代价(「an improvement must not have to pay a bookkeeping toll to land」),所以台账未动;但这是本单的实际收益,值得记一笔:

ℹ @objectstack/driver-mongodb: TEST_DEBT records 43, tsc now reports 10 (-33)
ℹ @objectstack/objectql:       TEST_DEBT records 355, tsc now reports 344 (-11)
ℹ @objectstack/rest:           TEST_DEBT records 163, tsc now reports 153 (-10)
ℹ @objectstack/mcp:            TEST_DEBT records 63,  tsc now reports 53  (-10)
ℹ @objectstack/lint:           TEST_DEBT records 42,  tsc now reports 32  (-10)
ℹ @objectstack/service-storage: DEBT records 52,      tsc now reports 42  (-10)
ℹ @objectstack/runtime:        TEST_DEBT records 227, tsc now reports 223 (-4)

只有 driver-mongodb 的 −33 做了归因验证,其余未归因(它们可能早于本分支就已下降,我没有度量,不替它们邀功)。mongodb 这一条是掀掉 tsconfig 的测试排除后直接对照量的:

状态 测试层原始错误数
origin/mainmongodb-driver.tsquery: QueryAST 43
本 PR 的 mongodb-driver.tsquery: DriverQuery 10

原因很直接:mongodb 的测试里本来就有一批只递 where / fields 的 query 字面量,在 QueryAST 必填 object 的年代它们全是错误、被冻在 TEST_DEBT 里;收窄之后它们本来就该编译通过了。

冻结面口径与一处需要维护者过目的判断

按认领评论的第三档口径,memory / mongodb 一并收紧,且硬约束是只允许改签名类型。全程遵守,但有一处值得单独说明,请复核:

memory-driver.ts 的私有 performAggregation(records, query: QueryInput) 只解构 { groupBy, aggregations }、从不读 object,但 QueryInputobject 列为必填,于是 find 收窄后传参报 TS2345。处理方式是把它改成 Omit< QueryInput, 'object' > —— 镜像掉同一个键,不改其余任何松紧度,因此它仍是纯签名类型改动,没有动逻辑、没有动行为、没有动断言,符合硬约束。之所以不写成 DriverQuery:那会连带收紧 groupBy 的元素类型(输入形 vs 输出形),超出「只搬走 object 这一个键」的范围。若维护者认为冻结面连这一处都不该动,我可以把 memory 半边整体拆出。

Fixture 三类处置(不是一把批量重拼)

收窄触发 399 处 TS2353,逐处按其守的是什么来判:

  1. 重拼(绝大多数)driver.find(X, { object: X, where }) 里的 object 就是 [spec] IDataDriver 的 query 参数要求 QueryAST.object 与第一实参重复 —— 下游被迫 as any(20 处实测),提议 Omit/optional 化 #5181 说的冗余——第一个实参已经是对象名。删键。每一处都核对过删掉的值与第一个实参相等(脚本比对,零处不等),所以不存在「测试原本靠 AST 的 object 覆盖实参」的情况。
  2. 恢复(误伤,已修回):blanket 改写误删了 turso-driver.ts 测试里 syncSchemasBatch([{ object, schema }])object,以及 memory-driver.test.tsdistinct(obj, field, query: QueryInput)object。这两个不是被收窄的六个方法,其中 syncSchemasBatchobject 是被真实读取的必填键。已逐处放回。
  3. 换承载类型turso-local-remote-null-parity.test.ts 整份用 as QueryAST 拓宽字面量喂 find / count,删键后该 cast 自相矛盾(TS2352)。cast 目标改为 as DriverQuery,与形参对齐。remote-transport-text-predicates.test.ts 的局部 helper where: unknown 同理收成 DriverQuery['where']

expand 条目里的 object 命名的是关联对象、不是冗余,tsc 也不会标它(expand 值仍是 QueryAST)——已确认驱动测试里没有这类站点,未被波及。

⚠️ 方法论订正:消费半径要用前缀省略号,别抄第一版

本 PR 第一版把消费半径扫反了方向,CI 因此真红过一次(@objectstack/dogfood#typecheck)。记在这里,免得后来者照抄错命令。

pnpm 的省略号是有方向的:

写法 含义 用途
--filter 'pkg...'后缀 该包 + 它依赖的包(上游闭包) ❌ 第一版误用了这个
--filter '...pkg'前缀 该包 + 依赖它的包(下游消费者) ✅ 签名收窄要的是这个

签名收窄打到的是下游——那些拿具体驱动类、写内联 { object, where } 字面量的调用方。所以第一版报告里「25 个包全绿」扫的全是上游依赖,真正的消费者(@objectstack/dogfood 等)一个都没进扫描面,结论完全不成立。

正确做法,且以全仓为准(CI 跑的就是全仓,别再用局部闭包下结论):

pnpm typecheck --concurrency=2        # turbo,125 个任务,与 CI 同面
# 或按下游闭包定位:
pnpm --workspace-concurrency=2 --no-bail \
  --filter '...@objectstack/driver-memory' --filter '...@objectstack/driver-mongodb' \
  --filter '...@objectstack/driver-sql'    --filter '...@objectstack/driver-sqlite-wasm' \
  --filter '...@objectstack/driver-turso'  typecheck

(注意 typecheck 在 turbo 里 dependsOn: ["^build"],走 turbo 会自己把依赖建好;直接用 pnpm 递归则不会,新 worktree 里容易撞上 AGENTS.md §9 的陈旧产物陷阱。)

⚠️ 而且全仓 pnpm typecheck 仍然不够 —— 排除了测试的包要靠台账闸门才量得到

修完 dogfood 之后全仓 pnpm typecheck 是 125/125 绿的,CI 却仍红在 check:type-check-debt

• @objectstack/plugin-auth: TEST_DEBT records 131 raw tsc error(s), `tsc --noEmit` now reports 132 (+1).

原因是 plugin-auth 的 tsconfig 把 **/*.test.ts 排除掉了,per-package tsc --noEmit 根本看不见它的测试层;只有台账闸门的全仓 re-measure 会把排除掀掉重量。所以「驱动签名收窄」这类会波及测试字面量的改动,验证面必须是 pnpm typecheck + pnpm check:type-check-debt 两条,缺一条就会像这次一样漏。

定位到的就是一处,改法照上面第 3 类:

src/auth-where-operator-coverage.test.ts(383,7): error TS2353:
  Object literal may only specify known properties, and 'object' does not exist in type 'DriverQuery'.
// FROM
const left = await driver.find('sys_user', { object: 'sys_user', fields: ['id'] } satisfies QueryAST);
// TO —— 删掉的值与第一个实参逐字相同
const left = await driver.find('sys_user', { fields: ['id'] } satisfies DriverQuery);

保留 satisfies 而不是改成 as any,是尊重该处原有的意图:它上方的注释写明「这里不能用 as any,否则会被 check:query-options-erasure#4674/#4918)记一笔」。只换承载类型,意图不变。

未抬台账数字。 门禁原话是「the ledger is a ratchet and may only shrink」(#5278),而这 +1 是本单自己引入的、属可约。改完复测回到 131,与台账记录一致,check:type-check-debt EXIT=0。

下游实际改了什么

文件 站点数 编译强制?
packages/qa/dogfood/test/storage-growth.dogfood.test.ts 8 1 处是(:260cold 是真实 SqlDriver),7 处走 DriverLike
packages/qa/dogfood/test/group-key-read-shape-parity.test.ts 1
packages/qa/dogfood/test/empty-group-bucket-parity.test.ts 1
packages/plugins/plugin-auth/src/auth-where-operator-coverage.test.ts 1 是(仅台账闸门可见)

dogfood 那 9 处非强制的走本地结构替身 DriverLike.count(object, query?: Record< string, unknown >),任何键都收、类型层看不见;一并按同一规矩删掉,避免同一个文件里两种写法并存、诱导后人「修回去」。

每处删键的相等性核对结论:11/11 全部相等,零处不等。 dogfood 那 10 处不是靠肉眼——用的是带反向引用的模式,只有 query 里的值与第一个实参逐字相同时才匹配:

s/(\.(count|find)\((['A-Za-z_]['A-Za-z_0-9]*), )\{ object: \3 \}/\1{}/g

等价性由模式本身保证,不相等的站点根本不会被匹配到。dogfood 包里其余的 { object: ... }——权限授予(publicFormGrant)、端点策略入参(objectParams)、report.swept 条目、以及 interface 里的类型声明——因此一处未被波及。plugin-auth 那处是单点,肉眼核对 'sys_user' vs 'sys_user'

⛔ 未为了让下游过而放松任何驱动侧签名。

顺带记一条反面教材(本 PR 未触,属其他包):packages/objectql/src/engine-unknown-option.test.ts:183engine.find('task', { object: 'person' } as any)故意让两者不相等的拒绝测试。任何按「见 object: 就删」的批量清扫都会毁掉它——这正是必须逐处核对相等性、而不是无脑 sweep 的原因。

验证

typecheck   全仓 pnpm typecheck --concurrency=2
              → Tasks: 125 successful, 125 total(EXIT=0)
debt ratchet pnpm check:type-check-debt
              → --re-measure: OK — 34 ledger entr(ies) re-measured, 2017 raw tsc error(s) total,
                none above its recorded number(EXIT=0)
              修复前:• @objectstack/plugin-auth … 131 → 132 (+1),EXIT=1
test        memory      17 files, 524 passed
            mongodb     10 passed | 5 skipped, 206 passed | 137 skipped(skip 需真实 MongoDB)
            sql         66 passed | 4 skipped, 910 passed | 46 skipped(含新增 pin 的 4 条)
            sqlite-wasm 18 files, 254 passed
            turso       24 files, 788 passed
            dogfood     86 files: 85 passed | 1 skipped, 520 passed | 3 skipped
            plugin-auth 38 files, 967 passed
gates       check:driver-conformance / check:query-options-erasure /
            check:empty-changeset / check:type-check-coverage /
            check:spec-parsed-alias / check:nul-bytes  → 全 PASS
eslint      改动的源文件零 error/warning

冻结面 memory / mongodb 测试全绿,即是零行为变化的证据。

验收 grep:

$ grep -rn "query: QueryAST" packages/drivers/*/src --include=*.ts
packages/drivers/driver-mongodb/src/mongodb-driver.ts:468:  async aggregate(...)
packages/drivers/driver-sql/src/sql-driver-query-signature.test.ts:15:  (散文引用)

六个方法上归零。aggregate 不在 IDataDriver 里(distinct 同理),不是本单范围。

Changeset

.changeset/driver-query-signatures-follow-through.md,五个驱动包 major,依据与 #5181 同一条:源码级破坏性(调用点内联字面量会 TS2353),运行时行为零变化。sqlite-wasm 自身虽无源码改动,但它继承 SqlDriver,公开类型面同样变了,故一并列入。check:api-surface 只记录导出存在与否、不记录签名,所以这条 changeset 是该变更唯一的下游载体——这一点也是 #5181 的原话。

@objectstack/dogfoodprivate 的 QA 包、不发布,plugin-auth 的改动只在测试文件、无公开面变化,两者都不进 changeset。

顺带记录的发现(均未认领、observation-class)

并行注记

PM 标注 #5907(聚合函数信封)同批在飞,触 sql-driver.ts 的聚合区与 remote-transport.ts。本 PR 的面是签名区与 turso-driver.tsremote-transport.ts 一个字未动,零重叠;后落地者 rebase。

#5181 (PR #6076) 把 IDataDriver 的六个 query 方法收窄为 DriverQuery
(Omit<QueryAST, 'object'>),但五个驱动的实现仍声明 query: QueryAST
(turso 侧是 query: any)。方法参数按双变比较所以它不红,可调用方现在
有权省略 object —— 实现的类型说 query.object 是 string,运行期却可能
是 undefined。这是休眠的类型谎言,不是活体缺陷:实测五个驱动没有一个
读 query.object。

把六个契约方法 (find/findOne/count/updateMany/deleteMany/explain) 的
query 参数统一为 DriverQuery,并跟进少量只转发 where/orderBy/groupBy
的私有辅助方法。收窄后「驱动读 query.object」成为编译错误 (TS2339)。

零运行时改动:非测试改动 100% 是类型注解,无逻辑、无行为、无 emit 差异。
测试面按 #5181 的迁移说明逐处重拼 —— 删掉调用字面量里冗余的 object 键;
syncSchemasBatch 条目与 distinct 的 QueryInput 不在收窄范围,保持原样。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
@vercel

vercel Bot commented Aug 7, 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 Aug 7, 2026 11:43am

Request Review

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso.

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

  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso)
  • content/docs/deployment/cli.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/environment-variables.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/vercel.mdx (via @objectstack/driver-memory)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/permissions/authentication.mdx (via @objectstack/driver-memory)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/index.mdx (via @objectstack/driver-memory)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)

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.

上一版的消费半径扫描用了 `--filter 'pkg...'`(后缀省略号 = 该包 + 它
依赖的包,上游闭包),方向扫反了:签名收窄打到的是**下游**消费者。
真正该用的是前缀形 `--filter '...pkg'`,或者直接以全仓 `pnpm typecheck`
为准(CI 跑的就是全仓)。@objectstack/dogfood 因此没进上一轮扫描面。

全仓复扫命中一处真实编译错误:storage-growth.dogfood.test.ts:260 的
`cold.count(...)`,`cold` 是真实的 SqlDriver,内联字面量里的冗余
`object` 被收窄后的 DriverQuery 拒绝。

同包另有 9 处同形调用未被 tsc 标出 —— 它们走本地结构替身
`DriverLike.count(object, query?: Record<string, unknown>)`,任何键都收,
类型层看不见。一并按同一规矩删除,避免同一文件里两种写法并存。

删键用的是带反向引用的模式,只在 query 里的值与第一个实参**逐字相同**
时才匹配,等价性由模式本身保证;dogfood 包里其余 `{ object: ... }`
(权限授予、端点策略入参、report 条目、类型声明)因此一处未被波及。

⛔ 未放松任何驱动侧签名。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
check:type-check-debt 的全仓 re-measure 报 plugin-auth 从 131 涨到 132
(+1)。plugin-auth 的 tsconfig 把 **/*.test.ts 排除在外,所以 per-package
`pnpm typecheck` 看不见它,只有把排除掀掉的全仓 re-measure 量得到。

定位到的就是一处:

  src/auth-where-operator-coverage.test.ts(383,7): error TS2353:
    Object literal may only specify known properties, and 'object'
    does not exist in type 'DriverQuery'.

`driver.find('sys_user', { object: 'sys_user', fields: ['id'] } satisfies
QueryAST)` —— 删掉的值与第一个实参逐字相同,属重拼类。承载类型按第三类
处置改为 `satisfies DriverQuery`:该处上方注释明确写了「这里不能用 as any,
否则会被 check:query-options-erasure (#4674/#4918) 记一笔」,所以保留
satisfies、只换目标类型,正是它原本的意图。

⛔ 未抬台账数字:门禁原话「the ledger is a ratchet and may only shrink」
(#5278),本单是自己引入的 +1,属可约。改完复测回到 131,与台账记录一致。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
@os-zhuang
os-zhuang marked this pull request as ready for review August 7, 2026 11:57
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit d367f03 Aug 7, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6075-driver-query-signatures branch August 7, 2026 12:09
os-zhuang pushed a commit that referenced this pull request Aug 7, 2026
- `find()` 调用去掉 `object` 键:#6210 把驱动查询参数收窄为 `DriverQuery`,
  对象名只由第一个实参给出。没有用 `as any` 绕过——拒收类测试一旦用越契约的
  输入构造查询,就不再是在验证调用方真能发出的形状。
- 反向验证计数重测:LIKE 臂停用 → 8 failed / 40 passed;成员臂停用 →
  4 failed / 44 passed(此前记的 32 / 36 是首写时对 40 个测试测的)。失败数
  两次都不变,是承重的那一半;通过数随邻测增删而漂移,已在注释里说明。
- 补记 analytics 侧同法实测:两个门回退到 main 版本 → 17 failed / 41 passed,
  而 `like-metacharacter-escape.test.ts` 在该回退下保持绿——它锁的是两包谓词
  之间的一致,拒收文件锁的是门确实调用了谓词,两者都需要。

Refs #5234

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
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/l tests tooling

Projects

None yet

2 participants