Migrationsalpha
Change the type of an actor's stored data between versions, and check an upgrade before you deploy it.
An upgrade keeps every variable in an actor. If the new version stores a variable with a type the old value doesn't fit, the upgrade is refused. This page shows how to check for that before deploying, and how to write a migration that converts the old data.
The examples run against moxzid, but the rules are Motoko's. They apply in the same way on the Internet Computer, and moxzi reports the same error codes as moc.
Which changes need a migration#
| Change to a stable variable | Without a migration |
|---|---|
| Add a new variable with an initial value | Allowed |
Change its type to a supertype, such as Nat to Int | Allowed |
| Add a field to a record, even an optional one | Refused, M0170 |
| Remove a field from a record | Refused, M0216: the field's data would be lost |
| Remove the variable | Refused, M0169 |
| Change it to an unrelated type | Refused, M0170 |
For example, removing note from an actor:
[M0169] stable variable `note` of the previous version cannot be implicitly discarded. The variable can only be dropped by an explicit migration function.
See https://docs.internetcomputer.org/languages/motoko/fundamentals/actors/compatibility/#explicit-migration-using-a-migration-function
Error: 1 incompatibility/ies: this upgrade would be rejected
A failed upgrade#
Version 1 stores member names as text:
import Array "mo:core/Array";
actor {
var members : [Text] = [];
public func join(name : Text) : async () {
members := Array.concat(members, [name]);
};
public query func list() : async [Text] {
members
};
};
cp v1/Main.mo Main.mo
moxzi build Main.mo -o members.wasm --stable-types
cp members.most v1.most
--stable-types also writes members.most, the actor's stable signature. Keep a copy of the signature of each version you deploy; here it's saved as v1.most.
curl -s -w '\n' -d '["ada"]' http://127.0.0.1:7000/call-json/members/join
curl -s -w '\n' -d '["linus"]' http://127.0.0.1:7000/call-json/members/join
curl -s -w '\n' -d '[]' http://127.0.0.1:7000/query-json/members/list
[]
[]
[["ada","linus"]]
Version 2 stores a record per member instead:
import Array "mo:core/Array";
actor {
type Member = { name : Text; admin : Bool };
var members : [Member] = [];
public func join(name : Text) : async () {
members := Array.concat(members, [{ name; admin = false }]);
};
public query func list() : async [Member] {
members
};
};
[Text] can't be read as [Member], so the upgrade is refused and version 1 keeps running:
cp v2/Main.mo Main.mo
moxzi build Main.mo -o members.wasm --stable-types
curl -s --data-binary @members.wasm http://127.0.0.1:7000/upgrade/members
Main.mo -> members.wasm + members.did (250185 bytes, 9 file(s), 114 steps, 1.60s)
compiled by ~/.local/lib/moxzi/compiler.wasm (sha256 9294817c6e9069cbd0e712d0e9dde0ec1b6893d0e7b42d798fa3dd4c8cd772cb), moxzi 0.1.0-alpha.10
upgrade refused: the new stable layout cannot read the persisted state.
[M0170] stable variable `members` is not compatible with the previous version.
Previous type
[Text]
is not a subtype of
[Member__925905266].
Write an explicit migration function to convert it.
See https://docs.internetcomputer.org/languages/motoko/fundamentals/actors/compatibility/#explicit-migration-using-a-migration-function
Error: 1 incompatibility/ies: this upgrade would be rejected
Repeat with /upgrade/<actor>?force=true to skip this pre-flight check; the runtime's own memory-compatibility check still has the final word.
Check an upgrade before deploying#
moxzi stable-compatible compares two signatures without a server or a canister. It exits with 0 if the upgrade is safe, and prints the reasons if it isn't:
moxzi stable-compatible v1.most members.most || echo "exit code $?"
[M0170] stable variable `members` is not compatible with the previous version.
Previous type
[Text]
is not a subtype of
[Member__925905266].
Write an explicit migration function to convert it.
See https://docs.internetcomputer.org/languages/motoko/fundamentals/actors/compatibility/#explicit-migration-using-a-migration-function
Error: 1 incompatibility/ies: this upgrade would be rejected
exit code 1
For a canister that's already on the Internet Computer, get the deployed signature with dfx canister metadata CANISTER_ID motoko:stable-types > deployed.most.
| Code | Meaning |
|---|---|
M0169 | A stable variable was removed |
M0170 | A stable variable's type changed incompatibly |
M0216 | The types are compatible but the upgrade would still lose data |
M0255 | The actor mixes the two migration styles below |
Write a migration function#
A migration function takes the old variables and returns the new ones. Put it in its own module:
import Array "mo:core/Array";
module {
type Member = { name : Text; admin : Bool };
public func run(old : { var members : [Text] }) : { var members : [Member] } {
{
var members = Array.map<Text, Member>(
old.members,
func(name) { { name; admin = name == "ada" } },
);
};
};
};
Attach it to the actor with (with migration = …):
import Array "mo:core/Array";
import Migration "Migration";
(with migration = Migration.run)
actor {
type Member = { name : Text; admin : Bool };
var members : [Member] = [];
public func join(name : Text) : async () {
members := Array.concat(members, [{ name; admin = false }]);
};
public query func list() : async [Member] {
members
};
};
The function runs once, during the upgrade, before the new code starts. Its parameter lists the old variables it reads, and its result lists the new variables it produces. Use var fields for var variables. Variables it doesn't mention are kept as they are.
cp v3/Main.mo v3/Migration.mo .
moxzi build Main.mo -o members.wasm --stable-types
moxzi stable-compatible v1.most members.most
curl -s --data-binary @members.wasm http://127.0.0.1:7000/upgrade/members
curl -s -w '\n' -d '[]' http://127.0.0.1:7000/query-json/members/list
Main.mo -> members.wasm + members.did (253357 bytes, 10 file(s), 116 steps, 1.63s)
compiled by ~/.local/lib/moxzi/compiler.wasm (sha256 9294817c6e9069cbd0e712d0e9dde0ec1b6893d0e7b42d798fa3dd4c8cd772cb), moxzi 0.1.0-alpha.10
compatible: upgrading from v1.most to members.most keeps every stable variable
upgraded
[[{"admin":true,"name":"ada"},{"admin":false,"name":"linus"}]]
Remove (with migration = …) before the next upgrade. The function's parameter describes version 1's data, so if you leave it attached, the next upgrade fails and the current version keeps running:
durable upgrade failed; restored prior generation: the upgrade failed; the previous version is still running: canister_post_upgrade trapped: RTS error: Memory-incompatible program upgrade
Migration chains#
If you upgrade often, keep migrations as a directory of files instead. Each deployed version records the last migration it applied, and an upgrade runs only the newer ones. A fresh install runs all of them.
Build with --enhanced-migration DIR. Each file in the directory exports a migration function, and the files run in name order:
module {
public func migration(_ : {}) : { var count : Nat; var name : Text } {
{ var count = 0; var name = "counter" };
};
};
With --enhanced-migration, stable variables have no initial value: their values come from the chain.
actor {
var count : Nat;
var name : Text;
public func inc() : async Nat {
count += 1;
count
};
public query func describe() : async Text {
name # ": " # debug_show count
};
};
moxzi build Main.mo --enhanced-migration migrations -o app.wasm
Main.mo -> app.wasm + app.did (250152 bytes, 2 file(s), 57 steps, 0.58s)
compiled by ~/.local/lib/moxzi/compiler.wasm (sha256 9294817c6e9069cbd0e712d0e9dde0ec1b6893d0e7b42d798fa3dd4c8cd772cb), moxzi 0.1.0-alpha.10
curl -s -w '\n' -d '[]' http://127.0.0.1:7000/call-json/app/inc
curl -s -w '\n' -d '[]' http://127.0.0.1:7000/query-json/app/describe
[1]
["counter: 1"]
To rename name to title, add a second migration and update the actor:
module {
public func migration(old : { var name : Text }) : { var title : Text } {
{ var title = "The " # old.name };
};
};
actor {
var count : Nat;
var title : Text;
public func inc() : async Nat {
count += 1;
count
};
public query func describe() : async Text {
title # ": " # debug_show count
};
};
count isn't mentioned by 002-title.mo, so it passes through unchanged.
Before building, save the signature of the version that's deployed. With --enhanced-migration, the build writes it to app.most automatically:
cp app.most deployed.most
Then build against it with --stable-baseline. The compiler checks that the chain turns the deployed variables into the new ones, and fails the build if it doesn't:
moxzi build Main.mo --enhanced-migration migrations --stable-baseline deployed.most -o app.wasm
curl -s --data-binary @app.wasm http://127.0.0.1:7000/upgrade/app
curl -s -w '\n' -d '[]' http://127.0.0.1:7000/query-json/app/describe
Main.mo -> app.wasm + app.did (253122 bytes, 3 file(s), 57 steps, 0.84s)
compiled by ~/.local/lib/moxzi/compiler.wasm (sha256 9294817c6e9069cbd0e712d0e9dde0ec1b6893d0e7b42d798fa3dd4c8cd772cb), moxzi 0.1.0-alpha.10
upgraded
["The counter: 1"]
Rules for chains:
| Rule | Error if broken |
|---|---|
| Stable variables have no initial value | M0250 |
| No side effects in the actor body; state comes from the chain | |
| Every stable variable is produced by a migration or carried from the deployed version | M0267 with --stable-baseline, a warning (M0254) without |
Transient variables (transient let, transient var) are not affected and can have initial values.
Which to use#
Use a single migration function for an occasional change to a canister you upgrade by hand. Use a chain when upgrades are automated or frequent: the directory is the history, each deployment knows where it is in it, and --stable-baseline checks every build against what's deployed.