Developers Home»How-to Guide»Basic Storage Migration

Basic Storage Migration

Goal

Write a storage migration for a pallet that adds an additional Vec<u8> storage item to runtime storage.

Use Cases

A pallet that adds a single storage item and needs to be included in a runtime upgrade.

Overview

This guide will step through a storage migration on FRAME's Nick's pallet. It shows how to modify a storage map to provide an optional field that includes a last name, and how to write the migration function ready to be triggered upon a runtime upgrade. This guide can equally be used in other contexts which require a simple storage migration that modifies a storage map in a runtime.

Steps

1. Create a storage struct and utility type

Write a struct to manage the previous and new storage items, first and last:

pub struct Nickname {
    first: Vec<u8>,
    last: Option<Vec<u8>>, // handles empty storage
}

Write a utility type enum to keep track of the storage versions:

#[derive(codec::Encode, codec::Decode, Clone, frame_support::RuntimeDebug, PartialEq)]
pub enum StorageVersion {
    V1Bytes,
    V2Struct,
}

2. Update your storage items

The Nicks pallet only keeps track of a lookup table in storage, but we also need to add PalletVersion to declare the current version in storage. To update these items, use the Nickname struct in the NameOf item and add the new storage item PalletVersion:

decl_storage! {
    trait Store for Module<T: Trait> as MyNicks {
        /// The lookup table for names.
        NameOf: map hasher(twox_64_concat) T::AccountId => Option<(Nickname, BalanceOf<T>)>;
        /// The current version of the pallet.
        PalletVersion: StorageVersion = StorageVersion::V1Bytes;
    }
}

3. Update all functions

All of the Nicks pallet functions need to account for the new last: Option<Vec<u8>> storage item. Update each function by adding it as a parameter, for example:

//--snip
fn force_name(origin,
    target: <T::Lookup as StaticLookup>::Source,
    first: Vec<u8>,
    last: Option<Vec<u8>>) {
//--snip
    }

In addition, update all storage writes with the Nickname struct:

<NameOf<T>>::insert(&sender, (Nickname { first, last }, deposit));

4. Declare a migration module

The migration module should contain two parts:

  1. A module indicating the deprecated storage to migrate from.
  2. The migration function which returns a weight.

The scaffolding of this module looks like this:

pub mod migration {
  use super::*;

  pub mod v1 {...} // only contains V1 storage format

  pub fn migrate_to_v2<T: Config>() -> frame_support::weights::Weight {...} // contains checks and transforms storage to V2 format
}

5. Write migrate_to_v2

Here's an overview of what this function needs to do:

  • Check the storage version to make sure a migration is needed (good practice)
  • Transform the storage values into the new storage format
  • Update the storage version
  • Return the weight consumed by the migration

Check the storage version

Construct the migrate_to_v2 logic around the check. If the storage migration doesn't need to happen, return 0:

if PalletVersion::get() == StorageVersion::V1Bytes {

    // migrate to v2

} else {
    frame_support::debug::info!(" >>> Unused migration!");
    0
}

Transform storage values

Using the translate storage method, transform the storage values to the new format. Since the existing nick value in storage can be made of a string separated by a space, split it at the ' ' and place anything after that into the new last storage item. If it isn't, last takes the None value:

NameOf::<T>::translate::<(Vec<u8>, BalanceOf<T>), _>(
  |k: T::AccountId, (nick, deposit): (Vec<u8>, BalanceOf<T>)| {
    // We split the nick at ' ' (<space>).
    match nick.iter().rposition(|&x| x == b" "[0]) {
        Some(ndx) => Some((Nickname {
          first: nick[0..ndx].to_vec(),
                  last: Some(nick[ndx + 1..].to_vec())
          }, deposit)),
          None => Some((Nickname { first: nick, last: None }, deposit))
      }
        }
    );

Tip

Remove 'Option' wrapping to make sure decoding works properly.

Return the consumed weight

To do this, count the number of storage reads and writes and return the corresponding weight:

let count = NameOf::<T>::iter().count();
T::DbWeight::get().reads_writes(count as Weight + 1, count as Weight + 1)

Use migrate_to_v2 in on_runtime_upgrade

Go back to the pallet's functions and specify the migrate_to_v2 function in on_runtime_upgrade:

fn on_runtime_upgrade() -> frame_support::weights::Weight {
  migration::migrate_to_v2::<T>()
  }

6. Create a types.json file

Put the new storage types in a types.json which you will need to trigger the migration using a UI. Our new types in JSON are:

{
  "Nickname": {
    "first": "Vec<u8>",
    "last": "Option<Vec<u8>>"
  },
  "StorageVersion": {
    "_enum": [
      "V1Bytes",
      "V2Struct"
    ]
  }
}

Examples

Resources

Rust docs

Last edit: on

Run into problems?
Let us Know