CC Switch 只加一张表,schema 版本却跳两级

2026-08-31

先说一个对不上的地方。CC Switch 在 v3.20.0 的发布说明里写着「本版包含数据库迁移」,并注明 schema 从 v16 迁到 v17(docs/release-notes/v3.20.0-zh.md:238)。但把 v3.20.1 的仓库翻开,src-tauri/src/database/mod.rs:56 那一行写的是:

pub(crate) const SCHEMA_VERSION: i32 = 18;

同一个字段在 v3.19.2 快照的同一行是 = 16。也就是说这两个版本之间,schema 版本号连跳两级。可再去数建库函数里的建表语句,v3.19.2 快照是 16 条,v3.20.1 快照是 17 条——只多了一张表。表数 +1,版本号 +2。

这个差额不是笔误,也不是谁忘了对齐。它是这类工具在做数据库演进时一条相当值得抄走的纪律:版本号编的是「迁移事件」,不是「表结构的形状」。 下面按源码位置把这件事拆开。

这篇是怎么核的

本文对应的仓库快照是 3217f725(仓库内三处版本号 package.json / src-tauri/tauri.conf.json / src-tauri/Cargo.toml 都是 3.20.1),核对日 2026-08-31,另有一份 v3.19.2 的旧快照仅用于 diff。全部结论来自对这两份源码与 docs/ 下文档的静态阅读和 grep 统计——我们没有编译、没有运行、也没有安装过这个桌面应用,所以本文不会出现任何关于界面、操作或速度的描述。文中出现的数字都是当时快照里读到的值,随版本会变。

三个数字,三套口径

「schema 改了多少」这句话至少可以指三样不同的东西,混着说必然自相矛盾。它们在源码里各有一处落点:

口径位置v3.19.2v3.20.1
SCHEMA_VERSION 常量src-tauri/src/database/mod.rs:561618
迁移分支条数(N => {}src-tauri/src/database/schema.rs 的迁移循环1618
基线建表语句条数create_tables_on_conn 函数体内的 CREATE TABLE1617

前两个必然相等——迁移循环是 while version < SCHEMA_VERSION(新快照 schema.rs:458)逐级推进的,从 0 一路走到常量值,缺一级就会掉进兜底分支报「未知的数据库版本」。第三个是另一回事:它数的是新库从零建起来长什么样,跟历史上迁移过几次没有对应关系。

数第三个数字时还有个坑:必须限定在建库函数体内。如果直接 grep -rni "CREATE TABLE" src-tauri/src | wc -l,两个快照分别是 89 和 96,因为把 #[cfg(test)] 里的测试夹具和迁移过程中的中转表全数进去了。这个口径不能当事实用。

顺带提一处更容易撞车的地方:schema.rs:299 的建表注释写的是「18. Session Log Sync 表 (会话日志同步状态)」,前面一条是 17.、后面 schema.rs:339 是「19. Profiles 表」。这个 18 是建表顺序的序号,跟 schema 版本号 18 只是数值上恰好撞上了。版本号只在 database/mod.rs:56

第一次迁移:v16 → v17,确实建了一张表

分发在 schema.rs:542-546,日志文案是「迁移数据库从 v16 到 v17(添加会话用量持久去重账本)」,实现在 schema.rs:1565-1578,一句 execute_batch 建一张表加一条索引:

CREATE TABLE IF NOT EXISTS session_usage_dedup (
    data_source TEXT NOT NULL,
    request_id TEXT NOT NULL,
    semantic_id TEXT NOT NULL,
    has_entry_id INTEGER NOT NULL DEFAULT 0,
    PRIMARY KEY (data_source, request_id)
);
CREATE INDEX IF NOT EXISTS idx_session_usage_dedup_semantic
ON session_usage_dedup(data_source, semantic_id, has_entry_id);

这张表为什么要单独存在,源码注释(schema.rs:319-320)一句话说清了:会话明细行在 rollup 之后会被剪掉,而识别「分叉 / 改写」所需的 request id 必须活得比明细更久,所以另开一本紧凑的持久账本。三个字段的分工也能从名字读出来:request_id 是硬 id,semantic_id 是没有 id 时按内容算出的等价键,has_entry_id 是区分这两种情况的标志位——索引因此建在 (data_source, semantic_id, has_entry_id) 上,而不是主键上。

这次迁移带来的就是那张多出来的表,create_tables_on_conn 里也同步补了同名的建表语句(schema.rs:321-337),所以新库和迁移过来的老库能对上。基线建表从 16 条变 17 条,是这一次贡献的。

第二次迁移:v17 → v18,一张新表都没建

分发在 schema.rs:547-551,日志文案「迁移数据库从 v17 到 v18(会话日志字节游标列)」,实现在 schema.rs:1588-1600,全文很短:

fn migrate_v17_to_v18(conn: &Connection) -> Result<(), AppError> {
    // 缺表的库(异常/测试夹具)跳过:create_tables 会以含列的新 DDL 建表。
    if Self::table_exists(conn, "session_log_sync")? {
        Self::add_column_if_missing(conn, "session_log_sync", "last_byte_offset", "INTEGER")?;
        Self::add_column_if_missing(conn, "session_log_sync", "last_tail_fingerprint", "INTEGER")?;
    }
    Ok(())
}

它只给一张已有的表补两个可空列。这就是「版本 +1 而表数不变」的那一次。

两个新列服务的是 v3.20.1 那条会话日志增量扫描的改动:last_byte_offset 存字节游标,last_tail_fingerprint 存游标边界前那段字节的指纹。它们的空值语义在建表注释里写死了schema.rs:299-306):last_byte_offset 为 NULL 表示尚无字节游标(旧的行号游标行,或者非 Claude 路径的行),此时回退全量读;last_tail_fingerprint 为 NULL 表示无指纹可校验,按纯追加处理。

这一点值得单拎出来:迁移没有给存量行回填一个「看起来合理」的默认值,而是让它们保持 NULL,并把 NULL 定义成一个明确的降级分支。给存量行编一个假的字节位置,代价是下一轮扫描会从错误的位置开始读;保持 NULL 则只是多读一遍。这个取舍在迁移代码里只体现为「什么都不做」,真正承担它的是读取侧的回退逻辑。

为什么第二次不能补进 v17

这是整件事最硬的一处。函数上方 schema.rs:1580-1587 的注释把理由写死了,原文:

独立成版而非搭 v17 车:v17 已在开发库上执行过(迁移不会重跑,CREATE TABLE IF NOT EXISTS 也不补列),追加进 v17 会让这些库永远缺列。

把这段话展开就是一条因果链:

  1. 迁移循环按 user_version 逐级推进,某个库跑完 v17 之后,它的 user_version 就写成了 17(分支在 schema.rs:542-546),v16 → v17 那个分支此生不会再进来
  2. 就算硬把加列语句塞进 migrate_v16_to_v17,也救不了这批库——它们不会再执行那个函数;
  3. 而基线建表里那句 CREATE TABLE IF NOT EXISTS,遇到已存在的表是直接跳过的,不会去比对列、更不会补列;
  4. 于是这批已经跑过 v17 的库,会永远停在缺两列的状态,且没有任何一条代码路径能发现这件事。

所以第二次改动必须换一个新号。版本号的作用不是描述「现在的表长什么样」,而是给每一次「已经发生过的迁移动作」发一个不可回收的编号——一旦有任何一个库把某个号跑完了,那个号的内容就冻结了,只能往后加。

这也解释了为什么这类版本号只会越跳越快、跟表数量对不上:同一条演进线上做两次独立的结构改动,就是两个号,哪怕第二次只加了两列。本例的这两次还分属两个发布——v16 → v17 在 v3.20.0 的发布说明里(v3.20.0-zh.md:238),v17 → v18 是 v3.20.1 才叠上去的。

这个决定被写成了测试

上面这段推理不是文章里的引申,它在测试里有对应的钉子。两个迁移各配一个测试(schema.rs:3430schema.rs:3448),后者的名字直接叫 migrate_v17_to_v18_adds_byte_cursor_to_existing_sync_table,而它的夹具形状恰好就是注释里担心的那种库——测试注释(schema.rs:3449-3450)写明:模拟的是「字节游标曾短暂搭 v17 车、已执行过 v17 的开发库」。

换句话说,这段测试体(schema.rs:3448-3480)专门去构造一个「已经跑过 v17、却缺这两列」的库,再让迁移跑一遍,看列有没有补上、存量行的取值是不是仍旧维持在上一节说的那个空值状态。上一节那段注释担心的场景,在这里被翻译成了机器可以复核的形式。

把「为什么这么写」的推理落成一个可执行的断言,比写在注释里更耐得住后人重构。 前一个测试 migrate_v16_to_v17_creates_session_usage_dedup_ledgerschema.rs:3430)同样把要验的事写进了函数名:v16 → v17 那一次,去重账本那张表确实被建了出来。

号只升不降,连带出两条防线

版本号既然只能往前发,就得处理「拿旧应用打开新库」这种情况。这一条在 schema.rs:449-455:进迁移循环之前先比一次,version > SCHEMA_VERSION 就直接报错返回,文案是「数据库版本过新({version}),当前应用仅支持 {SCHEMA_VERSION},请升级应用后再尝试。」

所以发布说明里那句「降级需还原备份」不是一条使用建议,是代码在这一行主动拒绝的结果——库一旦被新版本推过号,旧版本就打不开了。另有一个专门读磁盘 user_version 的函数(database/mod.rs:169-182),注释写「仅当它比应用支持的 SCHEMA_VERSION 更新时返回」,是给上层显示这个状态用的探测入口。

第二条防线是迁移前自动备份(database/mod.rs:132-135)。这个分支的形状是这样的(中间省略了实际落备份与失败处理的语句,只保留判定条件与日志文案):

if version > 0 && version < SCHEMA_VERSION {
    ... "Creating pre-migration database backup (v{version} → v{SCHEMA_VERSION})"
}

两个条件缺一不可:version > 0 排掉全新库(没什么可备份的),version < SCHEMA_VERSION 才说明真要迁移。两者都满足才落一份备份——迁移动作本身不可逆,这份备份是「降级需还原备份」那句提示背后唯一的实际抓手。

引用这个版本号的时候要带口径

最后回到开头那处对不上。发布说明 v3.20.0-zh.md:238 写的「schema 从 v16 迁移到 v17」,在它发布的那个时点是准确的;到 v3.20.1 的快照上,代码里的 SCHEMA_VERSION 已经是 18,因为中间又叠了一次 migrate_v17_to_v18。两处说的是同一条时间线上的不同点,以我们实读的仓库状态为准。

实践上的意思很直接:照抄发布说明里的版本号会写错。 要提这个数就得带上限定——「v3.19.2 时是 16,v3.20.1 已经是 18」,或者干脆只写机制、不写数值。这个数下个月还会再涨。

想自己核一遍,三条命令就够:

grep -n "SCHEMA_VERSION" src-tauri/src/database/mod.rs
grep -n "迁移数据库从" src-tauri/src/database/schema.rs
grep -n "fn create_tables_on_conn" src-tauri/src/database/schema.rs

第一条给常量当前值,第二条把每一级迁移的日志文案连同分支行号一起列出来(每条文案后面括号里就写了这一级干了什么),第三条定位建库函数,再在函数体范围内数 CREATE TABLE 就是基线表数。三个数对不上是正常的——现在你知道差额从哪来。


本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册、路由指南与发布说明,以及 src/src-tauri/tests/ 的源码整理, 核对日 2026-08-31,对应仓库快照 3217f725(仓库内版本号 3.20.1)。 本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用, 因此不涉及界面外观、操作手感与切换速度的任何描述。 文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。 该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。

留言讨论

评论发布后会被人工复核,违规内容将被删除。

    还没有人评论,来说说你的看法

    如果发表没有反应,可以前往联系我们告诉我们。

    在 CC Switch 里加一个国内直连的供应商

    力达云网关,注册送 ¥5 额度,一期提供 DeepSeek。

    去添加

    这个页面有问题?

    提交时会附带当前页面地址和浏览器信息,帮助我们定位问题。不填联系方式即为匿名。