发现自 #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 的引用完整性校验只看调用方给出的值 。这里调用方(和引擎)什么都没给,值是数据库自己供的——写入路径根本看不见它,所以既不是豁免也不是漏判,是它天然在检查的定义域之外。这也正是为什么它只能被一条事后巡检发现。
复现
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。
发现自 #4551 的悬空引用巡检在真实启动中的第一次运行(showcase,
pnpm dev -- --fresh -p 38109,PR #4559 分支)。这是巡检报出的唯一一类发现,且它是真的。存进
lookup('sys_user')列的值是字符串current_user—— 框架的 defaultValue 令牌本身,不是任何用户的 id。根因:不是引擎,是 DDL
引擎这一侧是对的。
ObjectQL.applyFieldDefaults(packages/objectql/src/engine.ts:1448)显式处理该令牌,并且在没有登录用户时刻意不落值:值是数据库写的。
SqlDriver建列时把任意非对象defaultValue原样下发为列 DEFAULT(packages/plugins/driver-sql/src/sql-driver.ts:5952):于是引擎故意留空的那一列,被 SQL 的列默认值填成了字面串
'current_user'。注意
'NOW()'就在紧邻的上一个分支里被翻译成了驱动原生默认值,理由写得很清楚——「而不是把字面串'NOW()'留给碰巧写入的上游代码」。current_user是同一个约定家族的成员,只是从来没被同样处理。为什么 #4441 抓不到
#4441 的引用完整性校验只看调用方给出的值。这里调用方(和引擎)什么都没给,值是数据库自己供的——写入路径根本看不见它,所以既不是豁免也不是漏判,是它天然在检查的定义域之外。这也正是为什么它只能被一条事后巡检发现。
复现
字段定义:
examples/app-showcase/src/data/objects/field-zoo.object.ts:120种子(
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。