import Kernel, except: [apply: 2] defmodule Ecto.Query.Builder.Update do @moduledoc false @keys [:set, :inc, :push, :pull] alias Ecto.Query.Builder @doc """ Escapes a list of quoted expressions. iex> escape([], [], __ENV__) {[], [], []} iex> escape([set: []], [], __ENV__) {[], [], []} iex> escape(quote(do: ^[set: []]), [], __ENV__) {[], [set: []], []} iex> escape(quote(do: [set: ^[foo: 1]]), [], __ENV__) {[], [set: [foo: 1]], []} iex> escape(quote(do: [set: [foo: ^1]]), [], __ENV__) {[], [set: [foo: 1]], []} """ @spec escape(Macro.t, Keyword.t, Macro.Env.t) :: {Macro.t, Macro.t, list} def escape(expr, vars, env) when is_list(expr) do escape_op(expr, [], [], [], vars, env) end def escape({:^, _, [v]}, _vars, _env) do {[], v, []} end def escape(expr, _vars, _env) do compile_error!(expr) end defp escape_op([{k, v}|t], compile, runtime, params, vars, env) when is_atom(k) and is_list(v) do validate_op!(k) {compile_values, runtime_values, params} = escape_kw(k, v, params, vars, env) compile = if compile_values == [], do: compile, else: [{k, Enum.reverse(compile_values)} | compile] runtime = if runtime_values == [], do: runtime, else: [{k, Enum.reverse(runtime_values)} | runtime] escape_op(t, compile, runtime, params, vars, env) end defp escape_op([{k, {:^, _, [v]}}|t], compile, runtime, params, vars, env) when is_atom(k) do validate_op!(k) escape_op(t, compile, [{k, v}|runtime], params, vars, env) end defp escape_op([], compile, runtime, params, _vars, _env) do {Enum.reverse(compile), Enum.reverse(runtime), params} end defp escape_op(expr, _compile, _runtime, _params, _vars, _env) do compile_error!(expr) end defp escape_kw(op, kw, params, vars, env) do Enum.reduce kw, {[], [], params}, fn {k, {:^, _, [v]}}, {compile, runtime, params} when is_atom(k) -> {compile, [{k, v} | runtime], params} {k, v}, {compile, runtime, params} -> k = escape_field!(k) {v, {params, :acc}} = Builder.escape(v, type_for_key(op, {0, k}), {params, :acc}, vars, env) {[{k, v} | compile], runtime, params} _, _acc -> Builder.error! "malformed #{inspect op} in update `#{Macro.to_string(kw)}`, " <> "expected a keyword list" end end defp escape_field!({:^, _, [k]}), do: quote(do: Ecto.Query.Builder.Update.field!(unquote(k))) defp escape_field!(k) when is_atom(k), do: k defp escape_field!(k) do Builder.error!( "expected an atom field or an interpolated field in `update`, got `#{inspect(k)}`" ) end def field!(field) when is_atom(field), do: field def field!(other) do raise ArgumentError, "expected a field as an atom in `update`, got: `#{inspect other}`" end defp compile_error!(expr) do Builder.error! "malformed update `#{Macro.to_string(expr)}` in query expression, " <> "expected a keyword list with set/push/pop as keys with field-value " <> "pairs as values" 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], Macro.t, Macro.Env.t) :: Macro.t def build(query, binding, expr, env) do {query, binding} = Builder.escape_binding(query, binding, env) {compile, runtime, params} = escape(expr, binding, env) query = if compile == [] do query else params = Builder.escape_params(params) update = quote do %Ecto.Query.QueryExpr{expr: unquote(compile), params: unquote(params), file: unquote(env.file), line: unquote(env.line)} end Builder.apply_query(query, __MODULE__, [update], env) end if runtime == [] do query else quote do Ecto.Query.Builder.Update.update!(unquote(query), unquote(runtime), unquote(env.file), unquote(env.line)) end end end @doc """ The callback applied by `build/4` to build the query. """ @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t def apply(%Ecto.Query{updates: updates} = query, expr) do %{query | updates: updates ++ [expr]} end def apply(query, expr) do apply(Ecto.Queryable.to_query(query), expr) end @doc """ If there are interpolated updates at compile time, we need to handle them at runtime. We do such in this callback. """ def update!(query, runtime, file, line) when is_list(runtime) do {runtime, {params, _count}} = Enum.map_reduce runtime, {[], 0}, fn {k, v}, acc when is_atom(k) and is_list(v) -> validate_op!(k) {v, params} = runtime_field!(query, k, v, acc) {{k, v}, params} _, _ -> runtime_error!(runtime) end expr = %Ecto.Query.QueryExpr{expr: runtime, params: Enum.reverse(params), file: file, line: line} apply(query, expr) end def update!(_query, runtime, _file, _line) do runtime_error!(runtime) end defp runtime_field!(query, key, kw, acc) do Enum.map_reduce kw, acc, fn {k, %Ecto.Query.DynamicExpr{} = v}, {params, count} when is_atom(k) -> {v, params, count} = Ecto.Query.Builder.Dynamic.partially_expand(:update, query, v, params, count) {{k, v}, {params, count}} {k, v}, {params, count} when is_atom(k) -> params = [{v, type_for_key(key, {0, k})} | params] {{k, {:^, [], [count]}}, {params, count + 1}} _, _acc -> raise ArgumentError, "malformed #{inspect key} in update `#{inspect(kw)}`, " <> "expected a keyword list" end end defp runtime_error!(value) do raise ArgumentError, "malformed update `#{inspect(value)}` in query expression, " <> "expected a keyword list with set/push/pop as keys with field-value pairs as values" end defp validate_op!(key) when key in @keys, do: :ok defp validate_op!(key), do: Builder.error! "unknown key `#{inspect(key)}` in update" # Out means the given type must be taken out of an array # It is the opposite of "left in right" in the query API. defp type_for_key(:push, type), do: {:out, type} defp type_for_key(:pull, type), do: {:out, type} defp type_for_key(_, type), do: type end