mcpField
M
migrating-motoko-enhanced

migrating-motoko-enhanced

github.com/dfinity開発3974 回インストール今日更新
リポジトリ

migrating-motoko-enhanced について

Motoko 開発者向けに、拡張マイグレーションモジュールの連鎖で canister の状態変更を管理するスキルです。コンパイラーが各段階の互換性と最終状態を検証します。

主な機能

  1. 01マイグレーションモジュールの順次連結
  2. 02フィールドの追加・削除・名前変更
  3. 03フィールド型とデータ構造の変換
  4. 04互換性のコンパイラー検証
  5. 05アップグレード時に未適用分のみ実行

活用シーン

  1. 01永続 Actor フィールドの追加や削除
  2. 02canister アップグレード時の状態再構成
  3. 03コレクション内レコード構造の移行

インストール

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

$npx skills add https://github.com/dfinity/icskills --skill migrating-motoko-enhanced

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

詳細

拡張マルチマイグレーション

マイグレーションモジュールのチェーンを通じて、Canister の状態の進化を管理します。各マイグレーションでは 1 つの論理的な変更(フィールドの追加、名前変更、削除、変換)を行い、コンパイラーがチェーン全体の整合性を検証します。

使用する場面

  • 永続化されるアクターのフィールドを追加、削除、または名前変更する場合
  • フィールドの型を変更する場合
  • Canister のアップグレードに伴って状態を再構成する場合
  • プロジェクトの mops.toml で [canisters.<name>.migrations] が設定されている場合

重要なルール

  • stable キーワード、preupgrade/postupgrade、またはインラインの (with migration = ...) は使用しないでください
  • アクター変数は初期化子なしで宣言します。値はマイグレーションチェーンから取得されます
  • アクター本体は静的である必要があります(タイマーのような <system> 呼び出しを除き、トップレベルで副作用を起こさないでください)
  • 各マイグレーションファイルは public func migration({...}) : {...} をエクスポートします
  • ファイルは辞書順で適用されます。タイムスタンプのプレフィックスを使用してください

ディレクトリ構成

backend/
├── main.mo
├── types.mo
├── lib/
├── mixins/
└── migrations/
    ├── 20250101_000000_Init.mo
    ├── 20250315_120000_AddProfile.mo
    └── 20250601_090000_RenameField.mo

アクターの構文

拡張マイグレーションでは、アクター変数に初期化子を指定しません。

actor {
  var name : Text;       // value comes from migration chain
  var balance : Nat;     // likewise
  let frozen : Bool;     // let bindings can also be uninitialized

  public func greet() : async Text {
    "Hello, " # name # "! Balance: " # debug_show balance;
  };
};

マイグレーションモジュールの構造

各マイグレーションモジュールは、入力フィールドのレコードを受け取り、出力フィールドのレコードを返します。

// migrations/20250101_000000_Init.mo
module {
  public func migration(_ : {}) : { name : Text; balance : Nat } {
    { name = ""; balance = 0 }
  }
}

入力/出力フィールドの意味

フィールドの出現箇所効果
入力と出力の両方フィールドが変換されます(古い値を読み取り、新しい値を生成します)
出力のみ状態に新しいフィールドが追加されます
入力のみフィールドが消費され、状態から削除されます
どちらにもないフィールドは変更されず、そのまま引き継がれます

状態が {a : Nat; b : Text; c : Bool} で、マイグレーションが次の場合:

module {
  public func migration(old : { a : Nat; b : Text }) : { a : Int; d : Float } {
    { a = old.a; d = 1.0 }
  }
}
  • a:Nat → Int に変換されます
  • b:消費されます(削除されます)
  • c:変更せず引き継がれます
  • d:新しく導入されます
  • 結果:{a : Int; c : Bool; d : Float}

よく使われるパターン

