发现于 #4001 批 20 的逐站点测量。只记录,不在批 20 里动。
事实
packages/spec/src/data/object.zod.ts 的 IndexSchema 声明了五个键,其中两个没有任何消费者:
| 键 |
消费者 |
name |
✅ syncDeclaredIndexes(索引名) |
fields |
✅ syncDeclaredIndexes(列) |
unique |
✅ syncDeclaredIndexes + ADR-0120 D3/D4 的 'organization' scope |
type |
❌ 无 —— z.enum(['btree','hash','gin','gist','fulltext']),还带 .default('btree') |
partial |
❌ 无 —— "Partial index condition (SQL WHERE clause for conditional indexes)" |
packages/plugins/driver-sql/src/sql-driver.ts 的 syncDeclaredIndexes 只用 name/fields/unique 建索引(table.unique(fields, …) / table.index(fields, name)),而 DeclaredIndexInput(packages/plugins/driver-sql/src/schema-drift.ts)的字段就是 name / fields / unique / nullSafeColumns。
排除一个容易看错的地方:sql-driver.ts 的 introspectIndexes 确实读 partial,但那是 parseIndexDdl 从数据库自己的 DDL 里解析出来给漂移检测用的(isSyncReproducibleIndex),与声明式元数据的 IndexSchema.partial 无关。
为什么值得单独处理
type 有 .default('btree'),所以它不只是被忽略——它会出现在每一次 parse 的输出里,让一个从未影响过任何 DDL 的键看起来是生效的配置。这正是 ADR-0078 no-silently-inert-metadata 与 ADR-0049 enforce-or-remove 针对的形状。
partial 的情况更具体:console 甚至为它渲染了一个输入框(拼写还漂移成了 where,见 #5247),所以这是一个管理员看得见、填得进去、然后什么也不会发生的字段。
两条路(ADR-0049)
- enforce —— 让
syncDeclaredIndexes 真正消费它们。partial 需要 CREATE INDEX … WHERE(knex 不直接支持,需 raw;SQLite ≥ 3.8.9 / Postgres 支持,MySQL 不支持);type 需要按方言映射(gin/gist 仅 Postgres,fulltext 仅 MySQL/部分方言),并要决定不支持的方言上是降级还是拒绝。注意这会和现有的漂移检测互动:isSyncReproducibleIndex 目前正是用 index.partial 把部分索引排除在增量同步之外。
- remove —— 按 spec-property-retirement 流程退役(
retiredKey 墓碑 + ADR-0087 conversion + 生成物基线),让写它的人立刻拿到升级说明。
我没有倾向性结论——两条路的成本差异真实存在,而且 partial 与 type 未必要走同一条(partial 有 UI 入口和真实语义,type 更像是从未接线的性能旋钮)。
阻塞关系
这条与 #5247 一起构成 #4001 收紧 IndexSchema 的前置条件:在 partial 仍然是死键的情况下把作者从 where 指向 partial,会构成一条声称超出平台实际交付的 guidance(strictness 台账 finding 18 记录了本战役已发过的四条假 guidance)。
发现于 #4001 批 20 的逐站点测量。只记录,不在批 20 里动。
事实
packages/spec/src/data/object.zod.ts的IndexSchema声明了五个键,其中两个没有任何消费者:namesyncDeclaredIndexes(索引名)fieldssyncDeclaredIndexes(列)uniquesyncDeclaredIndexes+ ADR-0120 D3/D4 的'organization'scopetypez.enum(['btree','hash','gin','gist','fulltext']),还带.default('btree')partialpackages/plugins/driver-sql/src/sql-driver.ts的syncDeclaredIndexes只用name/fields/unique建索引(table.unique(fields, …)/table.index(fields, name)),而DeclaredIndexInput(packages/plugins/driver-sql/src/schema-drift.ts)的字段就是name/fields/unique/nullSafeColumns。为什么值得单独处理
type有.default('btree'),所以它不只是被忽略——它会出现在每一次 parse 的输出里,让一个从未影响过任何 DDL 的键看起来是生效的配置。这正是 ADR-0078 no-silently-inert-metadata 与 ADR-0049 enforce-or-remove 针对的形状。partial的情况更具体:console 甚至为它渲染了一个输入框(拼写还漂移成了where,见 #5247),所以这是一个管理员看得见、填得进去、然后什么也不会发生的字段。两条路(ADR-0049)
syncDeclaredIndexes真正消费它们。partial需要CREATE INDEX … WHERE(knex 不直接支持,需 raw;SQLite ≥ 3.8.9 / Postgres 支持,MySQL 不支持);type需要按方言映射(gin/gist仅 Postgres,fulltext仅 MySQL/部分方言),并要决定不支持的方言上是降级还是拒绝。注意这会和现有的漂移检测互动:isSyncReproducibleIndex目前正是用index.partial把部分索引排除在增量同步之外。retiredKey墓碑 + ADR-0087 conversion + 生成物基线),让写它的人立刻拿到升级说明。我没有倾向性结论——两条路的成本差异真实存在,而且
partial与type未必要走同一条(partial有 UI 入口和真实语义,type更像是从未接线的性能旋钮)。阻塞关系
这条与 #5247 一起构成 #4001 收紧
IndexSchema的前置条件:在partial仍然是死键的情况下把作者从where指向partial,会构成一条声称超出平台实际交付的 guidance(strictness 台账 finding 18 记录了本战役已发过的四条假 guidance)。