diff --git a/priv/new_migration.esh b/priv/new_migration.esh new file mode 100755 index 0000000..e2911dd --- /dev/null +++ b/priv/new_migration.esh @@ -0,0 +1,51 @@ +#!/usr/bin/env escript +%% -*- erlang -*- +%%! -smp enable + +template(Name) -> + [ + "-module('" ++ Name ++ "').", + "-behavior(db_migration).", + "-export([up/0, down/0]).", + "", + "up() ->", + " ok.", + "", + "down() ->", + " ok." + ]. + +usage() -> + [ + "************************************************************************", + "This script can be used to create a new migration from a template.", + "It automatically calculates a new timestamp to use with the migration", + "and appends the provided description to the name of the migration.", + "", + "Usage:", + " new_migration.esh [ -h | description ]", + "", + " -h", + " This help.", + "", + " description", + " Will be added as the file name after the timestamp, e.g.:", + " 20130731163300_convert_permissions.erl", + "************************************************************************" + ]. + +main([]) -> print_usage(); +main(["-h"]) -> print_usage(); +main([Description]) -> + {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), + Args = [Year, Month, Day, Hour, Minute, Second, Description], + Name = "~w~2.2.0w~2.2.0w~2.2.0w~2.2.0w~2.2.0w_~s", + Module = lists:flatten(io_lib:format(Name, Args)), + FileName = Module ++ ".erl", + io:format("Creating new migration: '~p'~n", [FileName]), + Template = [X ++ "\n" || X <- template(Module)], + ok = file:write_file(FileName, Template), + io:format("Done.~n", []). + +print_usage() -> + [io:format(X ++ "~n", []) || X <- usage()]. diff --git a/src/migresia.app.src b/src/migresia.app.src index 8da3c3b..e83048d 100644 --- a/src/migresia.app.src +++ b/src/migresia.app.src @@ -27,7 +27,7 @@ {application, migresia, [ {description, "Simple mnesia database migration tool"}, - {vsn, "0.0.1"}, + {vsn, "0.0.2"}, {registered, []}, {applications, [kernel, stdlib]} ] diff --git a/src/migresia.erl b/src/migresia.erl index 8372620..9eabae4 100644 --- a/src/migresia.erl +++ b/src/migresia.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2013, Grzegorz Junka +%% Copyright (c) 2015, Grzegorz Junka %% All rights reserved. %% %% Redistribution and use in source and binary forms, with or without @@ -24,102 +24,91 @@ -module(migresia). --export([create_new_migration/2, check/1, migrate/1, rollback/2, list_nodes/0, ensure_started/1]). +-export([start_all_mnesia/0, + ensure_started/1, + check/1, + migrate/1, + rollback/2, + list_nodes/0]). --define(TABLE, schema_migrations). +%%------------------------------------------------------------------------------ --spec create_new_migration(atom(), string()) -> any(). -create_new_migration(App, Description) -> - {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), - Filename = lists:flatten(io_lib:format("~w~2.2.0w~2.2.0w~2.2.0w~2.2.0w~2.2.0w_", [Year, Month, Day, Hour, Minute, Second]) ++ Description), - Path = migresia_migrations:get_priv_dir(App), - FullPathAndExtension = filename:join(Path, Filename ++ ".erl"), - io:format("Creating new migration: ~p~n", [FullPathAndExtension]), - ok = filelib:ensure_dir(<>/binary>>), - ok = file:write_file(FullPathAndExtension, io_lib:fwrite("-module(~p).~n-behavior(db_migration).~n-export([up/0, down/0]). ~n~nup() -> ok.~n~ndown() -> throw(<<\"Downgraders not implemented.\">>).", [list_to_atom(Filename)])), - io:format("Migration written.~n~n"). - --spec check(atom()) -> ok | {error, any()}. -check(App) -> - case start_mnesia(false) of - ok -> - Loaded = migresia_migrations:list_unapplied_ups(App), - if Loaded == [] -> - []; - true -> - [ [X||{X,_} <- Loaded] ] - end; - {error, Error} -> {error, Error} - end. - --spec migrate(atom()) -> ok | {error, any()}. -migrate(App) -> - case start_mnesia(true) of - ok -> - case migresia_migrations:ensure_schema_table_exists() of - ok -> - io:format("Waiting for tables (max timeout 2 minutes)...", []), - ok = mnesia:wait_for_tables(mnesia:system_info(tables), 120000), - rpc:multicall(migresia_migrations, list_unapplied_ups, [App]), %Basically just to ensure everybody has loaded all the migrations, which is necessary in distributed Mnesia transforms. - Loaded = migresia_migrations:list_unapplied_ups(App), - lists:foreach(fun execute_up/1, Loaded); - {error, Error} -> Error - end; - {error, Error} -> {error, Error} - end. - --spec rollback(atom(), integer()) -> ok | {error, any()}. -rollback(App, Time) -> - case start_mnesia(true) of - ok -> - case migresia_migrations:ensure_schema_table_exists() of - ok -> - io:format("Waiting for tables (max timeout 2 minutes)...", []), - ok = mnesia:wait_for_tables(mnesia:system_info(tables), 120000), - rpc:multicall(migresia_migrations, list_all_ups, [App]), %Basically just to ensure everybody has loaded all the migrations, which is necessary in distributed Mnesia transforms. - ToRollBack = lists:reverse([X || X<- migresia_migrations:list_all_ups(App), binary_to_integer(element(2, X)) > Time]), - lists:foreach(fun execute_down/1, ToRollBack); - {error, Error} -> Error - end; - {error, Error} -> {error, Error} - end. - -execute_up({Module, Short}) -> - io:format("Executing up in ~s...~n", [Module]), - Module:up(), - mnesia:dirty_write(schema_migrations, {schema_migrations, Short, true}), - io:format(" => done~n", []). - -execute_down({Module, Time}) -> - io:format("Executing down in ~s...~n", [Module]), - Module:down(), - mnesia:dirty_delete(schema_migrations, Time), - io:format(" => done~n", []). - --spec start_mnesia(boolean()) -> ok | {error, any()}. -start_mnesia(RemoteToo) -> +-spec start_all_mnesia() -> ok | {error, any()}. +start_all_mnesia() -> io:format("Starting Mnesia...~n", []), case ensure_started(mnesia) of - Other when Other /= ok -> io:format(" => Error:~p~n", [Other]), Other; - ok -> - case RemoteToo of - false -> ok; - true -> {ResultList, BadNodes} = rpc:multicall(list_nodes(), migresia, ensure_started, [mnesia]), - BadStatuses = [X || X <- ResultList, X /= ok], - if BadNodes /= [] -> io:format(" => Error:~p~n", [not_all_nodes_running]), {error, not_all_nodes_running}; - BadStatuses /= [] -> io:format(" => Error:~p~n", [BadStatuses]), {error, BadStatuses}; - true -> io:format(" => started~n", []), ok - end - end + ok -> + ensure_started_on_remotes(list_nodes()); + Err -> + io:format(" => Error:~p~n", [Err]), + Err end. list_nodes() -> mnesia:table_info(schema, disc_copies). +ensure_started_on_remotes(Nodes) -> + io:format("Ensuring Mnesia is running on nodes:~n~p~n", [Nodes]), + {ResL, BadNodes} = rpc:multicall(Nodes, migresia, ensure_started, [mnesia]), + handle_err([X || X <- ResL, X /= ok], BadNodes). + +handle_err([], []) -> + io:format(" => started~n", []), + ok; +handle_err(Results, Bad) -> + if Results /= [] -> io:format(" => Error, received: ~p~n", [Results]) end, + if Bad /= [] -> io:format(" => Error, bad nodes: ~p~n", [Bad]) end, + {error, mnesia_not_started}. + -spec ensure_started(atom()) -> ok | {error, any()}. ensure_started(App) -> case application:start(App) of ok -> ok; - {error,{already_started,App}} -> ok; - A -> A - end. \ No newline at end of file + {error, {already_started, App}} -> ok; + {error, _} = Err -> Err + end. + +%%------------------------------------------------------------------------------ + +-spec check(atom()) -> ok | {error, any()}. +check(App) -> + case migresia_migrations:list_unapplied_ups(App) of + [] -> []; + {error, _} = Err -> Err; + Loaded -> [X || {X, _} <- Loaded] + end. + +%%------------------------------------------------------------------------------ + +-spec migrate(atom()) -> ok | {error, any()}. +migrate(App) -> + migrate(migresia_migrations:init_migrations(), App). + +migrate(ok, App) -> + io:format("Waiting for tables (max timeout 2 minutes)...", []), + ok = mnesia:wait_for_tables(mnesia:system_info(tables), 120000), + Loaded = migresia_migrations:list_unapplied_ups(App), + %% Load the transform function on all nodes, see: + %% http://toddhalfpenny.com/2012/05/21/possible-erlang-bad-transform-function-solution/ + rpc:multicall(nodes(), migresia_migrations, list_unapplied_ups, [App]), + lists:foreach(fun migresia_migrations:execute_up/1, Loaded); +migrate({error, _} = Err, _) -> + Err. + +%%------------------------------------------------------------------------------ + +-spec rollback(atom(), integer()) -> ok | {error, any()}. +rollback(App, Time) -> + rollback(migresia_migrations:init_migrations(), App, Time). + +rollback(ok, App, Time) -> + io:format("Waiting for tables (max timeout 2 minutes)...", []), + ok = mnesia:wait_for_tables(mnesia:system_info(tables), 120000), + Ups = migresia_migrations:list_all_ups(App), + %% Load the transform function on all nodes, see: + %% http://toddhalfpenny.com/2012/05/21/possible-erlang-bad-transform-function-solution/ + rpc:multicall(nodes(), migresia_migrations, list_all_ups, [App]), + ToRollBack = lists:reverse([X || {_, Ts, _} = X <- Ups, Ts > Time]), + lists:foreach(fun migresia_migrations:execute_down/1, ToRollBack); +rollback({error, _} = Err, _App, _Time) -> + Err. diff --git a/src/migresia_migrations.erl b/src/migresia_migrations.erl index ad592aa..afc696a 100644 --- a/src/migresia_migrations.erl +++ b/src/migresia_migrations.erl @@ -1,4 +1,4 @@ -%% Copyright (c) 2013, Grzegorz Junka +%% Copyright (c) 2015, Grzegorz Junka %% All rights reserved. %% %% Redistribution and use in source and binary forms, with or without @@ -24,57 +24,69 @@ -module(migresia_migrations). --export([list_unapplied_ups/1, list_all_ups/1, get_priv_dir/1, ensure_schema_table_exists/0]). +-export([init_migrations/0, + list_unapplied_ups/1, + list_all_ups/1, + get_priv_dir/1, + execute_up/1, + execute_down/1]). -define(DIR, <<"migrate">>). -define(TABLE, schema_migrations). --spec list_unapplied_ups(atom()) -> [{module(), binary()}]. +-type error() :: {error, any()}. +-type mod_bin_list() :: [{module(), binary()}]. + +%%------------------------------------------------------------------------------ + +-spec init_migrations() -> ok | error(). +init_migrations() -> + case lists:member(?TABLE, mnesia:system_info(tables)) of + true -> + ok; + false -> + io:format("Table schema_migration not found, creating...~n", []), + Attr = [{type, ordered_set}, {disc_copies, migresia:list_nodes()}], + case mnesia:create_table(?TABLE, Attr) of + {atomic, ok} -> io:format(" => created~n", []), ok; + {aborted, Reason} -> {error, Reason} + end + end. + +%%------------------------------------------------------------------------------ + +-spec list_unapplied_ups(atom()) -> mod_bin_list() | error(). list_unapplied_ups(App) -> get_migrations(get_priv_dir(App)). +-spec list_all_ups(atom()) -> mod_bin_list() | error(). list_all_ups(App) -> get_all_migrations(get_priv_dir(App)). --spec get_priv_dir(atom()) -> binary(). +-spec get_priv_dir(atom()) -> string() | binary() | error(). get_priv_dir(App) -> case application:load(App) of - ok -> filename:join(code:priv_dir(App), ?DIR); - {error, {already_loaded, App}} -> filename:join(code:priv_dir(App), ?DIR); - Error -> Error + ok -> + filename:join(code:priv_dir(App), ?DIR); + {error, {already_loaded, App}} -> + filename:join(code:priv_dir(App), ?DIR); + {error, _} = Err -> + Err end. - --spec ensure_schema_table_exists() -> ok | {error, any()}. -ensure_schema_table_exists() -> - case lists:member(?TABLE, mnesia:system_info(tables)) of - true -> ok; - false -> io:format("Table schema_migration not found, creating...~n", []), - Attr = [{type, ordered_set}, {disc_copies, migresia:list_nodes()}], - case mnesia:create_table(?TABLE, Attr) of - {atomic, ok} -> io:format(" => created~n", []), ok; - {aborted, Reason} -> {error, Reason} - end - end. - --spec get_migrations({error, any()} | binary()) -> list(). +-spec get_migrations({error, any()} | binary()) -> mod_bin_list() | error(). get_migrations({error, _} = Err) -> - {error, Err}; + Err; get_migrations(Dir) -> ToApply = check_dir(file:list_dir(Dir)), case check_table() of - {error, Error} -> {error, Error}; - Applied when is_list(Applied) -> - ToExecute = compile_unapplied(Dir, ToApply, Applied, []), - Fun = fun({Module, Short, Binary}) -> load_migration(Module, Short, Binary) end, - lists:map(Fun, ToExecute) + {error, _} = Err -> Err; + Applied -> compile_and_load(Dir, ToApply, Applied) end. get_all_migrations(Dir) -> ToApply = lists:sort(check_dir(file:list_dir(Dir))), - ToExecute = compile_unapplied(Dir, ToApply, [], []), - Fun = fun({Module, Short, Binary}) -> load_migration(Module, Short, Binary) end, - lists:map(Fun, ToExecute). + compile_and_load(Dir, ToApply, []). check_dir({error, Reason}) -> throw({file, list_dir, Reason}); @@ -84,9 +96,11 @@ check_dir({ok, Filenames}) -> normalize_names([<>|T], Acc) -> normalize_names(T, [{Short, Short}|Acc]); normalize_names([<> = Name|T], Acc) - when size(R) >= 4 andalso erlang:binary_part(R, size(R) - 4, 4) == <<".erl">> -> + when size(R) >= 4 + andalso erlang:binary_part(R, size(R) - 4, 4) == <<".erl">> -> Base = erlang:binary_part(Name, 0, size(Name) - 4), - normalize_names(T, [{Short, Base}|Acc]); + Int = list_to_integer(binary_to_list(Short)), + normalize_names(T, [{Int, Base}|Acc]); normalize_names([Name|T], Acc) when is_list(Name) -> normalize_names([list_to_binary(Name)|T], Acc); normalize_names([Name|T], Acc) -> @@ -95,7 +109,6 @@ normalize_names([Name|T], Acc) -> normalize_names([], Acc) -> lists:sort(Acc). - check_table() -> case lists:member(?TABLE, mnesia:system_info(tables)) of false -> @@ -107,11 +120,22 @@ check_table() -> io:format(" => done~n", []), Select = [{{?TABLE,'_','_'},[],['$_']}], List = mnesia:dirty_select(?TABLE, Select), - [ X || {schema_migrations, X, true} <- List ]; - Error -> {error, Error} + [ X || {?TABLE, X, true} <- List ]; + {error, _} = Err -> + Err; + Error -> + {error, Error} end end. +compile_and_load(Dir, ToApply, Applied) -> + ToExecute = compile_unapplied(Dir, ToApply, Applied, []), + Fun = fun({Module, Short, Binary}) -> + load_migration(Module, Short, Binary) + end, + lists:map(Fun, ToExecute). + +%%------------------------------------------------------------------------------ compile_unapplied(Dir, [{Short, Module}|TN], [] = Old, Acc) -> compile_unapplied(Dir, TN, Old, [compile_file(Dir, Short, Module)|Acc]); @@ -137,19 +161,30 @@ compile_file(Dir, Short, Name) -> {ok, Module, Binary, Warnings} -> io:format("Warnings: ~p~n", [Warnings]), {Module, Short, Binary}; - {error, Errors, Warnings} -> - io:format("Warnings: ~p~nErrors: ~p~nExiting...~n", [Warnings, Errors]), - throw(Errors); + {error, Err, Warn} -> + io:format("Warnings: ~p~nErrors: ~p~nAborting...~n", [Warn, Err]), + throw({compile, compile_error, Err}); error -> - io:format("Unknown error encoutered, Exiting...~n", []), - throw({compile, file, error}) + io:format("Unknown error encoutered, Aborting...~n", []), + throw({compile, unknown_error, File}) end. load_migration(Module, Short, Binary) -> case code:load_binary(Module, Module, Binary) of - {module, Module} -> - {Module, Short}; - {error, What} -> - throw({code, load_binary, What}) + {module, Module} -> {Module, Short}; + {error, What} -> throw({code, load_binary, What}) end. +%%------------------------------------------------------------------------------ + +execute_up({Module, Ts}) -> + io:format("Executing up in ~s...~n", [Module]), + Module:up(), + mnesia:dirty_write(?TABLE, {?TABLE, Ts, true}), + io:format(" => done~n", []). + +execute_down({Module, Ts}) -> + io:format("Executing down in ~s...~n", [Module]), + Module:down(), + mnesia:dirty_delete(?TABLE, Ts), + io:format(" => done~n", []).