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