Skip to content

[metadata-core] sys_view_definition 的"活跃行唯一"从未生效——归档视图仍占用 (name, organization_id, owner) 名额 #5839

Description

@os-zhuang

发现于 #5248(IndexSchema.type / partial 退役)的生产者清点。不在该 PR 范围内——退役本身是零 DDL 变化,这条是被退役暴露出来的既有行为缺口,修它是行为变更。

事实

packages/metadata-core/src/objects/sys-view-definition.object.ts 声明:

indexes: [
  // A given view name is unique per (organization, owner) among active rows —
  // a shared view (owner NULL) and each user's personal views don't collide.
  {
    name: 'idx_sys_view_def_active',
    fields: ['name', 'organization_id', 'owner'],
    unique: true,
    partial: "state = 'active'",   // ← #5248 退役前的原文
  },

注释说的是"among active rows",但 partial 从来没有任何 driver 消费者:SqlDriver.syncDeclaredIndexes 走 knex 的 table.unique(fields, { indexName }),无法表达 WHERE。所以实际建出来的一直是无谓词的全量 UNIQUE 索引。

实测(#5248 PR 里的 packages/drivers/driver-sql/src/declared-index-retired-keys.test.ts 同一手法,真实 SQLite 存储的 DDL):

WITH partial    -> CREATE UNIQUE INDEX `idx_sys_view_def_active` on `t` (`name`, `organization_id`, `owner`)
WITHOUT partial -> CREATE UNIQUE INDEX `idx_sys_view_def_active` on `t` (`name`, `organization_id`, `owner`)

两者逐字节相同,且都不含 WHERE。

与 sys_metadata 的关键差别

sys_metadata 的同类声明(idx_sys_metadata_overlay_active)有一个运行时兜底:packages/metadata-protocol/src/protocol.ts 的 ensureOverlayIndex() 用 raw SQL 发 CREATE UNIQUE INDEX … WHERE state = 'active'(并且顺带修了 COALESCE(package_id,'') 的 NULL-distinct 问题),所以那张表的活跃行范围是真的交付了的。

sys_view_definition 没有任何等价的运行时迁移——全仓检索 idx_sys_view_def_active 只有声明处一个命中。也就是说这张表的"活跃行唯一"在任何一层都没有实现。

用户可达的后果

sys_view_definition 有 state 字段(defaultValue: 'active')。一旦某个视图被置为非 active(归档/重置/软删),它的 (name, organization_id, owner) 组合仍然被全量 UNIQUE 索引占用,于是:

  • 用户删除/归档一个名为 "我的管道" 的个人视图后,无法再新建同名视图——唯一约束会拒绝插入;
  • 反过来,如果当初的意图是"同名的历史版本可以共存",那么当前的全量唯一约束比设计更严。

两种读法下当前行为都不是声明所描述的那个。

可选修法(需要拍板,故不自行实施)

  1. 补运行时迁移——照 ensureOverlayIndex 的形状,为 sys_view_definition 发 partial UNIQUE,MySQL 侧降级为普通复合索引 + 应用层 upsert 语义。交付注释所承诺的语义。
  2. 接受全量唯一并改注释——如果产品上就是"视图名在 (org, owner) 内永久唯一,含已归档",那么把注释里的 "among active rows" 删掉即可,零代码。
  3. 把 state 纳入索引列——(name, organization_id, owner, state),允许同名的一个 active + 多个非 active;但这不等价于 partial(多个 active 之间仍互斥,而多个同 state 的归档行会互斥),语义要单独想清楚。

倾向 1 或 2:取决于产品是否要求视图名可回收。这是行为决策,不是机械修复。

相关

Activity

  1. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    分诊:needs-user-decision + domain:engine-core。

    落点锚定:声明面在 packages/metadata-core/src/objects/sys-view-definition.object.ts ⇒ 域表 packages/metadata* ⇒ domain:engine-core。选项 1 的运行时迁移会照 ensureOverlayIndex() 的形状落在 packages/metadata-protocol/src/protocol.ts,同域,不跨车道;SqlDriver.syncDeclaredIndexes 只是被引用来解释「为什么 partial 从没生效」,不是修复落点,故不打 domain:drivers(连带:#5499 的 driver 冻结不适用于本单)。

    过时前提核验(origin/main 9a15446,逐条实读):

    • sys-view-definition.object.ts:125 的 idx_sys_view_def_active 与 :128 的 partial: "state = 'active'" 仍在——即 ADR-0049 enforce-or-remove:IndexSchema.type 与 IndexSchema.partial 没有任何 DDL 消费者 #5248 的退役尚未落地,正文引的「退役前原文」目前仍是 main 上的现状;
    • 全仓检索 idx_sys_view_def_active 只有这一处命中(声明处),证实「没有任何等价的运行时迁移」。零命中项按纪律做了邻近词反查:同一手法搜 ensureOverlayIndex 在 metadata-protocol 有命中,证明扫描器与路径都是好的。

    判 needs-user-decision 的理由:三个选项不是修法细节,是产品语义——「视图名在归档后能否回收」。选 1 交付注释承诺的语义(行为变更,用户可见);选 2 零代码、把承诺改成现状;选 3 是第三种语义。正文自己写明「这是行为决策,不是机械修复」,分诊座位不代拍。决策卡的最小提问:归档/软删一个视图后,同名视图是否应当可以重建?

    • 是 ⇒ 选项 1(补运行时 partial UNIQUE + MySQL 侧降级)
    • 否 ⇒ 选项 2(删注释里的 "among active rows",零代码)

    拍板后即可摘标转 pm:queue,落点已锚定、无需重新分诊。

    查重(三仓 open issue + PR 各一遍):无影子单。关联而非重复:#5248 / #4943(IndexSchema.partial 退役,本条的发现路径,只删死键不改 DDL)。⚠️ 顺序注记:若选项 1 被采纳而 #5248 已先合入,partial 声明键届时已不存在,选项 1 的实现要直接写运行时迁移、不要试图恢复该键——请拍板时把两单的先后一并考虑。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:

    裁「名额可回收」→ 选项 1——补运行时 partial UNIQUE 迁移(照 ensureOverlayIndex 范式),归档视图不再占 (name, organization_id, owner) 名额;MySQL 无 partial index 走既有降级路径。

    ⚠️ 实现约束:#5248(partial 声明退役)在飞——本单直接写运行时迁移,⛔ 勿恢复已退役的 partial 声明键,两单落地顺序由 engine-core 车道排。

    流转:摘 needs-user-decision → pm:queue,归 engine-core 车道。

    评估与落地会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  3. self-assigned this
    on Aug 7, 2026
  4. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    认领(执行席 PM,engine-core 车道,第 13 轮)

    按 2026-08-06 维护者裁决(选项 1「名额可回收」)执行。


    Generated by Claude Code

  5. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    迁移:domain:engine-core → domain:metadata(维护者 2026-08-07 批准拆分;座位贴 #6367,SKILL PR #6370)。落点证据:metadata-core/src/objects/sys-view-definition.object.ts 的「活跃行唯一」部分索引从未生效,修复镜像 protocol.ts 的 ensureOverlayIndex。全量分类审计 49 卡三判,本卡 MOVE/high。分诊会话:session_01BickTBKm2JYSNnrtPT8ysa。


    Generated by Claude Code

  6. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    ACCEPT(执行席 PM 验收,第 13 轮)+ 向 metadata 席报备

    PR #6415 验收通过,已翻 ready + auto-merge(CI 25 项零失败)。按维护者裁「名额可回收」落地,记档:① 迁移落点 assembleMetadataProtocol + kernel:ready(两种挂载模式共用入口 + 表先建后迁);② 对 ensureOverlayIndex 范式的先探针后替换改良采纳 —— 范式的 DROP-CREATE 失败窗口会让表零唯一索引,其自身缺陷已立 #6418(附可照搬修法);③ 范式裸 getDriver() 在 kernel:ready 会炸 bootstrap 的真错被消费半径扫描抓出(16 个启动测试翻红)、已修 + 回归钉;④ 存量冲突结构上不可能(新索引严格弱于旧),残余 out-of-band 路径按 ADR-0120 D4 口径拒建点名;⑤ NULL-distinct 缺口(共享/环境级视图不受约束)刻意不越裁决收紧,立 #6417(Blocked-by 本单)交产品拍板;⑥ #5248 顺序自洽(其注释本就把本单排为后续)。

    @metadata 席(座位贴 #6367):本单为 2026-08-07 拆域前的在飞认领,按「在飞认领由原认领者跟完」由 engine-core 席收尾至 MERGED;落点(metadata-protocol/metadata-core)自此归贵席领地,衍生单 #6417/#6418 均在贵席范围,已由分诊常规通道流转。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions