Skip to content

字段级 unique: true 与同列的声明式全局 unique 索引意图矛盾,authoring 层零提示 #3991

Description

@os-zhuang

#3955 的第二条缺陷(PR #3981)复核过程中发现。运行时侧的 churn 已在该 PR 修掉;这里剩下的是 authoring 层的缺口,单独立项。

现象

一个对象可以同时声明两种唯一性,而且两者语义互相矛盾:

// HotCRM crm_contact —— 真实代码email: Field.email({unique: true}),// 字段级:租户内唯一(#3696 语义)indexes: [{fields: ['email'],unique: true}],// 对象级:verbatim,跨租户全局唯一

这不是笔误,IndexSchema 的注释明确说明了两者的区别是刻意设计的:

A DECLARED index is materialized over exactly the columns listed in fields — no tenant column is injected, unlike field-level unique (#3696). … many declared indexes are legitimately platform-wide.

所以两种写法各自都合法、都有正当用途。问题在于同一列上同时出现时:

  • 字段级说"租户内唯一"(允许 org_b 复用 org_a 的 email);
  • 声明索引说"全局唯一"(禁止复用)。

物理上以更严格的那个为准 —— 全局唯一生效,字段级的租户复合索引成了永远不会被触发的死约束。作者写下的两个意图,有一个被静默丢弃。

HotCRM 的注释显示作者确实是这么理解的(而且认为是好事):

Email uniqueness is already enforced (globally, which is stricter than per-account) by the { fields: ['email'], unique: true } index above.

—— 也就是说作者以为 unique: true 是"per-account",实际上 #3696 之后它是 per-organization,而真正生效的是那条全局索引。三种理解在同一处代码上打架,平台一声不吭。

为什么值得管

按 Prime Directive #12(contract-first:在 producer 修 metadata,在 authoring/publish 拒绝),这正是"声明了但不生效"的形态 —— 和 #1475(spec 声明 9 种校验规则、写路径只执行 3 种)同构,只是方向相反:这里是两条声明都被接受,其中一条注定无效。

而且它的后果不只是"多一个没用的索引":

建议方向

加一条 authoring 期的 lint(建议 advisory,不阻断构建 —— 这个组合的产物是良定义的,代价是一个失效意图而非坏产物,与 action-target-execute-conflict 同级):

规则: 对象在租户作用域的表上,对同一列同时声明了字段级 unique: true(非 'global')与单列 unique 的对象级索引时告警,指出:

  • 实际生效的是声明索引的全局唯一,字段级的租户复合约束不会被触发;
  • 二选一的修法 —— 想要全局唯一就把字段级改成 unique: 'global' 并删掉重复的声明索引;想要租户内唯一就删掉声明索引(或把它显式写成 fields: ['organization_id', 'email'])。

落点参考现有实现:packages/cli/src/utils/lint-autonumber-formats.ts 的形状,由 compile.tsos lint 共同调用(与 lintAutonumberFormatscompile.ts:476 的接法一致),这样 os build / os validate 两条路径的结论一致。判定"声明索引叫什么名字"应复用 normalizeDeclaredIndex(packages/plugins/driver-sql/src/schema-drift.ts),避免第三份拼名逻辑 —— 不过该 helper 目前在 driver 包里,是否需要下沉到 spec 供 lint 复用,值得在实现时一并决定。

顺带:HotCRM 那三处(crm_contact.emailcrm_lead.emailcrm_product.sku,以及 crm_account.name / crm_competitor.name / crm_case.case_number 等只有声明索引的要另判)应当照规则复核一遍,确认每处到底想要哪种唯一性。

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions