diff --git a/CHANGELOG.md b/CHANGELOG.md index 5453669066..eaa3836d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog for v3.x +## Unreleased + +### Enhancements + + * [Ecto.Repo] Preload custom queries with `order_by` now take precedence over `:preload_order`. The `:preload_order` option is now only applied when no custom query with ordering is provided. + ## v3.13.3 (2025-09-19) ### Enhancements diff --git a/integration_test/cases/preload.exs b/integration_test/cases/preload.exs index 9e82559ccc..958f43342a 100644 --- a/integration_test/cases/preload.exs +++ b/integration_test/cases/preload.exs @@ -695,6 +695,23 @@ defmodule Ecto.Integration.PreloadTest do assert [%{name: "foz"}, %{name: "baz"}] = post2.ordered_users_by_join_table end + test "custom query order_by overrides preload_order" do + post = TestRepo.insert!(%Post{title: "1"}) + + TestRepo.insert!(%Comment{text: "2", post_id: post.id}) + TestRepo.insert!(%Comment{text: "1", post_id: post.id}) + TestRepo.insert!(%Comment{text: "3", post_id: post.id}) + + # Without custom query, preload_order (asc by text) should apply + post = TestRepo.preload(post, :ordered_comments) + assert [%{text: "1"}, %{text: "2"}, %{text: "3"}] = post.ordered_comments + + # With custom query having order_by, it should override preload_order + query = from(c in Comment, order_by: [desc: c.text]) + post = TestRepo.preload(post, [ordered_comments: query], force: true) + assert [%{text: "3"}, %{text: "2"}, %{text: "1"}] = post.ordered_comments + end + ## Others @tag :invalid_prefix diff --git a/lib/ecto/repo/preloader.ex b/lib/ecto/repo/preloader.ex index ad7f1f7fed..a714c1b931 100644 --- a/lib/ecto/repo/preloader.ex +++ b/lib/ecto/repo/preloader.ex @@ -483,6 +483,11 @@ defmodule Ecto.Repo.Preloader do defp add_preload_order([], query), do: query + defp add_preload_order(_order, %{order_bys: [_|_]} = query) do + # Skip applying preload_order when query already has custom order_by clauses + query + end + defp add_preload_order(order, query) when is_list(order) do Ecto.Query.prepend_order_by(query, [q], ^order) end diff --git a/lib/ecto/schema.ex b/lib/ecto/schema.ex index d54072c4ef..13a8a03929 100644 --- a/lib/ecto/schema.ex +++ b/lib/ecto/schema.ex @@ -786,6 +786,8 @@ defmodule Ecto.Schema do For example, if you set `Post.has_many :comments, preload_order: [asc: :content]`, whenever the `:comments` associations is preloaded, the comments will be ordered by the `:content` field. + Note that if you provide a custom query with its own `order_by` clause, + the custom ordering will take precedence and the `:preload_order` will not be applied. See `Ecto.Query.order_by/3` to learn more about ordering expressions. ## Examples @@ -1352,6 +1354,8 @@ defmodule Ecto.Schema do It may be a keyword list/list of fields or an MFA tuple, such as `{Mod, fun, []}`. Both cases must resolve to a valid `order_by` expression. See `Ecto.Query.order_by/3` to learn more about ordering expressions. + Note that if you provide a custom query with its own `order_by` clause, + the custom ordering will take precedence and the `:preload_order` will not be applied. See the [preload order](#many_to_many/3-preload-order) section below to learn how this option can be utilized