数据库怎么升级:迁移调度与建表注释里跳掉的三个编号
如果你想弄清 CC Switch 升级一次到底对你本机那个库做了什么,最快的入口不是 README,而是三个文件里的三处常量与一段循环:src-tauri/src/database/mod.rs 的 SCHEMA_VERSION、src-tauri/src/database/schema.rs 的 apply_schema_migrations_on_conn,以及独立的 src-tauri/src/database/migration.rs。这篇只讲这条升级路径。
以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2(package.json 与 src-tauri/Cargo.toml 两处一致)。我们只读源码文本,没有安装也没有运行过这个桌面应用,也没有编译过它。
先把两个 16 分开
翻这块代码的人十有八九会先撞上一个巧合:这里有两个 16,含义完全不同。
第一个 16 是 schema 版本号。src-tauri/src/database/mod.rs:54-56 写着 pub(crate) const SCHEMA_VERSION: i32 = 16;,紧挨着的文档注释要求「每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑」。它是迁移递推的终点。
第二个 16 是建表数量。create_tables_on_conn 在全新库上一次性建出 16 张表(src-tauri/src/database/schema.rs:25-330)。它是结构的宽度。
两者在当前快照上恰好都等于 16,但没有任何机制保证它们同步:版本号只在改表结构时递增,一次迁移里加两张表、或者只加一个列,版本号都只 +1。所以别把「schema 版本 16」读成「有 16 张表」,也别反过来推。下一次上游发版,这两个数极可能就分开了。
建表注释里跳掉的三个编号
真正反直觉的一处在这里。schema.rs 里每张表前面都有一行编号注释,从 // 1. Providers 表 一路排下去,到 // 12. Stream Check Logs 表(schema.rs:246)之后,下一行编号直接是 // 16. Proxy Live Backup 表(schema.rs:263),13、14、15 三个编号不存在。再往后是 17 Usage Daily Rollups、18 Session Log Sync、19 Profiles。
于是有三种数法会给出三个不同答案:
- 按最大编号数 → 19
- 按编号连续假设数 → 也是 19
- 按实际出现的编号个数数 → 12 + 4 = 16,这才是真正建出来的表数
12 与 16 之间只有一句孤零零的注释:// 注意:circuit_breaker_config 已合并到 proxy_config 表中(schema.rs:261)。这句话与跳号之间有没有关系、其余编号去哪了,我们不推断——按纪律,说完差异就停。你需要记住的只有一件事:这些编号是注释文本,不是表的序号,更不是表的数量。
想自己核一遍,两条命令就够:
grep -n "^\s*// [0-9]\+\. " src-tauri/src/database/schema.rs | head -16
grep -c "CREATE TABLE IF NOT EXISTS" src-tauri/src/database/schema.rs
以上两条是按仓库文本自行组合的核查命令,未经实测,以你本地仓库的实际输出为准。
第一条把编号注释按行号列出来,你能直接看到 12 之后跳到 16。这里的 head -16 不是随手写的:同一个匹配式还会命中迁移函数体内部的步骤注释,那些注释的编号会重新从 1 开始,跟建表编号没有任何关系。所以看到编号从 19 又跳回 1,别以为编号重排了一轮,那已经翻过界了,到此为止即可。
第二条会把迁移路径里的建表语句一起数进去,因此不能拿它当表数——要数「全新库建了几张」,得把范围限定在 create_tables_on_conn 这个函数体内。16 张表各自存什么,另有一篇专门在讲,本文不重复。
版本号落在哪:PRAGMA user_version
CC Switch 没有自建版本表,直接用 SQLite 自带的 PRAGMA user_version 存版本号,读写各一个函数:get_user_version 与 set_user_version,后者传负数会直接报错(schema.rs:2934-2948)。
这个选择有个很实际的好处:你不需要装任何东西,也不需要理解表结构,就能读出手上这个库处在哪一版。库文件默认在 ~/.cc-switch/cc-switch.db(src-tauri/src/database/mod.rs:101、src-tauri/src/config.rs:207)。这是你本机的敏感数据文件,操作前请自行做好副本。
迁移调度:一个 SAVEPOINT 包住的递推循环
apply_schema_migrations_on_conn(schema.rs:415-536)的结构可以一句话概括:先开 SAVEPOINT,再 while 递推,成功 RELEASE、失败 ROLLBACK TO 后再 RELEASE。
递推部分是 while version < SCHEMA_VERSION,按当前版本 match 分支:0→1、1→2,一直到 15→16,每走完一步立刻 set_user_version 落盘,然后重新读一次版本进入下一轮(schema.rs:429-518)。也就是说,从 v3 的老库升到 v16,会依次跑完 13 个 migrate_vN_to_vN+1,而不是有一条「直达」的捷径。
各步在日志里都标了内容,卡里核到的几条是这样:
| 迁移步 | 日志标注的内容 |
|---|---|
| v3 → v4 | OpenCode 支持 |
| v5 → v6 | 使用量聚合表 + Copilot 模板类型统一 |
| v9 → v10 | 添加 Hermes Agent 支持 |
| v11 → v12 | 添加项目 Profiles 表 |
| v13 → v14 | 添加 Grok Build 代理配置 |
| v15 → v16 | 重建 Codex 会话用量 |
这张表的读法是:每一行都是一次功能扩张在存储层留下的痕迹。你如果在排查「为什么升级完某个功能的数据看起来变了」,可以先用 PRAGMA user_version 读出升级前后跨过了哪几步,再对着日志文案去定位相关的那一步。其余步骤的日志文案本文不逐条列,你可以在 schema.rs:429-518 这段里按顺序读下来。
两道版本拒绝,分别拦什么
升级路径上有两处会直接停下来,语义完全不同,别搞混。
第一道拦「库比应用新」。 当 version > SCHEMA_VERSION,代码不做任何 DDL,先回滚并释放 savepoint,然后返回一条中文错误:「数据库版本过新({version}),当前应用仅支持 {SCHEMA_VERSION},请升级应用后再尝试。」(schema.rs:421-427)。典型触发场景是同一份数据被新旧两个版本轮流打开——比如你在另一台机器上用了更新的版本,又把库同步回一台还没升级的机器。
这道拦截在启动流程里还有一层前置:版本过新的预检必须早于任何 schema 写操作,命中后写入 InitErrorPayload{kind: "db_version_too_new"} 并强制显示主窗口,然后直接 return Ok(())(src-tauri/src/lib.rs:530-556)。启动序列的其余部分另有一篇在讲,这里只取与迁移直接相关的这一段。
第二道拦「版本号不认识」。 match 的兜底分支 _ 会报「未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}」(schema.rs:514-517)。它拦的不是「太新」,而是落在合法区间内却没有对应迁移分支的值。对使用者来说这种情况罕见,但对改这块代码的人是硬约束:SCHEMA_VERSION 一动,就必须同时补上对应的 migrate_vN_to_vN+1 分支,否则升级会停在这条错误上。
迁移之前那次备份
Database::init() 里有个容易被忽略的条件判断:只有当 0 < version < SCHEMA_VERSION 时,才在迁移前调 backup_database_file(),而且备份失败只 warn,不阻断迁移(src-tauri/src/database/mod.rs:128-140)。
把条件拆开看:version == 0(全新库或未标版本)不备份,version == SCHEMA_VERSION(本来就是最新)也不备份,只有真要动结构的那一次才备。备份走的是 rusqlite 的在线备份 API 产出一致性快照,落在数据库同级的 backups/ 子目录,文件名形如 db_backup_YYYYMMDD_HHMMSS.db(backup.rs:381-397、backup.rs:459-470)。保留份数默认 10、下限 1,由 AppSettings.backup_retain_count 覆盖(settings.rs:1077-1087)——注意 README_ZH 那一处把它写成固定的「保留最近 10 个」(README_ZH.md:306),代码里 10 只是默认值,两处口径不同,以代码为准。备份与轮换的完整机制另有一篇专门讲,本文只取「迁移前这一次」。
顺带记一处同属升级路径的动作:ensure_incremental_auto_vacuum 对已经建过表的老库,会先备份再 VACUUM 重建,重建完重新设 PRAGMA foreign_keys = ON(database/mod.rs:230-276)。
另一条完全独立的迁移线:JSON → SQLite
上面讲的都是「库内结构升级」。仓库里还有一套语义完全不同的迁移,放在独立文件 src-tauri/src/database/migration.rs(245 行),干的是把早期的 JSON 配置搬进 SQLite。
它有两个入口:migrate_from_json(走事务,真写)与 migrate_from_json_dry_run(在内存库里跑、不落盘)(migration.rs:12、migration.rs:28)。分域迁移函数 5 个:migrate_providers / migrate_mcp_servers / migrate_prompts / migrate_skills / migrate_common_config(migration.rs:68-217)。
有两个细节值得单独记:
一是旧文件不删。JSON 迁移成功后,旧的 config.json 被重命名为 config.json.migrated,而不是删掉(lib.rs:600-607)。二是这条线自己也在演进:migration.rs:195 有一条注释写着「v3.10.0+:Skills 的 SSOT 已迁移到文件系统(~/.cc-switch/skills/)+ 数据库统一结构」——也就是说,并非所有东西都以「进库」为终点,Skills 走的是反方向。
想验证这套机制是否按预期工作
src-tauri/src/database/tests.rs(921 行)有 17 个 #[test],其中几个的名字就把断言写在了脸上:schema_migration_rejects_future_version、migration_v10_to_v11_rebuilds_rollups_with_request_model_dimension、migration_from_v3_8_schema_v1_to_current_schema_v3、schema_dry_run_does_not_write_to_disk。你要判断「版本过新到底拦不拦」「dry run 到底会不会写盘」,读这几个测试比读实现更快。
需要说清的是:这 17 个测试是我们 grep 出来的,我们没有编译、没有运行过其中任何一个,测试存在不代表它们全部通过。
这篇没有核实的部分
按事实卡的边界,有几处必须明说:v0→v16 各个 migrate_vN_to_vN+1 函数体里的具体 DDL 内容我们没有逐个读,只读了调度循环本身;backup.rs 的备份导出与恢复实现细节我们也只读了签名、常量与部分注释。
还有一条纪律:本文提到的所有默认值——SCHEMA_VERSION = 16、保留 10 份备份——都是源码里的默认配置,不是对你升级时实际会发生什么的保证。你的库处在哪一版、会跨过哪几步、备份目录里最后剩几个文件,取决于你自己的历史与设置,项目没有给出通用值。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、
src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。