New Motoko Migration Pattern

I was discussing with @icme about the new migration pattern and my desire to jump in and migrate(:drum:) away from the old migration pattern that @ZhenyaUsenko created that has been pretty dependable. Here is an example of the new migration in ninja: ICP Ninja

A couple things popped up and I wanted to get some insight on how to handle them.

In my migration pattern, in the past, I’ve used caller, canisterId, upgrade args, and previous state to run a migration for various reasons. Examples of each:

  1. Canister ID - making sure a call is a self call
  2. Caller - setting admin permissions
  3. Upgrade Args - pushing some new data in that needs to go into these new state variable
likely avoidable if it is your canister, but if this is a canister template you’d want to pass particular stuff in.
  4. Previous state - obviously.

It looks like I only get number 4 in this new pattern. What are the prospects of getting ther rest of the info.

1 Like

The current migration pattern has the following signature

// A is the old state & fields (if declared in B is kept)
// B is the new state & fields
func<A, B>(old_state : A) : B // B

I’m curious why the old state wasn’t declared as a record, like this

func<A, B>({ state : A }) : B

Then it would be easier to extend with additional optional fields that one can pick out, similar to how the caller is parseable in a canister API

func<A, B, ...>({
  state : A;
  caller : Principal;
  initArgs : ...
}) : B

Now that we have migrations, it might be helpful to have a primitive allowing a canister to infer and assign the type of all stable variables in a canister, and hold the canister to implement that collection of stable variables type.

For example, if I have the following

persistent actor Test {
  let a = ...;
  let b = ...;
  ...
  let z = ...;
}

and then I want to do a migration on it, is there a way for me to pass type T, which is essentially the full typed record of stable variables a-z? Otherwise, I need to explicitly pass in every input to the migration, and all inputs must be included in the output of the migration or they are considered dropped.

Then you’d get something like this, but where you’d like to omit old parts of the state from the new record when copying things over.

type A = { a : Int; counter : Nat };
type B = { b : Float; counter : Nat };

func<A, B>({ state : A }) : B {
  { state with b = Float.fromInt(state.a) }
}

Exploring this was how I started running into the edge case errors mentioned here Pre-release `moc` 0.14.0 available in Playground - #18 by icme

Just to clarify, the constraint on the migration function is that the both the output and input are record types. You can uses either a single argument of record type or a pattern if you want to bind record fields individually.

Also, the migration function doesn’t have to take all the stable variables of the old actor, nor produce all the stable variables of the new actor. Instead, you can be more selective. Any other stable variables that are declared in the old and new just come along as usual. The intention was that, in the common case, most stable variables stay unchanged and only a few need special treatment (deletion/renaming/change of representation).

So (barring typos, haven’t checked this):

Migrating from old:

persistent actor {
   var a : Int = 1;
   var counter : Nat = 1
}

to new:

(migration = func({a : Int}) : {b:Float} {
  { b = Float.fromInt(a) } }) 
} persistent actor {
   var b : Float = 0;
   var counter : Nat = 0;
}

Should be fine, and leave 1.0 and 1 in b and counter. The 0.0 and 0 initializers won’t be evaluated, but are there in case you do a fresh install of the second version.

Note that the migration function didn’t mention counter because no special action is required. In this case, it’s preserved across the upgrade. If the old version hadn’t declared it, it would be initialized to 0.
You can think of all fields in the input of the migration function being removed/consumed, and all fields in the result being used to initialize corresponding stable variables in the body of the actor (skipping their initializers in the body).

So here a is removed and its value used to initialize b, and counter just gets preserved
or initialized as usual.

1 Like

Claudio,

This is a great feature, but the original question(maybe you were just addressing @icme 's comments?) were about external data that is needed out side of state. By all practical usage, a migration often requires data from outside the state for the new state(say a default value for a new property “admin”). If one controls the source then you can hard code it, but if you are making something generic that people will be deploying without touching then source(or maybe broadly deploying across a swarm of already deployed canister with different configurations, controllers and owners) you likely need to pass that default in or set it something outside the state.

The thing’s that I’ve used to do this in the past are listed above, but namely we need access to the init args, the caller, and the canisterId. (Perhaps adding canister and message specifics to experimentalInternetComputer would be a stop gap if I could get to EIC.currentMessage.caller. EIC.controllers, EIC.subnet, EIC.settings, etc.)
But I still think you need init args for actor classes.(I almost never use just a raw actor
always actor classes).

I was indeed only addressing @icme’s concerns.

You can in fact apply a migration function to an actor class, but you are correct that the migration function won’t be able to access caller or constructor argument, though the body of the class will have the same access as before.

It would be interesting to see real examples where this is too limiting. One alternative would have been to make the migration function a new system method, in the body of the actor (class), but we decided against that design in the end.