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.applyFieldDefaultspackages/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。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions