Packages
ash_sql
0.2.42
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
sort =
Enum.map(sort, fn
{key, val} when is_atom(key) ->
case Ash.Resource.Info.field(resource, key) do
%Ash.Resource.Calculation{calculation: {module, opts}} = calculation ->
{:ok, calculation} =
Ash.Query.Calculation.new(
calculation.name,
module,
opts,
calculation.type,
calculation.constraints
)
calculation =
Ash.Actions.Read.add_calc_context(
calculation,
query.__ash_bindings__.context[:private][:actor],
query.__ash_bindings__.context[:private][:authorize?],
query.__ash_bindings__.context[:private][:tenant],
query.__ash_bindings__.context[:private][:tracer],
nil
)
{calculation, val}
%Ash.Resource.Aggregate{} = aggregate ->
related = Ash.Resource.Info.related(resource, aggregate.relationship_path)
read_action =
aggregate.read_action ||
Ash.Resource.Info.primary_action!(
related,
:read
).name
with %{valid?: true} = aggregate_query <- Ash.Query.for_read(related, read_action),
%{valid?: true} = aggregate_query <-
Ash.Query.build(aggregate_query,
filter: aggregate.filter,
sort: aggregate.sort
) do
case Ash.Query.Aggregate.new(
resource,
aggregate.name,
aggregate.kind,
path: aggregate.relationship_path,
query: aggregate_query,
field: aggregate.field,
default: aggregate.default,
filterable?: aggregate.filterable?,
type: aggregate.type,
sortable?: aggregate.filterable?,
include_nil?: aggregate.include_nil?,
constraints: aggregate.constraints,
implementation: aggregate.implementation,
uniq?: aggregate.uniq?,
read_action:
aggregate.read_action ||
Ash.Resource.Info.primary_action!(
Ash.Resource.Info.related(resource, aggregate.relationship_path),
:read
).name,
authorize?: aggregate.authorize?
) do
{:ok, agg} ->
{agg, val}
{:error, error} ->
raise Ash.Error.to_ash_error(error)
end
else
%{errors: errors} -> raise Ash.Error.to_ash_error(errors)
end
_ ->
{key, val}
end
{key, val} ->
{key, val}
end)
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
{%Ash.Query.Aggregate{} = aggregate, _} ->
[aggregate]
{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.Aggregate{} = agg}, {:ok, query_expr, query} ->
type =
if agg.type do
query.__ash_bindings__.sql_behaviour.parameterized_type(
agg.type,
agg.constraints
)
else
nil
end
expr =
%Ash.Query.Ref{
attribute: agg,
resource: resource,
relationship_path: relationship_path
}
bindings = query.__ash_bindings__
{expr, acc} =
AshSql.Expr.dynamic_expr(
query,
expr,
bindings,
false,
type
)
{:cont,
{:ok, query_expr ++ [{order, expr}],
AshSql.Bindings.merge_expr_accumulator(query, acc)}}
{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 = query.__ash_bindings__
{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, [], query.__ash_bindings__.root_binding, 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