feat(driver-sql)!: index drift is planned, not silently executed at boot (#3728) - #3737
Merged
Merged
Conversation
…oot (#3728) The #3696 unique-scope migration converged in place: `syncTableIndexes` ran a `DROP` + `CREATE UNIQUE INDEX` during `initObjects`, in every environment, leaving one log line behind. `os migrate plan` showed nothing, because `detectManagedDrift` was column-only — `DriftOp` had no index dimension at all. An operator who wanted to review the DDL before it reached their database had no way to, and a managed schema was being auto-altered in production, which the #2186 contract explicitly forbids. Index drift is now a first-class dimension, reconciled through the same path as column drift. - `syncTableIndexes` is ADDITIVE ONLY. It creates indexes; it never drops or rewrites one. `dropLegacyGlobalUniques` is gone. - New `DriftOp` variants: `replace_unique_index` (safe — retire the legacy platform-wide unique in favour of the tenant composite), `create_index` (safe), `recreate_index` (needs-confirm; destructive when it tightens to UNIQUE, since the create can fail on existing duplicates after the drop) and `drop_index` (destructive). - `detectManagedDrift` reports them, `os migrate plan` renders them (index ops display as `table [index_name]`), `os migrate apply` executes them. Index DDL is portable, so it applies directly on every dialect — no SQLite rebuild. - `replace_unique_index` creates before it drops, and only drops once the replacement is confirmed present: a relaxation must never degrade into removing the constraint outright. - Declared `indexes[]` drift is covered too — an index metadata declares but the database lacks, and one whose definition no longer matches the declaration (the additive sync skips those by name, so they could never self-heal on their own). - Orphan detection is limited to ObjectStack's own generated naming (`uniq_…` / `idx_…`, plus the pre-#3696 `<table>_<column>_unique` knex spelling). A hand-rolled operational index is never reported as drift and `--allow-destructive` will not delete it. Behaviour change: boot no longer rewrites the index unconditionally. Dev (`autoMigrate: 'safe'`, what `os dev` / `os serve` use) still self-heals on restart, so local workflows are unchanged. Production now warns with an actionable `os migrate` hint and leaves the schema alone — the deployment stays on the legacy global unique until someone runs `os migrate apply`. That is the deliberate trade: a visible, pre-inspectable migration instead of an invisible one. Index name construction moves into `schema-drift.ts` as the single definition, so the names the driver creates and the names the differ looks for cannot diverge. Postgres index introspection reads `pg_index` rather than `pg_indexes` so indexes backing a UNIQUE CONSTRAINT — exactly what knex's old `col.unique()` produced — are visible to the detector. Also fixed: `managedObjectIndexes` was never cleared when an object dropped its `indexes[]`, so drift detection kept expecting an index nobody declared. `SchemaDiffEntryKind` gains `index_mismatch` and `unmapped_index`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LJzroBbHM7FTpHXrtv4pm
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 28, 2026 02:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3728.
问题
#3696 / #3717 的 unique 收敛落在
syncTableIndexes里就地执行:initObjects期间跑一次DROP+CREATE UNIQUE INDEX,所有环境都跑,只留一行日志。os migrate plan什么都看不到——因为detectManagedDrift只有列维度,DriftOp里根本没有索引这一类。想在 DDL 落库前审查的运维没有预检手段;而且这是在生产自动改 managed schema,恰恰是 #2186 明令禁止的("schema is never auto-altered in production")。处置(按 owner 拍板的方案)
索引成为一等的 drift 维度,和列 drift 走同一条路。
syncTableIndexes只增不改。它创建索引,永不 drop / 重写。dropLegacyGlobalUniques删除。DriftOp变体:replace_unique_indexsafecreate_indexsaferecreate_indexneeds_confirm/destructiveUNIQUE时判为 destructive——drop 之后 create 可能撞已有重复值drop_indexdestructivedetectManagedDrift检出 →os migrate plan渲染(索引条目显示为table [index_name])→os migrate apply执行。索引 DDL 跨方言可移植,直接执行,不需要 SQLite 整表 rebuild。replace_unique_index先建后删,并且确认替代索引真的存在之后才 drop——"放松"绝不能退化成"直接没约束了"。indexes[]的漂移也补齐了(issue 的「附带发现」):声明了但物理库缺失;以及同名而定义已变——后者syncDeclaredIndexes按名字幂等跳过,永远不会自愈。uniq_…/idx_…,外加 多租户缺陷:unique 物化为全局唯一索引、无视 tenancy,与按租户分裂的 autonumber 序列自相矛盾(跨租户必然撞号 + 存在性探测泄露) #3696 之前 knex 的<table>_<column>_unique拼法)。运维手搓的覆盖索引永远不会被报成漂移,--allow-destructive也不会删它。行为变化
启动不再无条件重写索引:
autoMigrate: 'safe',即os dev/os serve)autoMigrate: 'off'os migrate,不动 schema;plan完整可见,apply执行代价是明确的:没跑
os migrate的生产部署会继续带着遗留全局 unique 运行(多租户插入仍会撞号),直到有人执行os migrate apply。这是 issue 里选定的取舍——可见、可预检的迁移,胜过看不见的迁移——并且由启动告警兜底,没人会在不知情的情况下带病运行。其他
schema-drift.ts的单一定义,驱动创建的名字和差异器查找的名字不可能再分叉。pg_index而非pg_indexes,这样 UNIQUE CONSTRAINT 背后的索引(正是 knex 旧col.unique()产生的东西)对检测器可见——原来的代码只能盲试 drop。indexes[]时managedObjectIndexes从不清空,导致漂移检测一直期待一个没人声明的索引。SchemaDiffEntryKind新增index_mismatch/unmapped_index。测试
sql-driver-index-drift.test.ts(17 例):计划可见性、boot 不执行 DDL、autoMigrate: 'safe'自愈、NODE_ENV=production强制禁用、apply 幂等、声明式索引缺失/重定义/孤儿、纯差异器单元(遗留名但定义不符不误删、PK 索引永不算漂移、哈希截断长名识别)。sql-driver-unique-tenancy.test.ts:三个迁移用例改为显式autoMigrate: 'safe',锁定新的启动语义。schema-migrate.integration.test.ts:端到端跑真实bootSchemaStack,在NODE_ENV=production下断言plan同时看到relax_not_null与replace_unique_index,apply 后两者都消失且数据完好。🤖 Generated with Claude Code
https://claude.ai/code/session_013LJzroBbHM7FTpHXrtv4pm
Generated by Claude Code