Skip to main content

lib/ecto/adapters/recall.ex

defmodule Ecto.Adapters.Recall do
  @moduledoc """
  An Ecto 3 adapter for [Mnesia](https://www.erlang.org/doc/apps/mnesia/), the
  distributed, transactional database that ships with the BEAM.

  *Memory, recollected.*

  Unlike an ETS-backed adapter, Mnesia gives us real ACID transactions and
  optional disk persistence (`disc_copies`) for free — they map almost directly
  onto Ecto's `Ecto.Adapter.Transaction` and `Ecto.Adapter.Storage` behaviours.
  Query translation reuses Erlang match specifications, which Mnesia and ETS
  share verbatim.

  ## Example

      defmodule MyApp.Repo do
        use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.Recall
      end

  ## Configuration

    * `:storage` — `:ram_copies` (default), `:disc_copies`, or
      `:disc_only_copies`. Controls where auto-created tables live.
    * `:type` — `:ordered_set` (default) or `:set`. `:ordered_set` keeps records
      in primary-key order, so reads come back sorted; falls back to `:set` for
      `:disc_only_copies` (which Mnesia doesn't allow with `:ordered_set`). A
      generated `Recall.Schema` can override it per table.
    * `:nodes` — list of nodes that hold copies (defaults to `[node()]`).
    * `:dir` — the Mnesia data directory (only relevant for disc storage).
  """

  @behaviour Ecto.Adapter
  @behaviour Ecto.Adapter.Migration
  @behaviour Ecto.Adapter.Queryable
  @behaviour Ecto.Adapter.Schema
  @behaviour Ecto.Adapter.Storage
  @behaviour Ecto.Adapter.Transaction

  alias Ecto.Adapters.Recall.Meta
  alias Ecto.Adapters.Recall.Migration
  alias Ecto.Adapters.Recall.Queryable
  alias Ecto.Adapters.Recall.Schema
  alias Ecto.Adapters.Recall.Storage
  alias Ecto.Adapters.Recall.Transaction

  @impl Ecto.Adapter
  defmacro __before_compile__(_opts), do: :ok

  @impl Ecto.Adapter
  def ensure_all_started(_config, _type) do
    {:ok, _} = Application.ensure_all_started(:mnesia)
    {:ok, [:mnesia]}
  end

  @impl Ecto.Adapter
  def init(config) do
    {:ok, repo} = Keyword.fetch(config, :repo)

    meta = %Meta{
      repo: repo,
      storage: Keyword.get(config, :storage, :ram_copies),
      nodes: Keyword.get(config, :nodes, [node()]),
      type: Keyword.get(config, :type, :ordered_set)
    }

    # Mnesia tables are owned by the :mnesia application, not by a process in our
    # supervision tree, so we don't need to spin up table-keeper processes the
    # way an ETS adapter must. A no-op Agent keeps Ecto's child-spec contract
    # satisfied.
    child_spec = %{
      id: __MODULE__,
      start: {Agent, :start_link, [fn -> meta end]}
    }

    {:ok, child_spec, meta}
  end

  @impl Ecto.Adapter
  def checkout(_meta, _config, fun), do: fun.()

  @impl Ecto.Adapter
  def checked_out?(_meta), do: false

  # Mnesia stores native Erlang terms, not a SQL wire format, so — unlike a SQL
  # adapter — we have no reason to pack values into bytes on the way in and unpack
  # them on the way out. We store every value in the *same* representation Ecto
  # hands a loaded struct, which makes load the identity for every built-in type.
  #
  # In particular we do NOT special-case `:binary_id`/`:embed_id` to pack into a
  # 16-byte binary via `Ecto.UUID` (the SQL-adapter convention). `:binary_id` is
  # a base type whose load/dump are `same_binary/1`, so `[type]` stores and
  # returns the canonical UUID *string* verbatim. Storing the runtime form is
  # what lets `Recall.Schema` flag a schema `load_free?` and read it back
  # through `Ecto.Adapters.Recall.FastRead` without per-field type loading.
  @impl Ecto.Adapter
  def loaders(_primitive, type), do: [type]

  @impl Ecto.Adapter
  def dumpers(_primitive, type), do: [type]

  ## Ecto.Adapter.Schema

  @impl Ecto.Adapter.Schema
  defdelegate autogenerate(type), to: Schema

  @impl Ecto.Adapter.Schema
  defdelegate insert_all(
                meta,
                schema_meta,
                header,
                entries,
                on_conflict,
                returning,
                placeholders,
                opts
              ),
              to: Schema

  @impl Ecto.Adapter.Schema
  defdelegate insert(meta, schema_meta, fields, on_conflict, returning, opts), to: Schema

  @impl Ecto.Adapter.Schema
  defdelegate update(meta, schema_meta, fields, filters, returning, opts), to: Schema

  @impl Ecto.Adapter.Schema
  defdelegate delete(meta, schema_meta, filters, returning, opts), to: Schema

  ## Ecto.Adapter.Queryable

  @impl Ecto.Adapter.Queryable
  defdelegate prepare(atom, query), to: Queryable

  @impl Ecto.Adapter.Queryable
  defdelegate execute(meta, query_meta, query_cache, params, opts), to: Queryable

  @impl Ecto.Adapter.Queryable
  defdelegate stream(meta, query_meta, query_cache, params, opts), to: Queryable

  ## Ecto.Adapter.Transaction

  @impl Ecto.Adapter.Transaction
  defdelegate transaction(meta, opts, fun), to: Transaction

  @impl Ecto.Adapter.Transaction
  defdelegate in_transaction?(meta), to: Transaction

  @impl Ecto.Adapter.Transaction
  defdelegate rollback(meta, value), to: Transaction

  ## Ecto.Adapter.Storage

  @impl Ecto.Adapter.Storage
  defdelegate storage_up(opts), to: Storage

  @impl Ecto.Adapter.Storage
  defdelegate storage_down(opts), to: Storage

  @impl Ecto.Adapter.Storage
  defdelegate storage_status(opts), to: Storage

  ## Ecto.Adapter.Migration

  @impl Ecto.Adapter.Migration
  defdelegate supports_ddl_transaction?, to: Migration

  @impl Ecto.Adapter.Migration
  defdelegate lock_for_migrations(meta, opts, fun), to: Migration

  @impl Ecto.Adapter.Migration
  defdelegate execute_ddl(meta, command, opts), to: Migration
end