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