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.
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.tsL294 declares an authorable key on every index:Nothing consumes it.
SqlDriver.syncDeclaredIndexes(
packages/plugins/driver-sql/src/sql-driver.ts) builds every declared index throughknex's
table.unique(fields, { indexName })/table.index(fields, name), which haveno way to express a predicate;
normalizeDeclaredIndexinschema-drift.tsreads onlyname/fields/unique. A grep acrosspackages/plugins/**andpackages/objectql/**finds no other reader.
So an author who writes
partialgets a full index, silently — the declaration isaccepted, documented, rendered into
content/docs/references/, and inert.It is not hypothetical
packages/metadata-core/src/objects/sys-metadata.object.tsis itself an author of it: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'sensureOverlayIndexruntime migration, archived rows DO collide with the active one. Theruntime 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)
CREATE [UNIQUE] INDEX … WHERE <partial>via
knex.rawon 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.
spec-property-retirementplaybook, and let a runtimemigration 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-destructiveat framework-created indexes; its fix teaches introspection to readpartial/expression indexes correctly but deliberately does not start creating them.
This issue is the producer-side half.