Packages
ash_sql
0.1.1-rc.5
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.93
0.2.92
0.2.91
0.2.90
0.2.89
0.2.88
0.2.87
0.2.86
0.2.85
0.2.84
0.2.83
0.2.82
0.2.81
0.2.80
0.2.79
0.2.78
0.2.77
0.2.76
0.2.75
0.2.74
0.2.73
0.2.72
0.2.71
0.2.70
0.2.69
0.2.68
0.2.67
0.2.66
0.2.65
0.2.64
0.2.63
0.2.62
0.2.61
0.2.60
0.2.59
0.2.58
0.2.57
0.2.56
0.2.55
0.2.54
0.2.53
0.2.52
0.2.51
0.2.50
0.2.49
0.2.48
0.2.47
0.2.46
0.2.45
0.2.44
0.2.43
0.2.42
0.2.41
0.2.40
0.2.39
0.2.38
0.2.37
0.2.36
0.2.35
0.2.34
0.2.33
0.2.32
0.2.31
0.2.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.3
0.1.2
0.1.1-rc.20
0.1.1-rc.19
0.1.1-rc.18
0.1.1-rc.17
0.1.1-rc.16
0.1.1-rc.15
0.1.1-rc.14
0.1.1-rc.13
0.1.1-rc.12
0.1.1-rc.11
0.1.1-rc.10
0.1.1-rc.9
0.1.1-rc.8
0.1.1-rc.7
0.1.1-rc.6
0.1.1-rc.5
0.1.1-rc.4
0.1.1-rc.3
0.1.1-rc.2
0.1.1-rc.1
0.1.1-rc.0
Shared utilities for ecto-based sql data layers.
Current section
Files
Jump to
Current section
Files
lib/sort.ex
defmodule AshSql.Sort do
@moduledoc false
require Ecto.Query
def sort(
query,
sort,
resource,
relationship_path \\ [],
binding \\ 0,
type \\ :window
) do
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: %{},
parent_stack: query.__ash_bindings__[:parent_resources] || [],
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)
calcs =
Enum.flat_map(sort, fn
{%Ash.Query.Calculation{} = calculation, _} ->
{:ok, expression} =
calculation.opts
|> calculation.module.expression(calculation.context)
|> Ash.Filter.hydrate_refs(%{
resource: resource,
parent_stack: query.__ash_bindings__[:parent_resources] || [],
public?: false
})
[{calculation, Ash.Filter.move_to_relationship_path(expression, relationship_path)}]
_ ->
[]
end)
{:ok, query} =
AshSql.Join.join_all_relationships(
query,
%Ash.Filter{
resource: resource,
expression: Enum.map(calcs, &elem(&1, 1))
},
left_only?: true
)
case AshSql.Aggregate.add_aggregates(query, used_aggregates, resource, false, 0) do
{:error, error} ->
{:error, error}
{:ok, query} ->
sort
|> sanitize_sort()
|> Enum.reduce_while({:ok, [], query}, fn
{order, %Ash.Query.Calculation{} = calc}, {:ok, query_expr, query} ->
type =
if calc.type do
query.__ash_bindings__.sql_behaviour.parameterized_type(
calc.type,
calc.constraints
)
else
nil
end
calc.opts
|> calc.module.expression(calc.context)
|> Ash.Filter.hydrate_refs(%{
resource: resource,
parent_stack: query.__ash_bindings__[:parent_resources] || [],
public?: false
})
|> Ash.Filter.move_to_relationship_path(relationship_path)
|> case do
{:ok, expr} ->
bindings =
if query.__ash_bindings__[:parent_bindings] do
Map.update!(query.__ash_bindings__, :parent_bindings, fn parent ->
Map.put(parent, :parent_is_parent_as?, false)
end)
else
query.__ash_bindings__
end
{expr, acc} =
AshSql.Expr.dynamic_expr(
query,
expr,
bindings,
false,
type
)
{:cont,
{:ok, query_expr ++ [{order, expr}],
AshSql.Bindings.merge_expr_accumulator(query, acc)}}
{:error, error} ->
{:halt, {:error, error}}
end
{order, sort}, {:ok, query_expr, query} ->
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,
query.__ash_bindings__.sql_behaviour.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
typed_default =
query.__ash_bindings__.sql_behaviour.type_expr(
aggregate.default,
type
)
Ecto.Query.dynamic(
coalesce(
field(as(^binding), ^sort),
^typed_default
)
)
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 &&
AshSql.Aggregate.optimizable_first_aggregate?(resource, aggregate, query) do
{AshSql.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}], query}}
end)
|> case do
{:ok, [], query} ->
if type == :return do
{:ok, [], query}
else
{:ok, query}
end
{:ok, sort_exprs, query} ->
case type do
:return ->
{:ok, order_to_fragments(sort_exprs), query}
:window ->
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}
:direct ->
{:ok, query |> Ecto.Query.order_by(^sort_exprs) |> set_sort_applied()}
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([last]) do
[do_order_to_fragments(last, false)]
end
def order_to_fragments([first | rest]) do
[do_order_to_fragments(first, true) | order_to_fragments(rest)]
end
def do_order_to_fragments({order, sort}, comma?) do
case {order, comma?} do
{:asc, false} ->
Ecto.Query.dynamic([row], fragment("? ASC", ^sort))
{:desc, false} ->
Ecto.Query.dynamic([row], fragment("? DESC", ^sort))
{:asc_nulls_last, false} ->
Ecto.Query.dynamic([row], fragment("? ASC NULLS LAST", ^sort))
{:asc_nulls_first, false} ->
Ecto.Query.dynamic([row], fragment("? ASC NULLS FIRST", ^sort))
{:desc_nulls_first, false} ->
Ecto.Query.dynamic([row], fragment("? DESC NULLS FIRST", ^sort))
{:desc_nulls_last, false} ->
Ecto.Query.dynamic([row], fragment("? DESC NULLS LAST", ^sort))
"DESC NULLS LAST"
{:asc, true} ->
Ecto.Query.dynamic([row], fragment("? ASC, ", ^sort))
{:desc, true} ->
Ecto.Query.dynamic([row], fragment("? DESC, ", ^sort))
{:asc_nulls_last, true} ->
Ecto.Query.dynamic([row], fragment("? ASC NULLS LAST, ", ^sort))
{:asc_nulls_first, true} ->
Ecto.Query.dynamic([row], fragment("? ASC NULLS FIRST, ", ^sort))
{:desc_nulls_first, true} ->
Ecto.Query.dynamic([row], fragment("? DESC NULLS FIRST, ", ^sort))
{:desc_nulls_last, true} ->
Ecto.Query.dynamic([row], fragment("? DESC NULLS LAST, ", ^sort))
"DESC NULLS LAST"
end
end
def order_to_sql_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
def apply_sort(query, sort, resource, type \\ :window)
def apply_sort(query, sort, _resource, _) when sort in [nil, []] do
{:ok, query |> set_sort_applied()}
end
def apply_sort(query, sort, resource, type) do
AshSql.Sort.sort(query, sort, resource, [], 0, type)
end
defp set_sort_applied(query) do
Map.update!(query, :__ash_bindings__, &Map.put(&1, :sort_applied?, true))
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