状態を初期化する(最初のマイグレーションで必須)
// migrations/20250101_000000_Init.mo
module {
  public func migration(_ : {}) : { count : Nat; header : Text } {
    { count = 0; header = "default" }
  }
}
フィールドを追加する
// migrations/20250201_000000_AddEmail.mo
module {
  public func migration(_ : {}) : { email : Text } {
    { email = "" }
  }
}
オプショナルフィールドを追加する
module {
  public func migration(_ : {}) : { assignee : ?Principal } {
    { assignee = null }
  }
}
フィールドの型を変更する
// migrations/20250301_000000_CountToInt.mo
module {
  public func migration(old : { count : Nat }) : { count : Int } {
    { count = old.count }
  }
}
フィールドの名前を変更する
// migrations/20250401_000000_RenameHeader.mo
module {
  public func migration(old : { header : Text }) : { title : Text } {
    { title = old.header }
  }
}
フィールドを削除する
// migrations/20250501_000000_DropEmail.mo
module {
  public func migration(_ : { email : Text }) : {} {
    {}
  }
}
データを変換する(フィールドを分割する)
// migrations/20250601_000000_SplitName.mo
import Text "mo:core/Text";

module {
  public func migration(old : { name : Text }) : { firstName : Text; lastName : Text } {
    let parts = old.name.split(#char ' ');
    let first = switch (parts.next()) { case (?f) f; case (null) "" };
    let last = switch (parts.next()) { case (?l) l; case (null) "" };
    { firstName = first; lastName = last }
  }
}
Bool をバリアントに変換する
module {
  public func migration(old : { var completed : Bool }) : { var status : { #pending; #completed } } {
    { var status = if (old.completed) { #completed } else { #pending } }
  }
}
コレクションの各要素に処理を適用する
import Map "mo:core/Map";

module {
  type OldTask = { id : Nat; title : Text; var completed : Bool };
  type NewTask = { id : Nat; title : Text; var status : { #pending; #completed } };

  public func migration(old : { var tasks : Map.Map<Nat, OldTask> })
    : { var tasks : Map.Map<Nat, NewTask> } {
    let tasks = old.tasks.map<Nat, OldTask, NewTask>(
      func(_, task) {
        {
          id = task.id;
          title = task.title;
          var status = if (task.completed) { #completed } else { #pending };
        }
      }
    );
    { var tasks }
  }
}
Map 内の各レコードにフィールドを追加する
import Map "mo:core/Map";

module {
  type OldUser = { name : Text; email : Text };
  type NewUser = { name : Text; email : Text; bio : Text };

  public func migration(old : { users : Map.Map<Nat, OldUser> })
    : { users : Map.Map<Nat, NewUser> } {
    let users = old.users.map<Nat, OldUser, NewUser>(
      func(_, u) { { u with bio = "" } }
    );
    { users }
  }
}

マイグレーションの合成方法

マイグレーションはチェーンを形成します。コンパイラーは、各マイグレーションの入力が、先行するすべてのマイグレーションによって生成された状態と互換性があることを検証します。

マイグレーション入力出力効果
Init{}{name : Text; balance : Nat}両方のフィールドを初期化します
AddProfile{}{profile : Text}新しいフィールドを追加します
RenameField{name : Text}{displayName : Text}name の名前を displayName に変更します

チェーン全体の適用後:{displayName : Text; balance : Nat; profile : Text}。アクターでは、この最終状態と互換性のあるフィールドを宣言する必要があります。

ライフサイクルの例:Todo アプリ

4 回のデプロイにわたって、各パターンがどのように組み合わさるかを示します。

// migrations/20250101_000000_Init.mo
module {
  public func migration(_ : {}) : { var nextId : Nat } {
    { var nextId = 0 }
  }
}
// migrations/20250201_000000_AddTasks.mo
import Map "mo:core/Map";
module {
  type Task = { id : Nat; text : Text; completed : Bool };
  public func migration(_ : {}) : { tasks : Map.Map<Nat, Task> } {
    { tasks = Map.empty<Nat, Task>() }
  }
}
// migrations/20250301_000000_TaskStatus.mo — transform Bool → variant
import Map "mo:core/Map";
module {
  type OldTask = { id : Nat; text : Text; completed : Bool };
  type NewTask = { id : Nat; text : Text; status : { #pending; #inProgress; #completed } };
  public func migration(old : { tasks : Map.Map<Nat, OldTask> })
    : { tasks : Map.Map<Nat, NewTask> } {
    let tasks = old.tasks.map<Nat, OldTask, NewTask>(
      func(_, task) {
        { id = task.id; text = task.text;
          status = if (task.completed) #completed else #pending }
      }
    );
    { tasks }
  }
}
// migrations/20250401_000000_AddDueDate.mo — add field to each record
import Map "mo:core/Map";
module {
  type Status = { #pending; #inProgress; #completed };
  type OldTask = { id : Nat; text : Text; status : Status };
  type NewTask = { id : Nat; text : Text; status : Status; due : Int };
  public func migration(old : { tasks : Map.Map<Nat, OldTask> })
    : { tasks : Map.Map<Nat, NewTask> } {
    let tasks = old.tasks.map<Nat, OldTask, NewTask>(
      func(_, task) { { task with due = 0 } }
    );
    { tasks }
  }
}

最終状態:{ var nextId : Nat; tasks : Map.Map<Nat, { id : Nat; text : Text; status : { #pending; #inProgress; #completed }; due : Int }> }

実行時の動作

  • 新規デプロイ時:すべてのマイグレーションが順番に実行されます
  • アップグレード時:未適用のマイグレーションのみ実行されます(適用済みのマイグレーションはスキップされます)
  • ファストフォワード:中間のデプロイを安全にスキップできます。未適用のマイグレーションはすべて順番に実行されます
  • マイグレーションがトラップすると、アップグレードは中止され、Canister は旧バージョンのままになります

mops.toml の設定

[moc]
args = ["--default-persistent-actors"]

[canisters.backend]
main = "src/backend/main.mo"

[canisters.backend.migrations]
chain = "src/backend/migrations"

[canisters.<name>.migrations] を設定すると、mops は check/build/check-stable に --enhanced-migration を自動的に挿入します。--enhanced-migration を [canisters.<name>].args に追加しないでください。mops でエラーが発生します。

--enhanced-orthogonal-persistence はデフォルトで有効です。

その後は、通常どおり mops check --fix と mops build を実行できます。新しいマイグレーションファイルは、タイムスタンプのプレフィックスを付けて migrations/ の直下に追加します。

制限事項

  • --enhanced-migration とインラインの (with migration = ...) は併用できません
  • 拡張直交永続化が必要です
  • アクターの変数に初期化子を指定することはできません
  • アクター本体は静的である必要があります(トップレベルの副作用は <system> 呼び出しのみ許可されます)
  • 各マイグレーション後の状態は、次のマイグレーションの入力と互換性がある必要があります
  • 最終状態は、アクターで宣言されたフィールドと一致する必要があります
  • 最後のマイグレーションの出力に含まれる、アクターで宣言されていないフィールドは拒否されます

チェックリスト

  • アクターのソースと同じ場所に migrations/ ディレクトリがある
  • 最初のマイグレーションで、すべてのフィールドを初期化する(入力が空の Init.mo)
  • 正しい順序になるよう、ファイル名にタイムスタンプのプレフィックスを付ける
  • 各ファイルで public func migration({...}) : {...} をエクスポートする
  • アクターの変数を初期化子なしで宣言する
  • mops.toml で [canisters.<name>.migrations] を設定する(mops が --enhanced-migration を挿入します)
  • mops check --fix を実行して、チェーンの整合性を確認する
  • mops build を実行してコンパイルする

参考資料

  • Motoko 言語全般と mo:core API のリファレンスには motoko を読み込みます
  • --enhanced-migration を使わないインラインマイグレーションについては migrating-motoko を読み込みます
  • mops check、mops build、ツールチェーンの設定については mops-cli を読み込みます

コメント