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