发现于 #5860 的实施核对(PD #10 范围外发现,未在任何 PR 内修改)。
事实(origin/main)
packages/objectql/src/engine.ts:6112,delete() 的前置行(pre-image)需求门:
const wantsPreImage =
this . hasHooksFor ( 'beforeDelete' , object ) ||
this . hasHooksFor ( 'afterDelete' , object ) ||
this . getSummaryDescriptors ( object ) . length > 0 ;
packages/objectql/src/plugin.ts:871,ObjectQLPlugin 自己 注册的内建 hook:
{ name : 'sys_fetch_previous_delete' , object : '*' , events : [ 'beforeDelete' ] , priority : 5 , ... }
hasHooksFor 把 '*' 正确地当作命中每个对象,于是只要 ObjectQLPlugin 在 (即任何 kernel 托管的引擎),wantsPreImage 的第一项恒真、后两项永不被求值 —— 这个门在真实部署里从来没有判过假。
实测(计数驱动,只注册这一条内建形状,完全不装 plugin-audit):对一个既无 delete hook、也无 roll-up 汇总的对象做单 id delete(),driver.findOne 增量 = 1。
为什么这不是 #5272 的重复
#5272 把 delete 侧的前置读提前到派发 beforeDelete 之前并绑定 previous ,让 before/after 两个阶段共用一次读 —— 那次改动是对的,而且正因如此,内建 sys_fetch_previous_delete 的 if (!hookCtx.previous) 现在恒为假,它自己不再多读一次。
剩下的是另一个问题:那条 object: '*' 的注册仍然留在注册表里 ,而需求门是按注册面算的,于是门本身失去了判假的能力。换句话说,内建 hook 的存在理由(「引擎不填 previous」)在 delete 侧已经不成立,但它的注册还在,并且它现在唯一可观察的效果,就是让按对象门恒真。
可选方向(交分诊定,本单不预设)
既然 单记录 delete 从不绑定 hookContext.previous —— 契约声明「for update/delete」,引擎只在 update 分支赋值;#5038 之后批量 delete 反而比单记录 delete 更完整 #5272 之后 delete 侧的 previous 已由引擎自己绑定,sys_fetch_previous_delete 是否可以直接退休?需要先确认引擎的绑定覆盖了哪些分支(单 id 有;谓词 delete 走另一条路径)。
或者让内建 hook 不参与需求门的计算 —— 例如按 packageId: 'sys:audit' 在 hasHooksFor 中排除,或把它改成一个不经注册表的引擎内联步骤。⚠️ 前者会让「门」和「派发」的语义分叉,[17.x] 批量写按行语义实现:hook 按行触发 + record-change trigger 按行绑定 previous/record(#4800/#4862 拍板 A) #5038 的注释专门警告过这个方向,若走这条要显式论证。
Refs:#5860 、#5928 、#5272 、#5284 、#5038 、#5846
发现于 #5860 的实施核对(PD #10 范围外发现,未在任何 PR 内修改)。
事实(origin/main)
packages/objectql/src/engine.ts:6112,delete()的前置行(pre-image)需求门:packages/objectql/src/plugin.ts:871,ObjectQLPlugin 自己注册的内建 hook:hasHooksFor把'*'正确地当作命中每个对象,于是只要 ObjectQLPlugin 在(即任何 kernel 托管的引擎),wantsPreImage的第一项恒真、后两项永不被求值 —— 这个门在真实部署里从来没有判过假。实测(计数驱动,只注册这一条内建形状,完全不装 plugin-audit):对一个既无 delete hook、也无 roll-up 汇总的对象做单 id
delete(),driver.findOne增量 = 1。为什么这不是 #5272 的重复
#5272 把 delete 侧的前置读提前到派发
beforeDelete之前并绑定previous,让 before/after 两个阶段共用一次读 —— 那次改动是对的,而且正因如此,内建sys_fetch_previous_delete的if (!hookCtx.previous)现在恒为假,它自己不再多读一次。剩下的是另一个问题:那条
object: '*'的注册仍然留在注册表里,而需求门是按注册面算的,于是门本身失去了判假的能力。换句话说,内建 hook 的存在理由(「引擎不填previous」)在 delete 侧已经不成立,但它的注册还在,并且它现在唯一可观察的效果,就是让按对象门恒真。与 #5860 / #5928 的关系
object注册 ⇒ 引擎「按对象」需求门(#5284 单 id / #5038 批量)在 audit 启用时恒真 #5860(update 侧)的门只看afterUpdate,而两条内建'*'注册(sys_stamp_audit_update、sys_fetch_previous_update)都在 before 事件上 —— 所以 update 侧的门确实是被 plugin-audit 的全局afterUpdate顶住的,plugin-audit 的 5 个 hook 全部无object注册 ⇒ 引擎「按对象」需求门(#5284 单 id / #5038 批量)在 audit 启用时恒真 #5860 的验收判据在 update 侧成立。captureBefore的beforeDelete注册收窄,本单这条内建注册仍让门恒真。两者独立,需要分别解。可选方向(交分诊定,本单不预设)
hookContext.previous—— 契约声明「for update/delete」,引擎只在 update 分支赋值;#5038 之后批量 delete 反而比单记录 delete 更完整 #5272 之后 delete 侧的previous已由引擎自己绑定,sys_fetch_previous_delete是否可以直接退休?需要先确认引擎的绑定覆盖了哪些分支(单 id 有;谓词 delete 走另一条路径)。packageId: 'sys:audit'在hasHooksFor中排除,或把它改成一个不经注册表的引擎内联步骤。Refs:#5860、#5928、#5272、#5284、#5038、#5846