Current section
Files
Jump to
Current section
Files
lib/ecto/adapters/recall/layout.ex
defmodule Ecto.Adapters.Recall.Layout do
@moduledoc false
# The physical layout of a Mnesia table — its attribute names in storage order,
# plus the derived pieces the adapter needs to read/write records (the match-spec
# head/body and a name→position map).
#
# Unlike `Recall.Schema`, which bakes a field's storage position into the
# *schema module* at compile time, a `Layout` is resolved from Mnesia's own
# catalog at runtime (`:mnesia.table_info(tag, :attributes)`) and cached in
# `:persistent_term`. The table is the source of truth for positions, so a
# migration that reshapes a table is reflected on the next read without
# recompiling, and reordering fields in a schema module can't silently misalign
# reads. It also makes secondary indexes physical: `indexed` comes from
# Mnesia's catalog, so the planners use an index iff a `create index` migration
# actually built one (never created on the fly).
#
# The stored record is a tagged tuple `{tag, v1, v2, ...}`: element 1 is the
# table tag and elements 2.. are the attribute values in `attributes` order. So
# an attribute's 0-based slot is its index in `attributes`, and its 1-based
# Mnesia tuple position is that index + 2 (one for the leading tag, one for
# 1-based tuples).
alias __MODULE__
@enforce_keys [:tag, :attributes, :index_of, :field_count, :match_head, :match_body]
defstruct [
:tag,
:attributes,
:index_of,
:field_count,
:match_head,
:match_body,
:type,
indexed: []
]
@type t :: %Layout{
tag: atom,
attributes: [atom],
index_of: %{atom => non_neg_integer},
field_count: non_neg_integer,
match_head: tuple,
match_body: [atom],
type: :set | :ordered_set | nil,
indexed: [pos_integer]
}
@doc """
Builds a `Layout` from a table tag and its ordered attribute names. Pure, so it
can be exercised without a running Mnesia table and shared by `resolve/1`.
`:type` is the Mnesia table type and `:indexed` the list of 1-based attribute
positions carrying a secondary index (both as `:mnesia.table_info/2` reports
them).
"""
def from_attributes(tag, attributes, opts \\ []) when is_atom(tag) and is_list(attributes) do
count = length(attributes)
vars = match_vars(count)
%Layout{
tag: tag,
attributes: attributes,
index_of: attributes |> Enum.with_index() |> Map.new(),
field_count: count,
match_head: List.to_tuple([tag | vars]),
match_body: vars,
type: Keyword.get(opts, :type),
indexed: Keyword.get(opts, :indexed, [])
}
end
@doc """
The `Layout` for `tag`, resolved from Mnesia's catalog and cached in
`:persistent_term`. The table must already exist (callers create it on first
touch via `Ecto.Adapters.Recall.Table.ensure/2` before resolving).
"""
def resolve(tag) when is_atom(tag) do
case :persistent_term.get(key(tag), nil) do
nil -> tag |> read_from_mnesia() |> cache(tag)
%Layout{} = layout -> layout
end
end
@doc """
Drops the cached `Layout` for `tag`. Every schema operation that changes a
table's shape (create/alter/drop/index) must call this so the next `resolve/1`
re-reads the new layout from Mnesia.
"""
def invalidate(tag) when is_atom(tag) do
:persistent_term.erase(key(tag))
:ok
end
@doc """
The 0-based slot of an attribute within the record's value elements — its index
into a projected row and into `match_body`.
"""
def index_of(%Layout{index_of: map}, name), do: Map.fetch!(map, name)
@doc """
The 1-based Mnesia tuple position of an attribute (the tag is position 1, the
first value position 2).
"""
def position(%Layout{index_of: map}, name), do: Map.fetch!(map, name) + 2
@doc """
Whether `name` carries a secondary index on the table — i.e. its 1-based tuple
position is in `indexed` (as `:mnesia.table_info(tag, :index)` reports). Indexes
are physical: they exist only because a migration created one. An attribute the
table doesn't have answers `false` (never raises).
"""
def indexed?(%Layout{index_of: map, indexed: indexed}, name) do
case Map.fetch(map, name) do
{:ok, slot} -> (slot + 2) in indexed
:error -> false
end
end
@doc false
# The ordered match variables `[:"$1", ..., :"$count"]`.
def match_vars(count) when count > 0, do: Enum.map(1..count, &:"$#{&1}")
defp read_from_mnesia(tag) do
from_attributes(tag, :mnesia.table_info(tag, :attributes),
type: :mnesia.table_info(tag, :type),
indexed: :mnesia.table_info(tag, :index)
)
end
defp cache(%Layout{} = layout, tag) do
:persistent_term.put(key(tag), layout)
layout
end
defp key(tag), do: {__MODULE__, tag}
end