Skip to content

fix(driver-sql): 全新数据库不再"开机即漂移",也不再把 --allow-destructive 指向框架自己创建的索引 (#4884) - #4954

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4884-schema-drift-coalesce
Aug 3, 2026
Merged

fix(driver-sql): 全新数据库不再"开机即漂移",也不再把 --allow-destructive 指向框架自己创建的索引 (#4884)#4954
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4884-schema-drift-coalesce

Conversation

@xuyushun441-sys

@xuyushun441-sysxuyushun441-sys commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes#4884

问题复现与前提核实

先验证 issue 的判断是否成立(证伪也是好结果)。用 better-sqlite3 直接建出 ensureOverlayIndex 的规范形状:

index_list: [{"name":"idx_sys_metadata_overlay_draft","unique":1,"origin":"c","partial":1},
{"name":"idx_sys_metadata_overlay_active","unique":1,"origin":"c","partial":1}, ...]
idx_sys_metadata_overlay_active -> index_info:
[{"seqno":0,"name":"type"},{"seqno":1,"name":"name"},{"seqno":2,"name":"organization_id"},
{"seqno":3,"cid":-2,"name":null}] ← COALESCE(package_id,'') 这一列,名字是 null

issue 的两条判断都成立,而且根因比"比较器不认识表达式"更靠前一层:探测层根本没把这一列读进来
introspectIndexesPRAGMA 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.tspackages/spec/** 零改动。

1. 索引键按定义读,而不是按方言的逐列目录视图读

PRAGMA index_info / pg_attribute / STATISTICS.COLUMN_NAME 三者都无法表达表达式键。现在:

  • SQLite 解析 sqlite_master.sql(约束自动索引 sql 为 NULL,退回 PRAGMA,那些本来就是纯列);
  • Postgres 改用 pg_get_indexdef(indexrelid) + indpred IS NOT NULL,不再 join pg_attribute
    (attnum = 0 的表达式列正是被那个 join 丢掉的);
  • MySQL 读 STATISTICS.EXPRESSION,并对不认识该列的旧版 / MariaDB 回退到原查询——否则整表索引探测会一起失败。

COALESCE(col, < literal >) 被认定为就是键在 col:ADR-0048 用它正是为了让无 package 的全局行
彼此唯一(普通 UNIQUE 把 NULL 当互不相同),所以它对同一列的约束严格更强,不是缺了一列。
识别范围刻意收窄——lower(name)a || bCOALESCE(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) 形态)。

pnpm --filter @objectstack/driver-sql test
Test Files 56 passed | 4 skipped (60)
Tests 653 passed | 44 skipped (697)
pnpm --filter @objectstack/driver-sql typecheck # tsc --noEmit,零错误

顺带发现(未在本 PR 修)

IndexSchema.partial 是可声明的,但没有任何驱动会发出 WHERE 子句——sys-metadata.object.ts 自己就是
它的作者之一。已按 Prime Directive #10 单独立单:#4943(未指派)。


🤖 Generated with Claude Code

https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX

…-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
@vercel

vercelBot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 3, 2026 5:04pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql.

9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx(via @objectstack/driver-sql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/driver-sql)
  • content/docs/plugins/anatomy.mdx(via @objectstack/driver-sql)
  • content/docs/plugins/packages.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/driver-sql)
  • content/docs/releases/implementation-status.mdx(via @objectstack/driver-sql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@xuyushun441-sys@claude