From 99d9588c357b1b7e3e6c7d299cd6496cbfea5da9 Mon Sep 17 00:00:00 2001 From: Derek Kraan Date: Tue, 1 Jul 2025 10:54:59 +0200 Subject: [PATCH 1/2] Propagate opts configured in `prepare_query/3` to preloads This ensures that when `:on_preloader_spawn` is set in the `prepare_query/3` callback, it is propagated to the preloader. --- lib/ecto/repo/queryable.ex | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/ecto/repo/queryable.ex b/lib/ecto/repo/queryable.ex index 43e8d07797..3264671d32 100644 --- a/lib/ecto/repo/queryable.ex +++ b/lib/ecto/repo/queryable.ex @@ -213,7 +213,7 @@ defmodule Ecto.Repo.Queryable do ## Helpers - defp execute(operation, name, query, {adapter_meta, opts} = tuplet) do + defp execute(operation, name, query, {adapter_meta, opts} = _tuplet) do %{adapter: adapter, cache: cache, repo: repo} = adapter_meta {query, opts} = repo.prepare_query(operation, query, opts) @@ -244,7 +244,14 @@ defmodule Ecto.Repo.Queryable do {count, rows |> Ecto.Repo.Assoc.query(assocs, sources, preprocessor) - |> Ecto.Repo.Preloader.query(name, preloads, take, assocs, postprocessor, tuplet)} + |> Ecto.Repo.Preloader.query( + name, + preloads, + take, + assocs, + postprocessor, + {adapter_meta, opts} + )} end end From c92d8a808e8c7cdaa0bd57cd534561c0dc71b11b Mon Sep 17 00:00:00 2001 From: Derek Kraan Date: Tue, 1 Jul 2025 14:46:19 +0200 Subject: [PATCH 2/2] WIP tests --- lib/ecto/repo.ex | 8 +++++++- test/ecto/repo_test.exs | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex index e570e0b038..7ad6320016 100644 --- a/lib/ecto/repo.ex +++ b/lib/ecto/repo.ex @@ -644,11 +644,17 @@ defmodule Ecto.Repo do def preload(struct_or_structs_or_nil, preloads, opts \\ []) do repo = get_dynamic_repo() + {adapter_meta, opts} = + _tuplet = + Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:preload, opts)) + + {_query, opts} = repo.prepare_query(:preload, struct_or_structs_or_nil, opts) + Ecto.Repo.Preloader.preload( struct_or_structs_or_nil, repo, preloads, - Ecto.Repo.Supervisor.tuplet(repo, prepare_opts(:preload, opts)) + {adapter_meta, opts} ) end diff --git a/test/ecto/repo_test.exs b/test/ecto/repo_test.exs index bd10e72f11..01d9c420aa 100644 --- a/test/ecto/repo_test.exs +++ b/test/ecto/repo_test.exs @@ -182,7 +182,7 @@ defmodule Ecto.RepoTest do end defmodule MySchemaOneField do - use Ecto.Schema + use Ecto.Schema @primary_key false schema "my_schema" do @@ -2211,6 +2211,13 @@ defmodule Ecto.RepoTest do def prepare_query(op, query, opts) do send(self(), {op, query, opts}) + + opts = + case Keyword.fetch(opts, :for_on_preloader_spawn) do + {:ok, fun} -> [{:on_preloader_spawn, fun} | opts] + _ -> opts + end + {%{query | prefix: "rewritten"}, opts} end end @@ -2270,6 +2277,32 @@ defmodule Ecto.RepoTest do assert_received {:callback_ran, pid2} when pid2 != self() assert pid1 != pid2 end + + test "preload with :on_preloader_spawn in prepare_query/3 callback" do + test_process = self() + fun = fn -> send(test_process, {:callback_ran, self()}) end + + %MySchemaWithMultiAssoc{parent_id: 1, mother_id: 2} + |> PrepareRepo.preload([:parent, :mother], for_on_preloader_spawn: fun) + + assert_received {:callback_ran, pid1} when pid1 != self() + assert_received {:callback_ran, pid2} when pid2 != self() + assert pid1 != pid2 + end + + test "all with preload with :on_preloader_spawn in prepare_query/3 callback" do + test_process = self() + fun = fn -> send(test_process, {:callback_ran, self()}) end + + from(p in MyParent, + preload: [:parent, :mother] + ) + |> PrepareRepo.all(for_on_preloader_spawn: fun) + + assert_received {:callback_ran, pid1} when pid1 != self() + assert_received {:callback_ran, pid2} when pid2 != self() + assert pid1 != pid2 + end end describe "prepare_transaction" do