Twenty 数据库迁移治理:为什么 legacy-typeorm-migrations-do-not-add 目录被冻结,以及 Instance Command 如何取代 TypeORM 迁移
Twenty 的数据库 schema 演进在历史上由 TypeORM 迁移文件驱动,但这些迁移文件如今被冻结在一个专门的目录中:legacy-typeorm-migrations-do-not-add。该目录承载 common/ 与 billing/ 两组历史迁移,仅用于让旧部署能够基于 _typeorm_migrations 表重放已执行过的迁移;当前所有新的 schema 与数据变更都必须改为编写 instance commands(fast / slow)或 workspace commands。读完本文,你将理解 Twenty 冻结 TypeORM 迁移系统的工程动机、旧迁移在数据源中的加载方式,以及如何用 npx nx run twenty-server:database:migrate:generate 生成并理解新的升级命令。
目录定位:一份"只读历史"的自述
该目录的 README 只有几段话,但把关键治理决策说得很清楚:
- 目录内是 历史 TypeORM 迁移,分为
common/与billing/两组,保留它们的唯一目的是让老部署可以对照_typeorm_migrations表继续重放; - TypeORM 迁移系统已被冻结(frozen),禁止再向这里添加新文件;
- 当前活跃的升级系统是 instance commands(fast / slow)与 workspace commands,通过
@RegisteredInstanceCommand和@RegisteredWorkspaceCommand装饰器注册,并用如下命令生成:
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
- 完整指南见 UPGRADE_COMMANDS.md,现有实现示例在 upgrade-version-command 目录。
README 还特别澄清了一个容易误判的点:../migrations/utils/ 目录 不属于 legacy 范围——那些 SQL 辅助函数仍被当前的 instance / workspace 命令 import,因此它们被有意放在本目录之外(即 migrations/utils/ 下),继续作为活跃代码维护。
从目录内容看,冻结并非一蹴而就:common/ 下的迁移时间戳跨度从 1700140427984(最早的 setupMetadataTables)一直延伸到 1769196250679(addNavigationMenuItemViewForeignKey 等),文件命名遵循 TypeORM 迁移的"时间戳-描述"惯例(如 1765499361805-addRLS.ts、1767876112877-removeWorkspaceMigration.ts);billing/ 下仅有 3 个文件(1708535112230-addBillingCoreTables.ts 等),对应计费模块的早期 schema。这些文件从此只作为"已执行历史"存在,不再增长。
源码印证:旧迁移为何仍要加载
冻结的目录之所以不能删,根源在 core 数据源的配置。core.datasource.ts 中的关键设置包括:
export const typeORMCoreModuleOptions: TypeOrmModuleOptions = {
url: process.env.PG_DATABASE_URL,
type: 'postgres',
schema: 'core',
synchronize: false,
migrationsRun: false,
migrationsTableName: '_typeorm_migrations',
// The TypeORM migration system is frozen — historical migrations live in
// `legacy-typeorm-migrations-do-not-add/` and are loaded here only so the
// `_typeorm_migrations` table stays consistent for older deployments.
// Do NOT add new files there: write a fast/slow instance command instead.
migrations:
process.env.IS_BILLING_ENABLED === 'true'
? [
`${distOrSrc}database/typeorm/core/legacy-typeorm-migrations-do-not-add/common/*{.ts,.js}`,
`${distOrSrc}database/typeorm/core/legacy-typeorm-migrations-do-not-add/billing/*{.ts,.js}`,
]
: [
`${distOrSrc}database/typeorm/core/legacy-typeorm-migrations-do-not-add/common/*{.ts,.js}`,
],
// ...
};
这段配置解释了三层事实:
- 加载即声明。
migrations数组显式指向legacy-typeorm-migrations-do-not-add/common(计费开启时额外加载billing),TypeORM 由此知道完整的历史迁移清单;migrationsTableName: '_typeorm_migrations'决定了重放记录落在哪张表。这与 README 中"kept so that older deployments can still replay them against the_typeorm_migrationstable"的描述一一对应。 - 只声明、不自动执行。
migrationsRun: false与synchronize: false意味着服务启动时不会自动跑迁移,schema 变更全部走显式的命令入口(见下节的database:migratetarget)。 - 目录名即规范。源码注释与 README 互为镜像:注释里直接写明 "Do NOT add new files there: write a fast/slow instance command instead",把治理规则固化在代码评审可见的位置。
从源码结构看,这一加载还带有条件性:仅当 IS_BILLING_ENABLED === 'true' 时才把 billing/ 迁移纳入清单,说明 billing 迁移属于可选部署路径的历史包袱,进一步印证了"legacy 只服务于旧部署"的定位。
新的升级体系:Instance / Workspace Commands 如何接管
README 指向的 UPGRADE_COMMANDS.md 定义了取代 TypeORM 迁移的完整体系,核心要点如下。
生成命令与注册机制
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
该命令在 project.json 中映射到 database:migrate:generate target,实际执行 node dist/command/command.js generate:instance-command(依赖 build),对应实现为 generate-instance-command.command.ts 与 instance-command-generation.service.ts。文档明确说明:生成器会创建一个带时间戳的文件,并自动注册进 instance-commands.constant.ts——不要手动编辑该文件。
按版本组织的真实实现可以在 upgrade-version-command 目录 中找到,例如 1-22 版本的 fast 命令示例。
Fast 命令:升级期立即执行的 schema 变更
Fast 命令在升级过程中立即运行,适用于"如果延迟执行会在数据库与服务之间造成破坏性不一致"的 schema 变更。它实现 FastInstanceCommand,提供 up / down 两个方法:
@RegisteredInstanceCommand('1.22.0', 1775758621017)
export class AddWorkspaceIdToTotoFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."toto" ADD "workspaceId" uuid`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."toto" DROP COLUMN "workspaceId"`,
);
}
}
Slow 命令:先做慢速数据迁移,再做 schema 变更
Slow 命令用于"在 schema 变更之前必须完成、且可能非常耗时"的数据迁移,只有显式传入 --include-slow 时才会执行。它实现 SlowInstanceCommand(继承自 FastInstanceCommand),额外提供在 up 之前执行的 runDataMigration 方法:
@RegisteredInstanceCommand('1.22.0', 1775758621018, { type: 'slow' })
export class BackfillWorkspaceIdSlowInstanceCommand
implements SlowInstanceCommand
{
async runDataMigration(dataSource: DataSource): Promise<void> {
// Backfill logic (can be slow — e.g. iterating over workspaces, cache recomputation)
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."toto" ALTER COLUMN "workspaceId" SET NOT NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."toto" ALTER COLUMN "workspaceId" DROP NOT NULL`,
);
}
}
文档给出的常用组合模式:一个 fast 命令先加可空列,一个 slow 命令回填存量行后再置为 NOT NULL。仓库中的 1-22-instance-command-slow 示例 正是这一"回填 workspaceId"模式的真实落地。
Workspace 命令:遍历所有活跃/挂起工作区
Workspace 命令用于逐工作区(per-workspace)的逻辑,通过 @RegisteredWorkspaceCommand 装饰器与 nest-commander 的 @Command 装饰器联合注册,并继承 ActiveOrSuspendedWorkspaceCommandRunner:
@RegisteredWorkspaceCommand('1.22.0', 1780000002000)
@Command({
name: 'upgrade:1-22:backfill-standard-skills',
description: 'Backfill standard skills for existing workspaces',
})
export class BackfillStandardSkillsCommand
extends ActiveOrSuspendedWorkspaceCommandRunner
{
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
// Per-workspace logic goes here
// options.dryRun, options.verbose are available for free
}
}
基类自动处理工作区迭代,并免费提供了 --dry-run、--verbose 与工作区过滤选项。文档还特别指出元数据迁移的两条路径(validateBuildAndRunWorkspaceMigration 默认走 side-effect 引擎,validateBuildAndRunLegacyWorkspaceMigration 跳过引擎、按字面矩阵执行),经验法则为:目标版本 < 2.19 用 legacy 方法,>= 2.19 用默认 side-effect 方法。
执行顺序与运行方式
同一版本内的升级流水线按如下顺序执行,组内按时间戳排序:
- Instance fast 命令
- Instance slow 命令
- Workspace 命令(在所有活跃/挂起工作区上顺序执行)
对应的执行入口在 project.json 中定义:database:migrate target 执行 node dist/command/command.js run-instance-commands --force,且存在一条依赖 nx database:migrate -- --include-slow 的组合流程(先 setup-db.js 建库,再带 slow 命令跑完整迁移)。文档同时说明了 Ctrl+C/SIGTERM 下的优雅中断语义(首次 Ctrl+C 在当前工作区步骤结束后停住,再次 Ctrl+C 强制退出)、断点续跑(从 upgradeMigration 表记录的最后一个命令恢复,不回滚),以及 yarn command:prod:background 系列命令用于 kubectl exec 场景的脱离式运行与日志重连。
对开发者的实操含义
结合 README 与源码,维护 Twenty 数据库 schema 时的正确姿势可以归纳为:
- 不要在 legacy-typeorm-migrations-do-not-add 下新建任何迁移文件——那里的清单只用于维持
_typeorm_migrations表的向后一致性; - 新的 schema / 数据变更用
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>生成 instance 命令,按版本目录组织在 upgrade-version-command 下; - 逐工作区逻辑用
@RegisteredWorkspaceCommand编写 workspace 命令; - 复用 SQL 辅助逻辑应参照 migrations/utils/ 中仍被活跃命令 import 的工具函数,而不是向 legacy 目录添加新工具;
- 理解数据源行为时记住
migrationsRun: false:TypeORM 不会自动执行任何东西,所有升级都由显式的 command 入口(run-instance-commands、upgrade等)驱动,这也是"冻结迁移系统"在运行期真正生效的机制。
这一套治理让 Twenty 的升级流程从"TypeORM 黑盒重放"演进为"按版本、按阶段、可断点续跑、支持 dry-run 的显式命令流水线",而 legacy 目录则是这次演进留下的、被有意保留的历史账本。
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 StartedRust0627
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