update.ex (6530B)
1 import Kernel, except: [apply: 2] 2 3 defmodule Ecto.Query.Builder.Update do 4 @moduledoc false 5 6 @keys [:set, :inc, :push, :pull] 7 alias Ecto.Query.Builder 8 9 @doc """ 10 Escapes a list of quoted expressions. 11 12 iex> escape([], [], __ENV__) 13 {[], [], []} 14 15 iex> escape([set: []], [], __ENV__) 16 {[], [], []} 17 18 iex> escape(quote(do: ^[set: []]), [], __ENV__) 19 {[], [set: []], []} 20 21 iex> escape(quote(do: [set: ^[foo: 1]]), [], __ENV__) 22 {[], [set: [foo: 1]], []} 23 24 iex> escape(quote(do: [set: [foo: ^1]]), [], __ENV__) 25 {[], [set: [foo: 1]], []} 26 27 """ 28 @spec escape(Macro.t, Keyword.t, Macro.Env.t) :: {Macro.t, Macro.t, list} 29 def escape(expr, vars, env) when is_list(expr) do 30 escape_op(expr, [], [], [], vars, env) 31 end 32 33 def escape({:^, _, [v]}, _vars, _env) do 34 {[], v, []} 35 end 36 37 def escape(expr, _vars, _env) do 38 compile_error!(expr) 39 end 40 41 defp escape_op([{k, v}|t], compile, runtime, params, vars, env) when is_atom(k) and is_list(v) do 42 validate_op!(k) 43 {compile_values, runtime_values, params} = escape_kw(k, v, params, vars, env) 44 compile = 45 if compile_values == [], do: compile, else: [{k, Enum.reverse(compile_values)} | compile] 46 runtime = 47 if runtime_values == [], do: runtime, else: [{k, Enum.reverse(runtime_values)} | runtime] 48 escape_op(t, compile, runtime, params, vars, env) 49 end 50 51 defp escape_op([{k, {:^, _, [v]}}|t], compile, runtime, params, vars, env) when is_atom(k) do 52 validate_op!(k) 53 escape_op(t, compile, [{k, v}|runtime], params, vars, env) 54 end 55 56 defp escape_op([], compile, runtime, params, _vars, _env) do 57 {Enum.reverse(compile), Enum.reverse(runtime), params} 58 end 59 60 defp escape_op(expr, _compile, _runtime, _params, _vars, _env) do 61 compile_error!(expr) 62 end 63 64 defp escape_kw(op, kw, params, vars, env) do 65 Enum.reduce kw, {[], [], params}, fn 66 {k, {:^, _, [v]}}, {compile, runtime, params} when is_atom(k) -> 67 {compile, [{k, v} | runtime], params} 68 {k, v}, {compile, runtime, params} -> 69 k = escape_field!(k) 70 {v, {params, _acc}} = Builder.escape(v, type_for_key(op, {0, k}), {params, %{}}, vars, env) 71 {[{k, v} | compile], runtime, params} 72 _, _acc -> 73 Builder.error! "malformed #{inspect op} in update `#{Macro.to_string(kw)}`, " <> 74 "expected a keyword list" 75 end 76 end 77 78 defp escape_field!({:^, _, [k]}), do: quote(do: Ecto.Query.Builder.Update.field!(unquote(k))) 79 defp escape_field!(k) when is_atom(k), do: k 80 81 defp escape_field!(k) do 82 Builder.error!( 83 "expected an atom field or an interpolated field in `update`, got `#{inspect(k)}`" 84 ) 85 end 86 87 def field!(field) when is_atom(field), do: field 88 89 def field!(other) do 90 raise ArgumentError, "expected a field as an atom in `update`, got: `#{inspect other}`" 91 end 92 93 defp compile_error!(expr) do 94 Builder.error! "malformed update `#{Macro.to_string(expr)}` in query expression, " <> 95 "expected a keyword list with set/push/pop as keys with field-value " <> 96 "pairs as values" 97 end 98 99 @doc """ 100 Builds a quoted expression. 101 102 The quoted expression should evaluate to a query at runtime. 103 If possible, it does all calculations at compile time to avoid 104 runtime work. 105 """ 106 @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 107 def build(query, binding, expr, env) do 108 {query, binding} = Builder.escape_binding(query, binding, env) 109 {compile, runtime, params} = escape(expr, binding, env) 110 111 query = 112 if compile == [] do 113 query 114 else 115 params = Builder.escape_params(params) 116 117 update = quote do 118 %Ecto.Query.QueryExpr{expr: unquote(compile), params: unquote(params), 119 file: unquote(env.file), line: unquote(env.line)} 120 end 121 122 Builder.apply_query(query, __MODULE__, [update], env) 123 end 124 125 if runtime == [] do 126 query 127 else 128 quote do 129 Ecto.Query.Builder.Update.update!(unquote(query), unquote(runtime), 130 unquote(env.file), unquote(env.line)) 131 end 132 end 133 end 134 135 @doc """ 136 The callback applied by `build/4` to build the query. 137 """ 138 @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 139 def apply(%Ecto.Query{updates: updates} = query, expr) do 140 %{query | updates: updates ++ [expr]} 141 end 142 def apply(query, expr) do 143 apply(Ecto.Queryable.to_query(query), expr) 144 end 145 146 @doc """ 147 If there are interpolated updates at compile time, 148 we need to handle them at runtime. We do such in 149 this callback. 150 """ 151 def update!(query, runtime, file, line) when is_list(runtime) do 152 {runtime, {params, _count}} = 153 Enum.map_reduce runtime, {[], 0}, fn 154 {k, v}, acc when is_atom(k) and is_list(v) -> 155 validate_op!(k) 156 {v, params} = runtime_field!(query, k, v, acc) 157 {{k, v}, params} 158 _, _ -> 159 runtime_error!(runtime) 160 end 161 162 expr = %Ecto.Query.QueryExpr{expr: runtime, params: Enum.reverse(params), 163 file: file, line: line} 164 165 apply(query, expr) 166 end 167 168 def update!(_query, runtime, _file, _line) do 169 runtime_error!(runtime) 170 end 171 172 defp runtime_field!(query, key, kw, acc) do 173 Enum.map_reduce kw, acc, fn 174 {k, %Ecto.Query.DynamicExpr{} = v}, {params, count} when is_atom(k) -> 175 {v, params, count} = Ecto.Query.Builder.Dynamic.partially_expand(:update, query, v, params, count) 176 {{k, v}, {params, count}} 177 {k, v}, {params, count} when is_atom(k) -> 178 params = [{v, type_for_key(key, {0, k})} | params] 179 {{k, {:^, [], [count]}}, {params, count + 1}} 180 _, _acc -> 181 raise ArgumentError, "malformed #{inspect key} in update `#{inspect(kw)}`, " <> 182 "expected a keyword list" 183 end 184 end 185 186 defp runtime_error!(value) do 187 raise ArgumentError, 188 "malformed update `#{inspect(value)}` in query expression, " <> 189 "expected a keyword list with set/push/pop as keys with field-value pairs as values" 190 end 191 192 defp validate_op!(key) when key in @keys, do: :ok 193 defp validate_op!(key), do: Builder.error! "unknown key `#{inspect(key)}` in update" 194 195 # Out means the given type must be taken out of an array 196 # It is the opposite of "left in right" in the query API. 197 defp type_for_key(:push, type), do: {:out, type} 198 defp type_for_key(:pull, type), do: {:out, type} 199 defp type_for_key(_, type), do: type 200 end