关于 convex-migrations
为 Convex 项目提供安全的数据迁移指导,涵盖 schema 变更与分批回填。适合需要调整既有文档结构并保持部署可通过验证的开发者。
主要功能
- 01六步迁移与部署顺序
- 02分批且可重复运行的回填
- 03迁移组件进度与状态管理
- 04试运行、取消及顺序执行
- 05常见 schema 验证错误排查
适用场景
- 01为已有表添加必填字段
- 02重命名字段或转换字段类型
- 03拆分或合并数据表
- 04处理超出单次 mutation 限制的回填
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ waynesutton/ builder-skills --skill convex-migrations安装后在 Claude Code 中输入/convex-migrations确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/small-logo.svgreferences/migration-patterns.mdmigrations-component.md详细介绍
Convex 迁移
生成一个架构变更和分批回填,确保每次部署都能顺利完成。唯一的原则:架构必须描述当前实际存在的文档,而不是你期望存在的文档。先变更数据,再收紧验证器。
Convex 没有迁移文件或 migrate 命令。npx convex dev 会推送架构,并根据架构验证每一份现有文档。现有数据不会自动转换。
何时需要使用
- 添加一个应设为必填的字段,但表中已经有数据
- 重命名字段、更改字段类型或删除字段
- 将一张表拆分为两张,或将两张表合并为一张
- 修改架构后,
npx convex dev报错Schema validation failed - 回填需要处理的行数超过单个 mutation 的处理能力
分步操作示例(重命名、更改类型、拆分、合并、添加必填字段、删除字段),请打开 references/migration-patterns.md。有关安装和运行 @convex-dev/migrations,请打开 references/migrations-component.md。
安全的操作顺序
每次迁移都遵循相同的六个步骤。跳过任何一步都可能导致部署失败。
- 在
convex/schema.ts中将字段设为可选(或将新字段添加为v.optional)。 - 使用
npx convex dev部署。更新读取逻辑以处理undefined,并更新写入逻辑以设置新的数据形状。 - 使用内部 mutation 或迁移组件,分批回填现有文档。
- 将验证器改为最终形状(从
v.optional(v.string())改为v.string(),或删除旧字段)。 - 再次部署。此时每份文档都已符合要求,因此验证会通过。
- 清理:删除回退代码、移除回填函数、删除过期索引。
步骤 1 和 2 必须先于步骤 3 部署。否则,回填运行时,新写入仍会生成旧的数据形状。
手动编写分批回填
编写一个 internalMutation,使用 paginate 分页遍历表,只修补仍需处理的文档,然后使用继续游标重新安排自身执行。每个批次都是独立事务,因此大型表不会触及单个函数的限制。
// convex/migrations.ts
import { internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
const BATCH_SIZE = 100;
export const backfillAvatarUrl = internalMutation({
args: { cursor: v.union(v.string(), v.null()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("users")
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor });
for (const user of result.page) {
// Idempotent: skip documents that already have the field
if (user.avatarUrl === undefined) {
await ctx.db.patch(user._id, {
avatarUrl: defaultAvatar(user.name),
});
}
}
// Reschedule with the cursor until the table is exhausted
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.backfillAvatarUrl, {
cursor: result.continueCursor,
});
}
return null;
},
});
function defaultAvatar(name: string): string {
return `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(name)}`;
}
使用 CLI 针对开发部署启动:
npx convex run migrations:backfillAvatarUrl '{"cursor": null}'
确保 mutation 是幂等的。部分失败后重新运行必须安全。使用 internalMutation,绝不要使用公开的 mutation,这样就无法从客户端调用。使用 ctx.db.patch 添加或更改字段,使用 ctx.db.patch(id, { field: undefined }) 删除字段。
何时改用组件
@convex-dev/migrations 封装了上述模式,并补充了手动编写版本所缺少的功能:按迁移持久化进度、状态检查、试运行、取消,以及按顺序运行多个迁移。遇到以下情况时,请使用它:
- 项目在生命周期内会运行不止一两个迁移
- 你需要确认某个迁移是否已在生产环境完成
- 你希望在处理真实数据前先试运行
- 多个迁移必须按固定顺序运行
最简设置:
// convex/convex.config.ts
import { defineApp } from "convex/server";
import migrations from "@convex-dev/migrations/convex.config.js";
const app = defineApp();
app.use(migrations);
export default app;
// convex/migrations.ts
import { Migrations } from "@convex-dev/migrations";
import { components } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";
export const migrations = new Migrations<DataModel>(components.migrations);
export const addDefaultRole = migrations.define({
table: "users",
migrateOne: async (ctx, user) => {
if (user.role === undefined) {
await ctx.db.patch(user._id, { role: "user" });
}
},
});
npx convex run migrations:addDefaultRole '{"dryRun": true}'
npx convex run migrations:addDefaultRole
小型表的一次性回填不需要使用组件。手动编写的 mutation 就够了。
解读部署时的架构错误
架构推送失败时,npx convex dev 会输出表名、一个出错文档的 ID,以及不匹配的字段:
Schema validation failed
Document with ID "j57abc..." in table "users" does not match the schema:
Object is missing the required field `avatarUrl`.
Consider wrapping the field validator in `v.optional(...)` if this is expected.
可以这样理解:users 中的现有数据早于该字段。解决方法绝不是删除文档。将字段包装在 v.optional 中,部署、回填,然后移除 v.optional。如果错误提示字段类型不正确(例如,预期为 number,实际却是 string),请将验证器放宽为 v.union(v.string(), v.number()),回填转换后的数据,然后再收窄验证器。
不要向 defineSchema 传入 { schemaValidation: false } 来让推送成功。这样会掩盖不匹配问题,并让错误转移到查询中。
常见错误
| 错误 | 问题原因 | 正确做法 |
|---|---|---|
| 在一次部署中添加必填字段 | 现有文档无法通过验证,推送会被拒绝 | 先将其设为 v.optional,回填数据,然后再设为必填 |
| 在部署新的写入逻辑之前开始回填 | 新行仍会以旧格式写入 | 先部署架构和代码,再进行回填 |
使用一个会对整张表执行 .collect() 的 mutation | 会触及事务大小和时间限制 | 使用 paginate 分页,并按批次重新调度 |
使用公开的 mutation 回填 | 任何人都可以调用并重新运行 | 使用 internalMutation 或 migrations.define |
| 补丁操作不具备幂等性 | 失败后重新运行会重复应用更改 | 补丁前先检查字段 |
| 在读取方停止使用旧字段之前将其删除 | 回退代码会读取到 undefined | 先切换读取逻辑,再删除旧字段 |
使用 .filter() 查找尚未迁移的行 | 每批都会执行全表扫描 | 对整张表分页,或使用带索引的 customRange |
为了通过部署而关闭 schemaValidation | 查询在运行时会遇到错误数据 | 修复数据,并保持验证开启 |
| 首先对生产环境运行回填 | 无法及时发现转换错误 | 先在开发环境运行,再使用 --prod |
检查清单
- 新增或变更的字段在首次部署时为
v.optional(或放宽后的v.union) - 回填开始前,读取方已处理
undefined,写入方已生成新格式 - 回填使用
internalMutation或migrations.define,绝不公开 - 回填使用
paginate分页,并通过ctx.scheduler.runAfter(0, internal....)重新调度 - 每次补丁操作都有保护条件,确保重新运行安全
- 在处理生产环境之前,已在开发环境完成回填
- 验证器已切换为最终格式,并已成功部署,没有验证错误
- 已移除回退代码、旧字段和回填函数
- 已删除或重命名引用旧字段的索引