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. 02在 canister 升级时重构状态
  3. 03迁移集合中记录的数据结构

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/migrating-motoko-enhanced确认已加载。

详细介绍

增强型多迁移

通过一系列迁移模块管理 canister 状态的演变。每个迁移对应一项逻辑变更(添加、重命名、删除或转换字段),编译器会验证整个迁移链的一致性。

适用场景

  • 添加、删除或重命名持久化 actor 字段
  • 更改字段类型
  • 在 canister 升级期间重组状态
  • 项目已在 mops.toml 中配置 [canisters.<name>.migrations]

重要规则

  • 绝不要使用 stable 关键字、preupgrade/postupgrade 或内联 (with migration = ...)
  • 声明 actor 变量时不带初始化器——值来自迁移链
  • actor 主体必须是静态的(除 <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 语法

使用增强型迁移时,actor 变量不带初始化器:

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;
  };
};

迁移模块结构

每个迁移模块接收一个包含输入字段的 record,并返回一个包含输出字段的 record:

// 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}。actor 必须声明与此最终状态兼容的字段。

生命周期示例:Todo 应用

展示如何在四次部署中组合这些模式。

// 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 = ...) 结合使用
  • 需要启用增强型正交持久化
  • Actor 变量不得带有初始化器
  • Actor 主体必须是静态的(除 <system> 调用外,不得有顶层副作用)
  • 每次迁移后的状态必须与下一次迁移的输入兼容
  • 最终状态必须与 Actor 声明的字段匹配
  • 如果最后一次迁移的输出中包含 Actor 未声明的字段,则会被拒绝

检查清单

  • migrations/ 目录与 Actor 源文件位于同一目录下
  • 第一个迁移初始化所有字段(使用空输入的 Init.mo)
  • 文件名带有时间戳前缀,以确保顺序正确
  • 每个文件都导出 public func migration({...}) : {...}
  • Actor 变量声明时不带初始化器
  • 在 mops.toml 中配置 [canisters.<name>.migrations](mops 会注入 --enhanced-migration)
  • 运行 mops check --fix 验证迁移链的一致性
  • 运行 mops build 进行编译

其他参考资料

  • 加载 motoko,查看 Motoko 语言概述和 mo:core API
  • 加载 migrating-motoko,了解不使用 --enhanced-migration 的内联迁移
  • 加载 mops-cli,了解 mops check、mops build 和工具链配置

评论

支持 Markdown