Skip to content

Commit 3b2d006

Browse files
authored
Use unique var names when computing bindings (#4625)
The previous version of the code emitted the query twice, potentially leading to an infinite loop. The solution is to assign the query to a unique variable, to avoid overlapping variable names leading to wrong runtime counts.
1 parent 9cdc5bf commit 3b2d006

File tree

2 files changed

+110
-37
lines changed

2 files changed

+110
-37
lines changed

lib/ecto/query/builder.ex

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,11 +1007,13 @@ defmodule Ecto.Query.Builder do
10071007
{query, vars}
10081008

10091009
{vars, [_ | tail]} ->
1010+
var = Macro.unique_var(:query, __MODULE__)
1011+
10101012
query =
10111013
quote do
1012-
query = Ecto.Queryable.to_query(unquote(query))
1013-
escape_count = Ecto.Query.Builder.count_binds(query)
1014-
query
1014+
unquote(var) = Ecto.Queryable.to_query(unquote(query))
1015+
escape_count = Ecto.Query.Builder.count_binds(unquote(var))
1016+
unquote(var)
10151017
end
10161018

10171019
tail =
@@ -1029,18 +1031,21 @@ defmodule Ecto.Query.Builder do
10291031
defp calculate_named_binds(query, []), do: {query, []}
10301032

10311033
defp calculate_named_binds(query, vars) do
1034+
var = Macro.unique_var(:query, __MODULE__)
1035+
10321036
assignments =
10331037
for {:named, key, name} <- vars do
10341038
quote do
1035-
unquote({key, [], __MODULE__}) = unquote(__MODULE__).count_alias!(query, unquote(name))
1039+
unquote({key, [], __MODULE__}) =
1040+
unquote(__MODULE__).count_alias!(unquote(var), unquote(name))
10361041
end
10371042
end
10381043

10391044
query =
10401045
quote do
1041-
query = Ecto.Queryable.to_query(unquote(query))
1046+
unquote(var) = Ecto.Queryable.to_query(unquote(query))
10421047
unquote_splicing(assignments)
1043-
query
1048+
unquote(var)
10441049
end
10451050

10461051
pairs =

lib/ecto/query/builder/join.ex

Lines changed: 99 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ defmodule Ecto.Query.Builder.Join do
4242
{:x, {:{}, [], [:fragment, [], [raw: "foo"]]}, nil, nil, []}
4343
4444
"""
45-
@spec escape(Macro.t, Keyword.t, Macro.Env.t) :: {atom, Macro.t | nil, Macro.t | nil, list}
45+
@spec escape(Macro.t(), Keyword.t(), Macro.Env.t()) ::
46+
{atom, Macro.t() | nil, Macro.t() | nil, list}
4647
def escape({:in, _, [{var, _, context}, expr]}, vars, env)
4748
when is_atom(var) and is_atom(context) do
4849
{_, expr, assoc, prelude, params} = escape(expr, vars, env)
@@ -76,14 +77,14 @@ defmodule Ecto.Query.Builder.Join do
7677
{:_, {string, schema}, nil, nil, []}
7778

7879
_ ->
79-
Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression"
80+
Builder.error!("malformed join `#{Macro.to_string(join)}` in query expression")
8081
end
8182
end
8283

8384
def escape({:assoc, _, [{var, _, context}, field]}, vars, _env)
8485
when is_atom(var) and is_atom(context) do
8586
ensure_field!(field)
86-
var = Builder.find_var!(var, vars)
87+
var = Builder.find_var!(var, vars)
8788
field = Builder.quoted_atom!(field, "field/2")
8889
{:_, nil, {var, field}, nil, []}
8990
end
@@ -103,7 +104,8 @@ defmodule Ecto.Query.Builder.Join do
103104
def escape(join, vars, env) do
104105
case Macro.expand(join, env) do
105106
^join ->
106-
Builder.error! "malformed join `#{Macro.to_string(join)}` in query expression"
107+
Builder.error!("malformed join `#{Macro.to_string(join)}` in query expression")
108+
107109
join ->
108110
escape(join, vars, env)
109111
end
@@ -114,10 +116,13 @@ defmodule Ecto.Query.Builder.Join do
114116
"""
115117
def join!(expr) when is_atom(expr),
116118
do: {nil, expr}
119+
117120
def join!(expr) when is_binary(expr),
118121
do: {expr, nil}
122+
119123
def join!({source, module}) when is_binary(source) and is_atom(module),
120124
do: {source, module}
125+
121126
def join!(expr),
122127
do: Ecto.Queryable.to_query(expr)
123128

@@ -128,8 +133,19 @@ defmodule Ecto.Query.Builder.Join do
128133
If possible, it does all calculations at compile time to avoid
129134
runtime work.
130135
"""
131-
@spec build(Macro.t, atom, [Macro.t], Macro.t, Macro.t, Macro.t, atom, nil | {:ok, Ecto.Schema.prefix}, nil | String.t | [String.t], Macro.Env.t) ::
132-
{Macro.t, Keyword.t, non_neg_integer | nil}
136+
@spec build(
137+
Macro.t(),
138+
atom,
139+
[Macro.t()],
140+
Macro.t(),
141+
Macro.t(),
142+
Macro.t(),
143+
atom,
144+
nil | {:ok, Ecto.Schema.prefix()},
145+
nil | String.t() | [String.t()],
146+
Macro.Env.t()
147+
) ::
148+
{Macro.t(), Keyword.t(), non_neg_integer | nil}
133149
def build(query, qual, binding, expr, count_bind, on, as, prefix, maybe_hints, env) do
134150
{:ok, prefix} = prefix || {:ok, nil}
135151
hints = List.wrap(maybe_hints)
@@ -141,18 +157,36 @@ defmodule Ecto.Query.Builder.Join do
141157
)
142158
end
143159

144-
prefix = case prefix do
145-
nil -> nil
146-
prefix when is_binary(prefix) -> prefix
147-
{:^, _, [prefix]} -> prefix
148-
prefix -> Builder.error!("`prefix` must be a compile time string or an interpolated value using ^, got: #{Macro.to_string(prefix)}")
149-
end
160+
prefix =
161+
case prefix do
162+
nil ->
163+
nil
150164

151-
as = case as do
152-
{:^, _, [as]} -> as
153-
as when is_atom(as) -> as
154-
as -> Builder.error!("`as` must be a compile time atom or an interpolated value using ^, got: #{Macro.to_string(as)}")
155-
end
165+
prefix when is_binary(prefix) ->
166+
prefix
167+
168+
{:^, _, [prefix]} ->
169+
prefix
170+
171+
prefix ->
172+
Builder.error!(
173+
"`prefix` must be a compile time string or an interpolated value using ^, got: #{Macro.to_string(prefix)}"
174+
)
175+
end
176+
177+
as =
178+
case as do
179+
{:^, _, [as]} ->
180+
as
181+
182+
as when is_atom(as) ->
183+
as
184+
185+
as ->
186+
Builder.error!(
187+
"`as` must be a compile time atom or an interpolated value using ^, got: #{Macro.to_string(as)}"
188+
)
189+
end
156190

157191
{query, binding} = Builder.escape_binding(query, binding, env)
158192
{join_bind, join_source, join_assoc, join_prelude, join_params} = escape(expr, binding, env)
@@ -163,11 +197,14 @@ defmodule Ecto.Query.Builder.Join do
163197

164198
{count_bind, query} =
165199
if is_nil(count_bind) do
200+
var = Macro.unique_var(:query, __MODULE__)
201+
166202
query =
167203
quote do
168-
Ecto.Queryable.to_query(unquote(query))
204+
unquote(var) = Ecto.Queryable.to_query(unquote(query))
169205
end
170-
{quote(do: Builder.count_binds(unquote(query))), query}
206+
207+
{quote(do: Builder.count_binds(unquote(var))), query}
171208
else
172209
{count_bind, query}
173210
end
@@ -252,10 +289,11 @@ defmodule Ecto.Query.Builder.Join do
252289

253290
defp ensure_on(on, _assoc, _qual, _source, _env) when on != nil, do: on
254291

255-
defp ensure_on(nil, _assoc = nil, qual, source, env) when qual not in [:cross, :cross_lateral] do
292+
defp ensure_on(nil, _assoc = nil, qual, source, env)
293+
when qual not in [:cross, :cross_lateral] do
256294
maybe_source =
257295
with {source, alias} <- source,
258-
source when source != nil <- source || alias do
296+
source when source != nil <- source || alias do
259297
" on #{inspect(source)}"
260298
else
261299
_ -> ""
@@ -275,6 +313,7 @@ defmodule Ecto.Query.Builder.Join do
275313
def apply(%Ecto.Query{joins: joins} = query, expr, nil, _count_bind) do
276314
%{query | joins: joins ++ [expr]}
277315
end
316+
278317
def apply(%Ecto.Query{joins: joins, aliases: aliases} = query, expr, as, count_bind) do
279318
aliases =
280319
case aliases do
@@ -284,6 +323,7 @@ defmodule Ecto.Query.Builder.Join do
284323

285324
%{query | joins: joins ++ [expr], aliases: aliases}
286325
end
326+
287327
def apply(query, expr, as, count_bind) do
288328
apply(Ecto.Queryable.to_query(query), expr, as, count_bind)
289329
end
@@ -295,20 +335,24 @@ defmodule Ecto.Query.Builder.Join do
295335

296336
def runtime_aliases(aliases, name, join_count) when is_integer(join_count) do
297337
if Map.has_key?(aliases, name) do
298-
Builder.error! "alias `#{inspect name}` already exists"
338+
Builder.error!("alias `#{inspect(name)}` already exists")
299339
else
300340
Map.put(aliases, name, join_count)
301341
end
302342
end
303343

304344
defp compile_aliases({:%{}, meta, aliases}, name, join_count)
305345
when is_atom(name) and is_integer(join_count) do
306-
{:%{}, meta, aliases |> Map.new |> runtime_aliases(name, join_count) |> Map.to_list}
346+
{:%{}, meta, aliases |> Map.new() |> runtime_aliases(name, join_count) |> Map.to_list()}
307347
end
308348

309349
defp compile_aliases(aliases, name, join_count) do
310350
quote do
311-
Ecto.Query.Builder.Join.runtime_aliases(unquote(aliases), unquote(name), unquote(join_count))
351+
Ecto.Query.Builder.Join.runtime_aliases(
352+
unquote(aliases),
353+
unquote(name),
354+
unquote(join_count)
355+
)
312356
end
313357
end
314358

@@ -319,9 +363,20 @@ defmodule Ecto.Query.Builder.Join do
319363
# join without expanded :on is built and applied to the query,
320364
# so that expansion of dynamic :on accounts for the new binding
321365
{on_expr, on_params, on_file, on_line} =
322-
Ecto.Query.Builder.Filter.filter!(:on, apply(query, join, as, count_bind), expr, count_bind, file, line)
366+
Ecto.Query.Builder.Filter.filter!(
367+
:on,
368+
apply(query, join, as, count_bind),
369+
expr,
370+
count_bind,
371+
file,
372+
line
373+
)
374+
375+
join = %{
376+
join
377+
| on: %QueryExpr{expr: on_expr, params: on_params, line: on_line, file: on_file}
378+
}
323379

324-
join = %{join | on: %QueryExpr{expr: on_expr, params: on_params, line: on_line, file: on_file}}
325380
apply(query, join, as, count_bind)
326381
end
327382

@@ -335,24 +390,37 @@ defmodule Ecto.Query.Builder.Join do
335390

336391
defp validate_bind(bind, all) do
337392
if bind != :_ and bind in all do
338-
Builder.error! "variable `#{bind}` is already defined in query"
393+
Builder.error!("variable `#{bind}` is already defined in query")
339394
end
340395
end
341396

342-
@qualifiers [:inner, :inner_lateral, :left, :left_lateral, :right, :full, :cross, :cross_lateral]
397+
@qualifiers [
398+
:inner,
399+
:inner_lateral,
400+
:left,
401+
:left_lateral,
402+
:right,
403+
:full,
404+
:cross,
405+
:cross_lateral
406+
]
343407

344408
@doc """
345409
Called at runtime to check dynamic qualifier.
346410
"""
347411
def qual!(qual) when qual in @qualifiers, do: qual
412+
348413
def qual!(qual) do
349414
raise ArgumentError,
350-
"invalid join qualifier `#{inspect qual}`, accepted qualifiers are: " <>
351-
Enum.map_join(@qualifiers, ", ", &"`#{inspect &1}`")
415+
"invalid join qualifier `#{inspect(qual)}`, accepted qualifiers are: " <>
416+
Enum.map_join(@qualifiers, ", ", &"`#{inspect(&1)}`")
352417
end
353418

354419
defp ensure_field!({var, _, _}) when var != :^ do
355-
Builder.error! "you passed the variable `#{var}` to `assoc/2`. Did you mean to pass the atom `:#{var}`?"
420+
Builder.error!(
421+
"you passed the variable `#{var}` to `assoc/2`. Did you mean to pass the atom `:#{var}`?"
422+
)
356423
end
424+
357425
defp ensure_field!(_), do: true
358426
end

0 commit comments

Comments
 (0)