Packages

ash

3.4.29
3.29.3 3.29.2 3.29.1 3.29.0 3.28.0 3.27.8 3.27.7 3.27.6 3.27.5 3.27.4 3.27.3 3.27.2 3.27.1 3.27.0 3.26.0 3.25.2 3.25.1 3.25.0 retired 3.24.7 3.24.6 3.24.5 3.24.4 3.24.3 3.24.2 3.24.1 3.24.0 3.23.1 3.23.0 3.22.2 3.22.1 3.22.0 3.21.3 3.21.2 3.21.1 3.21.0 3.20.0 3.19.3 3.19.2 3.19.1 3.19.0 3.18.0 3.17.1 3.17.0 3.16.0 3.15.0 3.14.1 3.14.0 retired 3.13.2 3.13.1 3.13.0 3.12.0 3.11.3 3.11.2 3.11.1 3.11.0 3.10.1 3.10.0 3.9.0 3.8.0 3.7.6 3.7.5 3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 retired 3.6.3 retired 3.6.2 3.6.1 3.6.0 3.5.43 3.5.42 3.5.41 3.5.40 3.5.39 3.5.38 3.5.37 3.5.36 3.5.35 3.5.34 3.5.33 3.5.32 3.5.31 3.5.30 3.5.29 3.5.28 3.5.27 3.5.26 3.5.25 3.5.24 3.5.23 3.5.22 3.5.21 3.5.20 3.5.19 3.5.18 3.5.17 3.5.16 3.5.15 3.5.14 3.5.13 3.5.12 3.5.11 3.5.10 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.74 retired 3.4.73 3.4.72 3.4.71 3.4.70 3.4.69 3.4.68 3.4.67 3.4.66 3.4.65 3.4.64 3.4.63 3.4.62 3.4.61 3.4.60 3.4.59 3.4.58 3.4.57 3.4.56 3.4.55 3.4.54 3.4.53 3.4.52 3.4.51 3.4.50 3.4.49 3.4.48 3.4.47 3.4.46 3.4.45 3.4.44 3.4.43 3.4.42 3.4.41 3.4.40 3.4.39 3.4.38 3.4.37 3.4.36 3.4.35 3.4.34 3.4.33 3.4.32 3.4.31 3.4.30 3.4.29 3.4.28 3.4.27 3.4.26 3.4.25 3.4.24 3.4.23 3.4.22 3.4.21 3.4.20 3.4.19 3.4.18 3.4.17 3.4.16 3.4.15 3.4.14 3.4.13 3.4.12 3.4.11 3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.5 3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.3 3.3.2 3.3.1 3.3.0 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 3.2.0 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.16 3.0.15 3.0.14 3.0.13 3.0.12 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 3.0.0-rc.46 3.0.0-rc.45 3.0.0-rc.44 3.0.0-rc.43 3.0.0-rc.42 3.0.0-rc.41 3.0.0-rc.40 3.0.0-rc.39 3.0.0-rc.38 3.0.0-rc.37 3.0.0-rc.36 3.0.0-rc.35 3.0.0-rc.34 3.0.0-rc.33 3.0.0-rc.32 3.0.0-rc.31 3.0.0-rc.29 3.0.0-rc.28 3.0.0-rc.27 3.0.0-rc.26 3.0.0-rc.25 3.0.0-rc.24 3.0.0-rc.23 3.0.0-rc.22 3.0.0-rc.21 3.0.0-rc.20 3.0.0-rc.19 3.0.0-rc.18 3.0.0-rc.17 3.0.0-rc.16 3.0.0-rc.15 3.0.0-rc.14 3.0.0-rc.13 3.0.0-rc.12 3.0.0-rc.11 3.0.0-rc.10 3.0.0-rc.9 3.0.0-rc.8 3.0.0-rc.7 3.0.0-rc.6 3.0.0-rc.5 3.0.0-rc.4 3.0.0-rc.3 3.0.0-rc.1 3.0.0-rc.0 2.21.15 2.21.14 2.21.13 2.21.12 2.21.11 2.21.10 2.21.9 2.21.8 2.21.7 2.21.6 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.3 2.20.2 2.20.1 2.20.0 2.19.14 2.19.13 2.19.12 2.19.11 2.19.10 2.19.9 2.19.8 2.19.7 2.19.6 2.19.5 2.19.4 2.19.3 retired 2.19.2 retired 2.19.1 retired 2.19.0 retired 2.18.2 2.18.1 2.18.0 2.17.24 2.17.23 2.17.22 2.17.21 2.17.20 2.17.19 2.17.18 2.17.17 2.17.16 2.17.15 2.17.14 2.17.13 2.17.12 2.17.11 2.17.10 2.17.9 2.17.8 2.17.7 2.17.6 2.17.5 2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.1 2.16.0 2.15.20 2.15.19 2.15.18 2.15.17 2.15.16 2.15.15 2.15.14 2.15.13 2.15.12 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.2 2.15.1 2.15.0 2.14.21 2.14.20 2.14.19 2.14.18 2.14.17 2.14.16 2.14.15 2.14.14 2.14.13 2.14.12 2.14.11 2.14.10 2.14.9 2.14.8 2.14.7 2.14.6 2.14.5 2.14.4 2.14.3 2.14.2 2.14.1 2.14.0 2.13.4 retired 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.11 2.11.10 2.11.9 2.11.8 2.11.7 2.11.6 2.11.5 2.11.4 2.11.3 2.11.2 2.11.1 2.11.0 2.11.0-rc.3 2.11.0-rc.2 2.11.0-rc.1 2.11.0-rc.0 2.10.2 2.10.1 2.10.0 2.9.29 2.9.28 2.9.27 2.9.26 2.9.25 2.9.24 2.9.23 2.9.22 2.9.21 2.9.20 2.9.19 2.9.18 2.9.17 2.9.16 2.9.15 2.9.14 2.9.13 2.9.12 2.9.11 2.9.10 2.9.9 2.9.8 2.9.7 2.9.6 2.9.5 2.9.4 2.9.3 2.9.2 2.9.1 2.9.0 2.8.1 2.8.0 2.7.1 2.7.0 2.6.31 2.6.30 2.6.29 2.6.28 2.6.27 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.16 2.5.15 2.5.14 2.5.13 2.5.12 2.5.11 2.5.10 2.5.9 2.5.8 2.5.7 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.5.0-rc.6 2.5.0-rc.5 2.5.0-rc.4 2.5.0-rc.3 2.5.0-rc.2 2.5.0-rc.1 2.5.0-rc.0 2.4.30 2.4.29 2.4.28 2.4.27 2.4.26 2.4.25 2.4.24 2.4.23 2.4.22 2.4.21 2.4.20 2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 2.0.0-rc.15 2.0.0-rc.14 2.0.0-rc.13 2.0.0-rc.12 2.0.0-rc.11 2.0.0-rc.10 2.0.0-rc.9 2.0.0-rc.8 2.0.0-rc.7 2.0.0-rc.6 2.0.0-rc.5 2.0.0-rc.4 2.0.0-rc.3 2.0.0-rc.2 2.0.0-rc.1 2.0.0-rc.0 2.0.0-pre.8 2.0.0-pre.7 2.0.0-pre.6 2.0.0-pre.5 2.0.0-pre.4 2.0.0-pre.3 2.0.0-pre.2 2.0.0-pre.1 2.0.0-pre.0 1.53.3 1.53.2 1.53.0 1.52.0-rc.22 1.52.0-rc.21 1.52.0-rc.20 1.52.0-rc.19 1.52.0-rc.18 1.52.0-rc.17 1.52.0-rc.16 1.52.0-rc.15 1.52.0-rc.14 1.52.0-rc.13 1.52.0-rc.12 1.52.0-rc.11 1.52.0-rc.10 1.52.0-rc.9 1.52.0-rc.8 1.52.0-rc.7 1.52.0-rc.6 1.52.0-rc.5 1.52.0-rc.4 1.52.0-rc.3 1.52.0-rc.2 1.52.0-rc.1 1.52.0-rc.0 1.51.2 1.51.1 retired 1.51.0 1.50.21 1.50.20 1.50.19 1.50.18 1.50.17 1.50.16 1.50.15 1.50.14 1.50.13 1.50.12 1.50.11 1.50.10 1.50.9 1.50.8 1.50.7 1.50.6 1.50.5 1.50.4 1.50.3 1.50.2 1.50.1 1.50.0 1.49.0 1.48.0-rc.30 1.48.0-rc.29 1.48.0-rc.28 1.48.0-rc.27 1.48.0-rc.26 1.48.0-rc.25 1.48.0-rc.24 1.48.0-rc.23 1.48.0-rc.22 1.48.0-rc.21 1.48.0-rc.20 1.48.0-rc.19 1.48.0-rc.18 1.48.0-rc.17 1.48.0-rc.16 1.48.0-rc.15 1.48.0-rc.14 1.48.0-rc.13 1.48.0-rc.12 1.48.0-rc.11 1.48.0-rc.10 1.48.0-rc.9 1.48.0-rc.8 1.48.0-rc.7 1.48.0-rc.6 1.48.0-rc.5 1.48.0-rc.4 1.48.0-rc.3 1.48.0-rc.2 1.48.0-rc.1 1.48.0-rc.0 1.47.12 1.47.11 1.47.10 1.47.9 1.47.8 1.47.7 1.47.6 1.47.5 1.47.4 1.47.3 1.47.2 1.47.1 1.47.0 1.46.13 1.46.12 1.46.11 1.46.10 1.46.9 1.46.8 1.46.7 1.46.6 1.46.5 1.46.4 1.46.3 1.46.2 1.46.1 1.46.0 1.45.0-rc9 1.45.0-rc8 1.45.0-rc7 1.45.0-rc6 1.45.0-rc5 1.45.0-rc4 1.45.0-rc3 1.45.0-rc20 1.45.0-rc2 1.45.0-rc19 1.45.0-rc18 1.45.0-rc17 1.45.0-rc16 1.45.0-rc15 1.45.0-rc14 1.45.0-rc13 1.45.0-rc12 1.45.0-rc11 1.45.0-rc10 1.45.0-rc1 1.45.0-rc0 1.44.13 1.44.12 1.44.11 1.44.10 1.44.9 1.44.8 1.44.7 1.44.6 1.44.5 1.44.4 1.44.3 1.44.2 1.44.1 1.44.0 1.43.12 1.43.11 1.43.10 1.43.9 1.43.8 1.43.7 1.43.6 1.43.5 1.43.4 1.43.3 1.43.2 1.43.1 1.43.0 1.42.0 1.41.12 1.41.11 1.41.10 1.41.9 1.41.8 1.41.7 1.41.6 1.41.5 1.41.4 1.41.3 1.41.2 1.41.1 1.41.0 1.40.0 1.39.7 1.39.6 1.39.5 1.39.4 1.39.3 1.39.2 1.39.1 1.39.0 1.38.0 1.37.2 1.37.1 1.37.0 1.36.22 1.36.21 1.36.19 1.36.18 1.36.17 1.36.16 1.36.15 1.36.14 1.36.13 1.36.12 1.36.11 1.36.10 1.36.9 1.36.8 1.36.7 1.36.6 1.36.5 1.36.4 1.36.3 1.36.2 1.36.0 1.35.1 1.35.0 1.34.9 1.34.8 1.34.7 1.34.6 1.34.5 1.34.4 1.34.3 1.34.2 1.34.1 1.34.0 1.33.0 1.32.2 1.32.1 1.32.0 1.31.1 1.31.0 1.30.2 1.30.1 1.29.0-rc1 1.29.0-rc0 1.28.1 1.28.0 1.27.1 1.27.0 1.26.13 1.26.12 1.26.11 1.26.10 1.26.9 1.26.8 1.26.7 1.26.6 1.26.5 1.26.4 1.26.2 1.26.1 1.26.0 1.25.8 1.25.7 1.25.6 1.25.5 1.25.4 1.25.3 1.25.2 1.25.1 1.25.0 1.24.2 1.24.1 1.24.0 1.23.3 1.23.2 1.23.1 1.23.0 1.22.1 1.22.0 1.20.1 1.20.0 1.19.1 1.19.0 1.18.1 1.18.0 1.17.1 1.17.0 1.16.2 1.15.1 1.15.0 1.14.0 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.12.0 1.11.1 1.11.0 1.10.0 1.9.0 1.8.0 1.7.0 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.1 1.5.0 1.4.1 1.4.0 1.3.1 1.3.0 1.2.1 1.2.0 1.1.3 1.1.2 1.1.0 1.0.3 1.0.2 1.0.1 1.0.0 0.13.1 0.13.0 0.12.0 0.10.0 0.9.1 0.9.0 0.8.0 0.7.0 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.2 0.5.1 0.5.0 0.4.0 0.3.0 0.2.0 0.1.9 0.1.8 0.1.3 0.1.1 0.1.0

