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

Closed

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. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 2, 2026
  2. self-assigned this
    on Aug 2, 2026
  3. 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

  4. 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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions