Writing mnesia data migrations in Elixir
Migrating data in Mnesia is traditionally a dark matter. Even if you understand the concept of translating tables, it’s still not clear how to organize your migrations so that they are picked up and run during the application start.
In this short post I share how I’m doing it. See if you have a better recipe. I’ll be using Elixir pieces here, but it’s pretty much the same for the Erlang projects.
Here’s how the table definition looks in Amnesia for this example:
use Amnesia
defdatabase Database do
deftable Users, [:name, :email, :login_count], type: :bag do
@type t :: %Users{name: String.t, email: String.t, login_count: integer}
end
end
In the database definition we lay out the current version of the table.
Previously, the table Users had name
and email
fields only. In the most recent
version we added login_count
field and now want to write a migration
that will update the database by adding the attribute to the table rows
and initialize it with 0
.
Here I put the migrations in the main application module, but you may want to put them in a separate module or even a namespace to organize stuff nicely.
defmodule MyApp do
use Application
use Amnesia
alias Database.Users
def start(_type, _args) do
import Supervisor.Spec
Amnesia.Schema.create()
Amnesia.start()
Database.create(disk: [node()])
case migrate_users() do
:ok ->
children = [
# ...
]
opts = [strategy: :one_for_one, name: PicsafeKeyServer.Supervisor]
Supervisor.start_link(children, opts)
:error ->
{:error, :migration_error}
end
end
# Versions of the user table
@users_v1 [:name, :email]
@users_v2 [:name, :email, :login_count]
# Perform migrations of the users table until the recent version is current.
# Returns :ok or :error.
defp migrate_users() do
case Users.info(:attributes) do
@users_v1 ->
Logger.info "Migrating users to 2"
Amnesia.Table.wait([Users], 5000)
res = Amnesia.Table.transform(Users, @users_v2, fn({Users, name, email}) ->
{Users, name, email, 0}
end)
case res do
:ok ->
migrate()
{:error, error} ->
Logger.error "Users migration aborted: #{inspect error}"
:error
end
@users_v2 ->
Logger.info "Users is up-to-date"
:error
unknown_state ->
Logger.error "Users table is in unknown state: #{inspect unknown_state}"
:error
end
end
end
In the application module we initialize the schema, start database and create it on disk. Then we run our migrations and continue with starting child processes or terminate depending on the outcome.
In the migrate_users()
function we get the current version of the
database by requesting the set of the attributes. If we see it’s not
current, we run the migration. If it is current, we return.
To migrate data Amnesia.Table.transform(table_name, new_attributes, transform_fun)
function is used. The transform_fun
is the function
that accepts the current row and returns a new one. Note that the set of
attributes in the new row must correspond to the new_attributes
that
you give as a second parameter to the call. In the example below,
login_count
field that comes last is initialized with 0
.
Hope it made it a bit more clear.