lib/ecto/adapters/mnesia/constraint/foreign_key.ex

defmodule Ecto.Adapters.Mnesia.Constraint.ForeignKey do
  @moduledoc """
  Represents a foreignkey constraint
  """
  alias Ecto.Adapters.Mnesia.Source

  require Logger

  defstruct name: nil, from: nil, to: nil, fields: [], errors: []

  @type t :: %__MODULE__{
          name: String.t() | nil,
          from: Source.t() | nil,
          to: Source.t() | nil,
          fields: Keyword.t(),
          errors: [term()]
        }

  @type opt() :: {:name, String.t()}
  @type opts() :: [opt()]

  @doc """
  Returns newly created foreign key struct
  """
  @spec new(Source.t(), atom(), opts()) :: t()
  def new(from, rel, opts \\ [])

  def new(%Source{} = from, rel, opts) when is_atom(rel) and is_list(opts) do
    name =
      from.schema.__schema__(:association, rel)
      |> case do
        nil ->
          Keyword.fetch!(opts, :name)

        %Ecto.Association.BelongsTo{owner_key: col} ->
          Keyword.get_lazy(opts, :name, fn -> "#{from.table}_#{col}_fkey" end)
      end

    %__MODULE__{name: name, from: from}
    |> add_assoc(from.schema.__schema__(:association, rel))
  end

  def new(_, _, _) do
    %__MODULE__{errors: ["invalid arguments"]}
  end

  defp add_assoc(%{from: from, fields: fields} = c, %Ecto.Association.BelongsTo{} = a) do
    to = Source.new(%{schema: a.related})

    field =
      {from.schema.__schema__(:field_source, a.owner_key),
       to.schema.__schema__(:field_source, a.related_key)}

    %{c | to: to, fields: [field | fields]}
  end

  defimpl Ecto.Adapters.Mnesia.Constraint.Proto do
    alias Ecto.Adapters.Mnesia.Record

    def check(c, from_params) do
      c.fields
      |> Enum.reduce([], fn {from, to}, acc ->
        case Keyword.fetch(from_params, from) do
          {:ok, nil} -> acc
          {:ok, value} -> [{to, value} | acc]
          :error -> acc
        end
      end)
      |> case do
        [] ->
          :ok

        to_params ->
          pattern =
            c.to.match_all
            |> Record.update(to_params, c.to)

          case :mnesia.match_object(c.to.table, pattern, :read) do
            [] -> {:error, {:foreign_key, c.name}}
            _ -> :ok
          end
      end
    end

    def table(c), do: c.from.table
  end
end