Current section
Files
Jump to
Current section
Files
lib/aggregate.ex
defmodule QueryBuilder.Aggregate do
@moduledoc false
require Ecto.Query
import QueryBuilder.Utils
defstruct op: nil, arg: nil, modifier: nil, order_by: nil, filter: nil
def to_dynamic(assoc_list, %__MODULE__{} = aggregate) do
to_dynamic_with_resolver(aggregate, &find_field_and_binding_from_token(assoc_list, &1))
end
defp to_dynamic_with_resolver(%__MODULE__{op: :count, arg: nil}, _resolve) do
Ecto.Query.dynamic([], count())
end
defp to_dynamic_with_resolver(%__MODULE__{op: :count, arg: token, modifier: :distinct}, resolve)
when is_atom(token) or is_binary(token) do
{field, binding} = resolve.(token)
Ecto.Query.dynamic([{^binding, x}], count(field(x, ^field), :distinct))
end
defp to_dynamic_with_resolver(%__MODULE__{op: :count, arg: token, modifier: nil}, resolve)
when is_atom(token) or is_binary(token) do
{field, binding} = resolve.(token)
Ecto.Query.dynamic([{^binding, x}], count(field(x, ^field)))
end
defp to_dynamic_with_resolver(%__MODULE__{op: :avg, arg: token, modifier: nil}, resolve)
when is_atom(token) or is_binary(token) do
{field, binding} = resolve.(token)
Ecto.Query.dynamic([{^binding, x}], avg(field(x, ^field)))
end
defp to_dynamic_with_resolver(%__MODULE__{op: :sum, arg: token, modifier: nil}, resolve)
when is_atom(token) or is_binary(token) do
{field, binding} = resolve.(token)
Ecto.Query.dynamic([{^binding, x}], sum(field(x, ^field)))
end
defp to_dynamic_with_resolver(%__MODULE__{op: :min, arg: token, modifier: nil}, resolve)
when is_atom(token) or is_binary(token) do
{field, binding} = resolve.(token)
Ecto.Query.dynamic([{^binding, x}], min(field(x, ^field)))
end
defp to_dynamic_with_resolver(%__MODULE__{op: :max, arg: token, modifier: nil}, resolve)
when is_atom(token) or is_binary(token) do
{field, binding} = resolve.(token)
Ecto.Query.dynamic([{^binding, x}], max(field(x, ^field)))
end
defp to_dynamic_with_resolver(
%__MODULE__{
op: :array_agg,
arg: token,
modifier: modifier,
order_by: order_by,
filter: filter
},
resolve
)
when is_atom(token) or is_binary(token) do
{field, binding} = resolve.(token)
value_dynamic = Ecto.Query.dynamic([{^binding, x}], field(x, ^field))
order_by = List.wrap(order_by)
distinct? = modifier == :distinct
order_terms =
Enum.map(order_by, fn {direction, expr} ->
expr_dynamic = order_expr_to_dynamic!(expr, resolve)
order_term_to_dynamic!(direction, expr_dynamic)
end)
agg_dynamic = array_agg_dynamic!(value_dynamic, order_terms, distinct?)
filter_dynamic = filter_to_dynamic!(filter, resolve)
case filter_dynamic do
nil ->
agg_dynamic
%Ecto.Query.DynamicExpr{} = filter_dynamic ->
Ecto.Query.dynamic([], fragment("? FILTER (WHERE ?)", ^agg_dynamic, ^filter_dynamic))
end
end
defp to_dynamic_with_resolver(%__MODULE__{} = aggregate, _resolve) do
raise ArgumentError, "invalid aggregate expression: #{inspect(aggregate)}"
end
defp order_expr_to_dynamic!(%Ecto.Query.DynamicExpr{} = dynamic, _resolve), do: dynamic
defp order_expr_to_dynamic!(fun, resolve) when is_function(fun, 1) do
case fun.(resolve) do
%Ecto.Query.DynamicExpr{} = dynamic ->
dynamic
other ->
raise ArgumentError,
"array_agg/2 expects order_by functions to return an Ecto dynamic expression, got: #{inspect(other)}"
end
end
defp order_expr_to_dynamic!(token, resolve) when is_atom(token) or is_binary(token) do
{field, binding} = resolve.(token)
Ecto.Query.dynamic([{^binding, x}], field(x, ^field))
end
defp order_expr_to_dynamic!(other, _resolve) do
raise ArgumentError,
"array_agg/2 expects order_by expressions to be tokens (atoms/strings), dynamics, or 1-arity functions; got: #{inspect(other)}"
end
defp order_term_to_dynamic!(direction, %Ecto.Query.DynamicExpr{} = expr_dynamic)
when is_atom(direction) do
case direction do
:asc ->
Ecto.Query.dynamic([], fragment("? ASC", ^expr_dynamic))
:desc ->
Ecto.Query.dynamic([], fragment("? DESC", ^expr_dynamic))
:asc_nulls_first ->
Ecto.Query.dynamic([], fragment("? ASC NULLS FIRST", ^expr_dynamic))
:asc_nulls_last ->
Ecto.Query.dynamic([], fragment("? ASC NULLS LAST", ^expr_dynamic))
:desc_nulls_first ->
Ecto.Query.dynamic([], fragment("? DESC NULLS FIRST", ^expr_dynamic))
:desc_nulls_last ->
Ecto.Query.dynamic([], fragment("? DESC NULLS LAST", ^expr_dynamic))
other ->
raise ArgumentError, "unsupported order_by direction for array_agg/2: #{inspect(other)}"
end
end
defp order_term_to_dynamic!(direction, _expr_dynamic) do
raise ArgumentError,
"array_agg/2 expects order_by directions to be atoms, got: #{inspect(direction)}"
end
defp filter_to_dynamic!(nil, _resolve), do: nil
defp filter_to_dynamic!([], _resolve), do: nil
defp filter_to_dynamic!(%Ecto.Query.DynamicExpr{} = dynamic, _resolve), do: dynamic
defp filter_to_dynamic!(fun, resolve) when is_function(fun, 1) do
case fun.(resolve) do
%Ecto.Query.DynamicExpr{} = dynamic ->
dynamic
other ->
raise ArgumentError,
"array_agg/2 expects filter functions to return an Ecto dynamic expression, got: #{inspect(other)}"
end
end
defp filter_to_dynamic!(filters, resolve) when is_list(filters) or is_tuple(filters) do
QueryBuilder.Query.Where.build_dynamic_query_with_resolver(nil, filters, [], resolve)
end
defp filter_to_dynamic!(other, _resolve) do
raise ArgumentError,
"array_agg/2 expects filter to be a keyword list, a list of filters, a filter tuple, a dynamic, or a 1-arity function; got: #{inspect(other)}"
end
defp array_agg_dynamic!(value_dynamic, order_terms, distinct?) do
case {distinct?, order_terms} do
{false, []} ->
Ecto.Query.dynamic([], fragment("array_agg(?)", ^value_dynamic))
{true, []} ->
Ecto.Query.dynamic([], fragment("array_agg(DISTINCT ?)", ^value_dynamic))
{false, [t1]} ->
Ecto.Query.dynamic([], fragment("array_agg(? ORDER BY ?)", ^value_dynamic, ^t1))
{true, [t1]} ->
Ecto.Query.dynamic([], fragment("array_agg(DISTINCT ? ORDER BY ?)", ^value_dynamic, ^t1))
{false, [t1, t2]} ->
Ecto.Query.dynamic([], fragment("array_agg(? ORDER BY ?, ?)", ^value_dynamic, ^t1, ^t2))
{true, [t1, t2]} ->
Ecto.Query.dynamic(
[],
fragment("array_agg(DISTINCT ? ORDER BY ?, ?)", ^value_dynamic, ^t1, ^t2)
)
{false, [t1, t2, t3]} ->
Ecto.Query.dynamic(
[],
fragment("array_agg(? ORDER BY ?, ?, ?)", ^value_dynamic, ^t1, ^t2, ^t3)
)
{true, [t1, t2, t3]} ->
Ecto.Query.dynamic(
[],
fragment("array_agg(DISTINCT ? ORDER BY ?, ?, ?)", ^value_dynamic, ^t1, ^t2, ^t3)
)
{false, [t1, t2, t3, t4]} ->
Ecto.Query.dynamic(
[],
fragment("array_agg(? ORDER BY ?, ?, ?, ?)", ^value_dynamic, ^t1, ^t2, ^t3, ^t4)
)
{true, [t1, t2, t3, t4]} ->
Ecto.Query.dynamic(
[],
fragment(
"array_agg(DISTINCT ? ORDER BY ?, ?, ?, ?)",
^value_dynamic,
^t1,
^t2,
^t3,
^t4
)
)
{false, [t1, t2, t3, t4, t5]} ->
Ecto.Query.dynamic(
[],
fragment("array_agg(? ORDER BY ?, ?, ?, ?, ?)", ^value_dynamic, ^t1, ^t2, ^t3, ^t4, ^t5)
)
{true, [t1, t2, t3, t4, t5]} ->
Ecto.Query.dynamic(
[],
fragment(
"array_agg(DISTINCT ? ORDER BY ?, ?, ?, ?, ?)",
^value_dynamic,
^t1,
^t2,
^t3,
^t4,
^t5
)
)
{_distinct?, order_terms} ->
raise ArgumentError,
"array_agg/2 supports up to 5 order_by terms, got: #{inspect(length(order_terms))}"
end
end
def comparison_fun(%__MODULE__{} = aggregate, operator, value, operator_opts \\ []) do
fn resolve ->
aggregate
|> compare_dynamic(resolve, operator, value, operator_opts)
end
end
def normalize_having_filters(filters) when is_list(filters) do
Enum.map(filters, fn
%__MODULE__{} = aggregate ->
raise ArgumentError,
"invalid having filter: aggregate expression #{inspect(aggregate)} must be compared; " <>
"expected `{aggregate, value}` or `{aggregate, operator, value}` (e.g. `{count(:id), :gt, 2}`)"
{%__MODULE__{} = aggregate, value} ->
comparison_fun(aggregate, :eq, value)
{%__MODULE__{} = aggregate, operator, value} when is_atom(operator) ->
comparison_fun(aggregate, operator, value)
{%__MODULE__{} = aggregate, operator, _value} ->
raise ArgumentError,
"invalid having filter: aggregate expression #{inspect(aggregate)} expects an atom operator; " <>
"got: #{inspect(operator)}"
{%__MODULE__{} = aggregate, operator, value, operator_opts} ->
cond do
not is_atom(operator) ->
raise ArgumentError,
"invalid having filter: aggregate expression #{inspect(aggregate)} expects an atom operator; " <>
"got: #{inspect(operator)}"
not is_list(operator_opts) ->
raise ArgumentError,
"invalid having filter: aggregate expression #{inspect(aggregate)} expects a list of operator options; " <>
"got: #{inspect(operator_opts)}"
true ->
comparison_fun(aggregate, operator, value, operator_opts)
end
{%__MODULE__{} = aggregate, _a, _b, _c, _d} = tuple ->
raise ArgumentError,
"invalid having filter: aggregate expression #{inspect(aggregate)} has an unsupported shape; " <>
"got: #{inspect(tuple)}"
other ->
other
end)
end
def normalize_having_or_filters(or_filters) when is_list(or_filters) do
Enum.map(or_filters, fn
{:or, group} when is_list(group) ->
{:or, normalize_having_filters(group)}
other ->
other
end)
end
defp compare_dynamic(%__MODULE__{} = aggregate, resolve, operator, value, operator_opts) do
if operator_opts != [] do
raise ArgumentError,
"aggregate filter does not support operator options, got: #{inspect(operator_opts)}"
end
op = normalize_operator(operator)
agg_dynamic = to_dynamic_with_resolver(aggregate, resolve)
case {op, value} do
{:eq, nil} ->
Ecto.Query.dynamic(is_nil(^agg_dynamic))
{:ne, nil} ->
Ecto.Query.dynamic(not is_nil(^agg_dynamic))
{:eq, _} ->
Ecto.Query.dynamic(^agg_dynamic == ^value)
{:ne, _} ->
Ecto.Query.dynamic(^agg_dynamic != ^value)
{:gt, _} ->
Ecto.Query.dynamic(^agg_dynamic > ^value)
{:ge, _} ->
Ecto.Query.dynamic(^agg_dynamic >= ^value)
{:lt, _} ->
Ecto.Query.dynamic(^agg_dynamic < ^value)
{:le, _} ->
Ecto.Query.dynamic(^agg_dynamic <= ^value)
{:in, _} ->
Ecto.Query.dynamic(^agg_dynamic in ^value)
{:not_in, _} ->
Ecto.Query.dynamic(^agg_dynamic not in ^value)
{other, _} ->
raise ArgumentError,
"unsupported aggregate filter operator #{inspect(other)} for #{inspect(aggregate)}"
end
end
defp normalize_operator(op) when op in [:eq, :equal_to], do: :eq
defp normalize_operator(op) when op in [:ne, :other_than], do: :ne
defp normalize_operator(op) when op in [:gt, :greater_than], do: :gt
defp normalize_operator(op) when op in [:ge, :greater_than_or_equal_to], do: :ge
defp normalize_operator(op) when op in [:lt, :less_than], do: :lt
defp normalize_operator(op) when op in [:le, :less_than_or_equal_to], do: :le
defp normalize_operator(:in), do: :in
defp normalize_operator(:not_in), do: :not_in
defp normalize_operator(other), do: other
end