fix(objectql,spec): filter folds to where on EVERY engine method; top/limit joins the #3795 slot table (#4346) - #4356
Merged
Conversation
…/limit joins the #3795 slot table (#4346) The filter → where fold that #3795 settled at the protocol layer existed at the engine layer in exactly one of six methods. find() folded it (with a comment saying why); findOne/count/update/delete/aggregate passed the option bag through with ast.where === undefined, which every driver reads as "no predicate" — so a caller filtering with { filter } silently matched EVERY row: an over-grant on the reads, an unbounded write on update/delete (multi: true rewrote or emptied the whole table). The deprecated DataEngine*OptionsSchema contracts declare filter on all of them, the ScopedContext/ObjectRepository facade forwards hook-body arguments verbatim, and the spec's own hook docs taught the broken call — auth-manager's stampIdentitySource was a live in-repo victim (findOne({filter}) read the table's first row; count({filter}) counted the whole table). Every entry point now folds through the spec's own machinery (RPC_QUERY_ALIAS_SLOTS + foldQueryAliasSlots) instead of find's hand-rolled copy, under the #4181 rule: alias alone folds, identical duplicates collapse, different values throw, an explicit null alias is a withdrawal. Write paths fold BEFORE withResolvedWhere, so filter-carried placeholder tokens resolve too (#3810 parity). top → limit — the pair the #3795 scope note excluded — joins the slot table. The protocol normalizer folded it BACKWARDS (options.limit = Number(options.top): alias overwrote canonical) while engine.find folded it canonical-wins, so {top: 1, limit: 3} answered 1 over HTTP and 3 in-process. All three readers (wire, RPC parse, engine) now agree: top alone still limits, a conflicting pair is refused. The hook.zod.ts / createContext doc examples teach where now. Pinned per method with a real engine + in-memory driver, write paths included (nothing lands before the refusal); the protocol SLOTS table and the RPC parse pins gained the sixth pair. Co-Authored-By: Claude Fable 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 3 package(s): 110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Jul 31, 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.
Closes #4346. Refs #3795, #4181, #3713, ADR-0057.
问题
filter→where的别名折叠在引擎层只存在于六个方法中的一个。ObjectQL.find()折叠(注释还写明了后果),findOne/count/update/delete/aggregate原样透传 ——ast.where === undefined,每个 driver 都读作"无谓词":find({filter: {status:'done'}})[b, c]✅(唯一折叠的)findOne({filter: {status:'done'}})count({filter})update(data, {filter, multi:true})delete({filter, multi:true})aggregate({filter, …})可达且真实:废弃的
DataEngine{Query,Update,Delete,Count,Aggregate}OptionsSchema都声明filter为合法选项,ScopedContext/ObjectRepository(交给 L2 hook 体的跨对象 API)原样转发,spec 自己的 hook 文档教的就是这个坏调用。仓内也有真实受害者:auth-manager.ts的stampIdentitySource用findOne({filter: {id}})(拿到的是表首行)和count({filter: …})(数的是整表)—— 本修复使这两处回归声明语义。修复
一次折叠,三个读者。 每个引擎入口经由 #3795 的既有机制(
RPC_QUERY_ALIAS_SLOTS+foldQueryAliasSlots)折叠,替换find手写的守卫,不再新增开写副本:别名单独出现即折入规范键、相同值冗余坍缩、不同值抛错("Send exactly one",#4181 规则)、显式null别名视为撤回。写路径在withResolvedWhere之前折叠,foldedfilter里的{filter-placeholder}token 同样解析(#3810 语义对齐)。第六对别名。
top→limit加入RPC_QUERY_ALIAS_SLOTS(#3795 的 scope note 把它排除在外)。协议层原来是反向折叠(options.limit = Number(options.top)—— 别名覆盖规范键),引擎层是规范键优先,{top: 1, limit: 3}经 HTTP 得 1、进程内得 3。现在 wire 归一化、RPC parse、引擎三个读者语义一致:top单独出现仍限行,冲突拒绝。文档。
hook.zod.ts的 hook 示例与engine.ts的createContext示例改教where;check:docs通过(无生成物漂移)。针脚(pin)
engine-filter-alias.test.ts(新,真实引擎 + 内存 driver,issue 的三行数据集):六方法{filter}单独生效、六方法{filter, where}冲突拒绝且写路径确认未落盘、相同值坍缩、null撤回、{top}限行、{top, limit}冲突拒绝/相同坍缩、findOne({top})仍单行、{filter: {id}}走 by-id 快径、ScopedContext/ObjectRepository 钩子门面路径(文档教的那个调用)。protocol-data.test.ts:SLOTS 表加['top','limit',5,3,5]行,自动获得 alias-alone / conflict / identical 三组针脚。data-engine.test.ts(spec):RPC parse 折叠top→limit并丢弃别名;冲突用例加{top:1, limit:3}。验证
check:docs✅,全仓pnpm test除@objectstack/dogfood的 2FA lockout 用例 5s 超时外全绿 —— 该用例与本改动无交集(better-auth 2FA 路径全程where),单文件复跑 5/5 绿,符合本机 turbo 并行资源噪音的已知模式。行为变化说明
之前各层各自"选赢家"的冲突拼写(
{where: X, filter: Y}、{top: 1, limit: 3})现在在每条路径上一致地大声拒绝,不再按层静默取不同值。🤖 Generated with Claude Code