在实现 #5272(单记录 delete 绑定 hookContext.previous)时发现,PD #10 记录,未在该 PR 内修改(越界即停)。
事实
packages/objectql/src/engine.ts,update() 单 id 分支的 demand-driven 前置行门:
if (needsPriorRecord(updateSchema as any) || (this.hooks.get('afterUpdate')?.length ?? 0) > 0) {
const priorAst: QueryAST = { object, where: { id: hookContext.input.id }, limit: 1 };
priorRecord = await driver.findOne(object, priorAst, hookContext.input.options as any);
}
this.hooks.get('afterUpdate') 返回的是全部 afterUpdate 注册项,不区分对象。而引擎自己另有一个按对象回答同一问题的谓词 —— hasHooksFor(event, object)(同文件,private hasHooksFor),#5038 的批量 update / 批量 delete 路径用的正是它:
if (this.hasHooksFor('afterDelete', object)) { ... }
于是同一件事在同一个文件里有两种精度:批量路径按对象问,单 id update 按全局问。
影响
只要任意一个对象注册了 afterUpdate hook,所有对象的每一次单 id update() 都会多做一次 driver.findOne。这在真实部署里不是边角情形:plugin-audit 之类的插件会在多对象上注册 afterUpdate,一旦启用,平台范围内每次单记录 update 都吃到这次读。
这不是正确性缺陷 —— 门只会过度取,不会漏取,previous 的语义一律正确;纯粹是每次写多一次数据库往返。严重度我判断不准(读放大是否已在真实负载上可见,我没有测量数据),按 #4949 的纪律平铺记录,交 PM 分诊定级。
修法(若采纳)
把该门换成按对象:
if (needsPriorRecord(updateSchema as any) || this.hasHooksFor('afterUpdate', object)) {
⚠️ 收窄这条门有一个已被代码注释预告的陷阱,必须一并处理。同一处 [#4784] 的注释写着:
Deliberately: adding a "does the condition reference previous?" analysis on top would be dead code today. If this gate is ever NARROWED (e.g. scoped per object), hook conditions reading previous must be counted into the new demand test — pinned by hook-condition-previous-scope.test.ts.
也就是说,今天一个 beforeUpdate hook 的 condition 里写 previous.*,是靠「本对象或别的对象存在 afterUpdate hook」顺带捞到的前置行才求得出值的。改成按对象之后,一个只有 beforeUpdate hook、且其 condition 读 previous 的对象会失去这次读,该 hook 立刻按 #4775 fail-loud 打回 —— 也就是把 #5272 刚在 delete 侧修掉的那类故障,在 update 侧造出来。所以按对象化的门必须把「本对象存在 任一 update 侧 hook(before/after 皆算)」计入需求测试,而不只是 afterUpdate。
#5272 的 delete 侧新门就是按这个形状写的(hasHooksFor('beforeDelete', object) || hasHooksFor('afterDelete', object) || 有 roll-up summary),可直接对照。
关联
在实现 #5272(单记录 delete 绑定
hookContext.previous)时发现,PD #10 记录,未在该 PR 内修改(越界即停)。事实
packages/objectql/src/engine.ts,update()单 id 分支的 demand-driven 前置行门:this.hooks.get('afterUpdate')返回的是全部afterUpdate注册项,不区分对象。而引擎自己另有一个按对象回答同一问题的谓词 ——hasHooksFor(event, object)(同文件,private hasHooksFor),#5038 的批量 update / 批量 delete 路径用的正是它:于是同一件事在同一个文件里有两种精度:批量路径按对象问,单 id update 按全局问。
影响
只要任意一个对象注册了
afterUpdatehook,所有对象的每一次单 idupdate()都会多做一次driver.findOne。这在真实部署里不是边角情形:plugin-audit 之类的插件会在多对象上注册afterUpdate,一旦启用,平台范围内每次单记录 update 都吃到这次读。这不是正确性缺陷 —— 门只会过度取,不会漏取,
previous的语义一律正确;纯粹是每次写多一次数据库往返。严重度我判断不准(读放大是否已在真实负载上可见,我没有测量数据),按 #4949 的纪律平铺记录,交 PM 分诊定级。修法(若采纳)
把该门换成按对象:
也就是说,今天一个
beforeUpdatehook 的condition里写previous.*,是靠「本对象或别的对象存在 afterUpdate hook」顺带捞到的前置行才求得出值的。改成按对象之后,一个只有beforeUpdatehook、且其condition读previous的对象会失去这次读,该 hook 立刻按 #4775 fail-loud 打回 —— 也就是把 #5272 刚在 delete 侧修掉的那类故障,在 update 侧造出来。所以按对象化的门必须把「本对象存在 任一 update 侧 hook(before/after 皆算)」计入需求测试,而不只是afterUpdate。#5272 的 delete 侧新门就是按这个形状写的(
hasHooksFor('beforeDelete', object) || hasHooksFor('afterDelete', object) || 有 roll-up summary),可直接对照。关联
hookContext.previous—— 契约声明「for update/delete」,引擎只在 update 分支赋值;#5038 之后批量 delete 反而比单记录 delete 更完整 #5272 / PR fix(objectql): 单记录 delete 绑定hookContext.previous—— 让引擎符合契约已声明的「for update/delete」 (#5272) #5283(单记录 delete 绑定previous)hasHooksFor由该单引入)、bug(objectql/docs): hookcondition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784(上述注释的出处)、hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775(求不出值 = 失败,是这条门收窄后的失败形态)