defmodule ActiveMemory.Adapters.Mnesia.Migration do @moduledoc """ Migrations will get run on app startup and are designed to modify :mnesia's schema. ## Table Copies In the `options` of an ActiveMemory.Table, the copy type and nodes which should have them can be specified. ### Ram copies Tables that only reside in ram on the nodes specified. The default is `node()` Example table using default setting: ```elixir defmodule Test.Support.Dogs.Dog do use ActiveMemory.Table, options: [compressed: true, read_concurrency: true] . # module code . end ``` The default will be `[node()]` and this table will reside on the `node()` ram. Example table spcifing nodes and ram copies: ```elixir defmodule Test.Support.Dogs.Dog do use ActiveMemory.Table, options: [compressed: true, read_concurrency: true, ram_copes: [node() | Node.list()] . # module code . end ``` All the active nodes in Node.list() and node() will have ram copes of the table. ### Disc copies Disc copy tables reside **both** in ram and disc on the nodes specified. In order to persist to disc the schema must be setup on at lest one running node. The default is [] (no nodes). Example table spcifing nodes and disc copies: ```elixir defmodule Test.Support.Dogs.Dog do use ActiveMemory.Table, options: [compressed: true, read_concurrency: true, disc_copes: [node()] . # module code . end ``` The table will have a ram copy and disc copy on `node()` ### Disc only copies Disc oly tables reside **only** on disc on the nodes specified. In order to persist to disc the schema must be setup on at lest one running node. The default is [] (no nodes). Example table spcifing nodes and disc copies: ```elixir defmodule Test.Support.Dogs.Dog do use ActiveMemory.Table, options: [compressed: true, read_concurrency: true, disc_only_copes: [node()] . # module code . end ``` The table will only have a disc copy on `node()` ## Table Read and Write Access Mnesia tables can be set to `read_only` or `read_write`. The default is `read_write`. Read only tables updates cannot be performed. if you need to change the access use the following syntax: `[access_mode: :read_only]` ## Table Types Tables can be either a `:set`, `:ordered_set`, or a `:bag`. The default is `:set` if you need to change the type use the following syntax: `[type: :bag]` ## Indexes If Indexes are desired specify an atom attribute list for which Mnesia is to build and maintain an extra index table. The qlc query compiler may be able to optimize queries if there are indexes available. To specify Indexes use the following syntax: `[index: [:age, :hair_color, :cylon?]]` ## Table Load Order The load order priority is by default 0 (zero) but can be set to any integer. The tables with the highest load order priority are loaded first at startup. If you need to change the load order use the following syntax: `[load_order: 2]` ## Majority If true, any (non-dirty) update to the table is aborted, unless a majority of the table replicas are available for the commit. When used on a fragmented table, all fragments are given the same the same majority setting. If you need to modify the majority use the following syntax: `[majority: true]` """ def migrate_table_options(table) do table.__attributes__(:table_options) |> migrate_table_copies_to_add(table) |> migrate_table_copies_to_delete(table) |> migrate_access_mode(table) |> migrate_indexes(table) |> migrate_load_order(table) |> migrate_majority(table) :ok end # Supporting methods in alphabetical order defp add_copy_type([], _table, _copy_type), do: :ok defp add_copy_type(nodes, table, copy_type) do for node <- nodes do case :mnesia.add_table_copy(table, node, copy_type) do {:aborted, {:already_exists, _, _}} -> change_table_copy_type(table, node, copy_type) {:atomic, :ok} -> :ok end end :ok end defp add_copy_types(options_nodes, table, copy_type) do table |> :mnesia.table_info(copy_type) |> Enum.sort() |> compare_nodes_to_add(options_nodes) |> add_copy_type(table, copy_type) end defp add_indexes([], _table), do: nil defp add_indexes(indexes, table) do Enum.each(indexes, fn index -> :mnesia.add_table_index(table, index) end) end defp change_table_copy_type(table, node, copy_type) do case :mnesia.change_table_copy_type(table, node, copy_type) do {:atomic, :ok} -> :ok other -> other end end defp compare_nodes_to_add([], options_nodes), do: options_nodes defp compare_nodes_to_add(_current_nodes, []), do: [] defp compare_nodes_to_add(current_nodes, options_nodes) do options_nodes -- current_nodes end defp compare_nodes_to_remove([], _options_nodes), do: [] defp compare_nodes_to_remove(current_nodes, []), do: current_nodes defp compare_nodes_to_remove(current_nodes, options_nodes) do current_nodes -- options_nodes end defp copy_type_validation(_ram_nodes, [], []), do: :ok defp copy_type_validation([], _disc_nodes, []), do: :ok defp copy_type_validation([], [], _disc_only_nodes), do: :ok defp copy_type_validation([], disc_nodes, disc_only_nodes) do disc_nodes_validation(disc_nodes, disc_only_nodes) end defp copy_type_validation(ram_nodes, [], disc_only_nodes) do ram_nodes |> Enum.any?(&Enum.member?(disc_only_nodes, &1)) |> parse_check(:ram_copies) end defp copy_type_validation(ram_nodes, disc_nodes, []) do ram_nodes |> Enum.any?(&Enum.member?(disc_nodes, &1)) |> parse_check(:ram_copies) end defp copy_type_validation(ram_nodes, disc_nodes, disc_only_nodes) do with {:ok, :ram_copies} <- ram_nodes_validation(ram_nodes, disc_nodes, disc_only_nodes), {:ok, :disc_copies} <- disc_nodes_validation(disc_nodes, disc_only_nodes) do :ok end end defp delete_copy_type([], _table), do: :ok defp delete_copy_type(nodes, table) do Enum.each(nodes, &:mnesia.del_table_copy(table, &1)) end defp delete_indexes([], _table), do: nil defp delete_indexes(indexes, table) do Enum.each(indexes, fn index -> :mnesia.del_table_index(table, index) end) end defp disc_nodes_validation(disc_nodes, disc_only_nodes) do disc_nodes |> Enum.any?(&Enum.member?(disc_only_nodes, &1)) |> parse_check(:disc_copies) end defp get_indexes([], _attributes), do: [] defp get_indexes(indexes, attributes) do indexes |> Enum.map(fn index -> Enum.at(attributes, index - 2) end) end defp migrate_access_mode(options, table) do option = Keyword.get(options, :access_mode, :read_write) case :mnesia.table_info(table, :access_mode) do ^option -> :ok _ -> :mnesia.change_table_access_mode(table, option) end options end defp migrate_indexes(options, table) do new_indexes = Keyword.get(options, :index, []) indexes = :mnesia.table_info(table, :index) current_indexes = get_indexes(indexes, :mnesia.table_info(table, :attributes)) add_indexes(new_indexes -- current_indexes, table) delete_indexes(current_indexes -- new_indexes, table) options end defp migrate_load_order(options, table) do load_order = Keyword.get(options, :load_order, 0) case :mnesia.table_info(table, :load_order) do ^load_order -> :ok _ -> :mnesia.change_table_load_order(table, load_order) end options end defp migrate_majority(options, table) do majority = Keyword.get(options, :majority, false) case :mnesia.table_info(table, :majority) do ^majority -> :ok _ -> :mnesia.change_table_majority(table, majority) end options end defp migrate_table_copies_to_add(options, table) do options_ram_nodes = Keyword.get(options, :ram_copies, [node()]) |> Enum.sort() options_disc_nodes = Keyword.get(options, :disc_copies, []) |> Enum.sort() options_disc_only_nodes = Keyword.get(options, :disc_only_copies, []) |> Enum.sort() with :ok <- copy_type_validation(options_ram_nodes, options_disc_nodes, options_disc_only_nodes), :ok <- add_copy_types(options_ram_nodes, table, :ram_copies), :ok <- add_copy_types(options_disc_nodes, table, :disc_copies), :ok <- add_copy_types( options_disc_only_nodes, table, :disc_only_copies ) do options end end defp migrate_table_copies_to_delete(options, table) do with :ok <- remove_copy_types(options, table, :ram_copies, [node()]), :ok <- remove_copy_types(options, table, :disc_copies), :ok <- remove_copy_types(options, table, :disc_only_copies) do options end end defp parse_check(false, _copy_type), do: :ok defp parse_check(true, copy_type), do: {:error, "#{copy_type} options are invalid. Please read the documentation"} defp remove_copy_types(options, table, copy_type, default_nodes \\ []) do options_nodes = options |> Keyword.get(copy_type, default_nodes) |> Enum.sort() table |> :mnesia.table_info(copy_type) |> Enum.sort() |> compare_nodes_to_remove(options_nodes) |> delete_copy_type(table) end defp ram_nodes_validation(ram_nodes, disc_nodes, disc_only_nodes) do ram_nodes |> Enum.any?(&(Enum.member?(disc_nodes, &1) or Enum.member?(disc_only_nodes, &1))) |> parse_check(:ram_copies) end end