join.ex (9796B)
1 import Kernel, except: [apply: 2] 2 3 defmodule Ecto.Query.Builder.Join do 4 @moduledoc false 5 6 alias Ecto.Query.Builder 7 alias Ecto.Query.{JoinExpr, QueryExpr} 8 9 @doc """ 10 Escapes a join expression (not including the `on` expression). 11 12 It returns a tuple containing the binds, the on expression (if available) 13 and the association expression. 14 15 ## Examples 16 17 iex> escape(quote(do: x in "foo"), [], __ENV__) 18 {:x, {"foo", nil}, nil, []} 19 20 iex> escape(quote(do: "foo"), [], __ENV__) 21 {:_, {"foo", nil}, nil, []} 22 23 iex> escape(quote(do: x in Sample), [], __ENV__) 24 {:x, {nil, Sample}, nil, []} 25 26 iex> escape(quote(do: x in __MODULE__), [], __ENV__) 27 {:x, {nil, __MODULE__}, nil, []} 28 29 iex> escape(quote(do: x in {"foo", :sample}), [], __ENV__) 30 {:x, {"foo", :sample}, nil, []} 31 32 iex> escape(quote(do: x in {"foo", Sample}), [], __ENV__) 33 {:x, {"foo", Sample}, nil, []} 34 35 iex> escape(quote(do: x in {"foo", __MODULE__}), [], __ENV__) 36 {:x, {"foo", __MODULE__}, nil, []} 37 38 iex> escape(quote(do: c in assoc(p, :comments)), [p: 0], __ENV__) 39 {:c, nil, {0, :comments}, []} 40 41 iex> escape(quote(do: x in fragment("foo")), [], __ENV__) 42 {:x, {:{}, [], [:fragment, [], [raw: "foo"]]}, nil, []} 43 44 """ 45 @spec escape(Macro.t, Keyword.t, Macro.Env.t) :: {atom, Macro.t | nil, Macro.t | nil, list} 46 def escape({:in, _, [{var, _, context}, expr]}, vars, env) 47 when is_atom(var) and is_atom(context) do 48 {_, expr, assoc, params} = escape(expr, vars, env) 49 {var, expr, assoc, params} 50 end 51 52 def escape({:subquery, _, [expr]}, _vars, _env) do 53 {:_, quote(do: Ecto.Query.subquery(unquote(expr))), nil, []} 54 end 55 56 def escape({:subquery, _, [expr, opts]}, _vars, _env) do 57 {:_, quote(do: Ecto.Query.subquery(unquote(expr), unquote(opts))), nil, []} 58 end 59 60 def escape({:fragment, _, [_ | _]} = expr, vars, env) do 61 {expr, {params, _acc}} = Builder.escape(expr, :any, {[], %{}}, vars, env) 62 {:_, expr, nil, params} 63 end 64 65 def escape({string, schema} = join, _vars, env) when is_binary(string) do 66 case Macro.expand(schema, env) do 67 schema when is_atom(schema) -> 68 {:_, {string, schema}, nil, []} 69 70 _ -> 71 Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression" 72 end 73 end 74 75 def escape({:assoc, _, [{var, _, context}, field]}, vars, _env) 76 when is_atom(var) and is_atom(context) do 77 ensure_field!(field) 78 var = Builder.find_var!(var, vars) 79 field = Builder.quoted_atom!(field, "field/2") 80 {:_, nil, {var, field}, []} 81 end 82 83 def escape({:^, _, [expr]}, _vars, _env) do 84 {:_, quote(do: Ecto.Query.Builder.Join.join!(unquote(expr))), nil, []} 85 end 86 87 def escape(string, _vars, _env) when is_binary(string) do 88 {:_, {string, nil}, nil, []} 89 end 90 91 def escape(schema, _vars, _env) when is_atom(schema) do 92 {:_, {nil, schema}, nil, []} 93 end 94 95 def escape(join, vars, env) do 96 case Macro.expand(join, env) do 97 ^join -> 98 Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression" 99 join -> 100 escape(join, vars, env) 101 end 102 end 103 104 @doc """ 105 Called at runtime to check dynamic joins. 106 """ 107 def join!(expr) when is_atom(expr), 108 do: {nil, expr} 109 def join!(expr) when is_binary(expr), 110 do: {expr, nil} 111 def join!({source, module}) when is_binary(source) and is_atom(module), 112 do: {source, module} 113 def join!(expr), 114 do: Ecto.Queryable.to_query(expr) 115 116 @doc """ 117 Builds a quoted expression. 118 119 The quoted expression should evaluate to a query at runtime. 120 If possible, it does all calculations at compile time to avoid 121 runtime work. 122 """ 123 @spec build(Macro.t, atom, [Macro.t], Macro.t, Macro.t, Macro.t, atom, nil | {:ok, String.t | nil}, nil | String.t | [String.t], Macro.Env.t) :: 124 {Macro.t, Keyword.t, non_neg_integer | nil} 125 def build(query, qual, binding, expr, count_bind, on, as, prefix, maybe_hints, env) do 126 {:ok, prefix} = prefix || {:ok, nil} 127 hints = List.wrap(maybe_hints) 128 129 unless Enum.all?(hints, &is_binary/1) do 130 Builder.error!( 131 "`hints` must be a compile time string or list of strings, " <> 132 "got: `#{Macro.to_string(maybe_hints)}`" 133 ) 134 end 135 136 unless is_binary(prefix) or is_nil(prefix) do 137 Builder.error! "`prefix` must be a compile time string, got: `#{Macro.to_string(prefix)}`" 138 end 139 140 as = case as do 141 {:^, _, [as]} -> as 142 as when is_atom(as) -> as 143 as -> Builder.error!("`as` must be a compile time atom or an interpolated value using ^, got: #{Macro.to_string(as)}") 144 end 145 146 {query, binding} = Builder.escape_binding(query, binding, env) 147 {join_bind, join_source, join_assoc, join_params} = escape(expr, binding, env) 148 join_params = Builder.escape_params(join_params) 149 150 join_qual = validate_qual(qual) 151 validate_bind(join_bind, binding) 152 153 {count_bind, query} = 154 if is_nil(count_bind) do 155 query = 156 quote do 157 query = Ecto.Queryable.to_query(unquote(query)) 158 join_count = Builder.count_binds(query) 159 query 160 end 161 {quote(do: join_count), query} 162 else 163 {count_bind, query} 164 end 165 166 binding = binding ++ [{join_bind, count_bind}] 167 168 next_bind = 169 if is_integer(count_bind) do 170 count_bind + 1 171 else 172 quote(do: unquote(count_bind) + 1) 173 end 174 175 join = [ 176 as: as, 177 assoc: join_assoc, 178 file: env.file, 179 line: env.line, 180 params: join_params, 181 prefix: prefix, 182 qual: join_qual, 183 source: join_source, 184 hints: hints 185 ] 186 187 query = build_on(on || true, join, as, query, binding, count_bind, env) 188 {query, binding, next_bind} 189 end 190 191 def build_on({:^, _, [var]}, join, as, query, _binding, count_bind, env) do 192 quote do 193 query = unquote(query) 194 195 Ecto.Query.Builder.Join.join!( 196 query, 197 %JoinExpr{unquote_splicing(join), on: %QueryExpr{}}, 198 unquote(var), 199 unquote(as), 200 unquote(count_bind), 201 unquote(env.file), 202 unquote(env.line) 203 ) 204 end 205 end 206 207 def build_on(on, join, as, query, binding, count_bind, env) do 208 case Ecto.Query.Builder.Filter.escape(:on, on, count_bind, binding, env) do 209 {_on_expr, {_on_params, %{subqueries: [_ | _]}}} -> 210 raise ArgumentError, "invalid expression for join `:on`, subqueries aren't supported" 211 212 {on_expr, {on_params, _acc}} -> 213 on_params = Builder.escape_params(on_params) 214 215 join = 216 quote do 217 %JoinExpr{ 218 unquote_splicing(join), 219 on: %QueryExpr{ 220 expr: unquote(on_expr), 221 params: unquote(on_params), 222 line: unquote(env.line), 223 file: unquote(env.file) 224 } 225 } 226 end 227 228 Builder.apply_query(query, __MODULE__, [join, as, count_bind], env) 229 end 230 end 231 232 @doc """ 233 Applies the join expression to the query. 234 """ 235 def apply(%Ecto.Query{joins: joins} = query, expr, nil, _count_bind) do 236 %{query | joins: joins ++ [expr]} 237 end 238 def apply(%Ecto.Query{joins: joins, aliases: aliases} = query, expr, as, count_bind) do 239 aliases = 240 case aliases do 241 %{} -> runtime_aliases(aliases, as, count_bind) 242 _ -> compile_aliases(aliases, as, count_bind) 243 end 244 245 %{query | joins: joins ++ [expr], aliases: aliases} 246 end 247 def apply(query, expr, as, count_bind) do 248 apply(Ecto.Queryable.to_query(query), expr, as, count_bind) 249 end 250 251 @doc """ 252 Called at runtime to build aliases. 253 """ 254 def runtime_aliases(aliases, nil, _), do: aliases 255 256 def runtime_aliases(aliases, name, join_count) when is_integer(join_count) do 257 if Map.has_key?(aliases, name) do 258 Builder.error! "alias `#{inspect name}` already exists" 259 else 260 Map.put(aliases, name, join_count) 261 end 262 end 263 264 defp compile_aliases({:%{}, meta, aliases}, name, join_count) 265 when is_atom(name) and is_integer(join_count) do 266 {:%{}, meta, aliases |> Map.new |> runtime_aliases(name, join_count) |> Map.to_list} 267 end 268 269 defp compile_aliases(aliases, name, join_count) do 270 quote do 271 Ecto.Query.Builder.Join.runtime_aliases(unquote(aliases), unquote(name), unquote(join_count)) 272 end 273 end 274 275 @doc """ 276 Called at runtime to build a join. 277 """ 278 def join!(query, join, expr, as, count_bind, file, line) do 279 # join without expanded :on is built and applied to the query, 280 # so that expansion of dynamic :on accounts for the new binding 281 {on_expr, on_params, on_file, on_line} = 282 Ecto.Query.Builder.Filter.filter!(:on, apply(query, join, as, count_bind), expr, count_bind, file, line) 283 284 join = %{join | on: %QueryExpr{expr: on_expr, params: on_params, line: on_line, file: on_file}} 285 apply(query, join, as, count_bind) 286 end 287 288 defp validate_qual(qual) when is_atom(qual) do 289 qual!(qual) 290 end 291 292 defp validate_qual(qual) do 293 quote(do: Ecto.Query.Builder.Join.qual!(unquote(qual))) 294 end 295 296 defp validate_bind(bind, all) do 297 if bind != :_ and bind in all do 298 Builder.error! "variable `#{bind}` is already defined in query" 299 end 300 end 301 302 @qualifiers [:inner, :inner_lateral, :left, :left_lateral, :right, :full, :cross] 303 304 @doc """ 305 Called at runtime to check dynamic qualifier. 306 """ 307 def qual!(qual) when qual in @qualifiers, do: qual 308 def qual!(qual) do 309 raise ArgumentError, 310 "invalid join qualifier `#{inspect qual}`, accepted qualifiers are: " <> 311 Enum.map_join(@qualifiers, ", ", &"`#{inspect &1}`") 312 end 313 314 defp ensure_field!({var, _, _}) when var != :^ do 315 Builder.error! "you passed the variable `#{var}` to `assoc/2`. Did you mean to pass the atom `:#{var}`?" 316 end 317 defp ensure_field!(_), do: true 318 end