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.

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