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-sys xuyushun441-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

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 5:04pm

Request Review

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

github-actions Bot commented Aug 3, 2026

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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants