Immich 数据库迁移实战:基于 sql-tools 的 schema 变更、ORDER 清单与回滚操作
在 Immich 中,服务端的全部表结构以 TypeScript 形式定义在 server/src/schema 目录下,任何对该目录的修改都必须通过一次数据库迁移(migration)才能真正注册到 PostgreSQL 中。本文围绕官方开发者文档 database-migrations.md 讲解完整流程:如何生成迁移、迁移文件的结构、ORDER 清单机制的设计动机、服务启动时如何自动应用新迁移,以及如何回滚最近一次迁移;并结合 server/mise.toml、server/package.json 与 schema-check 命令 的源码,补充漂移检测与本地重置等实用操作。
为什么改完 schema 代码后必须跑一次迁移
Immich 的 schema 由三部分构成,全部位于 server/src/schema:
tables/:约 64 个表定义文件(如asset.table.ts、album.table.ts、plugin.table.ts),用@immich/sql-tools提供的声明式 API 描述表结构;enums.ts、functions.ts:枚举与数据库函数/触发器定义;migrations/:按时间戳排序的迁移文件,外加一个ORDER清单文件(详见下文)。
tables/ 中的定义描述的是"数据库应该长什么样",而 migrations/ 里的 up() 函数才是"把已有数据库改成这样"的实际执行单元。两者之间靠迁移工具 @immich/sql-tools(workspace 中锁定为 0.6.3,见 pnpm-lock.yaml)桥接:它比对声明式 schema 与真实数据库的差异,自动生成迁移 SQL,并在启动/测试时按 ORDER 清单顺序执行。
创建一次迁移的标准流程
文档给出的四步流程如下,每步都对应 server/mise.toml 中的具体任务。
第 1 步:生成迁移文件
mise //server:migrations generate <migration-name>
其中 //server: 前缀表示在 monorepo 根目录(mise.toml 声明了 monorepo_root = true)下执行 server 包的任务。该任务在 server/mise.toml 中定义为:
[tasks.migrations]
env._.path = "./node_modules/.bin"
run = "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations"
description = "Run database migration commands (create, generate, run, sync-order, verify-order, ...)"
即 mise //server:migrations <子命令> 实际展开为 sql-tools -u <连接串> migrations <子命令>,generate 等子命令会追加到末尾。两个要点:
DB_URL环境变量指定目标数据库,未设置时默认连接postgres://postgres:postgres@localhost:5432/immich——也就是本地 Docker 开发环境中的 Postgres;generate会在server目录下生成一个带时间戳前缀的.ts文件,文件名形如1745244781846-AddUserAvatarColorColumn.ts(时间戳 + PascalCase 名称)。
第 2 步:人工检查迁移内容
迁移文件导出 up() 与 down() 两个异步函数,内部使用 kysely 的 sql 标签模板执行原生 SQL。以真实迁移 1745244781846-AddUserAvatarColorColumn.ts 为例:
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "users" ADD "avatarColor" character varying;`.execute(db);
await sql`
UPDATE "users"
SET "avatarColor" = "user_metadata"."value"->'avatar'->>'color'
FROM "user_metadata"
WHERE "users"."id" = "user_metadata"."userId" AND "user_metadata"."key" = 'preferences';`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "users" DROP COLUMN "avatarColor";`.execute(db);
}
up 先加列、再把存量数据从 JSON 元数据回填到新列,down 则负责反向操作。另有一些迁移是"空操作"的占位文件,如 1750323941566-UnsetPrewarmDimParameter.ts,其 up/down 都是 noop——这类文件存在的意义仅是维持 ORDER 清单与磁盘文件的对应关系。检查时应确认:生成的 DDL 是否符合预期、down 是否可安全回退、是否遗漏了数据回填逻辑。
第 3 步:把迁移文件移动到 server/src/schema/migrations
generate 产生的文件并不直接落在最终目录,需要在代码编辑器中把它移动到 server/src/schema/migrations。当前该目录共有 97 个迁移文件,命名统一为 <毫秒时间戳>-<PascalCase名称>.ts,从 1744910873969-InitialMigration.ts(初始迁移)一直排到最新的 1787148183730-DeleteMismatchedMemoryAssets.ts。时间戳前缀保证了同目录内按字典序即执行顺序。
第 4 步:sync-order 更新 ORDER 清单
mise //server:migrations sync-order
这一步把新迁移追加到 migrations/ORDER 清单文件中。该文件每行记录一个迁移名(去掉 .ts 后缀),例如前几行是:
1744910873969-InitialMigration
1744991379464-AddNotificationsTable
1745244781846-AddUserAvatarColorColumn
...
文档明确解释了为什么这一步必须提交:ORDER 是被 git 跟踪的清单,两个分支各自新增迁移时,会在该文件上产生 git 合并冲突,强制开发者显式决定先后顺序;而如果只靠目录里的时间戳文件,两个分支会"静默地"以错误顺序合并——谁先被执行谁的 DDL 就可能依赖不存在的表,最终导致服务启动失败。这是一个用"冲突噪音"换取"顺序确定性"的有意设计。
迁移如何被自动应用
按文档说明,服务端会监听 *.ts 文件变更并自动重启;而服务启动流程本身就包含"运行所有未应用的新迁移"这一环节,因此只要你在开发环境中重启/重载了 server,新迁移会被立即应用到本地数据库,无需手动 run。
CI 侧则有对应的校验任务。server/mise.toml 中的 checklist 任务在单测与中测(medium test)之后还会执行:
{ task = ":migrations", args = ["verify-order"] }
verify-order 用于确认磁盘上的迁移文件与 ORDER 清单完全一致,防止漏掉第 4 步提交。
回滚最近一次迁移
在开发或测试 schema 变更时,如果需要撤销最近一次已应用的迁移(例如验证 down 逻辑是否真的可逆),文档给出的命令是:
mise //server:migrations revert
该命令会执行最新一条迁移的 down(),把数据库 schema 恢复到迁移前的状态。对应地,server/package.json 中还暴露了一组等价的 npm scripts,方便直接在 server 目录内使用:
| 脚本 | 作用 |
|---|---|
migrations:create |
创建空迁移骨架 |
migrations:generate |
比对 schema 自动生成迁移 DDL |
migrations:debug |
同 generate,附带调试输出 |
migrations:run |
执行所有未应用的迁移 |
migrations:revert |
回滚最近一次迁移 |
migrations:sync-order |
将新迁移登记进 ORDER 清单 |
migrations:verify-order |
校验清单与文件一致性(CI 使用) |
进阶:漂移检测与本地重置
schema-check:迁移状态与 schema 漂移报告
仓库内置了 schema-check 服务命令(实现见 server/src/commands/schema-check.ts),用于核对"磁盘迁移"与"数据库实际状态"是否一致,并检测 schema 漂移。它把每个迁移归为三种状态之一:
applied:已应用(正常路径);deleted:数据库里已应用,但磁盘上文件不见了;missing:磁盘上存在,但尚未应用到数据库。
若检测到漂移,命令会列出漂移项(借助 @immich/sql-tools 的 asHuman 渲染),并附一段自动生成的修复 SQL——源码中特别标注了 "Use at your own risk!",提醒该 SQL 仅供参考、执行前需人工确认。
schema-drop / schema-reset:本地开发一键重建
server/mise.toml 还定义了两个本地重建任务(仅限开发环境,会清空数据):
[tasks."schema-drop"]
run = { task = "migrations query 'DROP schema public cascade; CREATE schema public;'" }
[tasks."schema-reset"]
run = [
{ task = ":schema-drop" },
{ task = "migrations run" },
]
即先 DROP SCHEMA public CASCADE 重建空 schema,再 migrations run 按 ORDER 清单顺序重放全部 97 个迁移,得到与代码完全一致的干净数据库。当本地库状态与迁移历史不一致(例如手工改过表、误删过迁移文件)导致 schema-check 报错时,这是最可靠的恢复手段。
小结:一次 schema 变更的完整检查清单
- 修改 server/src/schema/tables 等声明式定义;
mise //server:migrations generate <name>生成迁移,人工审阅up/down(注意数据回填与可回退性);- 将迁移文件移入 server/src/schema/migrations;
mise //server:migrations sync-order,并连同ORDER清单一起提交;- 重启本地 server 验证迁移自动应用,必要时用
revert测试回滚、用schema-check确认无漂移; - 提交前确认
mise //server:migrations verify-order通过(CI checklist 会执行它)。
以上流程的前提是本地有一个可达的 Postgres(默认 DB_URL 指向开发用 Docker Compose 中的 localhost:5432/immich),且使用的是当前仓库的 @immich/sql-tools 0.6.3 工具链;对生产环境的任何 schema-drop 类操作都不应照搬本文的本地用法。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00