lib/ecto/adapters/mnesia/query.ex

defmodule Ecto.Adapters.Mnesia.Query do
  @moduledoc """
  This module is responsible for building a query out of an Ecto Query.

  Based on the complexity of the query, different translators can be used:
  * `Ecto.Adapters.Mnesia.Query.Get` for simple `Repo.get` -like queries
  * `Ecto.Adapters.Mnesia.Query.Qlc` for more complex ones
  """
  alias Ecto.Query.BooleanExpr
  alias Ecto.Adapters.Mnesia
  alias Ecto.Adapters.Mnesia.Connection
  alias Ecto.Adapters.Mnesia.Query
  alias Ecto.Adapters.Mnesia.Record
  alias Ecto.Adapters.Mnesia.Source
  alias Ecto.Query.QueryExpr

  defstruct original: nil,
            type: nil,
            sources: nil,
            query: nil,
            sort: nil,
            answers: nil,
            new_record: nil,
            cache: :nocache

  @type t :: %__MODULE__{
          original: Ecto.Query.t(),
          type: :all | :update_all | :delete_all,
          sources: [Source.t()],
          query: (params :: list() -> query_handle :: :qlc.query_handle()),
          sort: (query_handle :: :qlc.query_handle() -> query_handle :: :qlc.query_handle()),
          answers: (query_handle :: :qlc.query_handle(), context :: Keyword.t() -> list(tuple())),
          new_record: (tuple(), list() -> tuple()),
          cache: :nocache | :cache
        }

  defmodule ImplSelector do
    @moduledoc false
    defstruct single_pkey?: false, join_query?: false, pk_query?: false, pk: nil
  end

  @callback query(select :: term(), joins :: term(), sources :: term()) ::
              {:cache | :nocache, (params :: term() -> term())}
  @callback sort(order_bys :: term(), select :: term(), sources :: term()) :: (term() -> term())
  @callback answers(limit :: term(), offset :: term()) ::
              (term(), context :: term() -> Enumerable.t())

  @spec from_ecto_query(type :: atom(), ecto_query :: Ecto.Query.t()) ::
          mnesia_query :: t()
  def from_ecto_query(type, %Ecto.Query{} = original) do
    impl = select_impl(original)

    %Mnesia.Query{original: original, type: type, cache: :cache}
    |> set_sources()
    |> set_query(impl)
    |> set_sort(impl)
    |> set_answers(impl)
    |> set_new_record()
  end

  @doc false
  def select_impl(original) do
    %ImplSelector{}
    |> single_pkey?(original)
    |> join_query?(original)
    |> pk_query?(original)
    |> case do
      %{single_pkey?: true, join_query?: false, pk_query?: true} -> Query.Get
      _ -> Query.Qlc
    end
  end

  defp set_sources(%__MODULE__{original: original} = q) do
    %{q | sources: sources(original.sources)}
  end

  defp set_query(%__MODULE__{original: original, sources: sources} = q, impl) do
    {cache, prepared} = impl.query(original.select, original.joins, sources)
    query = prepared.(original.wheres)
    %{q | query: query, cache: cache}
  end

  defp set_sort(%__MODULE__{original: original, sources: sources} = q, impl) do
    %{q | sort: impl.sort(original.order_bys, original.select, sources)}
  end

  defp set_answers(%__MODULE__{original: original} = q, impl) do
    %{q | answers: impl.answers(original.limit, original.offset)}
  end

  defp set_new_record(%__MODULE__{original: original, sources: sources} = q) do
    %{q | new_record: new_record(Enum.at(sources, 0), original.updates)}
  end

  defp single_pkey?(acc, %Ecto.Query{sources: {source}}) do
    with [source] <- sources({source}),
         [pk] <- source.schema.__schema__(:primary_key) do
      %{acc | single_pkey?: true, pk: pk}
    else
      _ ->
        %{acc | single_pkey?: false}
    end
  end

  defp single_pkey?(acc, _), do: %{acc | single_pkey?: false}

  defp join_query?(acc, %Ecto.Query{select: %Ecto.Query.SelectExpr{fields: fields}}) do
    Enum.any?(fields, fn
      {{:., [type: :id], _}, [], []} -> true
      _ -> false
    end)
    |> (&%{acc | join_query?: &1}).()
  end

  defp join_query?(acc, _), do: %{acc | join_query?: false}

  defp pk_query?(%{pk: pk} = acc, %Ecto.Query{
         wheres: [
           %BooleanExpr{
             expr:
               {:==, [],
                [{{:., [], [{:&, [], [_source_index]}, pk]}, [], []}, {:^, [], [_index]}]}
           }
         ]
       }) do
    %{acc | pk_query?: true}
  end

  defp pk_query?(acc, _), do: %{acc | pk_query?: false}

  defp sources(sources) do
    sources
    |> Tuple.to_list()
    |> Enum.map(&Connection.source/1)
  end

  defp new_record(source, updates) do
    fn tuple, params ->
      record = Record.new(tuple, source)

      params =
        params |> Enum.with_index() |> Enum.reduce(%{}, fn {p, i}, acc -> Map.put(acc, i, p) end)

      case updates do
        [%QueryExpr{expr: [set: replacements]}] ->
          replacements
          |> Enum.reduce(record, fn {field, {:^, [], [param_index]}}, acc ->
            Record.update(acc, [{field, Map.get(params, param_index)}], source)
          end)

        _ ->
          record
      end
    end
  end
end