fix(metadata): sys_view_definition 的「活跃行唯一」补运行时 partial UNIQUE 迁移 (#5839) - #6415
Merged
Merged
Conversation
) `idx_sys_view_def_active` 的注释一直承诺「among active rows」,但该语义从未 在任何一层交付:声明面的 `partial` 键没有任何 driver 消费者(knex 的 `table.unique()` 无法表达 `WHERE`),已随 #5248 / #4943 退役;而与 `sys_metadata` 不同,这张表背后没有等价的运行时迁移。结果建出来的一直是无 谓词的全量 UNIQUE 索引——用户归档一个视图后无法再新建同名视图。 补 `ensureViewDefinitionActiveIndex`(照 `ensureOverlayIndex` 范式),在 `kernel:ready` 用 raw SQL 发 `CREATE UNIQUE INDEX … WHERE state = 'active'`, 复用声明的索引名以便 `syncDeclaredIndexes`(按名跳过)不会在后续启动把全量 索引加回来。 与范式的两处有意偏离,均在模块头注释里写明理由: - 先用临时探针索引验证方言与数据确实能建出部分索引,成功后才替换既有索引, 因此「旧索引已删、新索引没建成」的无约束窗口不存在(范式存在该窗口); - `resolveIndexExec` 逐个 probe 加 try/catch,并优先 `getDriverForObject`: `ObjectQL.getDriver(objectName)` 必须带对象名、否则抛错,范式因整体 try/catch 而掩盖了这一点。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…w-active-partial-unique
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
本包除该测试外不需要任何 SQL 依赖,为跑测试而把一个原生模块写进 lockfile 并不划算。Node 内置的 `node:sqlite` 提供同样真实的 SQLite —— 真实的 partial index、真实的 UNIQUE 约束 —— 且零依赖。 顺带的好处:pnpm-lock.yaml 回到与 main 完全一致,本 PR 不再触发 「Validate Package Dependencies」的 OSV 扫描。该扫描只在 lockfile 变动时 运行,而它当前会因 main 上既有的 dompurify@3.4.12 公告 (GHSA-55q2-fjhq-7xh7, dependabot #16) 判红 —— 与本 PR 无关。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
baozhoutao
marked this pull request as ready for review
August 7, 2026 17:42
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5839
按维护者 2026-08-06 裁决(选项 1:名额可回收)实施:补运行时 partial UNIQUE 迁移,归档视图不再占用
(name, organization_id, owner)名额。前提核验(三条全部成立)
ensureOverlayIndex范式在 main 且形态如假设packages/metadata-protocol/src/protocol.ts:2519,raw SQLCREATE UNIQUE INDEX … WHERE state = 'active'+ 方言降级partial现为retiredKey墓碑,本单未恢复该键P1 实测(用
declared-index-retired-keys.test.ts已钉住的、驱动真实产出的 DDL):用户归档「我的管道」之后,被一条自己刚扔掉的记录挡住,无法重建同名视图——正文口径复现成功。
落点
packages/metadata-protocol/src/migrations/view-definition-active-index.ts(新增)packages/metadata-protocol/src/plugin.ts的assembleMetadataProtocol,在kernel:ready触发packages/metadata-core/src/objects/sys-view-definition.object.ts仅改注释⛔ 未触
engine.ts/protocol.ts热区。接线选在assembleMetadataProtocol而非protocol.ts,是因为它是两种挂载共用的唯一入口(MetadataProtocolPlugin委托模式 与ObjectQLPlugin的registerProtocol !== false内建便利模式)——只在委托插件上挂 hook 会漏掉默认挂载。选kernel:ready是因为表必须先存在:ObjectQLPlugin在start()里经syncRegisteredSchemas建表,晚于所有插件的init()。零新增依赖:
pnpm-lock.yaml与 main 完全一致。测试用 Node 内置的node:sqlite跑真实数据库,而不是为跑测试把better-sqlite3这个原生模块写进 lockfile。为什么复用声明的索引名
syncDeclaredIndexes按名跳过(if (existing.has(name)) continue;)。用idx_sys_view_def_active这个声明里的同名建 partial 索引,才能让后续每次启动都看到名额已占、不再把全量 UNIQUE 加回来。换个名字会在下次启动被静默撤销;而删掉声明则会让不跑该迁移的宿主完全失去唯一性——所以声明保留为降级形态。与范式的两处有意偏离(均在模块头注释写明)
1. 先探针、后替换。
ensureOverlayIndex是「先 DROP 旧索引、再建 partial」;建失败(方言不支持 / 数据冲突)时,表会静默地一个唯一索引都不剩。本迁移反转顺序:先用临时探针名建一次 partial 索引,确证当前方言与数据都能接受,才 DROP 旧索引并以声明名重建。任何建不成的情况下,原有全量 UNIQUE 索引原样保留——只会退回今天的行为,不会更低。代价是迁移那一次启动把小索引建了两遍。(范式自身的这个窗口已另单记录:#6418。)2.
resolveIndexExec逐个 probe 加 try/catch,并优先getDriverForObject。ObjectQL.getDriver(objectName)必须带对象名,不带会抛No driver available for object 'undefined';范式因整体 try/catch 掩盖了这一点,而本迁移跑在kernel:ready钩子里,抛出会失败整个 bootstrap。这不是假设——消费半径扫描时它真的让 16 个 ObjectQL 启动测试翻红,已按此修复并加钉子测试。优先getDriverForObject(sys_view_definition)还更正确:多数据源内核上平台对象可能落在自己的数据源上。测试(18 条,全部跑真实 SQLite)
packages/metadata-protocol/src/migrations/view-definition-active-index.test.tsBEFORE the migration…把缺陷本身钉住state='active'同名同域行仍被拒;多条归档行可共存,但活跃行仍只允许一条;不同 owner / 不同 org 互不干扰info记录(非操作者错误);模拟冲突行 → 按 ADR-0120 D4 口径在error点名未生效的列并指向os migrate plan,旧索引保留,不阻断启动getDriver抛错时不抛出(上面那条回归的钉子);getDriverForObject优先于引擎级默认反向验证(肢 A:去掉迁移)
预测方向 翻红,实测一致:
ensureViewDefinitionActiveIndex改为直接return { status: 'created' }后 7 红 9 绿,其中本单钉子AFTER the migration, an archived view frees its slot翻红。如实记录两点:
still rejects two ACTIVE rows…与does NOT close the NULL-distinct hole在肢 A 下保持绿——前者由被保留的全量索引提供,后者是既有事实的钉子,二者都不是迁移的证明,而是「不放宽 / 不越界」的守卫。另外is idempotent — 逐字节相同在肢 A 下空绿(什么都没做当然不变),真正覆盖它的是同组的行为版is idempotent in BEHAVIOUR too,该条翻红。已知未涵盖(已另单:#6417)
owner为 NULL 的共享视图与organization_id为 NULL 的环境级视图,因 SQL UNIQUE 的 NULL-distinct 语义本来就不受该索引约束——实测两条同名活跃共享视图今天可以并存。这是早于本次修复的既有缺口,且不是 #5839 裁决的内容;本迁移只改行范围(WHERE state = 'active')而不动键的拼写,这也正是它严格弱于被替换的索引、因而不可能在存量数据上建失败的原因。已加测试如实钉住该缺口。验证
@objectstack/metadata-protocol无typecheckscript(既有状况),其tsupDTS 构建即类型门,已通过。objectql的protocol-meta.test.ts一度出现 3 条红,排查为 AGENTS.md §9 的陈旧产物陷阱——合并 origin/main 后只重建了本单两个包,spec/lint 的 dist 仍是旧的。用origin/main的对照工作树复核(同一文件 89/89 绿),再在本分支重建完整依赖链后同样 89/89 绿,与本 PR 无关。