preload.ex (5259B)
1 import Kernel, except: [apply: 3] 2 3 defmodule Ecto.Query.Builder.Preload do 4 @moduledoc false 5 alias Ecto.Query.Builder 6 7 @doc """ 8 Escapes a preload. 9 10 A preload may be an atom, a list of atoms or a keyword list 11 nested as a rose tree. 12 13 iex> escape(:foo, []) 14 {[:foo], []} 15 16 iex> escape([foo: :bar], []) 17 {[foo: [:bar]], []} 18 19 iex> escape([:foo, :bar], []) 20 {[:foo, :bar], []} 21 22 iex> escape([foo: [:bar, bar: :bat]], []) 23 {[foo: [:bar, bar: [:bat]]], []} 24 25 iex> escape([foo: {:^, [], ["external"]}], []) 26 {[foo: "external"], []} 27 28 iex> escape([foo: [:bar, {:^, [], ["external"]}], baz: :bat], []) 29 {[foo: [:bar, "external"], baz: [:bat]], []} 30 31 iex> escape([foo: {:c, [], nil}], [c: 1]) 32 {[], [foo: {1, []}]} 33 34 iex> escape([foo: {{:c, [], nil}, bar: {:l, [], nil}}], [c: 1, l: 2]) 35 {[], [foo: {1, [bar: {2, []}]}]} 36 37 iex> escape([foo: {:c, [], nil}, bar: {:l, [], nil}], [c: 1, l: 2]) 38 {[], [foo: {1, []}, bar: {2, []}]} 39 40 iex> escape([foo: {{:c, [], nil}, :bar}], [c: 1]) 41 {[foo: [:bar]], [foo: {1, []}]} 42 43 iex> escape([foo: [bar: {:c, [], nil}]], [c: 1]) 44 ** (Ecto.Query.CompileError) cannot preload join association `:bar` with binding `c` because parent preload is not a join association 45 46 """ 47 @spec escape(Macro.t, Keyword.t) :: {[Macro.t], [Macro.t]} 48 def escape(preloads, vars) do 49 {preloads, assocs} = escape(preloads, :both, [], [], vars) 50 {Enum.reverse(preloads), Enum.reverse(assocs)} 51 end 52 53 defp escape(atom, _mode, preloads, assocs, _vars) when is_atom(atom) do 54 {[atom|preloads], assocs} 55 end 56 57 defp escape(list, mode, preloads, assocs, vars) when is_list(list) do 58 Enum.reduce list, {preloads, assocs}, fn item, acc -> 59 escape_each(item, mode, acc, vars) 60 end 61 end 62 63 defp escape({:^, _, [inner]}, _mode, preloads, assocs, _vars) do 64 {[inner|preloads], assocs} 65 end 66 67 defp escape(other, _mode, _preloads, _assocs, _vars) do 68 Builder.error! "`#{Macro.to_string other}` is not a valid preload expression. " <> 69 "preload expects an atom, a list of atoms or a keyword list with " <> 70 "more preloads as values. Use ^ on the outermost preload to interpolate a value" 71 end 72 73 defp escape_each({key, {:^, _, [inner]}}, _mode, {preloads, assocs}, _vars) do 74 key = escape_key(key) 75 {[{key, inner}|preloads], assocs} 76 end 77 78 defp escape_each({key, {var, _, context}}, mode, {preloads, assocs}, vars) when is_atom(context) do 79 assert_assoc!(mode, key, var) 80 key = escape_key(key) 81 idx = Builder.find_var!(var, vars) 82 {preloads, [{key, {idx, []}}|assocs]} 83 end 84 85 defp escape_each({key, {{var, _, context}, list}}, mode, {preloads, assocs}, vars) when is_atom(context) do 86 assert_assoc!(mode, key, var) 87 key = escape_key(key) 88 idx = Builder.find_var!(var, vars) 89 {inner_preloads, inner_assocs} = escape(list, :assoc, [], [], vars) 90 assocs = [{key, {idx, Enum.reverse(inner_assocs)}}|assocs] 91 case inner_preloads do 92 [] -> {preloads, assocs} 93 _ -> {[{key, Enum.reverse(inner_preloads)}|preloads], assocs} 94 end 95 end 96 97 defp escape_each({key, list}, _mode, {preloads, assocs}, vars) do 98 key = escape_key(key) 99 {inner_preloads, []} = escape(list, :preload, [], [], vars) 100 {[{key, Enum.reverse(inner_preloads)}|preloads], assocs} 101 end 102 103 defp escape_each(other, mode, {preloads, assocs}, vars) do 104 escape(other, mode, preloads, assocs, vars) 105 end 106 107 defp escape_key(atom) when is_atom(atom) do 108 atom 109 end 110 111 defp escape_key({:^, _, [expr]}) do 112 quote(do: Ecto.Query.Builder.Preload.key!(unquote(expr))) 113 end 114 115 defp escape_key(other) do 116 Builder.error! "malformed key in preload `#{Macro.to_string(other)}` in query expression" 117 end 118 119 defp assert_assoc!(mode, _atom, _var) when mode in [:both, :assoc], do: :ok 120 defp assert_assoc!(_mode, atom, var) do 121 Builder.error! "cannot preload join association `#{Macro.to_string atom}` with binding `#{var}` " <> 122 "because parent preload is not a join association" 123 end 124 125 @doc """ 126 Called at runtime to check dynamic preload keys. 127 """ 128 def key!(key) when is_atom(key), 129 do: key 130 def key!(key) do 131 raise ArgumentError, 132 "expected key in preload to be an atom, got: `#{inspect key}`" 133 end 134 135 @doc """ 136 Applies the preloaded value into the query. 137 138 The quoted expression should evaluate to a query at runtime. 139 If possible, it does all calculations at compile time to avoid 140 runtime work. 141 """ 142 @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 143 def build(query, binding, expr, env) do 144 {query, binding} = Builder.escape_binding(query, binding, env) 145 {preloads, assocs} = escape(expr, binding) 146 Builder.apply_query(query, __MODULE__, [Enum.reverse(preloads), Enum.reverse(assocs)], env) 147 end 148 149 @doc """ 150 The callback applied by `build/4` to build the query. 151 """ 152 @spec apply(Ecto.Queryable.t, term, term) :: Ecto.Query.t 153 def apply(%Ecto.Query{preloads: p, assocs: a} = query, preloads, assocs) do 154 %{query | preloads: p ++ preloads, assocs: a ++ assocs} 155 end 156 def apply(query, preloads, assocs) do 157 apply(Ecto.Queryable.to_query(query), preloads, assocs) 158 end 159 end