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