Skip to content

Commit 0123f91

Browse files
authored
support selecting a subset of a subquery as a struct (#4678)
1 parent 12dd3d0 commit 0123f91

File tree

2 files changed

+52
-3
lines changed

2 files changed

+52
-3
lines changed

lib/ecto/query/planner.ex

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2207,8 +2207,8 @@ defmodule Ecto.Query.Planner do
22072207
{{:ok, {:struct, _}}, {:fragment, _, _}} ->
22082208
error!(query, "it is not possible to return a struct subset of a fragment")
22092209

2210-
{{:ok, {:struct, _}}, %Ecto.SubQuery{}} ->
2211-
error!(query, "it is not possible to return a struct subset of a subquery")
2210+
{{:ok, {:struct, fields}}, %Ecto.SubQuery{select: select}} ->
2211+
subquery_select_fields(select, fields, ix, query)
22122212

22132213
{{:ok, {_, []}}, {_, _, _}} ->
22142214
error!(
@@ -2280,6 +2280,35 @@ defmodule Ecto.Query.Planner do
22802280
end)
22812281
end
22822282

2283+
defp subquery_select_fields(select, requested_fields, ix, query) do
2284+
available_fields = subquery_source_fields(select)
2285+
requested_fields = List.wrap(requested_fields)
2286+
2287+
schema =
2288+
case select do
2289+
{:source, {_, schema}, _, _} when not is_nil(schema) -> schema
2290+
2291+
_ ->
2292+
error!(query, "it is not possible to return a struct subset of a subquery that does not return a schema struct")
2293+
end
2294+
2295+
types =
2296+
Enum.map(requested_fields, fn field ->
2297+
case subquery_type_for(select, field) do
2298+
{:ok, type} ->
2299+
{field, type}
2300+
2301+
:error ->
2302+
error!(query, "field `#{field}` in struct/2 is not available in the subquery. " <>
2303+
"Subquery only returns fields: #{inspect(available_fields)}")
2304+
end
2305+
end)
2306+
2307+
field_exprs = Enum.map(requested_fields, &select_field(&1, ix, :always))
2308+
2309+
{{:source, {nil, schema}, nil, types}, field_exprs}
2310+
end
2311+
22832312
defp select_field(field, ix, writable) do
22842313
{{:., [writable: writable], [{:&, [], [ix]}, field]}, [], []}
22852314
end

test/ecto/query/subquery_test.exs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,28 @@ defmodule Ecto.Query.SubqueryTest do
447447
subquery = from p in Post, select: %{id: p.id, title: p.title}
448448
query = normalize(from(p in subquery(subquery), select: map(p, [:title])))
449449
assert [{{:., _, [{:&, [], [0]}, :title]}, [], []}] = query.select.fields
450+
end
451+
452+
test "struct/2 with subqueries" do
453+
subquery = from p in Post, select: p
454+
query = normalize(from(p in subquery(subquery), select: struct(p, [:id, :title])))
455+
assert query.select.fields == [
456+
{{:., [writable: :always], [{:&, [], [0]}, :id]}, [], []},
457+
{{:., [writable: :always], [{:&, [], [0]}, :title]}, [], []}
458+
]
459+
460+
subquery = from p in Post, select: p
461+
query = normalize(from(c in Comment, join: p in subquery(subquery), on: true, select: struct(p, [:title])))
462+
assert query.select.fields == [{{:., [writable: :always], [{:&, [], [1]}, :title]}, [], []}]
463+
464+
subquery = from p in Post, select: struct(p, [:id, :title, :text])
465+
query = normalize(from(p in subquery(subquery), select: struct(p, [:id, :title])))
466+
assert query.select.fields == [
467+
{{:., [writable: :always], [{:&, [], [0]}, :id]}, [], []},
468+
{{:., [writable: :always], [{:&, [], [0]}, :title]}, [], []}
469+
]
450470

451-
assert_raise Ecto.QueryError, ~r/it is not possible to return a struct subset of a subquery/, fn ->
471+
assert_raise Ecto.QueryError, ~r/it is not possible to return a struct subset of a subquery that does not return a schema struct/, fn ->
452472
subquery = from p in Post, select: %{id: p.id, title: p.title}
453473
normalize(from(p in subquery(subquery), select: struct(p, [:title])))
454474
end

0 commit comments

Comments
 (0)