From e5eb0c2def39e9dd5c800cd6b6b233ba3455ac07 Mon Sep 17 00:00:00 2001 From: jeroen11dijk Date: Thu, 17 Sep 2026 23:26:13 +0200 Subject: [PATCH 1/2] fix: discard a uniq? aggregate's sort when it is not the aggregated field `array_agg(DISTINCT x ORDER BY y)` is rejected by postgres with 42P10 unless every ORDER BY expression also appears in the argument list. A `uniq?: true` list aggregate reached that error two ways: an explicit `sort` on the aggregate, and a `sort` declared on the relationship it traverses, which was inherited silently. The ordering key is exactly what deduplication discards, so the sort cannot be honoured either way. Keep only the sort terms on the aggregated field and fall through to the existing unsorted `array_agg(DISTINCT ?)` branch otherwise. Sorting a uniq? aggregate by the field it aggregates is unaffected. --- lib/aggregate.ex | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/lib/aggregate.ex b/lib/aggregate.ex index b532436..11b4ed3 100644 --- a/lib/aggregate.ex +++ b/lib/aggregate.ex @@ -2096,6 +2096,25 @@ defmodule AshSql.Aggregate do defp has_sort?(%{sort: _}), do: true defp has_sort?(_), do: false + # `array_agg(DISTINCT x ORDER BY y)` is rejected by postgres unless every ORDER BY + # expression also appears in the argument list, so a `uniq?` aggregate can only be + # ordered by the field it aggregates. Sorting by anything else is discarded rather + # than emitted as invalid SQL - it is unsatisfiable either way, since the ordering + # key is exactly what deduplication throws away. This also keeps a `sort` declared + # on the relationship from reaching an aggregate that never asked to be sorted. + defp distinct_safe_sort(sort, aggregate) do + if Map.get(aggregate, :uniq?) do + aggregated_field = Map.get(aggregate, :field) + + Enum.filter(sort, fn + {field, _direction} -> field == aggregated_field + _ -> false + end) + else + sort + end + end + def add_subquery_aggregate_select( query, relationship_path, @@ -2327,15 +2346,22 @@ defmodule AshSql.Aggregate do has_sort? = has_sort?(aggregate.query) - {sorted, include_nil_filter_field, query} = - if has_sort? || (first_relationship && first_relationship.sort not in [nil, []]) do - {sort, binding} = - if has_sort? do - {aggregate.query.sort, binding} - else - {List.wrap(first_relationship.sort), query.__ash_bindings__.root_binding} - end + {sort, binding} = + cond do + has_sort? -> + {aggregate.query.sort, binding} + + first_relationship && first_relationship.sort not in [nil, []] -> + {List.wrap(first_relationship.sort), query.__ash_bindings__.root_binding} + true -> + {[], binding} + end + + sort = distinct_safe_sort(sort, aggregate) + + {sorted, include_nil_filter_field, query} = + if sort != [] do {:ok, sort_expr, query} = AshSql.Sort.sort( query, From 8f398ee23ebde88f4030bbe63e3d38bb659251c2 Mon Sep 17 00:00:00 2001 From: jeroen11dijk Date: Thu, 17 Sep 2026 23:26:18 +0200 Subject: [PATCH 2/2] fix: apply the from_many? gate when sorting a filter subquery `related_subquery` was passed `sort?` twice in the same keyword list. It reads the option with `Keyword.get/3`, which returns the first match, so `sort?: Map.get(relationship, :from_many?)` never took effect. Keyword lists permit duplicate keys, so nothing warned. The result is that a sort declared on a relationship's read action is carried into the join subquery for a `belongs_to`, where the join discards the ordering again. On a large table that is a full sort per query. Combine both intents rather than dropping either, so a caller passing `sort?: false` can still opt out. --- lib/join.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/join.ex b/lib/join.ex index d8a1893..12f8560 100644 --- a/lib/join.ex +++ b/lib/join.ex @@ -1189,13 +1189,12 @@ defmodule AshSql.Join do current_binding ) do case related_subquery(relationship, query, - sort?: sort?, apply_filter: apply_filter, start_bindings_at: 500, refs_at_path: path, require_lateral?: require_lateral?, filter_subquery?: true, - sort?: Map.get(relationship, :from_many?), + sort?: sort? && !!Map.get(relationship, :from_many?), on_subquery: fn subquery -> if !Map.get(relationship, :from_many?) || Map.get(relationship, :no_attributes?) do subquery