mcpField
C
convex-migrations

convex-migrations

github.com/waynesuttonデータ・データベース4052.6k 回インストール4 週間前に更新
リポジトリ

convex-migrations について

Convex のデータ移行を安全に進めるためのスキルで、スキーマ変更と分割バックフィルを扱います。既存ドキュメントの形を変更し、デプロイ時の検証を保ちたい開発者向けです。

主な機能

  1. 016 段階の移行・デプロイ手順
  2. 02分割実行と再実行に対応したバックフィル
  3. 03移行コンポーネントによる進捗管理
  4. 04ドライラン、キャンセル、順序実行
  5. 05スキーマ検証エラーの対処案内

活用シーン

  1. 01既存テーブルへの必須フィールド追加
  2. 02フィールド名や型の変更
  3. 03テーブルの分割・統合
  4. 04単一 mutation の上限を超えるバックフィル

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/waynesutton/builder-skills --skill convex-migrations

インストール後、Claude Code で/convex-migrationsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
assets/
small-logo.svg
references/
migration-patterns.md
migrations-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つの手順に従います。手順を飛ばすと、デプロイに問題が生じます。

  1. convex/schema.ts でフィールドをオプションにします(または、新しいフィールドを v.optional として追加します)。
  2. npx convex dev でデプロイします。リーダーを更新して undefined を処理できるようにし、ライターを更新して新しい形式を設定するようにします。
  3. 内部ミューテーションまたはマイグレーションコンポーネントを使い、既存のドキュメントをバッチでバックフィルします。
  4. バリデーターを最終的な形式に切り替えます(v.optional(v.string()) ではなく v.string() にするか、古いフィールドを削除します)。
  5. 再度デプロイします。すべてのドキュメントがすでに一致しているため、検証は成功します。
  6. 後片付けをします。フォールバックコードを削除し、バックフィル関数を取り除き、古いインデックスを削除します。

手順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....) で再スケジュールされる
  • 各パッチにガードがあり、安全に再実行できる
  • 本番環境に触れる前に、開発環境でバックフィルが完了している
  • バリデーターを最終形に切り替え、バリデーションエラーなしでデプロイしている
  • フォールバックコード、古いフィールド、バックフィル関数を削除している
  • 古いフィールドを参照していたインデックスを削除または名前変更している

ドキュメント

コメント