From 1d30630ef3a17dd1dfd985d9091331292bc40c6c Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Tue, 9 Jun 2026 21:30:24 +0200 Subject: [PATCH] select_all proposal --- integration_test/cases/repo.exs | 54 +++++++++++++++++++ lib/ecto/repo.ex | 95 +++++++++++++++++++++++++++++++++ lib/ecto/repo/queryable.ex | 16 ++++-- test/ecto/repo_test.exs | 14 +++++ 4 files changed, 175 insertions(+), 4 deletions(-) diff --git a/integration_test/cases/repo.exs b/integration_test/cases/repo.exs index 68549b0c34..8f66dac508 100644 --- a/integration_test/cases/repo.exs +++ b/integration_test/cases/repo.exs @@ -98,6 +98,60 @@ defmodule Ecto.Integration.RepoTest do assert TestRepo.all_by(Post, title: "b") |> Enum.sort() == [post3] end + test "select_all" do + post1 = TestRepo.insert!(%Post{title: "a"}) + post2 = TestRepo.insert!(%Post{title: "b"}) + post3 = TestRepo.insert!(%Post{title: "c"}) + + # Test with full schema + {count, posts} = TestRepo.select_all(Post) + assert count == 3 + assert Enum.sort(posts) == [post1, post2, post3] + + # Test with query and select + query = from p in Post, where: p.title in ["a", "b"], select: p.title + {count, titles} = TestRepo.select_all(query) + assert count == 2 + assert Enum.sort(titles) == ["a", "b"] + + # Test with empty result + query = from p in Post, where: p.title == "nonexistent" + {count, posts} = TestRepo.select_all(query) + assert count == 0 + assert posts == [] + + # Test without schema + {count, titles} = + TestRepo.select_all(from(p in "posts", order_by: p.title, select: p.title)) + assert count == 3 + assert titles == ["a", "b", "c"] + end + + test "select_all_by" do + post1 = TestRepo.insert!(%Post{title: "a"}) + post2 = TestRepo.insert!(%Post{title: "a"}) + post3 = TestRepo.insert!(%Post{title: "b"}) + + {count, posts} = TestRepo.select_all_by(Post, title: "a") + assert count == 2 + assert Enum.sort(posts) == [post1, post2] + + {count, posts} = TestRepo.select_all_by(Post, title: "b") + assert count == 1 + assert posts == [post3] + + # Test with query + query = from p in Post + {count, posts} = TestRepo.select_all_by(query, title: "a") + assert count == 2 + assert Enum.sort(posts) == [post1, post2] + + # Test with empty result + {count, posts} = TestRepo.select_all_by(Post, title: "nonexistent") + assert count == 0 + assert posts == [] + end + test "insert, update and delete" do post = %Post{title: "insert, update, delete", visits: 1} meta = post.__meta__ diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex index c99d5fabca..d9021b7aac 100644 --- a/lib/ecto/repo.ex +++ b/lib/ecto/repo.ex @@ -546,6 +546,31 @@ defmodule Ecto.Repo do ) end + def select_all(queryable, opts \\ []) do + validate_query_opts!(opts, :select_all) + + repo = get_dynamic_repo() + + Ecto.Repo.Queryable.select_all( + repo, + queryable, + Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:all, opts)) + ) + end + + def select_all_by(queryable, clauses, opts \\ []) do + validate_query_opts!(opts, :select_all_by) + + repo = get_dynamic_repo() + + Ecto.Repo.Queryable.select_all_by( + repo, + queryable, + clauses, + Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:all, opts)) + ) + end + def all_by(queryable, clauses, opts \\ []) do validate_query_opts!(opts, :all_by) @@ -1485,6 +1510,76 @@ defmodule Ecto.Repo do @doc group: "Query API" @callback all(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: [Ecto.Schema.t() | term] + @doc """ + Fetches all entries from the data store matching the given query. + + Similar to `c:all/2`, but returns a tuple with the count of rows and the list of results. + + May raise `Ecto.QueryError` if query validation fails. + + ## Options + + * `:prefix` - The prefix to run the query on (such as the schema path + in Postgres or the database in MySQL). This will be applied to all `from` + and `join`s in the query that did not have a prefix previously given + either via the `:prefix` option on `join`/`from` or via `@schema_prefix` + in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the + `Ecto.Query` documentation. + + See the ["Shared options"](#module-shared-options) section at the module + documentation for more options. + + ## Example + + # Fetch all post titles with count + query = from p in Post, + select: p.title + {count, titles} = MyRepo.select_all(query) + + # With returning the full struct + {count, posts} = from(p in Post, select: p) |> MyRepo.select_all() + """ + @doc group: "Query API" + @callback select_all(queryable :: Ecto.Queryable.t(), opts :: Keyword.t()) :: + {non_neg_integer, [Ecto.Schema.t() | term]} + + @doc """ + Fetches all entries from the data store matching the given query and conditions. + + Similar to `c:all_by/3`, but returns a tuple with the count of rows and the list of results. + + May raise `Ecto.QueryError` if query validation fails. + + This function is a shortcut for `c:select_all/2` when adjusting the given query with simple conditions. + + See also `c:select_all/2`, `c:all_by/3`, and `c:get_by/3`. + + ## Options + + * `:prefix` - The prefix to run the query on (such as the schema path + in Postgres or the database in MySQL). This will be applied to all `from` + and `join`s in the query that did not have a prefix previously given + either via the `:prefix` option on `join`/`from` or via `@schema_prefix` + in the schema. For more information see the ["Query Prefix"](`m:Ecto.Query#module-query-prefix`) section of the + `Ecto.Query` documentation. + + See the ["Shared options"](#module-shared-options) section at the module + documentation for more options. + + ## Example + + {count, posts} = MyRepo.select_all_by(Post, author_id: 1) + + query = from p in Post + {count, posts} = MyRepo.select_all_by(query, author_id: 1) + """ + @doc group: "Query API" + @callback select_all_by( + queryable :: Ecto.Queryable.t(), + clauses :: Keyword.t() | map, + opts :: Keyword.t() + ) :: {non_neg_integer, [Ecto.Schema.t() | term]} + @doc """ Fetches all entries from the data store matching the given query and conditions. diff --git a/lib/ecto/repo/queryable.ex b/lib/ecto/repo/queryable.ex index dc462a6a60..1872b15133 100644 --- a/lib/ecto/repo/queryable.ex +++ b/lib/ecto/repo/queryable.ex @@ -10,22 +10,30 @@ defmodule Ecto.Repo.Queryable do require Ecto.Query - def all(name, queryable, tuplet) do + def select_all(name, queryable, tuplet) do query = queryable |> Ecto.Queryable.to_query() |> Ecto.Query.Planner.ensure_select(true) - execute(:all, name, query, tuplet) |> elem(1) + execute(:all, name, query, tuplet) end - def all_by(name, queryable, clauses, tuplet) do + def select_all_by(name, queryable, clauses, tuplet) do query = queryable |> Ecto.Query.where([], ^Enum.to_list(clauses)) |> Ecto.Query.Planner.ensure_select(true) - execute(:all, name, query, tuplet) |> elem(1) + execute(:all, name, query, tuplet) + end + + def all(name, queryable, tuplet) do + select_all(name, queryable, tuplet) |> elem(1) + end + + def all_by(name, queryable, clauses, tuplet) do + select_all_by(name, queryable, clauses, tuplet) |> elem(1) end def stream(_name, queryable, {adapter_meta, opts}) do diff --git a/test/ecto/repo_test.exs b/test/ecto/repo_test.exs index 31991ef54a..9875ab6f15 100644 --- a/test/ecto/repo_test.exs +++ b/test/ecto/repo_test.exs @@ -2318,6 +2318,20 @@ defmodule Ecto.RepoTest do assert_received {:all, %{prefix: "rewritten"}} end + test "select_all" do + query = from p in MyParent, select: p + + PrepareRepo.select_all(query, hello: :world) + assert_received {:all, ^query, [hello: :world]} + assert_received {:all, %{prefix: "rewritten"}} + end + + test "select_all_by" do + PrepareRepo.select_all_by(MyParent, [id: 1], hello: :world) + assert_received {:all, _query, [hello: :world]} + assert_received {:all, %{prefix: "rewritten"}} + end + test "update_all" do query = from p in MyParent, update: [set: [n: 1]] PrepareRepo.update_all(query, [], hello: :world)