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),不是新造契约:

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

生产元数据里 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

vercelBot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 3, 2026 6:57pm

Request Review

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@xuyushun441-sys@claude