Packages

A toolkit for data mapping and language integrated query for Elixir

Current section

Files

Jump to
ecto_tc lib ecto query builder windows.ex
Raw

lib/ecto/query/builder/windows.ex

import Kernel, except: [apply: 2]
defmodule Ecto.Query.Builder.Windows do
@moduledoc false
alias Ecto.Query.Builder
alias Ecto.Query.Builder.{GroupBy, OrderBy}
@sort_order [:partition_by, :order_by, :frame]
@doc """
Escapes a window params.
## Examples
iex> escape(quote do [order_by: [desc: 13]] end, {[], :acc}, [x: 0], __ENV__)
{[order_by: [desc: 13]], [], {[], :acc}}
"""
@spec escape([Macro.t], {list, term}, Keyword.t, Macro.Env.t | {Macro.Env.t, fun})
:: {Macro.t, [{atom, term}], {list, term}}
def escape(kw, params_acc, vars, env) when is_list(kw) do
{compile, runtime} = sort(@sort_order, kw, :compile, [], [])
{compile, params_acc} = Enum.map_reduce(compile, params_acc, &escape_compile(&1, &2, vars, env))
{compile, runtime, params_acc}
end
def escape(kw, _params_acc, _vars, _env) do
error!(kw)
end
defp sort([key | keys], kw, mode, compile, runtime) do
case Keyword.pop(kw, key) do
{nil, kw} ->
sort(keys, kw, mode, compile, runtime)
{{:^, _, [var]}, kw} ->
sort(keys, kw, :runtime, compile, [{key, var} | runtime])
{_, _} when mode == :runtime ->
[{runtime_key, _} | _] = runtime
raise ArgumentError, "window has an interpolated value under `#{runtime_key}` " <>
"and therefore `#{key}` must also be interpolated"
{expr, kw} ->
sort(keys, kw, mode, [{key, expr} | compile], runtime)
end
end
defp sort([], [], _mode, compile, runtime) do
{Enum.reverse(compile), Enum.reverse(runtime)}
end
defp sort([], kw, _mode, _compile, _runtime) do
error!(kw)
end
defp escape_compile({:partition_by, fields}, params_acc, vars, env) do
{fields, params_acc} = GroupBy.escape(:partition_by, fields, params_acc, vars, env)
{{:partition_by, fields}, params_acc}
end
defp escape_compile({:order_by, fields}, params_acc, vars, env) do
{fields, params_acc} = OrderBy.escape(:order_by, fields, params_acc, vars, env)
{{:order_by, fields}, params_acc}
end
defp escape_compile({:frame, frame_clause}, params_acc, vars, env) do
{frame_clause, params_acc} = escape_frame(frame_clause, params_acc, vars, env)
{{:frame, frame_clause}, params_acc}
end
defp escape_frame({:fragment, _, _} = fragment, params_acc, vars, env) do
Builder.escape(fragment, :any, params_acc, vars, env)
end
defp escape_frame(other, _, _, _) do
Builder.error!("expected a dynamic or fragment in `:frame`, got: `#{inspect other}`")
end
defp error!(other) do
Builder.error!(
"expected window definition to be a keyword list " <>
"with partition_by, order_by or frame as keys, got: `#{inspect other}`"
)
end
@doc """
Builds a quoted expression.
The quoted expression should evaluate to a query at runtime.
If possible, it does all calculations at compile time to avoid
runtime work.
"""
@spec build(Macro.t, [Macro.t], Keyword.t, Macro.Env.t) :: Macro.t
def build(query, binding, windows, env) when is_list(windows) do
{query, binding} = Builder.escape_binding(query, binding, env)
{compile, runtime} =
windows
|> Enum.map(&escape_window(binding, &1, env))
|> Enum.split_with(&elem(&1, 2) == [])
compile = Enum.map(compile, &build_compile_window(&1, env))
runtime = Enum.map(runtime, &build_runtime_window(&1, env))
query = Builder.apply_query(query, __MODULE__, [compile], env)
if runtime == [] do
query
else
quote do
Ecto.Query.Builder.Windows.runtime!(
unquote(query),
unquote(runtime),
unquote(env.file),
unquote(env.line)
)
end
end
end
def build(_, _, windows, _) do
Builder.error!(
"expected window definitions to be a keyword list with window names as keys and " <>
"a keyword list with the window definition as value, got: `#{inspect windows}`"
)
end
defp escape_window(vars, {name, expr}, env) do
{compile_acc, runtime_acc, {params, _}} = escape(expr, {[], :acc}, vars, env)
{name, compile_acc, runtime_acc, Builder.escape_params(params)}
end
defp build_compile_window({name, compile_acc, _, params}, env) do
{name,
quote do
%Ecto.Query.QueryExpr{
expr: unquote(compile_acc),
params: unquote(params),
file: unquote(env.file),
line: unquote(env.line)
}
end}
end
defp build_runtime_window({name, compile_acc, runtime_acc, params}, _env) do
{:{}, [], [name, Enum.reverse(compile_acc), runtime_acc, Enum.reverse(params)]}
end
@doc """
Invoked for runtime windows.
"""
def runtime!(query, runtime, file, line) do
windows =
Enum.map(runtime, fn {name, compile_acc, runtime_acc, params} ->
{acc, params} = do_runtime_window!(runtime_acc, query, compile_acc, params)
expr = %Ecto.Query.QueryExpr{expr: Enum.reverse(acc), params: Enum.reverse(params), file: file, line: line}
{name, expr}
end)
apply(query, windows)
end
defp do_runtime_window!([{:order_by, order_by} | kw], query, acc, params) do
{order_by, params} = OrderBy.order_by_or_distinct!(:order_by, query, order_by, params)
do_runtime_window!(kw, query, [{:order_by, order_by} | acc], params)
end
defp do_runtime_window!([{:partition_by, partition_by} | kw], query, acc, params) do
{partition_by, params} = GroupBy.group_or_partition_by!(:partition_by, query, partition_by, params)
do_runtime_window!(kw, query, [{:partition_by, partition_by} | acc], params)
end
defp do_runtime_window!([{:frame, frame} | kw], query, acc, params) do
case frame do
%Ecto.Query.DynamicExpr{} ->
{frame, params, _count} = Builder.Dynamic.partially_expand(:windows, query, frame, params, length(params))
do_runtime_window!(kw, query, [{:frame, frame} | acc], params)
_ ->
raise ArgumentError,
"expected a dynamic or fragment in `:frame`, got: `#{inspect frame}`"
end
end
defp do_runtime_window!([], _query, acc, params), do: {acc, params}
@doc """
The callback applied by `build/4` to build the query.
"""
@spec apply(Ecto.Queryable.t, Keyword.t) :: Ecto.Query.t
def apply(%Ecto.Query{windows: windows} = query, definitions) do
merged = Keyword.merge(windows, definitions, fn name, _, _ ->
Builder.error! "window with name #{name} is already defined"
end)
%{query | windows: merged}
end
def apply(query, definitions) do
apply(Ecto.Queryable.to_query(query), definitions)
end
end