Skip to content

data: the SQL driver emits the current_user framework token as a literal column DEFAULT, so the database itself writes a non-id into a lookup('sys_user') #4560

Description

@os-zhuang

发现自 #4551 的悬空引用巡检在真实启动中的第一次运行(showcase, pnpm dev -- --fresh -p 38109,PR #4559 分支)。这是巡检报出的唯一一类发现,且它是真的。

WARN [dangling-refs] stored references that resolve to nothing (reported, never rewritten)
{"scannedObjects":63,"scannedRows":182,"scannedReferences":295,"dangling":2,"undetermined":0,"truncated":false,
 "references":["showcase_field_zoo.f_owner[IFMh0g-ouRI8ZOQp] → sys_user:current_user",
               "showcase_field_zoo.f_owner[TP2KMqigH9TAFu0J] → sys_user:current_user"]}

存进 lookup('sys_user') 列的值是字符串 current_user —— 框架的 defaultValue 令牌本身,不是任何用户的 id。

根因:不是引擎,是 DDL

引擎这一侧是对的。ObjectQL.applyFieldDefaults(packages/objectql/src/engine.ts:1448)显式处理该令牌,并且在没有登录用户时刻意不落值:

} else if (dv === 'current_user') {
  // When there is no authenticated user (system/anonymous), leave it unset
  // and let required-validation decide — never stamp a bogus owner.
  if (execCtx?.userId != null) out[f.name] = String(execCtx.userId);
}

值是数据库写的。SqlDriver 建列时把任意非对象 defaultValue 原样下发为列 DEFAULT(packages/plugins/driver-sql/src/sql-driver.ts:5952):

} else if (field.defaultValue !== undefined && field.defaultValue !== null) {
  const dv = field.defaultValue;
  if (isNowDefaultValue(dv)) {
    col.defaultTo(this.nowColumnDefault(type));   // 'NOW()' 被特判了
  } else if (typeof dv !== 'object') {
    col.defaultTo(dv as any);                     // 'current_user' 没有 —— 字面量入库
  }
}

于是引擎故意留空的那一列,被 SQL 的列默认值填成了字面串 'current_user'。

注意 'NOW()' 就在紧邻的上一个分支里被翻译成了驱动原生默认值,理由写得很清楚——「而不是把字面串 'NOW()' 留给碰巧写入的上游代码」。current_user 是同一个约定家族的成员,只是从来没被同样处理。

为什么 #4441 抓不到

#4441 的引用完整性校验只看调用方给出的值。这里调用方(和引擎)什么都没给,值是数据库自己供的——写入路径根本看不见它,所以既不是豁免也不是漏判,是它天然在检查的定义域之外。这也正是为什么它只能被一条事后巡检发现。

复现

pnpm dev -- --fresh -p <port>     # showcase
# 等过 lifecycle 时钟 60s 首跑延迟(需要 PR #4559 的巡检才会打印)
grep dangling-refs <log>

字段定义:examples/app-showcase/src/data/objects/field-zoo.object.ts:120

f_owner: Field.user({ label: 'Owner (current_user default)', defaultValue: 'current_user' }),

种子(examples/app-showcase/src/data/seed/index.ts:268 起)没有设置 f_owner,两条 specimen 行因此都吃到了列默认值 —— 与报文里的 2 条一一对应。

不依赖 showcase:任何 Field.user({ defaultValue: 'current_user' }) + SQL 数据源 + 一次不带登录用户的插入(种子重放、包安装、启动期供给)都会复现。

影响

  • lookup 列里存了一个不是 id 的值 —— 与 sys_metadata_history.recorded_by 的哨兵(PR fix(data): 审计锚点归引擎所有,lookup 必须能解析 (#4447, #4441) #4511 记录)同类,但这一条没有任何设计意图,纯粹是令牌泄漏。
  • 任何 ?expand=f_owner 或按 owner 关联的读取都会解析到空。
  • 若 Field.user({ defaultValue: 'current_user' }) 被用在 owner/审批人一类字段上,这就是一条静默的错误归属。

待决策的修法方向(未认领)

最小且与既有约定一致的修法,是在 DDL 侧把 current_user 与 'NOW()'** 同等对待:它是一个**运行时**令牌,没有任何数据库原生对应物,因此**不应该发出列 DEFAULT**(引擎已经在应用层负责它了)。是否还有别的字符串令牌处在同样位置,值得一并盘一遍——col.defaultTo(dv)` 目前对所有非对象 defaultValue 一视同仁,这个兜底才是真正的口子。

范围上这需要动 driver 的 DDL 发出逻辑与既有表的迁移(已建的列带着错误的 DEFAULT),所以不在 #4551 的巡检范围内,单独立此 issue。

Activity

  1. self-assigned this
    on Aug 2, 2026
  2. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 4 轮(全量 backlog 加速批次 1/N)
    分支:claude/issue-4560-ddl-runtime-token-default
    Worktree:objectstack-issue-4560
    分诊决定(PM 自决,按恢复不变量类不再请示):采用 issue 提议的最小修法——运行时令牌(current_user 及盘点出的同族令牌)不发出列 DEFAULT,与 'NOW()' 特判同一约定;既有表的错误 DEFAULT 走 DDL 迁移修正;已写入的错误行不改写(与 #4551 巡检「只报告不改写」哲学一致,留给运维依据巡检报告处置)。


    Generated by Claude Code

  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 验收:ACCEPT → PR #4598(已转正并启用 merge-when-ready,CI 全绿自动入队)。

    按分诊决定落地且结构优于预期:令牌词汇表收敛到 packages/spec/src/data/default-value-tokens.ts 单一事实源(引擎与 DDL 共读——正是缺陷本身;明天新增令牌自动免疫);列默认值发出收敛 applyDeclaredColumnDefault 单点;既有表复用 #2186 schema-drift 通道以 safe 级 drop_column_default 修正(SQLite 重建路径顺手堵住「重建抹掉兄弟 NOW() 默认值」的原有隐患);已写入错误行不改写,沿 #4551 哲学留给巡检与运维。测试 20 例含遍历令牌表的 revert-proof(硬编码倒退会在下个令牌加入时立刻红);反向验证 3 例复现字面 DEFAULT。api-surface 已重生成提交。

    顺带发现已分诊:#4597(NOW() 在非 SQL 数据源引擎不解析,涉及「省略 datetime 由谁产值」的契约级分工)→ 收件箱等维护者定夺——agent 未自决的克制是对的。


    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