fix(driver-sql): 全新数据库不再"开机即漂移",也不再把 --allow-destructive 指向框架自己创建的索引 (#4884) - #4954
Merged
Merged
Conversation
…-allow-destructive at a framework-created index (#4884) A brand-new empty SQLite file printed two `[schema-drift]` warnings before the server was ready, both about the ADR-0048 overlay indexes the same boot had just created. The second told the operator to `os migrate apply --allow-destructive` away `idx_sys_metadata_overlay_draft` — the unique index enforcing draft-overlay uniqueness. Three fixes, all on the detector side (no metadata declaration changed: `sys-metadata.object.ts` documents its four-column entry as the fallback shape for drivers WITHOUT the runtime migration, and that contract still holds): - Read the index key from its DEFINITION, not from the dialect's per-column catalogue view. `PRAGMA index_info` reports a NULL column for an expression key, so `(type, name, organization_id, COALESCE(package_id,''))` arrived as three columns. SQLite/Postgres now parse `sqlite_master.sql` / `pg_get_indexdef`, MySQL reads `STATISTICS.EXPRESSION` where available, and `COALESCE(col, <literal>)` is attributed to `col`. - Capture the partial predicate. An index `syncDeclaredIndexes` can neither create nor rebuild is never claimed as ours, never called orphaned, and never given a remedy this differ could not undo (`isSyncReproducibleIndex`). - Keep a ledger of the index DDL the driver itself executed through raw `execute()` — how `ensureOverlayIndex` issues its migration — so an index the framework created on this boot is provably its own, including the plain-index fallback it takes on dialects that reject partial indexes. Genuine drift is untouched: orphaned generated indexes, redefined declared indexes and the #3696 legacy-unique replacement are all still detected and categorised exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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.
Fixes #4884
问题复现与前提核实
先验证 issue 的判断是否成立(证伪也是好结果)。用 better-sqlite3 直接建出
ensureOverlayIndex的规范形状:issue 的两条判断都成立,而且根因比"比较器不认识表达式"更靠前一层:探测层根本没把这一列读进来。
introspectIndexes对PRAGMA index_info返回的name: null直接continue,四列索引因此以三列的形态进入比较器;比较器再拿它去比四列声明,当然报 mismatch。所以修在比较器上只能治标——第 4 列必须先被读出来。
三处修改,全部落在
packages/plugins/driver-sql/按 PM 给的优先级(危险的那一半必须落地),同时没有改
sys-metadata.object.ts:它的注释白纸黑字写着那份四列声明是 "the fallback shape for drivers without the runtime migration",
改它会破坏其它驱动依赖的契约。走的是"探测器承认 runtime-managed 标记"这条路。
packages/metadata-protocol/src/protocol.ts与packages/spec/**零改动。1. 索引键按定义读,而不是按方言的逐列目录视图读
PRAGMA index_info/pg_attribute/STATISTICS.COLUMN_NAME三者都无法表达表达式键。现在:sqlite_master.sql(约束自动索引sql为 NULL,退回 PRAGMA,那些本来就是纯列);pg_get_indexdef(indexrelid)+indpred IS NOT NULL,不再 joinpg_attribute(
attnum = 0的表达式列正是被那个 join 丢掉的);STATISTICS.EXPRESSION,并对不认识该列的旧版 / MariaDB 回退到原查询——否则整表索引探测会一起失败。COALESCE(col, < literal >)被认定为就是键在col上:ADR-0048 用它正是为了让无 package 的全局行彼此唯一(普通 UNIQUE 把 NULL 当互不相同),所以它对同一列的约束严格更强,不是缺了一列。
识别范围刻意收窄——
lower(name)、a || b、COALESCE(a, b)一律判为"无法归属",不做任何猜测。2. 捕获 partial 谓词,并据此拒绝认领
syncDeclaredIndexes走 knex 的table.unique(fields)/table.index(fields):没有谓词,没有表达式。带
WHERE或表达式键的索引,它既造不出来、也重建不回来。而本模块给索引开的每一个方子都建立在"删掉还能按声明重建"这个前提上(
drop_index是这样,recreate_index先删后建更是这样)。于是新增
isSyncReproducibleIndex():造不出来的索引,不认领、不判 orphan、不给一个自己收不了场的方子。这一条是跨重启持久有效的那一半保证。
3. 驱动为自己执行过的索引 DDL 记账
ensureOverlayIndex是通过driver.execute(sql)下发迁移的。execute()现在在语句成功之后记录CREATE INDEX/ 注销DROP INDEX。这是事实台账而不是启发式:一条记录的含义是"本进程跑过这条 CREATE且成功了"。它同时覆盖了那条 issue 里没提到的分支——方言拒绝 partial 时,
ensureOverlayIndex会回退成一条普通索引,此时定义本身已经无法为它辩护,只有台账能。
进程内有效是设计如此;重启后由第 2 条兜底,两者互为独立证据。
没有放松的部分
真实漂移一条没丢:orphan 的生成索引、被改定义的声明索引、#3696 的 legacy unique 替换,
探测结果与分级(
safe/needs_confirm/destructive)完全不变——这三条各有回归测试守着。代价说清楚:一个显式命名的声明索引被从 metadata 里删掉后,若它恰好是 partial / 表达式索引,
现在不再被报为 orphan(留一条陈旧索引,只损性能,不损正确性)。相对于"开机建议删掉一条刚建好的唯一性保证",
这个方向是对的。
测试
packages/plugins/driver-sql/src/sql-driver-overlay-index-drift.test.ts(17 个用例),其中钉住的两条:sys_metadata声明跑initObjects,再原样下发ensureOverlayIndex的四条 SQL,detectManagedDrift()必须返回空;--allow-destructive—— 断言没有任何 message 含该字样,且不存在指向
idx_sys_metadata_overlay_*的drop_index/recreate_index。外加:第二次启动(换新 driver、台账为空、同一个文件库)仍然零漂移;
reconcileAndWarnDrift一条[schema-drift]都不打;探测层读出四列 +partial: true+expressions;台账对带引号 / 带 schema 前缀的DDL 也解析正确;以及
classifyIndexKeyPart/parseIndexDdl的纯单元用例(含pg_get_indexdef的COALESCE((package_id)::text, ''::text)形态)。顺带发现(未在本 PR 修)
IndexSchema.partial是可声明的,但没有任何驱动会发出WHERE子句——sys-metadata.object.ts自己就是它的作者之一。已按 Prime Directive #10 单独立单:#4943(未指派)。
🤖 Generated with Claude Code
https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX