lib/ecto/adapters/mnesia/migration.ex

defmodule Ecto.Adapters.Mnesia.Migration do
  @moduledoc """
  Functions for dealing with schema migrations
  """
  alias Ecto.Adapters.Mnesia.Constraint
  alias Ecto.Adapters.Mnesia.Source

  @type t :: {module(), Keyword.t()}

  @type table() :: atom()
  @type access_opt() :: {:access_mode, :read_write | :read_only}
  @type disc_copies_opt() :: {:disc_copies, [node()]}
  @type disc_only_copies_opt() :: {:disc_only_copies, [node()]}
  @type index_opt() :: {:index, [atom()]}
  @type load_order_opt() :: {:load_order, integer()}
  @type majority_opt :: {:majority, boolean()}
  @type ram_copies_opt :: {:ram_copies, [node()]}
  @type storage_properties_opt :: {:storage_properties, [{atom(), term()}]}
  @type local_content_opt :: {:local_content, boolean()}
  @type create_opt() ::
          access_opt()
          | disc_copies_opt()
          | disc_only_copies_opt()
          | index_opt()
          | load_order_opt()
          | majority_opt()
          | ram_copies_opt()
          | storage_properties_opt()
          | local_content_opt()
  @type create_opts() :: [create_opt()]
  @type sync_create_opts() :: [{:timeout, integer()} | create_opts()]

  @doc """
  Creates mnesia table.

  See `http://erlang.org/doc/man/mnesia.html#create_table-2` for options, except
  from the following ones:
  * `attributes`: ignored, computed from schema
  * `index`: in addition to primary keys indices
  * `record_name`: ignored, computed from schema
  * `snmp`: unsupported
  * `type`: ignored, all tables are of type `set`


  Returns created table name
  """
  @spec create_table(module(), module(), create_opts()) ::
          {:ok, table()} | :ignore | {:error, term()}
  def create_table(repo, schema, opts \\ []) when is_list(opts) do
    repo.checkout(fn ->
      source = Source.new(%{schema: schema})
      opts = build_options(source, opts)

      case :mnesia.create_table(source.table, opts) do
        {:atomic, :ok} -> {:ok, source.table}
        {:aborted, {:already_exists, _}} -> :ignore
        {:aborted, error} -> {:error, error}
      end
    end)
  end

  @doc """
  Returns true if table is stored in RAM
  """
  def ram_storage?({_, opts}) do
    case Keyword.get(opts, :ram_copies, []) do
      [] -> false
      _ -> true
    end
  end

  @doc """
  Creates table and wait for its creation
  """
  @spec sync_create_table(module(), module(), sync_create_opts()) :: :ok | {:error, term()}
  def sync_create_table(repo, schema, opts \\ []) when is_list(opts) do
    timeout = Keyword.get(opts, :timeout, 5_000)

    case create_table(repo, schema, opts) do
      {:ok, table} -> wait_for_table(table, timeout)
      :ignore -> :ok
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Drop table

  Returns ok if table has been deleted or did not exist
  """
  @spec drop_table(module(), module()) :: :ok | {:error, term()}
  def drop_table(repo, schema) do
    repo.checkout(fn ->
      %{table: table} = Source.new(%{schema: schema})

      table
      |> :mnesia.delete_table()
      |> case do
        {:atomic, :ok} -> :ok
        {:aborted, {:no_exists, ^table}} -> :ok
        {:aborted, reason} -> {:error, reason}
      end
    end)
  end

  @doc """
  Add a foreign key constraint
  """
  @spec references(module(), atom(), Constraint.ForeignKey.opts()) :: :ok | {:error, term()}
  def references(from, relation, opts \\ []) do
    %{schema: from}
    |> Source.new()
    |> Constraint.ForeignKey.new(relation, opts)
    |> case do
      %Constraint.ForeignKey{errors: []} = c -> Constraint.register(c)
      c -> {:error, c.errors}
    end
  end

  @doc false
  def build_options(source, opts) do
    extra_keys =
      case source.extra_key do
        nil -> []
        extra -> Map.keys(extra)
      end

    index =
      opts
      |> Keyword.get(:index, [])
      |> MapSet.new()
      |> MapSet.union(MapSet.new(extra_keys))
      |> MapSet.to_list()

    [
      index: index,
      attributes: source.attributes,
      type: :ordered_set,
      record_name: source.record_name
    ]
    |> Keyword.merge(
      Keyword.take(opts, [
        :access_mode,
        :disc_copies,
        :disc_copies_only,
        :load_order,
        :majority,
        :ram_copies,
        :storage_properties,
        :local_content,
        :type
      ])
    )
  end

  defp wait_for_table(table, timeout) do
    case :mnesia.wait_for_tables([table], timeout) do
      :ok -> :ok
      {:error, reason} -> {:error, reason}
      {:timeout, [table]} -> {:error, {:timeout, table}}
    end
  end
end