Skip to content

applyFormulaPlan 的 nowSnapshot 形参零调用者 —— 同一次 insert 里 defaultValue 的 now() 与 formula 的 now() 观察到两个不同瞬时 #5699

Description

@os-zhuang

Blocked-by: #5504(同文件在飞:PR #5698 的写路径共享水合点即本单的第三个调用点;其落地后 sweep 回队)。

观察类发现,在 #5504(PR #5698)选水合求值语义时撞到,当下没有用户会踩到,故不带 pm:queue,请 PM 分诊定级。

事实

packages/objectql/src/engine.ts 的 applyFormulaPlan 声明了第四个可选形参 nowSnapshot?: Date:

function applyFormulaPlan(plan, records, execCtx?, nowSnapshot?): void {
  const now = nowSnapshot ?? new Date();
  ...
}

全仓 applyFormulaPlan( 的调用点在 #5504 之前是两个(find / findOne),之后是三个(加上写路径的共享水合点),没有任何一个传 nowSnapshot。也就是说这个形参自诞生起就走的是 ?? new Date() 分支,是一段休眠代码。

为什么它当初存在

该函数自己的 docstring 写明了意图:

  • 「a now pinned ONCE per operation(每行、每个 formula 字段在一次 find 里观察同一个瞬时 —— 确定性,不做逐次求值的 new Date() 漂移)」
  • 「The eval context mirrors applyFieldDefaults so formula and default expressions see the same shape」

第一条目前由「每次调用 applyFormulaPlan 只取一次 new Date()」自然满足,所以形参不传也不影响读路径的行内确定性。第二条则只做到了一半:applyFieldDefaults 与 applyFormulaPlan 各自取自己的 now。

#5504 之后新增的可观察面

engine.insert 的中间件体开头已经有一个 nowSnap = new Date(),交给 applyFieldDefaults 用于解析 defaultValue 里的 NOW() / current_user 令牌。#5504 让同一次 insert 在写完之后又做了一次 formula 水合,而它按「与 find/findOne 完全一致」的原则没有传 nowSnap。于是同一次 insert 内:

  • defaultValue 里的 now() = 写前那一刻;
  • formula 里的 now() = 写后那一刻;

两者相差一次 driver 往返(通常毫秒级)。跨越秒/日边界时,一个「created_at 默认值」与一个「基于 now() 的 formula」理论上可以落在不同的日历日上。

需要说清楚的是:这不是 #5504 引入的新语义,读路径本来就是读时求值、与写时的 default 瞬时无关;#5504 只是让「同一次操作里两个瞬时」第一次变得可观察。PR #5698 刻意选了「与 find/findOne 逐字一致」而不是「顺手传 nowSnap」,因为分派单明确要求水合语义与读路径一致,且悄悄让写路径比读路径多一条确定性保证会造成两侧不对称。

可能的处置(留给分诊)

  1. 接上:insert 水合点传 nowSnap,让 default 与 formula 在一次写里共享瞬时。代价是写路径比读路径多一条保证,两侧语义不再逐字相同 —— 需要判定这是想要的。
  2. 退休形参:确认「一次调用一个 new Date()」就是全部需要的确定性,删掉零调用者的 nowSnapshot,并把 docstring 里 mirrors applyFieldDefaults 的措辞收窄到实际成立的范围(ADR-0049 enforce-or-remove 的精神)。
  3. 维持现状:承认毫秒级偏差不构成业务问题,但把这一段写进 docstring,免得下一个读者以为形参是活的。

倾向 2 或 3 —— 1 会制造读写不对称,而这个偏差目前没有任何被测量到的业务拉力。真正的价值在于:现在这个形参看起来是活的,任何按它推理的人都会推出错误结论。

落点

  • packages/objectql/src/engine.ts —— applyFormulaPlan 定义、其 docstring,以及 insert 中间件体里的 nowSnap

Activity

  1. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    发现分诊(finding 轮):晋级 —— 摘 finding,入 pm:queue + 挂 pm:blocked(Blocked-by: #5504 已补正文),域 domain:engine-core(已在)。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    解锁 sweep(engine-core 车道 PM,会话 session_019Q7oc7ASjh8yxyS3Yz78We):Blocked-by: #5504 已随 PR #5698 合入 origin/main(2b52bc8),摘 pm:blocked 回队。

    两条排程约束预记:


    Generated by Claude Code

  3. self-assigned this
    on Aug 6, 2026
  4. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    认领:PM 循环第 3 轮追加(engine-core 车道;engine.ts 已随 PR #5850 合入释放,与在飞 #5841(metadata-protocol)不同包不相交)
    会话:session_019Q7oc7ASjh8yxyS3Yz78We
    分支:claude/issue-5699-retire-nowsnapshot-param
    Worktree:objectstack-issue-5699
    域:domain:engine-core
    文件面:packages/objectql/src/engine.ts(仅 applyFormulaPlan 定义与 docstring 区域;⛔ 不碰 update 单 id 门与 #5850 刚校准的注释区)+ 相关测试 + .changeset/*.md 或 skip-changeset(dev 论证)

    范围按分诊裁定:限方向 (2)(退休零调用者形参 + docstring 收窄,ADR-0049)或 (3)(docstring 如实记录);PM 预裁优先 (2),除非实测发现形参即将获得调用者;⛔ 方向 (1) 属语义决策,要走升级决策箱。前序 #5850 的必答项已确认本单不受其影响,原价派发。


    Generated by Claude Code

  5. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    复核通过,ACCEPT(engine-core 车道 PM,第 3 轮):交付于 PR #5894。CI 尚余 10 项在跑、零红;全绿后转 ready 入合并队列。

    验收要点:方向 (2) 按预裁落地(形参退休 + docstring「mirrors」收窄到实际成立的范围并点名本单);三方向反向验证是本单最扎实的部分 —— A(死枝原样加回 → 预判并实测全绿,如实记录「不可能红」而非制造假红,这正是死枝的证明)、B(时钟读进逐次求值 → 两条同一性 pin 红)、C(真实现方向 (1) → 唯一 tripwire 红);新 pin 用对象同一性而非值相等断言(同毫秒内值比较会侥幸绿 —— 顺带把既有 #1979 pin 的这个弱点立单 #5896,不动多 agent 热文件,处理正确)。必答项:#5749/#5755/#5288 均无影响,原价保留。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions