首页
/ Immich 数据库迁移实战:基于 sql-tools 的 schema 变更、ORDER 清单与回滚操作

Immich 数据库迁移实战:基于 sql-tools 的 schema 变更、ORDER 清单与回滚操作

2026-09-06 13:06:13作者:邬祺芯Juliet

在 Immich 中,服务端的全部表结构以 TypeScript 形式定义在 server/src/schema 目录下,任何对该目录的修改都必须通过一次数据库迁移(migration)才能真正注册到 PostgreSQL 中。本文围绕官方开发者文档 database-migrations.md 讲解完整流程:如何生成迁移、迁移文件的结构、ORDER 清单机制的设计动机、服务启动时如何自动应用新迁移,以及如何回滚最近一次迁移;并结合 server/mise.tomlserver/package.jsonschema-check 命令 的源码,补充漂移检测与本地重置等实用操作。

为什么改完 schema 代码后必须跑一次迁移

Immich 的 schema 由三部分构成,全部位于 server/src/schema

  • tables/:约 64 个表定义文件(如 asset.table.tsalbum.table.tsplugin.table.ts),用 @immich/sql-tools 提供的声明式 API 描述表结构;
  • enums.tsfunctions.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-toolsasHuman 渲染),并附一段自动生成的修复 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 runORDER 清单顺序重放全部 97 个迁移,得到与代码完全一致的干净数据库。当本地库状态与迁移历史不一致(例如手工改过表、误删过迁移文件)导致 schema-check 报错时,这是最可靠的恢复手段。

小结:一次 schema 变更的完整检查清单

  1. 修改 server/src/schema/tables 等声明式定义;
  2. mise //server:migrations generate <name> 生成迁移,人工审阅 up/down(注意数据回填与可回退性);
  3. 将迁移文件移入 server/src/schema/migrations
  4. mise //server:migrations sync-order,并连同 ORDER 清单一起提交;
  5. 重启本地 server 验证迁移自动应用,必要时用 revert 测试回滚、用 schema-check 确认无漂移;
  6. 提交前确认 mise //server:migrations verify-order 通过(CI checklist 会执行它)。

以上流程的前提是本地有一个可达的 Postgres(默认 DB_URL 指向开发用 Docker Compose 中的 localhost:5432/immich),且使用的是当前仓库的 @immich/sql-tools 0.6.3 工具链;对生产环境的任何 schema-drop 类操作都不应照搬本文的本地用法。

登录后查看全文
热门项目推荐
相关项目推荐