mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-20 13:33:50 +12:00
257 lines
7.6 KiB
Elixir
257 lines
7.6 KiB
Elixir
defmodule AshPostgres.Sort do
|
|
@moduledoc false
|
|
require Ecto.Query
|
|
|
|
def sort(
|
|
query,
|
|
sort,
|
|
resource,
|
|
relationship_path \\ [],
|
|
binding \\ 0,
|
|
return_order_by? \\ false
|
|
) do
|
|
query = AshPostgres.DataLayer.default_bindings(query, resource)
|
|
|
|
used_aggregates =
|
|
Enum.flat_map(sort, fn
|
|
{%Ash.Query.Calculation{} = calculation, _} ->
|
|
case Ash.Filter.hydrate_refs(
|
|
calculation.module.expression(calculation.opts, calculation.context),
|
|
%{
|
|
resource: resource,
|
|
aggregates: %{},
|
|
calculations: %{},
|
|
public?: false
|
|
}
|
|
) do
|
|
{:ok, hydrated} ->
|
|
Ash.Filter.used_aggregates(hydrated)
|
|
|
|
_ ->
|
|
[]
|
|
end
|
|
|
|
{key, _} ->
|
|
case Ash.Resource.Info.aggregate(resource, key) do
|
|
nil ->
|
|
[]
|
|
|
|
aggregate ->
|
|
[aggregate]
|
|
end
|
|
|
|
_ ->
|
|
[]
|
|
end)
|
|
|
|
case AshPostgres.Aggregate.add_aggregates(query, used_aggregates, resource, false, 0) do
|
|
{:error, error} ->
|
|
{:error, error}
|
|
|
|
{:ok, query} ->
|
|
sort
|
|
|> sanitize_sort()
|
|
|> Enum.reduce_while({:ok, []}, fn
|
|
{order, %Ash.Query.Calculation{} = calc}, {:ok, query_expr} ->
|
|
type =
|
|
if calc.type do
|
|
AshPostgres.Types.parameterized_type(calc.type, [])
|
|
else
|
|
nil
|
|
end
|
|
|
|
calc.opts
|
|
|> calc.module.expression(calc.context)
|
|
|> Ash.Filter.hydrate_refs(%{
|
|
resource: resource,
|
|
aggregates: query.__ash_bindings__.aggregate_defs,
|
|
calculations: %{},
|
|
public?: false
|
|
})
|
|
|> Ash.Filter.move_to_relationship_path(relationship_path)
|
|
|> case do
|
|
{:ok, expr} ->
|
|
expr =
|
|
AshPostgres.Expr.dynamic_expr(query, expr, query.__ash_bindings__, false, type)
|
|
|
|
{:cont, {:ok, query_expr ++ [{order, expr}]}}
|
|
|
|
{:error, error} ->
|
|
{:halt, {:error, error}}
|
|
end
|
|
|
|
{order, sort}, {:ok, query_expr} ->
|
|
expr =
|
|
case find_aggregate_binding(
|
|
query.__ash_bindings__.bindings,
|
|
relationship_path,
|
|
sort
|
|
) do
|
|
{:ok, binding} ->
|
|
aggregate =
|
|
Ash.Resource.Info.aggregate(resource, sort) ||
|
|
raise "No such aggregate for query aggregate #{inspect(sort)}"
|
|
|
|
{:ok, attribute_type} =
|
|
if aggregate.field do
|
|
related = Ash.Resource.Info.related(resource, aggregate.relationship_path)
|
|
|
|
attr = Ash.Resource.Info.attribute(related, aggregate.field)
|
|
|
|
if attr && related do
|
|
{:ok, AshPostgres.Types.parameterized_type(attr.type, attr.constraints)}
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
else
|
|
{:ok, nil}
|
|
end
|
|
|
|
default_value =
|
|
aggregate.default || Ash.Query.Aggregate.default_value(aggregate.kind)
|
|
|
|
if is_nil(default_value) do
|
|
Ecto.Query.dynamic(field(as(^binding), ^sort))
|
|
else
|
|
if attribute_type do
|
|
Ecto.Query.dynamic(
|
|
coalesce(
|
|
field(as(^binding), ^sort),
|
|
type(^default_value, ^attribute_type)
|
|
)
|
|
)
|
|
else
|
|
Ecto.Query.dynamic(coalesce(field(as(^binding), ^sort), ^default_value))
|
|
end
|
|
end
|
|
|
|
:error ->
|
|
aggregate = Ash.Resource.Info.aggregate(resource, sort)
|
|
|
|
{binding, sort} =
|
|
if aggregate && aggregate.kind == :first &&
|
|
AshPostgres.Aggregate.single_path?(resource, aggregate.relationship_path) do
|
|
{AshPostgres.Join.get_binding(
|
|
resource,
|
|
aggregate.relationship_path,
|
|
query,
|
|
[
|
|
:left,
|
|
:inner
|
|
]
|
|
), aggregate.field}
|
|
else
|
|
{binding, sort}
|
|
end
|
|
|
|
Ecto.Query.dynamic(field(as(^binding), ^sort))
|
|
end
|
|
|
|
{:cont, {:ok, query_expr ++ [{order, expr}]}}
|
|
end)
|
|
|> case do
|
|
{:ok, []} ->
|
|
{:ok, query}
|
|
|
|
{:ok, sort_exprs} ->
|
|
if return_order_by? do
|
|
{:ok, order_to_fragments(sort_exprs)}
|
|
else
|
|
new_query = Ecto.Query.order_by(query, ^sort_exprs)
|
|
|
|
sort_expr = List.last(new_query.order_bys)
|
|
|
|
new_query =
|
|
new_query
|
|
|> Map.update!(:windows, fn windows ->
|
|
order_by_expr = %{sort_expr | expr: [order_by: sort_expr.expr]}
|
|
Keyword.put(windows, :order, order_by_expr)
|
|
end)
|
|
|> Map.update!(:__ash_bindings__, &Map.put(&1, :__order__?, true))
|
|
|
|
{:ok, new_query}
|
|
end
|
|
|
|
{:error, error} ->
|
|
{:error, error}
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_aggregate_binding(bindings, relationship_path, sort) do
|
|
Enum.find_value(
|
|
bindings,
|
|
:error,
|
|
fn
|
|
{key, %{type: :aggregate, path: ^relationship_path, aggregates: aggregates}} ->
|
|
if Enum.any?(aggregates, &(&1.name == sort)) do
|
|
{:ok, key}
|
|
end
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
)
|
|
end
|
|
|
|
def order_to_fragments([]), do: []
|
|
|
|
def order_to_fragments(order) when is_list(order) do
|
|
Enum.map(order, &do_order_to_fragments(&1))
|
|
end
|
|
|
|
def do_order_to_fragments({order, sort}) do
|
|
case order do
|
|
:asc ->
|
|
Ecto.Query.dynamic([row], fragment("? ASC", ^sort))
|
|
|
|
:desc ->
|
|
Ecto.Query.dynamic([row], fragment("? DESC", ^sort))
|
|
|
|
:asc_nulls_last ->
|
|
Ecto.Query.dynamic([row], fragment("? ASC NULLS LAST", ^sort))
|
|
|
|
:asc_nulls_first ->
|
|
Ecto.Query.dynamic([row], fragment("? ASC NULLS FIRST", ^sort))
|
|
|
|
:desc_nulls_first ->
|
|
Ecto.Query.dynamic([row], fragment("? DESC NULLS FIRST", ^sort))
|
|
|
|
:desc_nulls_last ->
|
|
Ecto.Query.dynamic([row], fragment("? DESC NULLS LAST", ^sort))
|
|
"DESC NULLS LAST"
|
|
end
|
|
end
|
|
|
|
def order_to_postgres_order(dir) do
|
|
case dir do
|
|
:asc -> nil
|
|
:asc_nils_last -> " ASC NULLS LAST"
|
|
:asc_nils_first -> " ASC NULLS FIRST"
|
|
:desc -> " DESC"
|
|
:desc_nils_last -> " DESC NULLS LAST"
|
|
:desc_nils_first -> " DESC NULLS FIRST"
|
|
end
|
|
end
|
|
|
|
defp sanitize_sort(sort) do
|
|
sort
|
|
|> List.wrap()
|
|
|> Enum.map(fn
|
|
{sort, {order, context}} ->
|
|
{ash_to_ecto_order(order), {sort, context}}
|
|
|
|
{sort, order} ->
|
|
{ash_to_ecto_order(order), sort}
|
|
|
|
sort ->
|
|
sort
|
|
end)
|
|
end
|
|
|
|
defp ash_to_ecto_order(:asc_nils_last), do: :asc_nulls_last
|
|
defp ash_to_ecto_order(:asc_nils_first), do: :asc_nulls_first
|
|
defp ash_to_ecto_order(:desc_nils_last), do: :desc_nulls_last
|
|
defp ash_to_ecto_order(:desc_nils_first), do: :desc_nulls_first
|
|
defp ash_to_ecto_order(other), do: other
|
|
end
|