A declarative, extensible framework for building Elixir applications.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ash lib ash actions sort.ex
Raw

lib/ash/actions/sort.ex

defmodule Ash.Actions.Sort do
@moduledoc false
alias Ash.Error.Query.{
AggregatesNotSupported,
InvalidSortOrder,
NoSuchField,
UnsortableField
}
@sort_orders [:asc, :desc, :asc_nils_first, :asc_nils_last, :desc_nils_first, :desc_nils_last]
def process(_resource, empty, _aggregates, context \\ %{})
def process(_resource, empty, _aggregates, _context) when empty in [nil, []], do: {:ok, []}
def process(resource, sort, aggregates, context) do
sort
|> List.wrap()
|> Enum.map(fn
{key, {context, order}} when is_atom(order) ->
{key, {context, order}}
{key, val} ->
if is_atom(val) do
{key, val}
else
{key, {val, :asc}}
end
val ->
{val, :asc}
end)
|> Enum.reduce({[], []}, fn
{field, {_, inner_order} = order}, {sorts, errors} when inner_order in @sort_orders ->
case Ash.Resource.Info.calculation(resource, field) do
nil ->
{sorts,
[
"Cannot provide arguments to a non-calculation field while sorting"
| errors
]}
%{name: name, sortable?: false} ->
{sorts, [UnsortableField.exception(resource: resource, field: name) | errors]}
calc ->
{module, _opts} = calc.calculation
if module.has_expression?() do
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation_sort) do
calculation_sort(
resource,
field,
calc,
order,
sorts,
errors,
context
)
else
{sorts, ["Datalayer cannot sort on calculations"]}
end
else
{sorts, ["Calculations cannot be sorted on unless they define an expression"]}
end
end
{%Ash.Query.Calculation{sortable?: false} = calc, _order}, {sorts, errors} ->
{sorts, [UnsortableField.exception(resource: resource, field: calc) | errors]}
{%Ash.Query.Calculation{} = calc, order}, {sorts, errors} ->
if String.starts_with?(to_string(calc.name), "__expr_sort__") do
%{opts: [{:expr, expr}]} = calc
expr
|> Ash.Filter.list_refs()
|> Enum.reduce_while(:ok, fn %{relationship_path: path, attribute: attribute}, :ok ->
{ref_attribute, field_name} =
case attribute do
atom when is_atom(attribute) ->
case Ash.Resource.Info.related(resource, path) do
nil ->
raise Ash.Error.Framework.AssumptionFailed,
message: """
Listing refs in #{inspect(expr)}:
Found reference #{inspect(attribute)} at path #{inspect(path)}
No related resource at path #{inspect(path)} from #{inspect(resource)}
"""
related ->
case Ash.Resource.Info.field(related, attribute) do
nil ->
raise Ash.Error.Framework.AssumptionFailed,
message: """
Listing refs in #{inspect(expr)}:
Found reference #{inspect(attribute)} at path #{inspect(path)}
Related resource at path #{inspect(path)} from #{inspect(resource)}
does not have field #{inspect(atom)}
"""
field ->
{field, atom}
end
end
%struct{} = attribute
when struct in [Ash.Query.Aggregate, Ash.Query.Calculation] ->
{attribute, attribute}
other ->
{other, other.name}
end
if ref_attribute.sortable? do
case find_non_sortable_relationship(resource, path, sort) do
nil ->
{:cont, :ok}
{resource, non_sortable_field} ->
{:halt, {:error, resource, non_sortable_field}}
end
else
{:halt, {:error, resource, field_name}}
end
end)
|> case do
:ok ->
if order in @sort_orders do
{sorts ++ [{calc, order}], errors}
else
{sorts, [InvalidSortOrder.exception(order: order) | errors]}
end
{:error, resource, non_sortable_field} ->
{sorts,
[UnsortableField.exception(resource: resource, field: non_sortable_field) | errors]}
end
else
if order in @sort_orders do
{sorts ++ [{calc, order}], errors}
else
{sorts, [InvalidSortOrder.exception(order: order) | errors]}
end
end
{%{__struct__: Ash.Query.Aggregate} = agg, order}, {sorts, errors} ->
if order in @sort_orders do
{sorts ++ [{agg, order}], errors}
else
{sorts, [InvalidSortOrder.exception(order: order) | errors]}
end
{field, order}, {sorts, errors} when order in @sort_orders ->
attribute = Ash.Resource.Info.attribute(resource, field)
cond do
aggregate = Ash.Resource.Info.aggregate(resource, field) ->
if aggregate.sortable? do
aggregate_sort(aggregate, order, resource, sorts, errors)
else
{sorts, [UnsortableField.exception(resource: resource, field: field) | errors]}
end
Map.has_key?(aggregates, field) ->
if aggregates[field].sortable? do
aggregate_sort(Map.get(aggregates, field), order, resource, sorts, errors)
else
{sorts,
[
UnsortableField.exception(resource: resource, field: Map.get(aggregates, field))
| errors
]}
end
calc = Ash.Resource.Info.calculation(resource, field) ->
if calc.sortable? do
{module, _} = calc.calculation
if module.has_expression?() do
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation_sort) do
calculation_sort(
resource,
field,
calc,
order,
sorts,
errors,
context
)
else
{sorts, ["Datalayer cannot sort on calculations"]}
end
else
{sorts, ["Calculations cannot be sorted on unless they define an expression"]}
end
else
{sorts, [UnsortableField.exception(resource: resource, field: field) | errors]}
end
!attribute ->
{sorts, [NoSuchField.exception(field: field, resource: resource) | errors]}
!attribute.sortable? ->
{sorts,
[UnsortableField.exception(resource: resource, field: attribute.name) | errors]}
Ash.Type.embedded_type?(attribute.type) ->
{sorts, ["Cannot sort on embedded types" | errors]}
!Ash.DataLayer.data_layer_can?(
resource,
{:sort, Ash.Type.storage_type(attribute.type, attribute.constraints)}
) ->
{sorts,
[
UnsortableField.exception(resource: resource, field: field, reason: :type)
| errors
]}
true ->
{sorts ++ [{field, order}], errors}
end
{_, order}, {sorts, errors} ->
{sorts, [InvalidSortOrder.exception(order: order) | errors]}
end)
|> case do
{sorts, []} -> {:ok, sorts}
{_, errors} -> {:error, errors}
end
end
def sorting_on_identity?(%{sort: nil}), do: false
def sorting_on_identity?(query) do
identity_keys =
query.resource
|> Ash.Resource.Info.identities()
|> Enum.map(& &1.keys)
count_of_sort = Enum.count(query.sort)
Enum.any?([Ash.Resource.Info.primary_key(query.resource) | identity_keys], fn keyset ->
last_n_fields = query.sort |> Enum.reverse() |> Enum.take(count_of_sort)
Enum.all?(keyset, fn key ->
Enum.any?(last_n_fields, fn
{sort, _} when is_atom(sort) ->
sort == key
_ ->
false
end)
end)
end)
end
defp find_non_sortable_relationship(_resource, [], _sort) do
nil
end
defp find_non_sortable_relationship(resource, [first | rest], sort) do
relationship = Ash.Resource.Info.relationship(resource, first)
cond do
!relationship ->
raise """
No such relationship #{first} for resource #{inspect(resource)} when evaluating #{inspect(sort)}
"""
!relationship.sortable? ->
{relationship.source, relationship.name}
true ->
find_non_sortable_relationship(relationship.destination, rest, sort)
end
end
defp aggregate_sort(field, order, resource, sorts, errors) do
{field, type} =
case field do
%{name: name, kind: :custom, type: type} ->
{name, type}
%Ash.Resource.Aggregate{} = agg ->
attribute =
if agg.field do
related = Ash.Resource.Info.related(resource, agg.relationship_path)
Ash.Resource.Info.attribute(related, agg.field)
end
attribute_type =
if attribute do
attribute.type
end
attribute_constraints =
if attribute do
attribute.constraints
end
{agg.name,
Ash.Query.Aggregate.kind_to_type(agg.kind, attribute_type, attribute_constraints)}
end
case type do
{:ok, type, _constraints} ->
if Ash.DataLayer.data_layer_can?(resource, :aggregate_sort) &&
Ash.DataLayer.data_layer_can?(
resource,
# do we need to get actual constraints for aggregates here?
{:sort, Ash.Type.storage_type(type, [])}
) do
{sorts ++ [{field, order}], errors}
else
{sorts,
[AggregatesNotSupported.exception(resource: resource, feature: "sorting") | errors]}
end
{:error, error} ->
{sorts, [error | errors]}
end
end
defp calculation_sort(
resource,
field,
calc,
order,
sorts,
errors,
context
) do
{order, calc_context} =
case order do
order when is_atom(order) ->
{order, %{}}
{value, order} when is_list(value) ->
{order, Map.new(value)}
{value, order} when is_map(value) ->
{order, value}
other ->
{other, %{}}
end
case Ash.Query.Calculation.from_resource_calculation(resource, calc,
source_context: context || %{},
args: calc_context
) do
{:ok, calc} ->
calc = Map.put(calc, :load, field)
{sorts ++ [{calc, order}], errors}
{:error, error} ->
{sorts, [error | errors]}
end
end
@doc """
Sort records at runtime
Opts
* `:domain` - The domain to use if data needs to be loaded
* `:lazy?` - Whether to use already loaded values or to re-load them when necessary. Defaults to `false`
* `:resource` - The resource being sorted.
"""
def runtime_sort(results, sort, opts \\ [])
def runtime_sort([], _empty, _), do: []
def runtime_sort(results, empty, _) when empty in [nil, []], do: results
def runtime_sort([single_result], _, _), do: [single_result]
def runtime_sort(results, [{field, direction}], opts) do
resource = get_resource(results, opts)
results
|> load_field(field, resource, opts)
|> Enum.sort_by(&resolve_field(&1, field), to_sort_by_fun(direction))
end
def runtime_sort(results, [{field, direction} | rest], opts) do
# we need check if the field supports simple equality, and if so then we can use
# uniq_by
#
# otherwise, we need to do our own matching
resource = get_resource(results, opts)
results
|> load_field(field, resource, opts)
|> Enum.group_by(&resolve_field(&1, field))
|> Enum.sort_by(fn {key, _value} -> key end, to_sort_by_fun(direction))
|> Enum.flat_map(fn {_, records} ->
runtime_sort(records, rest, Keyword.put(opts, :rekey?, false))
end)
|> maybe_rekey(results, resource, Keyword.get(opts, :rekey?, true))
end
defp get_resource(results, opts) do
case opts[:resource] do
nil ->
case results do
[%resource{} | _] ->
resource
_other ->
raise ArgumentError,
"Resource must be provided when sorting a value that is not a simple list of records"
end
resource ->
resource
end
end
defp maybe_rekey(new_results, results, resource, true) do
Enum.map(new_results, fn new_result ->
Enum.find(results, fn result ->
resource.primary_key_matches?(new_result, result)
end)
end)
end
defp maybe_rekey(new_results, _, _, _), do: new_results
def runtime_distinct(results, sort, opts \\ [])
def runtime_distinct([], _empty, _), do: []
def runtime_distinct(results, empty, _) when empty in [nil, []], do: results
def runtime_distinct([single_result], _, _), do: [single_result]
def runtime_distinct([%resource{} | _] = results, distinct, opts) do
# we need check if the field supports simple equality, and if so then we can use
# uniq_by
#
# otherwise, we need to do our own matching
fields = Enum.map(distinct, &elem(&1, 0))
results
|> load_field(fields, resource, opts)
|> Enum.to_list()
|> runtime_sort(distinct, opts)
|> Enum.uniq_by(&Map.take(&1, fields))
end
defp load_field(records, field, resource, opts) do
if is_nil(opts[:domain]) do
records
else
records
|> Stream.chunk_every(100)
|> Stream.flat_map(fn batch ->
query =
resource
|> Ash.Query.select([])
|> Ash.Query.load(field)
|> Ash.Query.set_context(%{private: %{internal?: true}})
Ash.load!(batch, query,
domain: opts[:domain],
reuse_values?: true,
lazy?: opts[:lazy?] || false
)
end)
end
end
defp resolve_field(record, %{__struct__: struct} = agg)
when struct in [Ash.Query.Calculation, Ash.Query.Aggregate] do
if agg.load do
Map.get(record, agg.load)
else
if struct == Ash.Query.Calculation do
Map.get(record.calculations, agg.name)
else
Map.get(record.aggregates, agg.name)
end
end
end
defp resolve_field(record, field) do
record
|> Map.get(field)
|> case do
%Ash.ForbiddenField{} -> nil
other -> other
end
end
defp to_sort_by_fun(:desc) do
to_sort_by_fun(:desc_nils_first)
end
defp to_sort_by_fun(:asc) do
to_sort_by_fun(:asc_nils_last)
end
defp to_sort_by_fun(:asc_nils_last) do
fn x, y ->
cond do
is_nil(x) and is_nil(y) ->
true
is_nil(x) ->
false
is_nil(y) ->
true
true ->
Comp.less_or_equal?(x, y)
end
end
end
defp to_sort_by_fun(:desc_nils_last) do
fn x, y ->
cond do
is_nil(x) and is_nil(y) ->
true
is_nil(x) ->
false
is_nil(y) ->
true
true ->
Comp.greater_or_equal?(x, y)
end
end
end
defp to_sort_by_fun(:asc_nils_first) do
fn x, y ->
cond do
is_nil(x) and is_nil(y) ->
true
is_nil(x) ->
true
is_nil(y) ->
false
true ->
Comp.less_or_equal?(x, y)
end
end
end
defp to_sort_by_fun(:desc_nils_first) do
fn x, y ->
cond do
is_nil(x) and is_nil(y) ->
true
is_nil(x) ->
true
is_nil(y) ->
false
true ->
Comp.greater_or_equal?(x, y)
end
end
end
end