Skip to content

[spec/driver-sql] IndexSchema.partial is authorable but no driver ever emits the WHERE clause (declared ≠ enforced) #4943

Description

@xuyushun441-sys

Found while fixing #4884 (drift detector misreading the ADR-0048 overlay indexes). Filing rather than fixing — out of that PR's scope.

The gap

packages/spec/src/data/object.zod.ts L294 declares an authorable key on every index:

partial: z.string().optional().describe('Partial index condition (SQL WHERE clause for conditional indexes)'),

Nothing consumes it. SqlDriver.syncDeclaredIndexes
(packages/plugins/driver-sql/src/sql-driver.ts) builds every declared index through
knex's table.unique(fields, { indexName }) / table.index(fields, name), which have
no way to express a predicate; normalizeDeclaredIndex in schema-drift.ts reads only
name / fields / unique. A grep across packages/plugins/** and packages/objectql/**
finds no other reader.

So an author who writes partial gets a full index, silently — the declaration is
accepted, documented, rendered into content/docs/references/, and inert.

It is not hypothetical

packages/metadata-core/src/objects/sys-metadata.object.ts is itself an author of it:

{
  name: 'idx_sys_metadata_overlay_active',
  fields: ['type', 'name', 'organization_id', 'package_id'],
  unique: true,
  partial: "state = 'active'",
}

The comment right above it explains that overlay uniqueness must be "restricted to active
rows so resets / archived versions don't collide" — and the index the additive sync
actually creates is unrestricted, so on a driver without metadata-protocol's
ensureOverlayIndex runtime migration, archived rows DO collide with the active one. The
runtime migration is what makes this work today on the SQL driver; the declaration alone
does not deliver what it says.

Decision needed (ADR-0049 enforce-or-remove)

  • Enforce — teach the index-creation path to emit CREATE [UNIQUE] INDEX … WHERE <partial>
    via knex.raw on the dialects that support it (SQLite, Postgres), and decide what MySQL
    (no partial indexes) does: reject at publish time, or degrade with a loud warning.
    This also gives the drift comparator a real predicate to diff, instead of ignoring the
    dimension as it does today.
  • Remove — retire the key per the spec-property-retirement playbook, and let a runtime
    migration remain the only way to declare a partial index.

Enforcing looks right on the merits (the platform's own metadata object needs it), but it
is a producer-side contract change plus a dialect-capability decision, so it wants a
maintainer call rather than a drive-by.

Not the same as #4884

#4884 is about the detector misreading expression columns and pointing
--allow-destructive at framework-created indexes; its fix teaches introspection to read
partial/expression indexes correctly but deliberately does not start creating them.
This issue is the producer-side half.

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊:关闭为 #5248 的 duplicate——同一 declared ≠ enforced 面(IndexSchema.partial 无任何 DDL 消费者、ADR-0049 enforce-or-remove 待裁),#5248 范围更广(还含 IndexSchema.type)且已挂 needs-user-decision + domain:spec 在维护者决策箱,一件事只留一个派发入口。

    本单独有、请 #5248 裁决时参考的证据:平台自己的 sys-metadata.object.ts 就是 partial 的作者(idx_sys_metadata_overlay_active 声明 partial: "state = 'active'"),今天靠 metadata-protocol 的 ensureOverlayIndex 运行时迁移兜底才不塌——这是 enforce 路线的现实依据;MySQL 无 partial index 的降级姿态是 enforce 路线必须回答的题。

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


    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

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