lib/ecto/adapters/mnesia/query/qlc.ex

defmodule Ecto.Adapters.Mnesia.Query.Qlc do
  @moduledoc """
  Builds qlc query out of Ecto.Query
  """
  require Ecto.Adapters.Mnesia.Query

  alias Ecto.Adapters.Mnesia.Query
  alias Ecto.Adapters.Mnesia.Query.Qlc.Context
  alias Ecto.Adapters.Mnesia.Source
  alias Ecto.Query.BooleanExpr
  alias Ecto.Query.QueryExpr
  alias Ecto.Query.SelectExpr

  @behaviour Query
  # @dialyzer {:nowarn_function, qlc_handle: 2}

  @order_mapping %{
    asc: :ascending,
    desc: :descending
  }

  def query(select, joins, sources) do
    context = Context.new(sources)

    q = fn
      [%BooleanExpr{}] = wheres -> build_query(select, joins, wheres, context)
      filters -> build_query(select, joins, filters, context)
    end

    {:cache, q}
  end

  def sort([], _select, _sources) do
    fn query -> query end
  end

  def sort(order_bys, select, sources) do
    context = Context.new(sources)

    fn query ->
      Enum.reduce(order_bys, query, fn
        %QueryExpr{expr: expr}, query1 ->
          Enum.reduce(expr, query1, fn {order, field_expr}, query2 ->
            field = field(field_expr, context)
            field_index = Enum.find_index(fields(select, context), fn e -> e == field end)
            :qlc.keysort(field_index + 1, query2, order: @order_mapping[order])
          end)
      end)
    end
  end

  @spec answers(limit :: %QueryExpr{} | nil, offset :: %QueryExpr{} | nil) ::
          (query_handle :: :qlc.query_handle(), context :: Keyword.t() -> list(tuple()))
  def answers(limit, offset) do
    fn query, params ->
      Stream.resource(
        fn ->
          limit = unbind_limit(limit, params)
          offset = unbind_offset(offset, params)
          cursor = :qlc.cursor(query)

          if offset > 0 do
            _ = :qlc.next_answers(cursor, offset)
          end

          {cursor, limit}
        end,
        fn
          {cursor, 0} ->
            {:halt, cursor}

          {cursor, limit} ->
            case :qlc.next_answers(cursor, limit) do
              [] -> {:halt, cursor}
              results -> {results, {cursor, max(0, limit - length(results))}}
            end
        end,
        fn cursor -> :qlc.delete_cursor(cursor) end
      )
    end
  end

  defp build_query(select, joins, filters, context) do
    {vars, generators} = select(select, context)

    context =
      context
      |> qualifiers(filters)
      |> joins(joins)

      binding_vars = Context.bindings(context)
      extra_bindings = Context.extra_bindings(context)

    pt_bindings =
      binding_vars
      |> Enum.map(& {&1, nil})
      |> Kernel.++(extra_bindings)

    expr = {:lc, anno(), vars, generators ++ context.joins ++ context.qualifiers}
    handle = qlc_handle(expr, pt_bindings)

    fn params ->
      bindings =
        binding_vars
        |> Enum.zip(params)
        |> Kernel.++(extra_bindings)

      {:value, qlc_lc, _} = :erl_eval.exprs(handle, bindings)
      :qlc.q(qlc_lc, [])
    end
  end

  @spec qlc_handle(any(), any()) :: any() | none()
  defp qlc_handle(expr, bindings) do
    {:ok, {:call, _, _, handle}} = :qlc_pt.transform_expression(expr, bindings)
    handle
  end

  defp anno, do: :erl_anno.new(1)

  defp unbind_limit(nil, _params), do: 10

  defp unbind_limit(%QueryExpr{expr: {:^, [], [param_index]}}, params) do
    Enum.at(params, param_index)
  end

  defp unbind_limit(%QueryExpr{expr: limit}, _params) when is_integer(limit), do: limit

  defp unbind_offset(nil, _context), do: 0

  defp unbind_offset(%QueryExpr{expr: {:^, [], [param_index]}}, params) do
    Enum.at(params, param_index)
  end

  defp unbind_offset(%QueryExpr{expr: offset}, _params) when is_integer(offset), do: offset

  defp select(select, context) do
    {q_fields(select, context),
     Enum.map(context.sources, fn source ->
       record_pattern = {:tuple, 1, [{:var, 1, :_} | Source.qlc_attributes_pattern(source)]}

       {:generate, 1, record_pattern,
        {:call, 1, {:remote, 1, {:atom, 1, :mnesia}, {:atom, 1, :table}},
         [{:atom, 1, source.table}]}}
     end)}
  end

  defp q_fields(%SelectExpr{fields: fields}, context) do
    {:tuple, 1, Enum.map(fields, &q_field(&1, context))}
  end

  defp q_fields(:all, %{sources: [source | _t]}) do
    {:tuple, 1, Source.qlc_attributes_pattern(source)}
  end

  defp q_fields(_, %{sources: [source | _t]}) do
    {:tuple, 1, Source.qlc_attributes_pattern(source)}
  end

  defp q_field({{_, _, [{:&, [], [source_index]}, field]}, [], []}, context) do
    {:var, 1, Source.to_erl_var(context.sources_index[source_index], field)}
  end

  defp q_field(_, _), do: nil

  defp fields(%SelectExpr{fields: fields}, context) do
    Enum.map(fields, &field(&1, context))
  end

  defp fields(:all, %{sources: [source | _t]}) do
    Source.qlc_attributes_pattern(source)
  end

  defp fields(_, %{sources: [source | _t]}) do
    Source.qlc_attributes_pattern(source)
  end

  defp field({{_, _, [{:&, [], [source_index]}, field]}, [], []}, context) do
    Source.to_erl_var(context.sources_index[source_index], field)
  end

  defp field(_, _), do: nil

  defp qualifiers(context, wheres) do
    context =
      wheres
      |> Enum.map(fn
        %BooleanExpr{expr: expr} -> expr
        {field, value} -> {field, value}
      end)
      |> Enum.reduce(context, fn where, acc ->
        {qlc, acc} = to_qlc(where, acc)
        %{acc | qualifiers: [qlc | acc.qualifiers]}
      end)

    %{context | qualifiers: Enum.reverse(context.qualifiers)}
  end

  defp joins(context, joins) do
    context =
      joins
      |> Enum.map(fn %{on: %{expr: expr}} -> expr end)
      |> Enum.reduce(context, fn join, acc ->
        {qlc, acc} = to_qlc(join, acc)
        %{acc | joins: [qlc | acc.joins]}
      end)

    %{context | joins: Enum.reverse(context.joins)}
  end

  # Returns erlang forms from Ecto Query AST
  defp to_qlc(true, context), do: {{:atom, 1, true}, context}

  defp to_qlc({field, value}, %{sources: [source]} = context) do
    erl_var = Source.to_erl_var(source, field)
    {var, context} = Context.extra_binding(context, value)
    {{:op, 1, :==, {:var, 1, erl_var}, {:var, 1, var}}, context}
  end

  defp to_qlc(
         {:and, [], [a, b]},
         context
       ) do
    {a_qlc, context} = to_qlc(a, context)
    {b_qlc, context} = to_qlc(b, context)
    {{:op, 1, :andalso, a_qlc, b_qlc}, context}
  end

  defp to_qlc(
         {:or, [], [a, b]},
         context
       ) do
    {a_qlc, context} = to_qlc(a, context)
    {b_qlc, context} = to_qlc(b, context)
    {{:op, 1, :orelse, a_qlc, b_qlc}, context}
  end

  defp to_qlc(
         {:is_nil, [], [{{:., [], [{:&, [], [source_index]}, field]}, [], []}]},
         context
       ) do
    erl_var = Context.source_var(context, source_index, field)
    {{:op, 1, :==, {:var, 1, erl_var}, {:atom, 1, nil}}, context}
  end

  defp to_qlc({:not, [], [expr]}, context) do
    {expr_qlc, context} = to_qlc(expr, context)
    {{:op, 1, :not, expr_qlc}, context}
  end

  defp to_qlc(
         {:in, [],
          [{{:., [], [{:&, [], [source_index]}, field]}, [], []}, {:^, [], [index, length]}]},
         context
       ) do
    erl_var = Context.source_var(context, source_index, field)

    {binding_vars, context} =
      Enum.reduce((index + (length - 1))..index, {{nil, 1}, context}, fn i, {acc, context} ->
        {var, context} = Context.binding_var(context, i)
        {{:cons, 1, {:var, 1, var}, acc}, context}
      end)

    {{:call, 1, {:remote, 1, {:atom, 1, :lists}, {:atom, 1, :member}},
      [{:var, 1, erl_var}, binding_vars]}, context}
  end

  defp to_qlc(
         {:in, [], [{{:., [], [{:&, [], [source_index]}, field]}, [], []}, values]},
         context
       )
       when is_list(values) do
    erl_var = Context.source_var(context, source_index, field)
    {var, context} = Context.extra_binding(context, values)

    {{:call, 1, {:remote, 1, {:atom, 1, :lists}, {:atom, 1, :member}},
      [{:var, 1, erl_var}, {:var, 1, var}]}, context}
  end

  defp to_qlc(
         {op, [], [{{:., [], [{:&, [], [source_index]}, field]}, [], []}, {:^, [], [index]}]},
         context
       ) do
    erl_var = Context.source_var(context, source_index, field)
    {var, context} = Context.binding_var(context, index)
    {{:op, 1, to_qlc_op(op), {:var, 1, erl_var}, {:var, 1, var}}, context}
  end

  defp to_qlc(
         {op, [],
          [
            {{:., [], [{:&, [], [left_source_index]}, left_field]}, [], []},
            {{:., [], [{:&, [], [right_source_index]}, right_field]}, [], []}
          ]},
         context
       ) do
    left_erl_var = Context.source_var(context, left_source_index, left_field)
    right_erl_var = Context.source_var(context, right_source_index, right_field)
    {{:op, 1, to_qlc_op(op), {:var, 1, left_erl_var}, {:var, 1, right_erl_var}}, context}
  end

  defp to_qlc(
         {op, [], [{{:., [], [{:&, [], [source_index]}, field]}, [], []}, value]},
         context
       ) do
    erl_var = Context.source_var(context, source_index, field)
    {var, context} = Context.extra_binding(context, value)
    {{:op, 1, to_qlc_op(op), {:var, 1, erl_var}, {:var, 1, var}}, context}
  end

  defp to_qlc_op(:!=), do: :"=/="
  defp to_qlc_op(:<=), do: :"=<"
  defp to_qlc_op(op), do: op
end