group_by.ex (3794B)
1 import Kernel, except: [apply: 2] 2 3 defmodule Ecto.Query.Builder.GroupBy do 4 @moduledoc false 5 6 alias Ecto.Query.Builder 7 8 @doc """ 9 Escapes a list of quoted expressions. 10 11 See `Ecto.Builder.escape/2`. 12 13 iex> escape(:group_by, quote do [x.x, 13] end, {[], %{}}, [x: 0], __ENV__) 14 {[{:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [0]]}, :x]]}, [], []]}, 15 13], 16 {[], %{}}} 17 """ 18 @spec escape(:group_by | :partition_by, Macro.t, {list, term}, Keyword.t, Macro.Env.t) :: 19 {Macro.t, {list, term}} 20 def escape(kind, expr, params_acc, vars, env) do 21 expr 22 |> List.wrap 23 |> Enum.map_reduce(params_acc, &do_escape(&1, &2, kind, vars, env)) 24 end 25 26 defp do_escape({:^, _, [expr]}, params_acc, kind, _vars, _env) do 27 {quote(do: Ecto.Query.Builder.GroupBy.field!(unquote(kind), unquote(expr))), params_acc} 28 end 29 30 defp do_escape(field, params_acc, _kind, _vars, _env) when is_atom(field) do 31 {Macro.escape(to_field(field)), params_acc} 32 end 33 34 defp do_escape(expr, params_acc, _kind, vars, env) do 35 Builder.escape(expr, :any, params_acc, vars, env) 36 end 37 38 @doc """ 39 Called at runtime to verify a field. 40 """ 41 def field!(_kind, field) when is_atom(field), 42 do: to_field(field) 43 def field!(kind, other) do 44 raise ArgumentError, 45 "expected a field as an atom in `#{kind}`, got: `#{inspect other}`" 46 end 47 48 @doc """ 49 Shared between group_by and partition_by. 50 """ 51 def group_or_partition_by!(kind, query, exprs, params) do 52 {expr, {params, _}} = 53 Enum.map_reduce(List.wrap(exprs), {params, length(params)}, fn 54 field, params_count when is_atom(field) -> 55 {to_field(field), params_count} 56 57 %Ecto.Query.DynamicExpr{} = dynamic, {params, count} -> 58 {expr, params, count} = Builder.Dynamic.partially_expand(kind, query, dynamic, params, count) 59 {expr, {params, count}} 60 61 other, _params_count -> 62 raise ArgumentError, 63 "expected a list of fields and dynamics in `#{kind}`, got: `#{inspect other}`" 64 end) 65 66 {expr, params} 67 end 68 69 defp to_field(field), do: {{:., [], [{:&, [], [0]}, field]}, [], []} 70 71 @doc """ 72 Called at runtime to assemble group_by. 73 """ 74 def group_by!(query, group_by, file, line) do 75 {expr, params} = group_or_partition_by!(:group_by, query, group_by, []) 76 expr = %Ecto.Query.QueryExpr{expr: expr, params: Enum.reverse(params), line: line, file: file} 77 apply(query, expr) 78 end 79 80 @doc """ 81 Builds a quoted expression. 82 83 The quoted expression should evaluate to a query at runtime. 84 If possible, it does all calculations at compile time to avoid 85 runtime work. 86 """ 87 @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 88 def build(query, _binding, {:^, _, [var]}, env) do 89 quote do 90 Ecto.Query.Builder.GroupBy.group_by!(unquote(query), unquote(var), unquote(env.file), unquote(env.line)) 91 end 92 end 93 94 def build(query, binding, expr, env) do 95 {query, binding} = Builder.escape_binding(query, binding, env) 96 {expr, {params, _acc}} = escape(:group_by, expr, {[], %{}}, binding, env) 97 params = Builder.escape_params(params) 98 99 group_by = quote do: %Ecto.Query.QueryExpr{ 100 expr: unquote(expr), 101 params: unquote(params), 102 file: unquote(env.file), 103 line: unquote(env.line)} 104 Builder.apply_query(query, __MODULE__, [group_by], env) 105 end 106 107 @doc """ 108 The callback applied by `build/4` to build the query. 109 """ 110 @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 111 def apply(%Ecto.Query{group_bys: group_bys} = query, expr) do 112 %{query | group_bys: group_bys ++ [expr]} 113 end 114 def apply(query, expr) do 115 apply(Ecto.Queryable.to_query(query), expr) 116 end 117 end