defmodule ActiveMemory.Adapters.Mnesia do @moduledoc """ An adapter for storing structs in Mnesia """ alias ActiveMemory.Adapters.Adapter alias ActiveMemory.Adapters.Mnesia.{Helpers, Migration} alias ActiveMemory.Query.{MatchGuards, MatchSpec} @behaviour Adapter @table_wait_timeout 5_000 @doc """ Return all structs stored in a table. ```elixir iex:> DogStore.all(Dog) [%Dog{}, %Dog{}] ``` """ @spec all(atom()) :: list(map()) def all(table) do case match_object(:mnesia.table_info(table, :wild_pattern)) do {:atomic, []} -> [] {:atomic, records} -> Enum.into(records, [], &to_struct(&1, table)) {:aborted, message} -> {:error, message} end end @doc """ Create a table in Mnesia using an ActiveMemory.Table. This function will take in the ActiveMemory.Table and parse the options for the table. Example Table (without auto generated uuid): ```elixir defmodule Test.Support.People.Person do use ActiveMemory.Table, options: [index: [:last, :cylon?]] attributes do ... end end ``` Once the ActiveMemory.Table is defined then the in memory table can be created. ```elixir iex:> PeopleStore.create_table(Test.Support.People.Person) :ok ``` """ @spec create_table(atom()) :: {:ok, :created | :recovered} | {:error, any()} def create_table(table) do options = [attributes: table.__attributes__(:query_fields)] |> Keyword.merge(table.__attributes__(:table_options)) case :mnesia.create_table(table, options) do {:atomic, :ok} -> wait_for_table(table) {:aborted, {:not_active, _table, new_node}} -> :mnesia.change_config(:extra_db_nodes, [new_node]) create_table(table) {:aborted, {:already_exists, _table}} -> Migration.migrate_table_options(table) {:ok, :recovered} {:aborted, {:already_exists, _table, _node}} -> Migration.migrate_table_options(table) {:ok, :recovered} {:aborted, message} -> {:error, message} end end @doc """ Delete a struct from a table. ```elixir iex:> PeopleStore.delete(%Person{}, Person) :ok ``` """ @spec delete(map(), atom()) :: :ok | {:error, any()} def delete(struct, table) do case delete_object(struct, table) do {:atomic, :ok} -> :ok {:aborted, message} -> {:error, message} end end @doc """ Delete all structs from a table. ```elixir iex:> PeopleStore.delete_all(Person) true ``` """ @spec delete_all(atom()) :: true | any() def delete_all(table) do :mnesia.clear_table(table) end @doc """ Find a single struct in a table using either a map query search or ActiveMemory.Query.MatchSpec. using a map query ```elixir iex:> DogStore.one(%{name: "gem", breed: "Shaggy Black Lab"}) {:ok, %Dog{}} ``` with ActiveMemory.Query.MatchSpec ```elixir iex:> DogStore.one(match(:name == "gem" and :breed == "Shaggy Black Lab")) {:ok, %Dog{}} ``` """ @spec one(map() | tuple(), atom()) :: {:ok, map()} | {:error, any()} def one(query_map, table) when is_map(query_map) do with {:ok, query} <- MatchGuards.build(table, query_map), match_query <- Tuple.insert_at(query, 0, table), {:atomic, record} when length(record) == 1 <- match_object(match_query) do {:ok, to_struct(hd(record), table)} else {:atomic, []} -> {:error, :not_found} {:atomic, records} when is_list(records) -> {:error, :more_than_one_result} {:error, message} -> {:error, message} end end def one(query, table) when is_tuple(query) do with match_spec = build_mnesia_match_spec(query, table), {:atomic, record} when length(record) == 1 <- select_object(match_spec, table) do {:ok, to_struct(hd(record), table)} else {:atomic, []} -> {:error, :not_found} {:atomic, records} when is_list(records) -> {:error, :more_than_one_result} {:error, message} -> {:error, message} end end @doc """ Find a single struct in a table using either a map query search or ActiveMemory.Query.MatchSpec. using a map query ```elixir iex:> DogStore.select(%{name: "gem", breed: "Shaggy Black Lab"}) {:ok, [%Dog{}, %Dog{}]} ``` with ActiveMemory.Query.MatchSpec ```elixir iex:> DogStore.select(match(:name == "gem" and :breed == "Shaggy Black Lab")) {:ok, [%Dog{}, %Dog{}]} ``` """ @spec select(map() | tuple(), atom()) :: {:ok, list(map())} | {:error, any()} def select(query_map, table) when is_map(query_map) do with {:ok, query} <- MatchGuards.build(table, query_map), match_query <- Tuple.insert_at(query, 0, table), {:atomic, records} when is_list(records) <- match_object(match_query) do {:ok, to_struct(records, table)} else {:atomic, []} -> {:ok, []} {:error, message} -> {:error, message} end end def select(query, table) when is_tuple(query) do with match_spec = build_mnesia_match_spec(query, table), {:atomic, records} when records != [] <- select_object(match_spec, table) do {:ok, to_struct(records, table)} else {:atomic, []} -> {:ok, []} {:error, message} -> {:error, message} end end @doc """ Atomically find a single struct matching the query, delete it, and return it. The find and delete run inside a single `:mnesia.transaction/1`, so under concurrent access exactly one caller receives `{:ok, struct}` for a given record; any others receive `{:error, :not_found}`. ```elixir iex:> DogStore.withdraw(%{name: "gem", breed: "Shaggy Black Lab"}) {:ok, %Dog{}} ``` """ @spec withdraw(map() | tuple(), atom()) :: {:ok, map()} | {:error, any()} def withdraw(query_map, table) when is_map(query_map) do with {:ok, query} <- MatchGuards.build(table, query_map), match_query <- Tuple.insert_at(query, 0, table), {:atomic, {:ok, record}} <- withdraw_match_object(match_query, table) do {:ok, to_struct(record, table)} else {:atomic, {:error, message}} -> {:error, message} {:error, message} -> {:error, message} {:aborted, message} -> {:error, message} end end def withdraw(query, table) when is_tuple(query) do with match_spec <- build_mnesia_match_spec(query, table), {:atomic, {:ok, record}} <- withdraw_select_object(match_spec, table) do {:ok, to_struct(record, table)} else {:atomic, {:error, message}} -> {:error, message} {:aborted, message} -> {:error, message} end end @doc """ Save a struct to a table. ```elixir iex:> DogStore.write(%Dog{name: "gem", breed: "Shaggy Black Lab"}) {:ok, %Dog{}} ``` """ @spec write(map(), atom()) :: {:ok, map()} | {:error, any()} def write(struct, table) do case write_object(to_tuple(struct), table) do {:atomic, :ok} -> {:ok, struct} {:aborted, message} -> {:error, message} end end defp delete_object(struct, table) do :mnesia.transaction(fn -> :mnesia.delete_object(table, to_tuple(struct), :write) end) end defp match_object(query) do :mnesia.transaction(fn -> :mnesia.match_object(query) end) end defp select_object(match_spec, table) do :mnesia.transaction(fn -> :mnesia.select(table, match_spec, :read) end) end defp wait_for_table(table) do case :mnesia.wait_for_tables([table], @table_wait_timeout) do :ok -> {:ok, :created} {:timeout, bad_tables} -> {:error, {:timeout, bad_tables}} {:error, reason} -> {:error, reason} end end defp withdraw_match_object(match_query, table) do :mnesia.transaction(fn -> delete_matched(:mnesia.match_object(table, match_query, :write), table) end) end defp withdraw_select_object(match_spec, table) do :mnesia.transaction(fn -> delete_matched(:mnesia.select(table, match_spec, :read), table) end) end defp write_object(object, table) do :mnesia.transaction(fn -> :mnesia.write(table, object, :write) end) end defp delete_matched([record], table) do :ok = :mnesia.delete_object(table, record, :write) {:ok, record} end defp delete_matched([], _table), do: {:error, :not_found} defp delete_matched(records, _table) when is_list(records), do: {:error, :more_than_one_result} defp build_mnesia_match_spec(query, table) do query_map = :erlang.apply(table, :__attributes__, [:query_map]) match_head = :erlang.apply(table, :__attributes__, [:match_head]) MatchSpec.build(query, query_map, match_head) end defp to_struct(records, table) when is_list(records) do Enum.into(records, [], &to_struct(&1, table)) end defp to_struct(record, table) when is_tuple(record), do: Helpers.to_struct(record, table) defp to_tuple(struct), do: Helpers.to_tuple(struct) end