zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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