Repository navigation
Closed
Description
跟踪 ADR-0104 D3 wave 2。所有权模型已于 2026-07-27 定案(ADR addendum),序列相应重排。
定案:字段引用是独占的
附件面(sys_attachment)故意共享一个文件给多条记录——那是它的特性。字段引用取相反模型:一个文件最多被一个 (对象, 记录, 字段) 槽位拥有,所有权记在 sys_file.ref_object / ref_id / ref_field;把已被拥有的 id 写进第二个槽位会复制字节到新的 sys_file,而不是共享行。
理由是失败模式不对称,不是优雅:
- 共享的最坏情况——文件的可读集合变成所有引用者的并集(把私有记录的 id 拷进公开记录 = 无声泄漏),以及一次漏记就授权不可逆删除。
- 独占的最坏情况——字节重复占用。花钱,可恢复。
存储成本应在字节层用内容哈希去重解决(sys_file.etag 已在),而不是在行层共享——去重字节,不去重行。
核心安全原则
一个文件变得可回收,只能因为平台观察到它唯一的所有者放手了,绝不能因为一次扫描推断出没人引用它。
独占所有权彻底消除了计数:没有计数,就没有漏记能授权删除;也化解了新账本的冷启动隐患。
序列与进度
- PR-1
/upload/complete返回 fileId,client 透出 — feat(lint,cli): flag flow update_record writes to readonly fields at design time (#3425) #3465 - PR-2 读路径 fileId →
FileValueSchema解析(批量、无 N+1、双模式安全)— feat(objectql): resolve file-field id references on read — ADR-0104 D3 wave 2 (PR-2) #3473 - PR-3 独占所有权:
ref_*列 + claim/release 写路径 + copy-on-claim — feat(storage): 字段引用文件的独占所有权 — ADR-0104 D3 wave 2 (PR-3) #3527 - PR-4 受管下载(从唯一拥有记录派生权限;
acl: 'public_read'为 opt-out)+verifyFileReferences()R4 验收门 — feat(storage): 字段文件的受管下载 + R4 可执行验收门 — ADR-0104 D3 wave 2 (PR-4) #3534 - PR-6 遗留值回填(本地 resolver URL /
data:URI 转换;外部 URL 只报告不重托管)— feat(storage): 遗留文件值回填 — ADR-0104 D3 wave 2 (PR-6) #3535 - PR-7 objectui 采纳(三形态读取、提交裸 id、修掉
mimeType/mime_type大小写漂移)— objectui#2828 - PR-5a 写切换:
accept/maxSize声明并服务端强制;存储形态收窄为sys_fileid,inline blob 降为expanded读形态 — feat(spec)!: 媒体字段 accept/maxSize 声明并强制 + 存储形态收窄为引用 — ADR-0104 D3 wave 2 (PR-5a) #3555 - PR-5b 开启回收:release 侧打墓碑 + reap guard 扫描时复核所有权列(同一 change),门禁读部署级标记(平台形态的迁移门禁:带自检的数据迁移 + 部署级标记 —— file-as-reference 回收与 strict 翻转都依赖它 #3617),sweep 前 fresh 复读 — feat(storage): 开启回收 — 释放的字段文件按部署门禁进入墓碑与回收 — ADR-0104 D3 wave 2 (PR-5b) (#3459) #4208
顺序修正(已落地)
旧计划把写切换和开 GC 打包、回填排在后面。两处都错了:
- 回填必须先于写切换。记录里还存着遗留值时就收窄可接受的存储形态,会让任何改写该字段的 update 被拒——在回填赶上之前,正常工作的应用会挂。
- 回填 + 对账必须先于开 GC。一本账在被证明与现实一致之前,不该被授予对不可逆删除的权力。
PR-5a 为什么今天不是破坏性变更
值形态检查是 warn-first(ADR-0104 R1/R2):还没回填的行照样写得进去,作者拿到的是指名字段的告警。硬拒绝只在部署方主动打开 OS_DATA_VALUE_SHAPE_STRICT_ENABLED 时发生——而那应该在跑完回填并用 verifyFileReferences() 确认对账之后。strict 默认值的翻转由 #3438 跟踪。
PR-5b 的两条硬约束
- 两半必须同一个 change:放松
scope === 'attachments'墓碑护栏 和 扩展sys_filereap guard 的扫描时复核到所有权列。只做前一半比完全不做更糟——打了墓碑而 guard 仍只复核sys_attachment(对字段文件恒为空),会让每一次释放都变成保证性的字节删除,而不只是有风险。这一点在releaseOwnership的文档注释和一条专门的回归测试里都锁死了。 - R4 验收门(可执行):
verifyFileReferences()必须在真实租户数据上、连续 ≥7 天报告零阻断项才允许合并。阻断项 =unowned_reference(持有但无人拥有)、foreign_owner(持有的文件被别的槽位拥有)、shared_reference(一个文件被两个槽位持有)。提示项(stale_owner/unreferenced_file)倒向保留,不阻断。
R5(公开姿态清单)和 R6(子键读取静态扫描)依然有效。
PR-5b 的合并与迁移的实际执行都需要明确的人工决定,不应自动进行。(已于 2026-07-30 经人工授权合并,#4208;各部署的迁移执行仍由其运维自行决定。)
Metadata
Metadata
Assignees
Labels
No labels
Activity
[-]tracking: ADR-0104 D3 wave 2 — file-as-reference (sys_file 引用存储 + GC + 受治理下载 + 迁移,v17 协议 major)[/-][+]ADR-0104 D3 wave 2 — file-as-reference: 独占所有权模型 + 到开启回收的分步序列[/+][-]ADR-0104 D3 wave 2 — file-as-reference: 独占所有权模型 + 到开启回收的分步序列[/-][+]ADR-0104 D3 wave 2 — file-as-reference:独占所有权模型 + 到开启回收的分步序列(PR-5b 门禁改由 #3617 承载)[/+]feat(migrate): 平台形态的迁移门禁 —— 带自检的数据迁移 + 部署级标记 (#3617) (#3638)
feat(storage): enable released-field-file collection behind the per-d…
feat(migrate): a datastore created from empty attests its data migrat…
test(dogfood): prove field-file collection end to end on a real boot (#…