Skip to content

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

Closed

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. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 6, 2026
  4. self-assigned this
    on Aug 7, 2026
  5. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

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

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


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 7, 2026
  7. 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

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

pm: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