Skip to main content

lib/adapters/mnesia.ex

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

  @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 | {: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} ->
        :mnesia.wait_for_tables([table], 5000)

      {: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)

      {:aborted, {:already_exists, _table, _node}} ->
        Migration.migrate_table_options(table)

      {: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 """
  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 write_object(object, table) do
    :mnesia.transaction(fn ->
      :mnesia.write(table, object, :write)
    end)
  end

  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