Skip to content

fix(objectql): resolve the NOW() defaultValue token in the engine (#4597) - #4993

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4597-now-token-engine-resolve
Aug 3, 2026
Merged

fix(objectql): resolve the NOW() defaultValue token in the engine (#4597)#4993
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4597-now-token-engine-resolve

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4597

按 issue 上的 PM 裁决取 方案 A —— 引擎解析:applyFieldDefaults 里像 current_user 一样特判 NOW(),所有驱动拿到同一个答案。packages/spec/**packages/plugins/driver-sql/** 零改动。

问题

applyFieldDefaults 只认两种 defaultValue:Expression 信封与 current_user 令牌,其余 out[f.name] = dv 原样落值 —— 包括 'NOW()',落下去是一个字面串。SQL 驱动 formatInput 的兜底在上线前把它换成真实时间戳,memory / mongodb 没有这层网。

这是 #4560 的镜像:那边 current_user 引擎认识、DDL 不认识,于是 DDL 把令牌文本写成了列默认值;这边 NOW() SQL 驱动认识、引擎不认识。

实测:裂缝有两张脸,issue 只记了响亮的那张

复现时发现受影响的不止「被拒绝的插入」:

字段 validateRecord 结果
普通(会被校验的)datetime 走校验 整条插入被拒:ValidationError: … must be a valid datetime (ISO-8601) —— 引擎自己填的值,被引擎自己的校验判非法,报错还指不到根因
readonly / system 跳过(if (def.system || def.readonly) continue) 无人报错,NOW() 四个字符被静默存进列里

第二行正是平台对象里那 ~100 处声明的形状(created_at / updated_at 一律 readonly: true),也就是说非 SQL 数据源上它们一直在静默写坏数据 —— 与 #4560 同一个失败模式。两张脸同一处修好,测试各钉一条。

顺带更正 issue 里的一句描述:「SQL 驱动有兜底」只对 readonly / system 字段成立。普通字段在 SQL 上同样会被 validateRecord 拦下(校验发生在驱动之前,formatInput 根本没机会跑),所以这条裂缝并非只影响非 SQL 数据源。

修法

applyFieldDefaults 自己解析该令牌:

  • 时间来源是调用方已有的 per-insert nowSnapshot(insert()const nowSnap = new Date(),批量整批共用),所以一次插入里的每个默认字段、一批里的每一行,拿到的是同一个瞬间。
  • 令牌拼写用 spec 已导出的 isNowDefaultToken(大小写不敏感、容空白),不在引擎里另写一份 —— 否则两侧对「哪些声明算令牌」会有分歧。

令牌家族盘点(裁决第 3 点)

DEFAULT_VALUE_TOKENS 目前恰好两个成员,没有 TODAY() 之类:

令牌 谁解析 本 PR
current_user 引擎 不变,回归测试原样保留并新增钉子
NOW() 之前只有 SQL 驱动 本 PR 改为引擎解析

家族已全覆盖,无部分支持残留。未发明新令牌。

按声明类型落值,不是一律 ISO 瞬间

Field.date 塞一个完整瞬间只会把旧的跨驱动漂移换成一个新的(SQL 的 formatInput 仍会 toDateOnly 收成日历日,memory/mongodb 不会)。所以按字段声明类型解析,形状与 SqlDriver.nowColumnDefault 早已按类型下发的完全一致(ADR-0053),不是新造契约:

字段类型 落值 对齐
date YYYY-MM-DD(UTC 日历日) toDateOnly / date 列默认值
time HH:MM:SS[.fff](UTC 墙钟,.000 裁掉) canonicalTimeOfDay
datetime 及任何非时间类型 YYYY-MM-DDTHH:MM:SS.sssZ canonicalUtcDatetime

生产元数据里 NOW() 全部落在 Field.datetime 上;date / time + NOW() 是 driver-sql 测试在用的受支持形状(#4022 / #3994)。

驱动侧原样保留(裁决第 4 点)

driver-sql 只读、零改动。formatInput 的兜底留着当纵深防御(引擎路径上已到不了),nowColumnDefault 继续服务绕过引擎的写入 —— 与 current_user 的分工一致。

测试

沿用 engine-default-value-tokens.test.ts 的既有惯例(#4560 建立的那份),current_user 四条原样保留作回归钉。新增 9 条,覆盖裁决第 5 点全部四项:

  • issue 的原始复现:memory 驱动省略 defaultValue: 'NOW()' 的 datetime 插入成功,存下合法 ISO-8601-Z
  • 调用方显式给值不被覆盖;显式 null 仍视为「未提供」并解析([objectql] 字段 defaultValue 语义:显式 null 不回填、解析晚于 hook、表单不预填 current_user #2706)
  • readonly 字段存瞬间而不是 NOW() 四个字符(静默那面;该对象上只有这一个令牌字段,以免被兄弟字段的报错顶掉)
  • 一次插入只解析一次:同一条记录的两个默认字段值完全相等;一批两行也相等
  • 匹配 spec 谓词(now() 这类宽松拼写能解析);近似拼写 NOW 仍是字面量
  • 按声明类型落值:date / time / datetime 三种形状,且三者切自同一瞬间

反向验证(防空转):把 engine.ts 的改动 stash 掉重跑,9 条新增里 7 条失败,报的正是 issue 里那条 ValidationError: Seen At must be a valid datetime (ISO-8601);另外 2 条是两侧都该成立的不变量(不覆盖调用方值、近似拼写不解析),因此两种情况下都通过。

命令与结果:

pnpm --filter @objectstack/objectql test      → Test Files 112 passed (112) / Tests 1788 passed (1788)
pnpm --filter @objectstack/objectql typecheck → tsc --noEmit,无输出
pnpm --filter @objectstack/driver-sql test    → Test Files 56 passed | 4 skipped / Tests 653 passed | 44 skipped
pnpm --filter @objectstack/driver-memory test → Test Files 10 passed (10) / Tests 286 passed (286)
pnpm --filter @objectstack/runtime test       → Test Files 80 passed (80) / Tests 1094 passed (1094)
eslint(两个改动文件)                          → 退出码 0

driver-sql / driver-memory / runtime 一并跑,是因为改动后引擎会在驱动看到记录之前就填好这些字段 —— 需要确认 SQL 侧最终落库形状不变、平台对象经引擎插入的集成路径不回归。全部通过。

兼容性

授权侧无变化:defaultValue: 'NOW()' 还是原来那句声明,调用方给的值仍然永不被覆盖。变的是它在 memory / mongodb 上终于和在 SQL 上是同一个意思。修复前在非 SQL 数据源上写入的记录可能仍存着字面串 NOW(),本 PR 不做回填重写

Changeset:.changeset/now-default-token-engine-resolved.md(@objectstack/objectql patch)。

🤖 Generated with Claude Code

https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX


Generated by Claude Code

…4597)

`applyFieldDefaults` special-cased exactly two `defaultValue` shapes — the
Expression envelope and the `current_user` token — and passed everything
else through verbatim, so `'NOW()'` was written into the record as a
literal string. `SqlDriver.formatInput`'s safety net swapped it for a real
timestamp before the wire; memory and mongodb have no such net.

The mirror of #4560: there `current_user` was known to the engine and not
to the DDL, so the DDL stored the token text. Here `NOW()` was known to the
SQL driver and not to the engine. It surfaced two ways — a validated field
was rejected by the engine's own write validator against a value the engine
itself had filled in, and a `readonly`/`system` field (which validateRecord
skips, i.e. the ~100 platform `created_at`/`updated_at` declarations) stored
the four characters `NOW()` silently.

The engine now resolves the token from the same per-insert `now` snapshot it
already passes to Expression defaults, so every field defaulted in one
insert — and every row of one batch — carries the identical instant. The
spelling it matches is the spec's (`isNowDefaultToken`), the same predicate a
driver's DDL consults; the engine does not re-derive its own.

Resolution follows the field's declared type, which is what
`SqlDriver.nowColumnDefault` already emits per type (ADR-0053), so no
datasource disagrees about the stored form: `date` -> `YYYY-MM-DD`, `time`
-> `HH:MM:SS[.fff]`, everything else -> `YYYY-MM-DDTHH:MM:SS.sssZ`.

Both driver-side mechanisms stay unchanged as defence in depth: the
`formatInput` safety net (now unreachable from this path) and the native
column DEFAULT, which still serves engine-bypassing writes — the same
division of labour `current_user` has.

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

vercel Bot commented Aug 3, 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 3, 2026 6:57pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

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.

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/m tests tooling

Projects

None yet

2 participants