makecindy / makecindy/cindy

[Bug] 覆盖安装旧客户端后本地数据库版本不兼容且无法恢复

Open
#175 6 comments 0 reactions 1 assignee Claimed by @DavidShenXD View on GitHub
bug
Dominant language
TypeScript
Stars
2.7k
Forks
395
Avg merge
21h 48m
Merged PRs (30d)
776

Description

## 问题描述

用户覆盖安装 Cindy 后,启动时提示:

> 本地数据库 schema 迁移失败
> applied migration runtime identity changed at seq 75
> (0075_complex_strong_guy.sql)

关闭弹窗并重启应用后,仍然出现完全相同的错误,无法进入客户端。

经现场状态与源码核对,本问题不是数据库正在执行 migration 时失败,也没有证据表明当前主干改写了 `0075`。实际情况是:

- 当前安装包只包含 migration `0000..0074`;
- 保留在 Cindy userData 中的本地数据库已经推进到 schema `79`;
- 数据库对应的 `.migration-runtime.json` 已记录 seq `75..79`;
- 旧安装包启动时找不到数据库已经应用的 seq 75,因此 fail closed。

也就是说,覆盖安装后实际运行的是一个 migration 版本落后于本地数据库的客户端。

## 当前运行方式提示

目前会同时运行以下两个 Cindy 实例:

1. 基于本地源代码启动的开发版;
2. 旧的打包客户端版本。

两者共享同一套 Cindy userData。开发版可能先把本地数据库升级到更高的 schema,随后启动旧打包客户端时,旧包因缺少数据库已经应用的 migration 而 fail closed。

因此该问题不一定表示覆盖安装过程修改了 migration 文件,也可能是“开发版先升级共享数据库,旧打包版随后打开新数据库”触发的版本不兼容。

## 现场状态

```text
安装包 migration head: 74
本地数据库 schema_version: 79
runtime sidecar 已记录 seq 75: true
安装包包含 0075: false
```

当前安装包资源目录中最高 migration 为:

```text
0074_bridge_legacy_migration_lineage.sql
```

本地数据库及 runtime sidecar 则已推进并记录:

```text
0075_complex_strong_guy.sql
...
0079_futuristic_hercules.sql
```

因此该安装包不能安全打开当前数据库。

## 根因分析

`prepareMigrationRuntimeManifest()` 会把数据库已经记录的 runtime identity 与当前安装包中的 migration 对比:

```ts
const current = expectedBySeq.get(identity.seq);
if (!current || !sameRuntimeIdentity(identity, current)) {
throw new Error(
`applied migration runtime identity changed at seq ${identity.seq} (${identity.fileName})`,
);
}
```

参考:

- `apps/desktop/src/main/localDb/migrationRunner.ts`

当前逻辑把以下两种情况合并成同一个错误:

1. migration 仍然存在,但 SQL 或 companion TS 的 hash 被修改;
2. 当前安装包版本过旧,根本不存在数据库已经应用的 migration。

本次命中第二种情况:当前安装包不存在 seq 75,但错误文案仍显示为 `runtime identity changed`,容易误导为 migration 文件被改写。

该异常随后被 `ensureReady()` 转为 `MIGRATE_FAILED`:

- `apps/desktop/src/main/localDb/index.ts`

覆盖安装和普通重启都会保留 Cindy userData,所以数据库仍是 schema 79,旧安装包每次启动都会在 seq 75 重复失败,无法通过重启自愈。

## 实际结果

1. 覆盖安装或启动旧打包客户端后无法进入 Cindy。
2. 错误文案把“安装包缺少已应用 migration”误报为“runtime identity changed”。
3. 点击“好的”并重启应用后仍然出现相同错误。
4. 用户没有直接、明确的恢复路径。

## 期望结果

1. 客户端应区分:
- migration 内容确实发生变化;
- 当前安装包 migration 版本落后于本地数据库。
2. 对“旧客户端打开新数据库”显示准确、可操作的错误提示,例如:
- 当前客户端版本过旧,无法打开已由新版本升级的本地数据;
- 请安装不低于指定版本的 Cindy;
- 如果已有兼容更新,直接提供“重启并安装更新”入口。
3. 安装或更新流程应避免把用户从支持当前数据库的版本降级到旧版本,或至少在启动前提供明确恢复路径。
4. 不应建议用户删除 runtime sidecar、降低 `schema_version` 或绕过兼容性检查。
5. 安装兼容版本后应能直接复用现有 userData 正常启动,不丢失数据。

## 建议处理方向

### 错误分类

为 runtime manifest 校验区分至少两种错误:

- `applied migration runtime identity changed`
- `current application is missing applied migration`

当 `expectedBySeq.get(identity.seq)` 不存在时,应明确归类为“当前客户端版本过旧/数据库版本超前”,而不是 hash drift。

### 恢复入口

复用当前 main 中的 `LocalDbFatalScreen` 更新恢复链路:

- 有兼容更新时提供“重启并安装更新”;
- 正在下载时展示进度;
- 无可用更新时说明需要下载安装更新版本,并提供明确操作路径;
- 保留技术详情供排查,但不要让技术错误成为用户唯一可见信息。

### 安装/更新保护

确认旧打包客户端为什么仍会在共享数据库已升级后被启动,并评估:

- 安装器是否应拒绝低版本覆盖高版本;
- 更新元数据是否发布或命中了旧构建;
- 版本号是否能真实反映 migration compatibility;
- 开发版与打包版共享 userData 时,是否需要更明确的兼容性提示或运行隔离指引;
- macOS 与 Windows 的覆盖安装是否都存在同样的降级路径。

## 验收标准

- [ ] 增加“安装包最高 migration 为 74,数据库 schema 为 79,sidecar 含 seq 75”的回归测试。
- [ ] 上述场景被识别为“当前客户端版本过旧/缺少已应用 migration”,不再误报为 identity changed。
- [ ] 客户端继续 fail closed,不允许旧代码直接打开新 schema。
- [ ] 存在兼容更新时,可以从错误界面完成更新并恢复启动。
- [ ] 没有可用更新时,用户能看到明确的下载安装指引,而不是陷入重启循环。
- [ ] 安装兼容版本后可以原样打开现有数据库,不需要删除或手工修改 userData。
- [ ] 覆盖开发版与旧打包版共享 userData 的复现场景。
- [ ] 验证 macOS 与 Windows 的覆盖安装及版本降级行为。
- [ ] 不回退 migration SQL + companion TS runtime identity 的冻结保护。

## 参考代码

- runtime identity 生成与比较:
`apps/desktop/src/main/localDb/migrationRunner.ts`
- localDb 启动和错误传播:
`apps/desktop/src/main/localDb/index.ts`
- migration 资源路径:
`apps/desktop/src/main/localDb/migrate.ts`
- 打包时复制 drizzle 资源:
`apps/desktop/forge.config.ts`
- 当前 migration 失败恢复界面:
`apps/desktop/src/renderer/components/error/LocalDbFatalScreen.tsx`
- 恢复界面状态映射:
`apps/desktop/src/renderer/components/error/localDbFatalView.ts`
- runtime identity 回归测试:
`apps/desktop/src/main/localDb/__tests__/migrationCompatibility.test.ts`

## 平台与其他形态

- macOS:已观察到开发版与旧打包版共享 userData 后,旧包打开新数据库的现场状态。
- Windows:使用相同 runtime manifest 与 migration 校验逻辑,需验证安装器是否同样允许旧版本覆盖。
- SSH 远程工作区:不涉及,localDb 位于控制端本机 userData。
- device-link / 手机版:不涉及,手机版不直接执行 desktop SQLite migration。

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.