Packages

ash

3.0.0-rc.0
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 argumentsw 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
Code.ensure_compiled!(module)
if function_exported?(module, :expression, 2) do
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation_sort) do
calculation_sort(
field,
calc,
module,
opts,
calc.type,
calc.constraints,
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{
name: :__expr_sort__,
module: Ash.Resource.Calculation.Expression,
opts: [{:expr, expr}]
} = calc, order},
{sorts, errors} ->
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) ->
{Ash.Resource.Info.field(Ash.Resource.Info.related(resource, path), attribute),
atom}
%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) 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
{%Ash.Query.Calculation{} = calc, order}, {sorts, errors} ->
if order in @sort_orders do
{sorts ++ [{calc, 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, opts} = calc.calculation
if module.has_expression?() do
if Ash.DataLayer.data_layer_can?(resource, :expression_calculation_sort) do
calculation_sort(
field,
calc,
module,
opts,
calc.type,
calc.constraints,
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(attribute: 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]}
match?({:array, _}, attribute.type) ->
{sorts, ["Cannot sort on array 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, []) do
nil
end
defp find_non_sortable_relationship(resource, [first | rest]) do
relationship = Ash.Resource.Info.relationship(resource, first)
if relationship.sortable? do
find_non_sortable_relationship(relationship.destination, [rest])
else
{relationship.source, relationship.name}
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(
field,
calc,
module,
opts,
type,
constraints,
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
with {:ok, input} <- Ash.Query.validate_calculation_arguments(calc, calc_context),
{:ok, calc} <-
Ash.Query.Calculation.new(
field,
module,
opts,
type,
constraints,
arguments: input,
filterable?: calc.filterable?,
sortable?: calc.sortable?,
sensitive?: calc.sensitive?,
load: calc.load,
source_context: context
) do
calc = Map.put(calc, :load, field)
{sorts ++ [{calc, order}], errors}
else
{: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`
"""
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([%resource{} | _] = results, [{field, direction} | rest], opts) do
results
|> load_field(field, resource, opts)
|> Enum.group_by(&resolve_field(&1, field, resource, domain: opts))
|> 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 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, [{field, direction} | rest], opts) do
results
|> load_field(field, resource, opts)
|> Enum.group_by(&resolve_field(&1, field, resource, domain: opts))
|> Enum.sort_by(fn {key, _value} -> key end, to_sort_by_fun(direction))
|> Enum.map(fn {_key, [first | _]} ->
first
end)
|> runtime_distinct(rest, Keyword.put(opts, :rekey?, false))
|> maybe_rekey(results, resource, Keyword.get(opts, :rekey?, true))
end
defp load_field(records, field, resource, opts) do
if is_nil(opts[:domain]) || (opts[:lazy?] && Ash.Resource.loaded?(records, field)) do
records
else
query =
resource
|> Ash.Query.select([])
|> Ash.Query.load(field)
|> Ash.Query.set_context(%{private: %{internal?: true}})
opts[:domain].load!(records, query)
end
end
defp resolve_field(record, %Ash.Query.Calculation{} = calc, resource, opts) do
cond do
calc.module.has_calculate?() ->
context = Map.put(calc.context, :domain, opts[:domain])
case calc.module.calculate([record], calc.opts, context) do
{:ok, [value]} -> value
_ -> nil
end
calc.module.has_expression?() ->
expression = calc.module.expression(calc.opts, calc.context)
case Ash.Filter.hydrate_refs(expression, %{
resource: resource,
aggregates: %{},
calculations: %{},
public?: false
}) do
{:ok, expression} ->
case Ash.Expr.eval_hydrated(expression, record: record, resource: resource) do
{:ok, value} ->
value
_ ->
nil
end
_ ->
nil
end
true ->
nil
end
|> case do
%Ash.ForbiddenField{} -> nil
other -> other
end
end
defp resolve_field(record, field, _resource, _) 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