convex-migrations について
Convex のデータ移行を安全に進めるためのスキルで、スキーマ変更と分割バックフィルを扱います。既存ドキュメントの形を変更し、デプロイ時の検証を保ちたい開発者向けです。
主な機能
- 016 段階の移行・デプロイ手順
- 02分割実行と再実行に対応したバックフィル
- 03移行コンポーネントによる進捗管理
- 04ドライラン、キャンセル、順序実行
- 05スキーマ検証エラーの対処案内
活用シーン
- 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で失敗する場合 - ひとつのミューテーションで処理できる件数を超える行をバックフィルする必要がある場合
名前の変更、型の変更、分割、統合、必須フィールドの追加、削除の手順については、references/migration-patterns.md を参照してください。@convex-dev/migrations のインストールと実行については、references/migrations-component.md を参照してください。
安全な手順
どのマイグレーションも、同じ6つの手順に従います。手順を飛ばすと、デプロイに問題が生じます。
convex/schema.tsでフィールドをオプションにします(または、新しいフィールドをv.optionalとして追加します)。npx convex devでデプロイします。リーダーを更新してundefinedを処理できるようにし、ライターを更新して新しい形式を設定するようにします。- 内部ミューテーションまたはマイグレーションコンポーネントを使い、既存のドキュメントをバッチでバックフィルします。
- バリデーターを最終的な形式に切り替えます(
v.optional(v.string())ではなくv.string()にするか、古いフィールドを削除します)。 - 再度デプロイします。すべてのドキュメントがすでに一致しているため、検証は成功します。
- 後片付けをします。フォールバックコードを削除し、バックフィル関数を取り除き、古いインデックスを削除します。
手順1と2は、手順3を開始する前にリリースする必要があります。そうしないと、バックフィルの実行中に新しい書き込みが古い形式のドキュメントを作り続けます。
手動で実装するバッチバックフィル
テーブルを paginate でページングするひとつの internalMutation を用意し、まだ変更が必要なドキュメントだけをパッチした後、続行カーソルを使って自身を再スケジュールします。各バッチは個別のトランザクションとなるため、大きなテーブルでも関数ごとの上限に達しません。
// 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}'
ミューテーションは冪等にしてください。部分的な失敗の後に再実行しても安全である必要があります。internalMutation を使い、クライアントから呼び出せる公開 mutation は決して使わないでください。フィールドの追加や変更には ctx.db.patch を使い、フィールドを削除するには ctx.db.patch(id, { field: undefined }) を使います。
コンポーネントを使う場合
@convex-dev/migrations は上記のパターンをラップし、手動実装では不足している機能を追加します。各マイグレーションの進捗の永続化、ステータスチェック、ドライラン、キャンセル、複数のマイグレーションの順次実行が可能です。次のような場合に利用してください。
- プロジェクトで、今後2回を超えるマイグレーションを実行する予定がある
- マイグレーションが本番環境ですでに完了しているか確認する必要がある
- 実際のデータに変更を加える前にドライランを実行したい
- 複数のマイグレーションを決まった順序で実行する必要がある
最小限のセットアップ:
// 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
小さなテーブルで一度だけ行うバックフィルに、このコンポーネントは必要ありません。手動で実装するミューテーションで十分です。
デプロイ時のスキーマエラーの読み方
スキーマのプッシュに失敗すると、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 } を渡してはいけません。不一致が隠され、クエリで失敗が発生するようになります。
よくある間違い
| 間違い | 問題が起きる理由 | 代わりに行うこと |
|---|---|---|
| 1回のデプロイで必須フィールドを追加する | 既存のドキュメントがバリデーションに失敗し、プッシュが拒否されます | v.optional として追加し、バックフィルしてから必須にします |
| 新しいライターをデプロイする前にバックフィルを開始する | 新しい行が古い形式のまま追加され続けます | 先にスキーマとコードをデプロイし、その後バックフィルします |
テーブル全体を .collect() する1つのミューテーションを使う | トランザクションのサイズ制限と時間制限に達します | paginate でページングし、バッチごとに再スケジュールします |
公開 mutation でバックフィルする | 誰でも呼び出せるため、再実行される可能性があります | internalMutation または migrations.define を使います |
| 冪等でないパッチを使う | 失敗後に再実行すると、二重に適用されます | パッチ適用前にフィールドを確認します |
| リーダーが使用をやめる前に古いフィールドを削除する | フォールバックコードが undefined を読み取ります | リーダーを切り替えてから削除します |
未移行の行を探すために .filter() を使う | バッチごとに全件スキャンが発生します | テーブル全体をページングするか、インデックスと customRange を使います |
デプロイを通すために schemaValidation を無効にする | 実行時に不正なデータがクエリに渡されます | データを修正し、バリデーションは有効のままにします |
| 最初に本番環境に対してバックフィルを実行する | 変換の誤りを検出する方法がありません | 開発環境で実行してから、--prod を使います |
チェックリスト
- 新規または変更したフィールドは、最初のデプロイでは
v.optional(または範囲を広げたv.union)になっている - リーダーが
undefinedに対応し、バックフィル開始前にライターが新しい形式を生成している - バックフィルは
internalMutationまたはmigrations.defineであり、公開されていない - バックフィルは
paginateでページングし、ctx.scheduler.runAfter(0, internal....)で再スケジュールされる - 各パッチにガードがあり、安全に再実行できる
- 本番環境に触れる前に、開発環境でバックフィルが完了している
- バリデーターを最終形に切り替え、バリデーションエラーなしでデプロイしている
- フォールバックコード、古いフィールド、バックフィル関数を削除している
- 古いフィールドを参照していたインデックスを削除または名前変更している