windows.ex (6616B)
1 import Kernel, except: [apply: 2] 2 3 defmodule Ecto.Query.Builder.Windows do 4 @moduledoc false 5 6 alias Ecto.Query.Builder 7 alias Ecto.Query.Builder.{GroupBy, OrderBy} 8 @sort_order [:partition_by, :order_by, :frame] 9 10 @doc """ 11 Escapes a window params. 12 13 ## Examples 14 15 iex> escape(quote do [order_by: [desc: 13]] end, {[], %{}}, [x: 0], __ENV__) 16 {[order_by: [desc: 13]], [], {[], %{}}} 17 18 """ 19 @spec escape([Macro.t], {list, term}, Keyword.t, Macro.Env.t | {Macro.Env.t, fun}) 20 :: {Macro.t, [{atom, term}], {list, term}} 21 def escape(kw, params_acc, vars, env) when is_list(kw) do 22 {compile, runtime} = sort(@sort_order, kw, :compile, [], []) 23 {compile, params_acc} = Enum.map_reduce(compile, params_acc, &escape_compile(&1, &2, vars, env)) 24 {compile, runtime, params_acc} 25 end 26 27 def escape(kw, _params_acc, _vars, _env) do 28 error!(kw) 29 end 30 31 defp sort([key | keys], kw, mode, compile, runtime) do 32 case Keyword.pop(kw, key) do 33 {nil, kw} -> 34 sort(keys, kw, mode, compile, runtime) 35 36 {{:^, _, [var]}, kw} -> 37 sort(keys, kw, :runtime, compile, [{key, var} | runtime]) 38 39 {_, _} when mode == :runtime -> 40 [{runtime_key, _} | _] = runtime 41 raise ArgumentError, "window has an interpolated value under `#{runtime_key}` " <> 42 "and therefore `#{key}` must also be interpolated" 43 44 {expr, kw} -> 45 sort(keys, kw, mode, [{key, expr} | compile], runtime) 46 end 47 end 48 49 defp sort([], [], _mode, compile, runtime) do 50 {Enum.reverse(compile), Enum.reverse(runtime)} 51 end 52 53 defp sort([], kw, _mode, _compile, _runtime) do 54 error!(kw) 55 end 56 57 defp escape_compile({:partition_by, fields}, params_acc, vars, env) do 58 {fields, params_acc} = GroupBy.escape(:partition_by, fields, params_acc, vars, env) 59 {{:partition_by, fields}, params_acc} 60 end 61 62 defp escape_compile({:order_by, fields}, params_acc, vars, env) do 63 {fields, params_acc} = OrderBy.escape(:order_by, fields, params_acc, vars, env) 64 {{:order_by, fields}, params_acc} 65 end 66 67 defp escape_compile({:frame, frame_clause}, params_acc, vars, env) do 68 {frame_clause, params_acc} = escape_frame(frame_clause, params_acc, vars, env) 69 {{:frame, frame_clause}, params_acc} 70 end 71 72 defp escape_frame({:fragment, _, _} = fragment, params_acc, vars, env) do 73 Builder.escape(fragment, :any, params_acc, vars, env) 74 end 75 defp escape_frame(other, _, _, _) do 76 Builder.error!("expected a dynamic or fragment in `:frame`, got: `#{inspect other}`") 77 end 78 79 defp error!(other) do 80 Builder.error!( 81 "expected window definition to be a keyword list " <> 82 "with partition_by, order_by or frame as keys, got: `#{inspect other}`" 83 ) 84 end 85 86 @doc """ 87 Builds a quoted expression. 88 89 The quoted expression should evaluate to a query at runtime. 90 If possible, it does all calculations at compile time to avoid 91 runtime work. 92 """ 93 @spec build(Macro.t, [Macro.t], Keyword.t, Macro.Env.t) :: Macro.t 94 def build(query, binding, windows, env) when is_list(windows) do 95 {query, binding} = Builder.escape_binding(query, binding, env) 96 97 {compile, runtime} = 98 windows 99 |> Enum.map(&escape_window(binding, &1, env)) 100 |> Enum.split_with(&elem(&1, 2) == []) 101 102 compile = Enum.map(compile, &build_compile_window(&1, env)) 103 runtime = Enum.map(runtime, &build_runtime_window(&1, env)) 104 query = Builder.apply_query(query, __MODULE__, [compile], env) 105 106 if runtime == [] do 107 query 108 else 109 quote do 110 Ecto.Query.Builder.Windows.runtime!( 111 unquote(query), 112 unquote(runtime), 113 unquote(env.file), 114 unquote(env.line) 115 ) 116 end 117 end 118 end 119 120 def build(_, _, windows, _) do 121 Builder.error!( 122 "expected window definitions to be a keyword list with window names as keys and " <> 123 "a keyword list with the window definition as value, got: `#{inspect windows}`" 124 ) 125 end 126 127 defp escape_window(vars, {name, expr}, env) do 128 {compile_acc, runtime_acc, {params, _acc}} = escape(expr, {[], %{}}, vars, env) 129 {name, compile_acc, runtime_acc, Builder.escape_params(params)} 130 end 131 132 defp build_compile_window({name, compile_acc, _, params}, env) do 133 {name, 134 quote do 135 %Ecto.Query.QueryExpr{ 136 expr: unquote(compile_acc), 137 params: unquote(params), 138 file: unquote(env.file), 139 line: unquote(env.line) 140 } 141 end} 142 end 143 144 defp build_runtime_window({name, compile_acc, runtime_acc, params}, _env) do 145 {:{}, [], [name, Enum.reverse(compile_acc), runtime_acc, Enum.reverse(params)]} 146 end 147 148 @doc """ 149 Invoked for runtime windows. 150 """ 151 def runtime!(query, runtime, file, line) do 152 windows = 153 Enum.map(runtime, fn {name, compile_acc, runtime_acc, params} -> 154 {acc, params} = do_runtime_window!(runtime_acc, query, compile_acc, params) 155 expr = %Ecto.Query.QueryExpr{expr: Enum.reverse(acc), params: Enum.reverse(params), file: file, line: line} 156 {name, expr} 157 end) 158 159 apply(query, windows) 160 end 161 162 defp do_runtime_window!([{:order_by, order_by} | kw], query, acc, params) do 163 {order_by, params} = OrderBy.order_by_or_distinct!(:order_by, query, order_by, params) 164 do_runtime_window!(kw, query, [{:order_by, order_by} | acc], params) 165 end 166 167 defp do_runtime_window!([{:partition_by, partition_by} | kw], query, acc, params) do 168 {partition_by, params} = GroupBy.group_or_partition_by!(:partition_by, query, partition_by, params) 169 do_runtime_window!(kw, query, [{:partition_by, partition_by} | acc], params) 170 end 171 172 defp do_runtime_window!([{:frame, frame} | kw], query, acc, params) do 173 case frame do 174 %Ecto.Query.DynamicExpr{} -> 175 {frame, params, _count} = Builder.Dynamic.partially_expand(:windows, query, frame, params, length(params)) 176 do_runtime_window!(kw, query, [{:frame, frame} | acc], params) 177 178 _ -> 179 raise ArgumentError, 180 "expected a dynamic or fragment in `:frame`, got: `#{inspect frame}`" 181 end 182 end 183 184 defp do_runtime_window!([], _query, acc, params), do: {acc, params} 185 186 @doc """ 187 The callback applied by `build/4` to build the query. 188 """ 189 @spec apply(Ecto.Queryable.t, Keyword.t) :: Ecto.Query.t 190 def apply(%Ecto.Query{windows: windows} = query, definitions) do 191 merged = Keyword.merge(windows, definitions, fn name, _, _ -> 192 Builder.error! "window with name #{name} is already defined" 193 end) 194 195 %{query | windows: merged} 196 end 197 198 def apply(query, definitions) do 199 apply(Ecto.Queryable.to_query(query), definitions) 200 end 201 end