zf

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

filter.ex (7232B)


      1 import Kernel, except: [apply: 3]
      2 
      3 defmodule Ecto.Query.Builder.Filter do
      4   @moduledoc false
      5 
      6   alias Ecto.Query.Builder
      7 
      8   @doc """
      9   Escapes a where or having clause.
     10 
     11   It allows query expressions that evaluate to a boolean
     12   or a keyword list of field names and values. In a keyword
     13   list multiple key value pairs will be joined with "and".
     14 
     15   Returned is `{expression, {params, subqueries}}` which is
     16   a valid escaped expression, see `Macro.escape/2`. Both params
     17   and subqueries are reversed.
     18   """
     19   @spec escape(:where | :having | :on, Macro.t, non_neg_integer, Keyword.t, Macro.Env.t) :: {Macro.t, {list, list}}
     20   def escape(_kind, [], _binding, _vars, _env) do
     21     {true, {[], %{subqueries: []}}}
     22   end
     23 
     24   def escape(kind, expr, binding, vars, env) when is_list(expr) do
     25     {parts, params_acc} =
     26       Enum.map_reduce(expr, {[], %{subqueries: []}}, fn
     27         {field, nil}, _params_acc ->
     28           Builder.error! "nil given for `#{field}`. Comparison with nil is forbidden as it is unsafe. " <>
     29                          "Instead write a query with is_nil/1, for example: is_nil(s.#{field})"
     30 
     31         {field, value}, params_acc when is_atom(field) ->
     32           value = check_for_nils(value, field)
     33           {value, params_acc} = Builder.escape(value, {binding, field}, params_acc, vars, env)
     34           {{:{}, [], [:==, [], [to_escaped_field(binding, field), value]]}, params_acc}
     35 
     36         _, _params_acc ->
     37           Builder.error! "expected a keyword list at compile time in #{kind}, " <>
     38                          "got: `#{Macro.to_string expr}`. If you would like to " <>
     39                          "pass a list dynamically, please interpolate the whole list with ^"
     40       end)
     41 
     42     expr = Enum.reduce parts, &{:{}, [], [:and, [], [&2, &1]]}
     43     {expr, params_acc}
     44   end
     45 
     46   def escape(_kind, expr, _binding, vars, env) do
     47     Builder.escape(expr, :boolean, {[], %{subqueries: []}}, vars, env)
     48   end
     49 
     50   @doc """
     51   Builds a quoted expression.
     52 
     53   The quoted expression should evaluate to a query at runtime.
     54   If possible, it does all calculations at compile time to avoid
     55   runtime work.
     56   """
     57   @spec build(:where | :having, :and | :or, Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
     58   def build(kind, op, query, _binding, {:^, _, [var]}, env) do
     59     quote do
     60       Ecto.Query.Builder.Filter.filter!(unquote(kind), unquote(op), unquote(query),
     61                                         unquote(var), 0, unquote(env.file), unquote(env.line))
     62     end
     63   end
     64 
     65   def build(kind, op, query, binding, expr, env) do
     66     {query, binding} = Builder.escape_binding(query, binding, env)
     67     {expr, {params, acc}} = escape(kind, expr, 0, binding, env)
     68 
     69     params = Builder.escape_params(params)
     70     subqueries = Enum.reverse(acc.subqueries)
     71 
     72     expr = quote do: %Ecto.Query.BooleanExpr{
     73                         expr: unquote(expr),
     74                         op: unquote(op),
     75                         params: unquote(params),
     76                         subqueries: unquote(subqueries),
     77                         file: unquote(env.file),
     78                         line: unquote(env.line)}
     79     Builder.apply_query(query, __MODULE__, [kind, expr], env)
     80   end
     81 
     82   @doc """
     83   The callback applied by `build/4` to build the query.
     84   """
     85   @spec apply(Ecto.Queryable.t, :where | :having, term) :: Ecto.Query.t
     86   def apply(query, _, %{expr: true}) do
     87     query
     88   end
     89   def apply(%Ecto.Query{wheres: wheres} = query, :where, expr) do
     90     %{query | wheres: wheres ++ [expr]}
     91   end
     92   def apply(%Ecto.Query{havings: havings} = query, :having, expr) do
     93     %{query | havings: havings ++ [expr]}
     94   end
     95   def apply(query, kind, expr) do
     96     apply(Ecto.Queryable.to_query(query), kind, expr)
     97   end
     98 
     99   @doc """
    100   Builds a filter based on the given arguments.
    101 
    102   This is shared by having, where and join's on expressions.
    103   """
    104   def filter!(kind, query, %Ecto.Query.DynamicExpr{} = dynamic, _binding, _file, _line) do
    105     {expr, _binding, params, subqueries, file, line} =
    106       Ecto.Query.Builder.Dynamic.fully_expand(query, dynamic)
    107 
    108     if subqueries != [] do
    109       raise ArgumentError, "subqueries are not allowed in `#{kind}` expressions"
    110     end
    111 
    112     {expr, params, file, line}
    113   end
    114 
    115   def filter!(_kind, _query, bool, _binding, file, line) when is_boolean(bool) do
    116     {bool, [], file, line}
    117   end
    118 
    119   def filter!(kind, _query, kw, binding, file, line) when is_list(kw) do
    120     {expr, params} = kw!(kind, kw, binding)
    121     {expr, params, file, line}
    122   end
    123 
    124   def filter!(kind, _query, other, _binding, _file, _line) do
    125     raise ArgumentError, "expected a keyword list or dynamic expression in `#{kind}`, got: `#{inspect other}`"
    126   end
    127 
    128   @doc """
    129   Builds the filter and applies it to the given query as boolean operator.
    130   """
    131   def filter!(:where, op, query, %Ecto.Query.DynamicExpr{} = dynamic, _binding, _file, _line) do
    132     {expr, _binding, params, subqueries, file, line} =
    133       Ecto.Query.Builder.Dynamic.fully_expand(query, dynamic)
    134 
    135     boolean = %Ecto.Query.BooleanExpr{
    136       expr: expr,
    137       params: params,
    138       line: line,
    139       file: file,
    140       op: op,
    141       subqueries: subqueries
    142     }
    143 
    144     apply(query, :where, boolean)
    145   end
    146 
    147   def filter!(kind, op, query, expr, binding, file, line) do
    148     {expr, params, file, line} = filter!(kind, query, expr, binding, file, line)
    149     boolean = %Ecto.Query.BooleanExpr{expr: expr, params: params, line: line, file: file, op: op}
    150     apply(query, kind, boolean)
    151   end
    152 
    153   defp kw!(kind, kw, binding) do
    154     case kw!(kw, binding, 0, [], [], kind, kw) do
    155       {[], params} -> {true, params}
    156       {parts, params} -> {Enum.reduce(parts, &{:and, [], [&2, &1]}), params}
    157     end
    158   end
    159 
    160   defp kw!([{field, nil}|_], _binding, _counter, _exprs, _params, _kind, _original) when is_atom(field) do
    161     raise ArgumentError, "nil given for #{inspect field}. Comparison with nil is forbidden as it is unsafe. " <>
    162                          "Instead write a query with is_nil/1, for example: is_nil(s.#{field})"
    163   end
    164   defp kw!([{field, value}|t], binding, counter, exprs, params, kind, original) when is_atom(field) do
    165     kw!(t, binding, counter + 1,
    166         [{:==, [], [to_field(binding, field), {:^, [], [counter]}]}|exprs],
    167         [{value, {binding, field}}|params],
    168         kind, original)
    169   end
    170   defp kw!([], _binding, _counter, exprs, params, _kind, _original) do
    171     {Enum.reverse(exprs), Enum.reverse(params)}
    172   end
    173   defp kw!(_, _binding, _counter, _exprs, _params, kind, original) do
    174     raise ArgumentError, "expected a keyword list in `#{kind}`, got: `#{inspect original}`"
    175   end
    176 
    177   defp to_field(binding, field),
    178     do: {{:., [], [{:&, [], [binding]}, field]}, [], []}
    179   defp to_escaped_field(binding, field),
    180     do: {:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [binding]]}, field]]}, [], []]}
    181 
    182   defp check_for_nils({:^, _, [var]}, field) do
    183     quote do
    184       ^Ecto.Query.Builder.Filter.not_nil!(unquote(var), unquote(field))
    185     end
    186   end
    187 
    188   defp check_for_nils(value, _field), do: value
    189 
    190   def not_nil!(nil, field) do
    191     raise ArgumentError, "nil given for `#{field}`. comparison with nil is forbidden as it is unsafe. " <>
    192                            "Instead write a query with is_nil/1, for example: is_nil(s.#{field})"
    193   end
    194 
    195   def not_nil!(other, _field), do: other
    196 end