From 6b88c49cd2216f64333b4aedb77e1f07062dc01b Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Mon, 20 Jul 2026 22:51:32 -0400 Subject: [PATCH 1/3] allow selecting fragment source with columns --- lib/ecto/query/planner.ex | 11 +++++++++-- lib/ecto/repo/queryable.ex | 4 ++++ test/ecto/query/planner_test.exs | 6 ++++++ test/ecto/query/subquery_test.exs | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/ecto/query/planner.ex b/lib/ecto/query/planner.ex index 129abc24a7..cbcb663d91 100644 --- a/lib/ecto/query/planner.ex +++ b/lib/ecto/query/planner.ex @@ -1282,8 +1282,11 @@ defmodule Ecto.Query.Planner do error!(query, "queries that do not have a schema need to explicitly pass a :select clause") end - def ensure_select(%{select: nil, from: %{source: {:fragment, _, _}}} = query, true) do - error!(query, "queries from a fragment need to explicitly pass a :select clause") + def ensure_select(%{select: nil, from: %{source: {:fragment, [], _}}} = query, true) do + error!( + query, + "queries from a fragment need to explicitly pass a :select clause or use the `:columns` option" + ) end def ensure_select(%{select: nil} = query, true) do @@ -2248,6 +2251,10 @@ defmodule Ecto.Query.Planner do {{:map, Enum.map(fields, &{&1, {:value, :any}})}, Enum.map(fields, &select_field(&1, ix, :always))} + {:error, {:fragment, [column_names: columns], _}} -> + {types, fields} = select_dump(columns, %{}, ix, drop) + {{:source, :fragment, nil, types}, fields} + {:error, {:fragment, _, _}} -> {{:value, :map}, [{:&, [], [ix]}]} diff --git a/lib/ecto/repo/queryable.ex b/lib/ecto/repo/queryable.ex index dc462a6a60..f8c87b7ef8 100644 --- a/lib/ecto/repo/queryable.ex +++ b/lib/ecto/repo/queryable.ex @@ -315,6 +315,10 @@ defmodule Ecto.Repo.Queryable do values_list_load!(types, row, [], true, adapter) end + defp process(row, {:source, :fragment, _prefix, types}, _from, adapter) do + struct_load!(types, row, [], true, %{}, adapter) + end + defp process(row, {:merge, left, right}, from, adapter) do {left, row} = process(row, left, from, adapter) {right, row} = process(row, right, from, adapter) diff --git a/test/ecto/query/planner_test.exs b/test/ecto/query/planner_test.exs index f128b838bb..c33d52547a 100644 --- a/test/ecto/query/planner_test.exs +++ b/test/ecto/query/planner_test.exs @@ -2502,6 +2502,12 @@ defmodule Ecto.Query.PlannerTest do end end + test "normalize: select source on fragment with columns" do + query = from f in fragment("select 1", columns: [:x]), select: f + {_, _, _, select} = normalize_with_params(query) + assert %{from: {_, {:source, :fragment, nil, [x: :any]}}} = select + end + test "normalize: select with map/2" do query = Post |> select([p], map(p, [:id, :title])) |> normalize() assert query.select.expr == {:&, [], [0]} diff --git a/test/ecto/query/subquery_test.exs b/test/ecto/query/subquery_test.exs index 8ce3601d43..1718f29232 100644 --- a/test/ecto/query/subquery_test.exs +++ b/test/ecto/query/subquery_test.exs @@ -288,7 +288,7 @@ defmodule Ecto.Query.SubqueryTest do test "raises on fragment source without :select" do query = from f in fragment("select 1 as x") - assert_raise Ecto.SubQueryError, ~r/queries from a fragment need to explicitly pass a :select clause in query/, fn -> + assert_raise Ecto.SubQueryError, ~r/queries from a fragment need to explicitly pass a :select clause/, fn -> plan(from(subquery(query), [])) end end From 5d06a69333a687444410391d9c01513c3e40ce0d Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Mon, 20 Jul 2026 22:55:03 -0400 Subject: [PATCH 2/3] update test --- test/ecto/query/planner_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ecto/query/planner_test.exs b/test/ecto/query/planner_test.exs index c33d52547a..fb2069e3d9 100644 --- a/test/ecto/query/planner_test.exs +++ b/test/ecto/query/planner_test.exs @@ -2503,7 +2503,7 @@ defmodule Ecto.Query.PlannerTest do end test "normalize: select source on fragment with columns" do - query = from f in fragment("select 1", columns: [:x]), select: f + query = from f in fragment("select 1", columns: [:x]) {_, _, _, select} = normalize_with_params(query) assert %{from: {_, {:source, :fragment, nil, [x: :any]}}} = select end From bbf48d6c8afb5d52071b02c9ea47ffb5ed38dc68 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Tue, 21 Jul 2026 08:05:38 -0400 Subject: [PATCH 3/3] fixes --- lib/ecto/query/planner.ex | 13 +++++++------ lib/ecto/repo/queryable.ex | 4 ---- test/ecto/query/planner_test.exs | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/ecto/query/planner.ex b/lib/ecto/query/planner.ex index cbcb663d91..1615f78d16 100644 --- a/lib/ecto/query/planner.ex +++ b/lib/ecto/query/planner.ex @@ -2251,12 +2251,13 @@ defmodule Ecto.Query.Planner do {{:map, Enum.map(fields, &{&1, {:value, :any}})}, Enum.map(fields, &select_field(&1, ix, :always))} - {:error, {:fragment, [column_names: columns], _}} -> - {types, fields} = select_dump(columns, %{}, ix, drop) - {{:source, :fragment, nil, types}, fields} - - {:error, {:fragment, _, _}} -> - {{:value, :map}, [{:&, [], [ix]}]} + {:error, {:fragment, meta, _}} -> + if columns = meta[:column_names] do + {{:map, Enum.map(columns, &{&1, {:value, :any}})}, + Enum.map(columns, &select_field(&1, ix, :always))} + else + {{:value, :map}, [{:&, [], [ix]}]} + end {:error, {:values, _, [types, _]}} -> fields = Keyword.keys(types) diff --git a/lib/ecto/repo/queryable.ex b/lib/ecto/repo/queryable.ex index f8c87b7ef8..dc462a6a60 100644 --- a/lib/ecto/repo/queryable.ex +++ b/lib/ecto/repo/queryable.ex @@ -315,10 +315,6 @@ defmodule Ecto.Repo.Queryable do values_list_load!(types, row, [], true, adapter) end - defp process(row, {:source, :fragment, _prefix, types}, _from, adapter) do - struct_load!(types, row, [], true, %{}, adapter) - end - defp process(row, {:merge, left, right}, from, adapter) do {left, row} = process(row, left, from, adapter) {right, row} = process(row, right, from, adapter) diff --git a/test/ecto/query/planner_test.exs b/test/ecto/query/planner_test.exs index fb2069e3d9..df91554426 100644 --- a/test/ecto/query/planner_test.exs +++ b/test/ecto/query/planner_test.exs @@ -2505,7 +2505,7 @@ defmodule Ecto.Query.PlannerTest do test "normalize: select source on fragment with columns" do query = from f in fragment("select 1", columns: [:x]) {_, _, _, select} = normalize_with_params(query) - assert %{from: {_, {:source, :fragment, nil, [x: :any]}}} = select + assert %{from: {_, {:map, [x: {:value, :any}]}}} = select end test "normalize: select with map/2" do