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