Skip to content

Commit 99d19b4

Browse files
committed
Another attempt at keeping source in subqueries
1 parent 0948cee commit 99d19b4

File tree

3 files changed

+45
-74
lines changed

3 files changed

+45
-74
lines changed

lib/ecto/query/planner.ex

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Ecto.Query.Planner do
99
end
1010

1111
@parent_as __MODULE__
12+
@aggs ~w(count avg min max sum row_number rank dense_rank percent_rank cume_dist ntile lag lead first_value last_value nth_value)a
1213

1314
@doc """
1415
Converts a query to a list of joins.
@@ -297,48 +298,39 @@ defmodule Ecto.Query.Planner do
297298
end
298299

299300
defp normalize_subquery_select(query, adapter, source?) do
300-
{expr, %{select: select} = query} = rewrite_subquery_select_expr(query, source?)
301+
{schema_or_source, expr, %{select: select} = query} = rewrite_subquery_select_expr(query, source?)
301302
{expr, _} = prewalk(expr, :select, query, select, 0, adapter)
302-
{meta, _fields, _from} = collect_fields(expr, [], :never, query, select.take, true)
303-
{query, meta}
303+
{{:map, types}, _fields, _from} = collect_fields(expr, [], :never, query, select.take, true)
304+
{query, subquery_source(schema_or_source, types)}
304305
end
305306

306-
# If we are selecting a source, we keep it as is.
307-
# Otherwise we normalize the select, which converts them into structs.
308-
# This means that `select: p` in subqueries will be nullable in a join.
309-
defp rewrite_subquery_select_expr(%{select: %{expr: {:&, _, [_]} = expr}} = query, _source?) do
310-
{expr, query}
307+
defp subquery_source(nil, types), do: {:map, types}
308+
defp subquery_source(name, types) when is_atom(name), do: {:struct, name, types}
309+
defp subquery_source({:source, schema, prefix, types}, only) do
310+
types = Enum.map(only, fn {field, _} -> {field, Keyword.get(types, field, :any)} end)
311+
{:source, schema, prefix, types}
311312
end
312313

313314
defp rewrite_subquery_select_expr(%{select: select} = query, source?) do
314315
%{expr: expr, take: take} = select
315316

316-
expr =
317-
case subquery_select(expr, take, query) do
318-
{nil, fields} ->
319-
{:%{}, [], fields}
320-
321-
{struct, fields} ->
322-
{:%, [], [struct, {:%{}, [], fields}]}
317+
case subquery_select(expr, take, query) do
318+
{schema_or_source, fields} ->
319+
expr = {:%{}, [], fields}
320+
{schema_or_source, expr, put_in(query.select.expr, expr)}
323321

324-
:error when source? ->
325-
error!(query, "subquery/cte must select a source (t), a field (t.field) or a map, got: `#{Macro.to_string(expr)}`")
322+
:error when source? ->
323+
error!(query, "subquery/cte must select a source (t), a field (t.field) or a map, got: `#{Macro.to_string(expr)}`")
326324

327-
:error ->
328-
expr
329-
end
330-
331-
{expr, put_in(query.select.expr, expr)}
325+
:error ->
326+
expr = {:%{}, [], [result: expr]}
327+
{nil, expr, put_in(query.select.expr, expr)}
328+
end
332329
end
333330

334331
defp subquery_select({:merge, _, [left, right]}, take, query) do
335332
{left_struct, left_fields} = subquery_select(left, take, query)
336333
{right_struct, right_fields} = subquery_select(right, take, query)
337-
338-
unless is_nil(left_struct) or is_nil(right_struct) or left_struct == right_struct do
339-
error!(query, "cannot merge #{inspect(left_struct)} and #{inspect(right_struct)} because they are different structs")
340-
end
341-
342334
{left_struct || right_struct, Keyword.merge(left_fields, right_fields)}
343335
end
344336
defp subquery_select({:%, _, [name, map]}, take, query) do
@@ -348,55 +340,43 @@ defmodule Ecto.Query.Planner do
348340
defp subquery_select({:%{}, _, [{:|, _, [{:&, [], [ix]}, pairs]}]} = expr, take, query) do
349341
assert_subquery_fields!(query, expr, pairs)
350342
{source, _} = source_take!(:select, query, take, ix, ix)
351-
{struct, fields} = subquery_struct_and_fields(source)
352-
353-
# Map updates may contain virtual fields, so we need to consider those
354-
valid_keys = if struct, do: Map.keys(struct.__struct__), else: fields
355-
update_keys = Keyword.keys(pairs)
356-
357-
case update_keys -- valid_keys do
358-
[] -> :ok
359-
[key | _] -> error!(query, "invalid key `#{inspect key}` for `#{inspect struct}` on map update in subquery/cte")
360-
end
361343

362344
# In case of map updates, we need to remove duplicated fields
363345
# at query time because we use the field names as aliases and
364346
# duplicate aliases will lead to invalid queries.
365-
kept_keys = fields -- update_keys
366-
{struct, subquery_fields(kept_keys, ix) ++ pairs}
347+
kept_keys = subquery_source_fields(source) -- Keyword.keys(pairs)
348+
{keep_source_or_struct(source), subquery_fields(kept_keys, ix) ++ pairs}
367349
end
368350
defp subquery_select({:%{}, _, pairs} = expr, _take, query) do
369351
assert_subquery_fields!(query, expr, pairs)
370352
{nil, pairs}
371353
end
372354
defp subquery_select({:&, _, [ix]}, take, query) do
373355
{source, _} = source_take!(:select, query, take, ix, ix)
374-
{struct, fields} = subquery_struct_and_fields(source)
375-
{struct, subquery_fields(fields, ix)}
356+
fields = subquery_source_fields(source)
357+
{keep_source_or_struct(source), subquery_fields(fields, ix)}
376358
end
377-
defp subquery_select({{:., _, [{:&, _, [ix]}, field]}, _, []}, _take, _query) do
378-
{nil, subquery_fields([field], ix)}
359+
defp subquery_select({{:., _, [{:&, _, [_]}, field]}, _, []} = expr, _take, _query) do
360+
{nil, [{field, expr}]}
379361
end
380362
defp subquery_select(_expr, _take, _query) do
381363
:error
382364
end
383365

384-
defp subquery_struct_and_fields({:source, {_, schema}, _, types}) do
385-
{schema, Keyword.keys(types)}
386-
end
387-
defp subquery_struct_and_fields({:struct, name, types}) do
388-
{name, Keyword.keys(types)}
389-
end
390-
defp subquery_struct_and_fields({:map, types}) do
391-
{nil, Keyword.keys(types)}
392-
end
393-
394366
defp subquery_fields(fields, ix) do
395367
for field <- fields do
396368
{field, {{:., [], [{:&, [], [ix]}, field]}, [], []}}
397369
end
398370
end
399371

372+
defp keep_source_or_struct({:source, _, _, _} = source), do: source
373+
defp keep_source_or_struct({:struct, name, _}), do: name
374+
defp keep_source_or_struct(_), do: nil
375+
376+
defp subquery_source_fields({:source, _, _, types}), do: Keyword.keys(types)
377+
defp subquery_source_fields({:struct, _, types}), do: Keyword.keys(types)
378+
defp subquery_source_fields({:map, types}), do: Keyword.keys(types)
379+
400380
defp subquery_type_for({:source, _, _, fields}, field), do: Keyword.fetch(fields, field)
401381
defp subquery_type_for({:struct, _name, types}, field), do: subquery_type_for_value(types, field)
402382
defp subquery_type_for({:map, types}, field), do: subquery_type_for_value(types, field)
@@ -413,6 +393,7 @@ defmodule Ecto.Query.Planner do
413393
Enum.each(pairs, fn
414394
{key, _} when not is_atom(key) ->
415395
error!(query, "only atom keys are allowed when selecting a map in subquery, got: `#{Macro.to_string(expr)}`")
396+
416397
{key, value} ->
417398
if valid_subquery_value?(value) do
418399
{key, value}
@@ -999,15 +980,14 @@ defmodule Ecto.Query.Planner do
999980

1000981
# We don't want to use normalize_subquery_select because we are
1001982
# going to prepare the whole query ourselves next.
1002-
{_, inner_query} = rewrite_subquery_select_expr(inner_query, true)
983+
{_, _, inner_query} = rewrite_subquery_select_expr(inner_query, true)
1003984
{inner_query, counter} = traverse_exprs(inner_query, :all, counter, fun)
1004985

1005986
# Now compute the fields as keyword lists so we emit AS in Ecto query.
1006987
%{select: %{expr: expr, take: take}} = inner_query
1007-
{source, fields, _from} = collect_fields(expr, [], :never, inner_query, take, true)
1008-
{_, keys} = subquery_struct_and_fields(source)
1009-
inner_query = put_in(inner_query.select.fields, Enum.zip(keys, Enum.reverse(fields)))
1010-
988+
{{:map, types}, fields, _from} = collect_fields(expr, [], :never, inner_query, take, true)
989+
fields = Enum.zip(Keyword.keys(types), Enum.reverse(fields))
990+
inner_query = put_in(inner_query.select.fields, fields)
1011991
{_, inner_query} = pop_in(inner_query.aliases[@parent_as])
1012992

1013993
{[{name, inner_query} | queries], counter}
@@ -1095,7 +1075,7 @@ defmodule Ecto.Query.Planner do
10951075
inner_query
10961076
else
10971077
update_in(inner_query.select.fields, fn fields ->
1098-
subquery.select |> subquery_struct_and_fields() |> elem(1) |> Enum.zip(fields)
1078+
subquery.select |> subquery_source_fields() |> Enum.zip(fields)
10991079
end)
11001080
end
11011081

@@ -1348,8 +1328,6 @@ defmodule Ecto.Query.Planner do
13481328

13491329
# Expression handling
13501330

1351-
@aggs ~w(count avg min max sum row_number rank dense_rank percent_rank cume_dist ntile lag lead first_value last_value nth_value)a
1352-
13531331
defp collect_fields({agg, _, [{{:., dot_meta, [{:&, _, [_]}, _]}, _, []} | _]} = expr,
13541332
fields, from, _query, _take, _keep_literals?)
13551333
when agg in @aggs do
@@ -1595,7 +1573,7 @@ defmodule Ecto.Query.Planner do
15951573
{{:source, {source, schema}, prefix || query.prefix, types}, fields}
15961574

15971575
{:error, %Ecto.SubQuery{select: select}} ->
1598-
{_, fields} = subquery_struct_and_fields(select)
1576+
fields = subquery_source_fields(select)
15991577
{select, Enum.map(fields, &select_field(&1, ix))}
16001578
end
16011579
end

test/ecto/query/planner_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,7 @@ defmodule Ecto.Query.PlannerTest do
11271127
|> normalize()
11281128
%{queries: [{"cte", query}]} = with_expr
11291129
assert query.sources == {{"comments", Comment, nil}}
1130-
assert {:&, [], [0]} = query.select.expr
1130+
assert {:%{}, [], [id: _, text: _] ++ _} = query.select.expr
11311131
assert [{:id, {{:., _, [{:&, _, [0]}, :id]}, _, []}},
11321132
{:text, {{:., _, [{:&, _, [0]}, :text]}, _, []}},
11331133
_ | _] = query.select.fields

test/ecto/query/subquery_test.exs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ defmodule Ecto.Query.SubqueryTest do
177177
describe "plan: subqueries select" do
178178
test "supports implicit select" do
179179
query = plan(from(subquery(Post), [])) |> elem(0)
180-
assert "&0" = Macro.to_string(query.from.source.query.select.expr)
180+
assert "%{id: &0.id(), title: &0.title(), text: &0.text()}" = Macro.to_string(query.from.source.query.select.expr)
181181
end
182182

183183
test "supports field selector" do
@@ -202,28 +202,21 @@ defmodule Ecto.Query.SubqueryTest do
202202
test "supports structs" do
203203
query = from p in Post, select: %Post{text: p.text}
204204
query = plan(from(subquery(query), [])) |> elem(0)
205-
assert "%Ecto.Query.SubqueryTest.Post{text: &0.text()}" =
205+
assert "%{text: &0.text()}" =
206206
Macro.to_string(query.from.source.query.select.expr)
207207
end
208208

209209
test "supports update in maps" do
210210
query = from p in Post, select: %{p | text: p.title}
211211
query = plan(from(subquery(query), [])) |> elem(0)
212-
assert "%Ecto.Query.SubqueryTest.Post{id: &0.id(), title: &0.title(), " <>
213-
"text: &0.title()}" =
212+
assert "%{id: &0.id(), title: &0.title(), text: &0.title()}" =
214213
Macro.to_string(query.from.source.query.select.expr)
215-
216-
query = from p in Post, select: %{p | unknown: p.title}
217-
assert_raise Ecto.SubQueryError, ~r/invalid key `:unknown`/, fn ->
218-
plan(from(subquery(query), []))
219-
end
220214
end
221215

222216
test "supports merge" do
223217
query = from p in Post, select: merge(p, %{text: p.title})
224218
query = plan(from(subquery(query), [])) |> elem(0)
225-
assert "%Ecto.Query.SubqueryTest.Post{id: &0.id(), title: &0.title(), " <>
226-
"text: &0.title()}" =
219+
assert "%{id: &0.id(), title: &0.title(), text: &0.title()}" =
227220
Macro.to_string(query.from.source.query.select.expr)
228221

229222
query = from p in Post, select: merge(%{}, %{})

0 commit comments

Comments
 (0)