Ecto: Repo.all behaves differently when querying with and without condition for nested association

Given the following schemas:

schema "artists" do
  has_many :albums, Album
  has_many :tracks, through: [:albums, :tracks]
end

schema "albums" do
  belongs_to Artist
  has_many :tracks, Track
end

schema "tracks" do
  belongs_to Album
end

The following preloads all the albums and tracks:

Repo.all(from Artist, preload: [albums: :tracks])

But the following doesn’t preload any albums and tracks:

q = from a in Artist, where: a.name == "foobar"
Repo.all(q, preload: [albums: :tracks])

I know I can achieve the goal by using :join and assoc/3 when building the query, and ignoring the fact that the Repo.all method requires two queries instead of one, I’m curious why the conditional query doesn’t work the same way as the non-conditional one. I likely have missed it, but is such “discrepancy” documented anywhere?

Thanks in advance.

Repo.all does not use preload as an option. What you see in your first example is Repo.all with one argument (preload is part of from). In the second one you are passing preload as an option in the second argument, but it gets ignored.

3 Likes

Ah, I see. I have the impression that Elixir libraries always flag invalid option keys but not in this case. In any case, thanks!

The Repo query functions accept arbitrary opts, not only for the adapters but also for extensibility. For example, the multi-tenancy guides use an org_id option to scope queries. It would not be possible to validate the opts for this reason.

Anyway, as you have probably pieced together, you would want to do this:

q = from a in Artist, where: a.name == "foobar"
Repo.all(from q, preload: [albums: :tracks])

Which is of course equivalent to:

Repo.all(from(q, [{:preload, [...]}]))
1 Like

Ah… that’s interesting. Thanks for sharing this nugget.