Packages

ash

3.4.61
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 read calculations.ex
Raw

lib/ash/actions/read/calculations.ex

defmodule Ash.Actions.Read.Calculations do
@moduledoc false
require Ash.Tracer
def calculate(resource_or_record, calculation, opts) do
{resource, record} =
case resource_or_record do
%resource{} = record -> {resource, record}
resource -> {resource, opts[:record]}
end
with {:calc,
%{
arguments: calc_arguments,
calculation: {module, calc_opts},
type: type,
constraints: constraints
}} <- {:calc, Ash.Resource.Info.calculation(resource, calculation)},
record <- struct(record || resource, opts[:refs] || %{}) do
args = opts[:args] || %{}
arguments =
Enum.reduce(calc_arguments, %{}, fn arg, arguments ->
if Map.has_key?(args, arg.name) do
Map.put(arguments, arg.name, args[arg.name])
else
if is_nil(arg.default) do
arguments
else
Map.put(arguments, arg.name, arg.default)
end
end
end)
primary_key =
case Ash.Resource.Info.primary_key(resource) do
[] ->
nil
primary_key ->
Enum.reduce_while(primary_key, %{}, fn key, acc ->
case Map.get(record, key) do
nil -> {:halt, nil}
%Ash.NotLoaded{} -> {:halt, nil}
other -> {:cont, Map.put(acc, key, other)}
end
end)
end
calc_context =
%Ash.Resource.Calculation.Context{
actor: opts[:actor],
domain: opts[:domain],
tenant: opts[:tenant],
authorize?: opts[:authorize?],
tracer: opts[:tracer],
resource: opts[:resource],
arguments: arguments,
type: type,
constraints: constraints,
source_context: opts[:context] || %{}
}
if module.has_expression?() do
expr =
case module.expression(calc_opts, calc_context) do
{:ok, result} -> {:ok, result}
{:error, error} -> {:error, error}
result -> {:ok, result}
end
with {:ok, expr} <- expr,
{:ok, expr} <-
Ash.Filter.hydrate_refs(expr, %{resource: resource}) do
if is_nil(primary_key) && requires_primary_key?(expr) do
{:error,
Ash.Error.Query.CalculationRequiresPrimaryKey.exception(
resource: resource,
calculation: calculation
)}
else
if is_nil(primary_key) do
expr = replace_refs(expr, Keyword.put(opts, :record, record))
evaled =
try do
if opts[:data_layer?] do
:unknown
else
Ash.Expr.eval(expr,
record: record,
resource: resource,
unknown_on_unknown_refs?: true
)
end
rescue
_ ->
:unknown
end
case evaled do
{:ok, result} ->
{:ok, result}
:unknown ->
data_layer_result =
if Ash.DataLayer.data_layer_can?(resource, :calculate) do
Ash.DataLayer.calculate(resource, [expr], %{
calculation_context: calc_context,
primary_key: primary_key
})
else
:cant_calculate
end
case data_layer_result do
{:ok, result} ->
{:ok, Enum.at(result, 0)}
:cant_calculate ->
{:error,
"Failed to run calculation in memory, or in the data layer, and no `calculate/3` is defined on #{inspect(module)}. Data layer does not support one-off calculations."}
{:error, error} ->
if module.has_calculate?() do
case with_trace(
fn -> module.calculate([record], calc_opts, calc_context) end,
resource,
calculation,
opts
) do
[result] ->
result
{:ok, [result]} ->
{:ok, result}
{:ok, _} ->
{:error, "Invalid calculation return"}
{:error, error} ->
{:error, error}
:unknown ->
{:error,
"Failed to run calculation in memory, or in the data layer. Data layer returned #{inspect(error)}"}
end
else
{:error,
"Failed to run calculation in memory, or in the data layer, and no `calculate/3` is defined on #{inspect(module)}. Data layer returned #{inspect(error)}"}
end
end
{:error, error} ->
{:error, error}
end
else
case Ash.load(record, [{calculation, arguments}],
actor: opts[:actor],
domain: opts[:domain],
tenant: opts[:tenant],
authorize?: opts[:authorize?],
tracer: opts[:tracer],
resource: opts[:resource],
context: opts[:context] || %{}
) do
{:ok, record} -> {:ok, Map.get(record, calculation)}
{:error, error} -> {:error, error}
end
end
end
end
else
if module.has_calculate?() do
case with_trace(
fn -> module.calculate([record], calc_opts, calc_context) end,
resource,
calculation,
opts
) do
[result] ->
{:ok, result}
{:ok, [result]} ->
{:ok, result}
{:ok, _} ->
{:error, "Invalid calculation return"}
{:error, error} ->
{:error, error}
end
else
{:error, "Module #{inspect(module)} does not have an expression or calculate function"}
end
end
else
{:calc, nil} ->
{:error, "No such calculation"}
end
end
defp requires_primary_key?(expr) do
Ash.Filter.find_value(expr, fn
%Ash.Query.Ref{attribute: %agg_struct{}}
when agg_struct in [Ash.Query.Aggregate, Ash.Resource.Aggregate] ->
true
%Ash.Query.Exists{} ->
true
%Ash.Query.Parent{} ->
true
_ ->
false
end) ||
false
end
defp replace_refs(expr, opts) do
Ash.Filter.map(expr, fn
%Ash.Query.Ref{relationship_path: path, attribute: %Ash.Resource.Attribute{} = attribute} ->
name =
case attribute do
%{name: name} -> name
name -> name
end
Ash.Expr.get_path(opts[:record] || %{}, path ++ [name])
%Ash.Query.Exists{expr: expr} = exists ->
%{
exists
| expr:
Ash.Filter.map(expr, fn
%Ash.Query.Parent{expr: expr} = parent ->
%{parent | expr: replace_refs(expr, opts)}
other ->
other
end)
}
other ->
other
end)
end
def run([], _, _, _calculations_in_query), do: {:ok, []}
def run(records, ash_query, calculations_at_runtime, calculations_in_query) do
do_run_calculations(
calculations_at_runtime,
records,
ash_query,
MapSet.new(calculations_in_query, & &1.name)
)
end
defp do_run_calculations(calculations, records, ash_query, done, tasks \\ [])
defp do_run_calculations([], records, _ash_query, _done, []), do: {:ok, records}
defp do_run_calculations(calculations, records, ash_query, done, tasks) do
{do_now, do_later} =
Enum.split_with(calculations, fn calc ->
ash_query.context[:calculation_dependencies][calc.name]
|> Kernel.||([])
|> Enum.all?(&(&1 in done))
end)
if tasks == [] and do_now == [] do
raise """
Circular calculation dependency detected. Remaining calculations
#{Enum.map_join(do_later, "\n\n", fn {key, calc} -> "* " <> inspect(calc) <> "\n" <> inspect(ash_query.context[:calculation_dependencies][key]) end)}
"""
end
{newly_done, remaining} =
do_now
|> do_run_calcs(ash_query, records)
|> Stream.concat(tasks)
|> Ash.Actions.Read.AsyncLimiter.await_at_least_one()
records =
Enum.reduce_while(newly_done, {:ok, records}, fn
{_name, calculation, calculation_results}, {:ok, records} ->
case calculation_results do
{:ok, value} ->
{:cont, {:ok, attach_calculation_results(calculation, records, value)}}
{:error, error} ->
{:halt, {:error, error}}
nil ->
{:cont, {:ok, records}}
other ->
{:cont, {:ok, attach_calculation_results(calculation, records, other)}}
end
end)
case records do
{:ok, records} ->
do_run_calculations(
do_later,
records,
ash_query,
MapSet.union(MapSet.new(newly_done, &elem(&1, 0)), done),
remaining
)
{:error, error} ->
{:error, error}
end
end
defp do_run_calcs(calcs, ash_query, records, acc \\ [])
defp do_run_calcs([], _ash_query, _records, acc) do
acc
end
defp do_run_calcs([calculation | rest], ash_query, records, acc) do
result =
Ash.Actions.Read.AsyncLimiter.async_or_inline(
ash_query,
Ash.Context.to_opts(calculation.context),
!calculation.async? && Enum.empty?(rest),
fn ->
{calculation.name, calculation, run_calculation(calculation, ash_query, records)}
end
)
do_run_calcs(rest, ash_query, records, [result | acc])
end
defp attach_calculation_results(calculation, records, nil) do
if calculation.load do
Enum.map(records, fn record ->
Map.put(record, calculation.load, nil)
end)
else
Enum.zip_with(records, fn record ->
Map.update!(record, :calculations, &Map.put(&1, calculation.name, nil))
end)
end
end
defp attach_calculation_results(calculation, [%resource{} | _] = records, values)
when is_map(values) and not is_struct(values) do
case Ash.Resource.Info.primary_key(resource) do
[key] ->
if calculation.load do
Enum.map(records, fn record ->
value =
case Map.fetch(values, Map.get(record, key)) do
{:ok, value} ->
value
:error ->
Map.get(record, Map.take(record, [key]))
end
Map.put(record, calculation.load, value)
end)
else
Enum.map(records, fn record ->
value =
case Map.fetch(values, Map.get(record, key)) do
{:ok, value} ->
value
:error ->
Map.get(record, Map.take(record, [key]))
end
Map.update!(record, :calculations, &Map.put(&1, calculation.name, value))
end)
end
pkey ->
if calculation.load do
Enum.map(records, fn record ->
value = Map.get(record, Map.take(record, pkey))
Map.put(record, calculation.load, value)
end)
else
Enum.map(records, fn record ->
value = Map.get(record, Map.take(record, pkey))
Map.update!(record, :calculations, &Map.put(&1, calculation.name, value))
end)
end
end
end
defp attach_calculation_results(calculation, records, values) do
if calculation.load do
Enum.zip_with([records, values], fn
[record, %Ash.NotLoaded{}] ->
raise Ash.Error.Framework.AssumptionFailed,
message: """
Invalid return from calculation, expected a value, got `%Ash.NotLoaded{}`
Calculation: #{inspect(calculation.name)}
Record: #{inspect(record)}
"""
[record, value] ->
Map.put(record, calculation.load, value)
end)
else
Enum.zip_with([records, values], fn
[record, %Ash.NotLoaded{}] ->
raise Ash.Error.Framework.AssumptionFailed,
message: """
Invalid return from calculation, expected a value, got `%Ash.NotLoaded{}`
Calculation: #{inspect(calculation.name)}
Record: #{inspect(record)}
"""
[record, value] ->
Map.update!(record, :calculations, &Map.put(&1, calculation.name, value))
end)
end
end
defp run_calculation(calculation, ash_query, records) do
context = Map.put(calculation.context, :domain, ash_query.domain)
opts =
Ash.Expr.fill_template(
calculation.opts,
calculation.context.actor,
calculation.context.arguments,
calculation.context.source_context
)
records
|> apply_transient_calculation_values(calculation, ash_query, [])
|> run_calculate(
calculation.module,
opts,
context,
ash_query.resource,
calculation.name,
Ash.Context.to_opts(context)
)
|> case do
:unknown ->
Enum.map(records, fn _ ->
nil
end)
result ->
result
end
end
defp run_calculate(records, module, opts, context, resource, calculation_name, run_opts) do
with_trace(
fn -> module.calculate(records, opts, context) end,
resource,
calculation_name,
run_opts
)
rescue
e ->
if Enum.any?(__STACKTRACE__, fn {m, f, a, meta} ->
(m == module && f == :calculate && a == 3) || meta[:fake?]
end) do
reraise e, __STACKTRACE__
else
{stacktrace_before, stacktrace_after} =
Enum.split_while(__STACKTRACE__, fn {module, function, arity, _meta} ->
module != __MODULE__ && function == :run_calculate && arity == 4
end)
reraise e,
stacktrace_before ++ [{module, :calculate, 3, [fake?: true]} | stacktrace_after]
end
end
defp with_trace(callback, resource, calculation_name, opts) do
short_name = Ash.Resource.Info.short_name(resource)
tracer = opts[:tracer]
Ash.Tracer.span :calculation,
fn ->
calculation_name =
if is_atom(calculation_name) do
to_string(calculation_name)
else
String.replace(inspect(calculation_name), ~r/[^a-zA-Z0-9_\-?]/, "")
end
"#{short_name}:calculation:#{calculation_name}"
end,
tracer do
metadata = fn ->
calculation_name =
if is_atom(calculation_name) do
to_string(calculation_name)
else
String.replace(inspect(calculation_name), ~r/[^a-zA-Z0-9_\-?]/, "")
end
%{
resource: resource,
resource_short_name: short_name,
calculation: calculation_name,
actor: opts[:actor],
tenant: opts[:tenant],
authorize?: opts[:authorize?]
}
end
Ash.Tracer.telemetry_span [:ash, :calculation],
metadata,
skip?: !!opts[:initial_data] do
Ash.Tracer.set_metadata(tracer, :action, metadata)
callback.()
end
end
end
defp apply_transient_calculation_values(records, calculation, ash_query, path) do
ash_query
|> get_all_rewrites(calculation, path)
|> rewrite(records)
end
def get_all_rewrites(ash_query, calculation, path) do
Enum.flat_map(
[:relationships, :calculations, :aggregates, :load_through],
&get_rewrites(ash_query, calculation, path, &1)
)
end
def rewrite([], records), do: records
def rewrite(_rewrites, nil), do: nil
def rewrite(_rewrites, []), do: []
def rewrite(_, %Ash.NotLoaded{} = not_loaded), do: not_loaded
def rewrite(_, %Ash.ForbiddenField{} = forbidden), do: forbidden
def rewrite(rewrites, %struct{results: results} = page)
when struct in [Ash.Page.Keyset, Ash.Page.Offset] do
%{page | results: rewrite(results, rewrites)}
end
def rewrite(rewrites, record) when not is_list(record) do
rewrites
|> rewrite([record])
|> Enum.at(0)
end
def rewrite(rewrites, [%resource{} | _] = records) do
rewrites
|> Enum.sort_by(fn
{{path, _, _, _}, _} ->
Enum.count(path)
_ ->
:infinity
end)
|> Enum.reduce(records, fn
{:cleanup_field_auth, further_load}, records ->
query =
resource
|> Ash.Query.load(further_load)
Ash.Actions.Read.cleanup_field_auth(records, query, false)
{{path, data, calc_name, calc_load}, source}, records when path != [] ->
rewrite_at_path(records, {{path, data, calc_name, calc_load}, source})
{{_, {:calc, to_name, to_load}, _, _}, source}, records ->
if to_load do
Enum.map(records, fn record ->
case record do
%Ash.NotLoaded{} ->
record
%Ash.ForbiddenField{} ->
record
nil ->
record
record ->
Map.put(record, to_load, Map.get(record.calculations, source))
end
end)
else
Enum.map(records, fn record ->
case record do
%Ash.NotLoaded{} ->
record
%Ash.ForbiddenField{} ->
record
nil ->
record
record ->
Map.update!(
record,
:calculations,
&Map.put(&1, to_name, Map.get(record.calculations, source))
)
end
end)
end
{{_, {:agg, to_name, to_load}, _, _}, source}, records ->
if to_load do
Enum.map(records, fn record ->
case record do
%Ash.NotLoaded{} ->
record
%Ash.ForbiddenField{} ->
record
nil ->
record
record ->
Map.put(record, to_load, Map.get(record.aggregates, source))
end
end)
else
Enum.map(records, fn record ->
case record do
%Ash.NotLoaded{} ->
record
%Ash.ForbiddenField{} ->
record
nil ->
record
record ->
Map.update!(
record,
:aggregates,
&Map.put(&1, to_name, Map.get(record.aggregates, source))
)
end
end)
end
{{path, {:rel, rel_name}, _, _}, source}, records ->
records
|> update_at_path(path, fn record ->
Map.put(record, rel_name, Map.get(record.calculations, source))
end)
end)
end
defp rewrite_at_path(
records,
{{[{:attr, type, constraints, name} | rest], data, calc_name, calc_load}, source}
) do
new_rewrites = [
{{rest, data, calc_name, calc_load}, source}
]
Enum.map(records, fn record ->
Map.update!(record, name, &Ash.Type.rewrite(type, &1, new_rewrites, constraints))
end)
end
defp rewrite_at_path(
records,
{{[{:rel, name} | rest], data, calc_name, calc_load}, source}
) do
new_rewrites = [
{{rest, data, calc_name, calc_load}, source}
]
Enum.map(records, fn record ->
Map.update!(record, name, &rewrite(new_rewrites, &1))
end)
end
defp rewrite_at_path(
records,
{{[{:calc, type, constraints, name, load} | rest], data, calc_name, calc_load}, source}
) do
new_rewrites = [
{{rest, data, calc_name, calc_load}, source}
]
if load do
Enum.map(records, fn record ->
Map.update!(record, load, &Ash.Type.rewrite(type, &1, new_rewrites, constraints))
end)
else
Enum.map(records, fn record ->
Map.update!(record, :calculations, fn calculations ->
calculations
|> Map.put_new(name, nil)
|> Map.update!(name, &Ash.Type.rewrite(type, &1, new_rewrites, constraints))
end)
end)
end
end
defp update_at_path(nil, [], _), do: nil
defp update_at_path(%Ash.NotLoaded{} = value, _, _), do: value
defp update_at_path(%Ash.ForbiddenField{} = value, _, _), do: value
defp update_at_path(records, path, func) when is_list(records) do
Enum.map(records, &update_at_path(&1, path, func))
end
defp update_at_path(record, [], func) do
func.(record)
end
defp update_at_path(record, [key | rest], func) do
Map.update!(record, key, &update_at_path(&1, rest, func))
end
defp load_through_calculation_rewrites(ash_query, calculation, path) do
ash_query.load_through[:calculations]
|> Kernel.||(%{})
|> Enum.flat_map(fn {calc_name, further_load} ->
through_calculation = ash_query.calculations[calc_name]
if through_calculation && through_calculation.type do
through_calculation.type
|> Ash.Type.get_rewrites(further_load, calculation, path, calculation.constraints || [])
|> Enum.map(fn
{{path, rewrite_data, calc_name, calc_load}, source} ->
{{path ++
[
{:calc, through_calculation.type, through_calculation.constraints,
through_calculation.name, through_calculation.load}
], rewrite_data, calc_name, calc_load}, source}
end)
else
[]
end
end)
end
defp load_through_attribute_rewrites(ash_query, calculation, path) do
ash_query.load_through[:attribute]
|> Kernel.||(%{})
|> Enum.flat_map(fn {attr_name, further_load} ->
attr = Ash.Resource.Info.attribute(ash_query.resource, attr_name)
attr.type
|> Ash.Type.get_rewrites(further_load, calculation, path, attr.constraints)
|> Enum.map(fn {{path, rewrite_data, calc_name, calc_load}, source} ->
{{path ++ [{:attr, attr.type, attr.constraints, attr.name}], rewrite_data, calc_name,
calc_load}, source}
end)
end)
end
defp get_rewrites(ash_query, calculation, path, :load_through) do
load_through_calculation_rewrites(ash_query, calculation, path) ++
load_through_attribute_rewrites(ash_query, calculation, path)
end
defp get_rewrites(ash_query, calculation, path, :relationships) do
ash_query.load
|> Enum.flat_map(fn
{name, []} ->
relationship = Ash.Resource.Info.relationship(ash_query.resource, name)
if calculation.module.strict_loads?() do
[]
else
query = Ash.Query.new(relationship.destination)
query
|> get_all_rewrites(calculation, path)
|> Enum.map(fn {{path, data, calc_name, calc_load}, source} ->
{{path ++ [{:rel, name}], data, calc_name, calc_load}, source}
end)
end
{name, query} ->
query
|> get_all_rewrites(calculation, path)
|> Enum.map(fn {{path, data, calc_name, calc_load}, source} ->
{{path ++ [{:rel, name}], data, calc_name, calc_load}, source}
end)
end)
|> Enum.concat(load_relationship_rewrites(ash_query, calculation, path))
end
defp get_rewrites(ash_query, calculation, path, key) do
calc_name = calculation.name
calc_load = calculation.load
ash_query
|> Kernel.||(%{})
|> Map.get(key, %{})
|> Enum.flat_map(fn {_, calculation} ->
case calculation.name do
{:__calc_dep__, values} = source ->
Enum.flat_map(values, fn
{^path, {_, _, _}, ^calc_name, ^calc_load} = rewrite ->
[{rewrite, source}]
{^path, {:rel, name}, ^calc_name, ^calc_load} = rewrite ->
[
{rewrite, source}
| get_all_rewrites(
calculation.opts[:query],
calculation,
path ++ [name]
)
]
_ ->
[]
end)
_ ->
[]
end
end)
end
defp load_relationship_rewrites(ash_query, top_calculation, path) do
ash_query.calculations
|> Enum.filter(fn {_calc_name, calculation} ->
calculation.module == Ash.Resource.Calculation.LoadRelationship &&
match?({:__calc_dep__, _}, calculation.name)
end)
|> Enum.flat_map(fn {_calc_name, calculation} ->
get_all_rewrites(
calculation.opts[:query],
top_calculation,
path ++ [{:rel, calculation.opts[:relationship]}]
)
end)
end
# TODO: This currently must assume that all relationship loads are different if
# authorize?: true, because the policies have not yet been applied.
def split_and_load_calculations(
domain,
ash_query,
missing_pkeys?,
initial_data,
reuse_values?,
authorize?
) do
can_expression_calculation? =
!missing_pkeys? &&
Ash.DataLayer.data_layer_can?(ash_query.resource, :expression_calculation)
ash_query =
Enum.reduce(ash_query.calculations, ash_query, fn {_name, calc}, ash_query ->
load_calculation_requirements(
domain,
ash_query,
calc,
can_expression_calculation?,
initial_data,
reuse_values?,
authorize?
)
end)
ash_query.calculations
|> Map.values()
|> Enum.reduce({[], [], ash_query}, fn calculation, {in_query, at_runtime, ash_query} ->
if calculation.module.has_expression?() do
expression =
calculation.opts
|> Ash.Expr.fill_template(
calculation.context.actor,
calculation.context.arguments,
calculation.context.source_context
)
|> calculation.module.expression(calculation.context)
|> Ash.Expr.fill_template(
calculation.context.actor,
calculation.context.arguments,
calculation.context.source_context
)
|> Ash.Actions.Read.add_calc_context_to_filter(
calculation.context.actor,
calculation.context.authorize?,
calculation.context.tenant,
calculation.context.tracer,
domain
)
case try_evaluate(
expression,
ash_query.resource,
calculation,
initial_data,
reuse_values?
) do
{:ok, new_calculation} ->
{in_query, [new_calculation | at_runtime], ash_query}
_ ->
if can_expression_calculation? do
if should_be_in_expression?(calculation, expression, ash_query) do
{[calculation | in_query], at_runtime, ash_query}
else
{in_query, [calculation | at_runtime], ash_query}
end
else
if calculation.module.has_calculate?() do
{in_query, [calculation | at_runtime], ash_query}
else
{in_query,
[
%{
calculation
| module: Ash.Resource.Calculation.RuntimeExpression,
opts: [expr: expression],
required_loads: [],
select: []
}
| at_runtime
], ash_query}
end
end
end
else
{in_query, [calculation | at_runtime], ash_query}
end
end)
end
defp try_evaluate(expression, resource, calculation, {:ok, initial_data}, reuse_values?) do
expression
|> Ash.Filter.list_refs(false, false, true, true)
|> then(fn refs ->
if refs == [] do
true
else
if reuse_values? do
Enum.all?(refs, fn ref ->
# consider doing `lists: :any`?
Ash.Resource.loaded?(initial_data, ref.relationship_path ++ [ref.attribute],
strict?: true,
type: :request
)
end)
else
false
end
end
end)
|> case do
true ->
Enum.reduce_while(initial_data, {:ok, []}, fn record, {:ok, results} ->
case Ash.Expr.eval(expression,
record: record,
resource: resource,
unknown_on_unknown_refs?: true
) do
{:ok, result} ->
{:cont, {:ok, [result | results]}}
_ ->
{:halt, :error}
end
end)
|> case do
{:ok, values} ->
{:ok,
%{
calculation
| module: Ash.Resource.Calculation.Literal,
opts: [value: Enum.reverse(values), precomputed?: true],
required_loads: [],
select: []
}}
_ ->
:error
end
false ->
:error
end
end
defp try_evaluate(expression, resource, calculation, _, _) do
case Ash.Expr.eval(expression,
resource: resource,
unknown_on_unknown_refs?: true
) do
{:ok, result} ->
{:ok,
%{
calculation
| module: Ash.Resource.Calculation.Literal,
opts: [value: result],
required_loads: [],
select: []
}}
_ ->
:error
end
end
defp should_be_in_expression?(calculation, expression \\ nil, ash_query) do
if calculation.module.has_expression?() do
case Map.fetch(calculation.context, :should_be_in_expression?) do
{:ok, value} ->
value
:error ->
expression =
expression ||
calculation.opts
|> calculation.module.expression(calculation.context)
|> Ash.Expr.fill_template(
calculation.context.actor,
calculation.context.arguments,
calculation.context.source_context
)
|> Ash.Actions.Read.add_calc_context_to_filter(
calculation.context.actor,
calculation.context.authorize?,
calculation.context.tenant,
calculation.context.tracer,
ash_query.domain
)
expression
|> Ash.Filter.hydrate_refs(%{resource: ash_query.resource, public?: false})
|> case do
{:ok, expression} ->
expression
|> Ash.Filter.used_calculations(ash_query.resource, :*)
|> Enum.all?(fn %{module: module} ->
module.has_expression?()
end)
{:error, _error} ->
false
end
end
else
false
end
end
defp load_calculation_requirements(
domain,
query,
calculation,
can_expression_calculation?,
initial_data,
reuse_values?,
authorize?,
calc_path \\ [],
relationship_path \\ [],
checked_calculations \\ []
) do
if {calculation.module, calculation.opts} in checked_calculations do
query
else
has_expression? = calculation.module.has_expression?()
if has_expression? && should_be_in_expression?(calculation, query) do
Map.update!(query, :calculations, fn calculations ->
Map.update!(calculations, calculation.name, fn calc ->
Map.update!(calc, :context, fn context ->
Map.put(context, :should_be_in_expression?, true)
end)
end)
end)
else
query =
if has_expression? do
Map.update!(query, :calculations, fn calculations ->
Map.update!(calculations, calculation.name, fn calc ->
Map.update!(calc, :context, fn context ->
Map.put(context, :should_be_in_expression?, false)
end)
end)
end)
else
query
end
checked_calculations = [{calculation.module, calculation.opts} | checked_calculations]
calculation.required_loads
|> List.wrap()
|> Enum.concat(List.wrap(calculation.select))
|> do_load_calculation_requirements(
domain,
query,
calculation.name,
calculation.load,
calc_path,
calculation.module.strict_loads?(),
relationship_path,
can_expression_calculation?,
checked_calculations,
initial_data,
reuse_values?,
authorize?
)
end
end
end
defp do_load_calculation_requirements(
requirements,
domain,
query,
calc_name,
calc_load,
calc_path,
strict_loads?,
relationship_path,
can_expression_calculation?,
checked_calculations,
initial_data,
reuse_values?,
authorize?
) do
requirements
|> Enum.map(fn
{key, value} ->
{key, value}
key ->
{key, []}
end)
|> Enum.reduce(
query,
&load_single_calculation_dependency(
&1,
&2,
domain,
calc_name,
calc_load,
calc_path,
strict_loads?,
relationship_path,
can_expression_calculation?,
checked_calculations,
initial_data,
reuse_values?,
authorize?
)
)
end
defp load_single_calculation_dependency(
{load, further},
query,
domain,
calc_name,
calc_load,
calc_path,
strict_loads?,
relationship_path,
can_expression_calculation?,
checked_calculations,
initial_data,
reuse_values?,
authorize?
) do
cond do
match?(%Ash.Query.Calculation{}, load) ->
load_depended_on_calc(
query,
load,
further,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
can_expression_calculation?,
checked_calculations,
initial_data,
strict_loads?,
reuse_values?,
authorize?
)
match?(%Ash.Query.Aggregate{}, load) ->
if loaded_and_reusable?(initial_data, relationship_path, load, reuse_values?) do
query
else
aggregate = load
case find_equivalent_aggregate(query, aggregate, authorize?) do
{:ok, equivalent_aggregate} ->
if equivalent_aggregate.load == aggregate.load and
equivalent_aggregate.name == aggregate.name do
query
else
new_calc_name =
{:__calc_dep__,
[
{calc_path, {:agg, equivalent_aggregate.name, equivalent_aggregate.load},
calc_name, calc_load}
]}
Ash.Query.calculate(
query,
new_calc_name,
equivalent_aggregate.type,
{Ash.Resource.Calculation.FetchAgg,
load: equivalent_aggregate.name, name: equivalent_aggregate.load},
%{},
equivalent_aggregate.constraints,
equivalent_aggregate.context
)
|> add_calculation_dependency(calc_name, new_calc_name)
end
:error ->
new_agg =
if query.aggregates[aggregate.name] do
%{
aggregate
| name:
{:__calc_dep__,
[
{calc_path, {:agg, aggregate.name, aggregate.load}, calc_name,
calc_load}
]},
load: nil
}
else
aggregate
end
Ash.Query.load(query, new_agg)
end
end
attr = Ash.Resource.Info.attribute(query.resource, load) ->
depended_on_fields = query.context[:private][:depended_on_fields] || []
query =
Ash.Query.set_context(query, %{
private: %{depended_on_fields: [attr.name | depended_on_fields]}
})
case Map.fetch(query.load_through[:attribute] || %{}, attr.name) do
:error ->
query =
Ash.Query.ensure_selected(query, attr.name)
if further in [nil, []] do
query
else
load_through =
query.load_through
|> Map.put_new(:attribute, %{})
|> Map.update!(:attribute, &Map.put(&1, attr.name, further))
%{query | load_through: load_through}
end
{:ok, value} ->
load_through =
Map.update!(query.load_through, :attribute, fn attributes_load_through ->
Map.put(
attributes_load_through,
attr.name,
merge_load_through(
value || [],
further || [],
attr.type,
attr.constraints,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
initial_data,
strict_loads?,
reuse_values?,
authorize?
)
)
end)
%{
query
| load_through: load_through
}
|> Ash.Query.ensure_selected(attr.name)
end
agg = Ash.Resource.Info.aggregate(query.resource, load) ->
if loaded_and_reusable?(initial_data, relationship_path, load, reuse_values?) do
query
else
Ash.Query.load(query, agg.name)
end
resource_calculation = Ash.Resource.Info.calculation(query.resource, load) ->
{args, load_through} =
case further do
{args, load_through} ->
{args, load_through}
args ->
{args, nil}
end
{name, load} =
cond do
Keyword.keyword?(args) ->
case Keyword.fetch(args, :as) do
{:ok, value} ->
{value, nil}
:error ->
{resource_calculation.name, resource_calculation.name}
end
is_map(args) ->
case Map.fetch(args, :as) do
{:ok, value} ->
{value, nil}
:error ->
{resource_calculation.name, resource_calculation.name}
end
true ->
{resource_calculation.name, resource_calculation.name}
end
case Ash.Query.Calculation.from_resource_calculation(query.resource, resource_calculation,
args: Map.new(args),
source_context: query.context
) do
{:ok, calculation} ->
calculation = %{calculation | load: load, name: name}
calculation =
Ash.Query.select_and_load_calc(
resource_calculation,
%{calculation | load: load, calc_name: resource_calculation.name},
query
)
load_single_calculation_dependency(
{calculation, load_through},
query,
domain,
calc_name,
calc_load,
calc_path,
strict_loads?,
relationship_path,
can_expression_calculation?,
checked_calculations,
initial_data,
reuse_values?,
authorize?
)
{:error, error} ->
Ash.Query.add_error(query, :load, error)
end
relationship = Ash.Resource.Info.relationship(query.resource, load) ->
query = Ash.Query.ensure_selected(query, relationship.source_attribute)
further = to_loaded_query(relationship.destination, further, strict_loads?)
if loaded_and_reusable?(initial_data, relationship_path, relationship, reuse_values?) do
case query.load[relationship.name] do
nil ->
related_query =
relationship.destination
|> Ash.Query.set_context(%{private: %{lazy?: true, reuse_values?: reuse_values?}})
|> Ash.Query.select([])
|> merge_query_load(
further,
relationship.domain || domain,
calc_path,
calc_name,
calc_load,
relationship_path ++ [relationship.name],
initial_data,
strict_loads?,
reuse_values?
)
Ash.Query.load(query, [{relationship.name, related_query}])
related_query ->
related_query =
merge_query_load(
related_query,
further,
relationship.domain || domain,
calc_path,
calc_name,
calc_load,
relationship_path ++ [relationship.name],
initial_data,
strict_loads?,
reuse_values?
)
Ash.Query.load(query, [{relationship.name, related_query}])
end
else
current_load = query.load[relationship.name]
if current_load && !authorize? do
if compatible_relationships?(current_load, further) do
%{
query
| load:
Keyword.put(
query.load,
relationship.name,
merge_query_load(
current_load,
further,
relationship.domain || domain,
calc_path,
calc_name,
calc_load,
relationship_path ++ [relationship.name],
initial_data,
strict_loads?,
reuse_values?
)
)
}
else
add_new_relationship_calc(
relationship,
query,
domain,
calc_name,
calc_load,
calc_path,
strict_loads?,
relationship_path,
initial_data,
reuse_values?,
further
)
end
else
if authorize? do
add_new_relationship_calc(
relationship,
query,
domain,
calc_name,
calc_load,
calc_path,
strict_loads?,
relationship_path,
initial_data,
reuse_values?,
further
)
else
Ash.Query.load(query, [{relationship.name, further}])
end
end
end
true ->
raise "unknown load for #{inspect(query)}: #{inspect(load)}"
end
end
defp add_new_relationship_calc(
relationship,
query,
domain,
calc_name,
calc_load,
calc_path,
strict_loads?,
relationship_path,
initial_data,
reuse_values?,
further
) do
{type, constraints} =
case relationship.cardinality do
:many -> {{:array, :struct}, items: [instance_of: relationship.destination]}
:one -> {:struct, instance_of: relationship.destination}
end
case Enum.find(query.calculations, fn {_name, existing_calculation} ->
existing_calculation.module == Ash.Resource.Calculation.LoadRelationship &&
existing_calculation.opts[:relationship] == relationship.name &&
existing_calculation.opts[:opts][:authorize?] == false &&
compatible_relationships?(existing_calculation.opts[:query], further) &&
match?(%{name: {:__calc_dep__, _}}, existing_calculation)
end) do
nil ->
new_calc_name =
{:__calc_dep__,
[
{calc_path, {:rel, relationship.name}, calc_name, calc_load}
]}
query
|> Ash.Query.calculate(
new_calc_name,
type,
{
Ash.Resource.Calculation.LoadRelationship,
relationship: relationship.name,
query: further,
opts: [authorize?: false],
domain: relationship.domain || domain
},
%{},
constraints
)
|> add_calculation_dependency(calc_name, new_calc_name)
{key, existing_calculation} ->
new_calculation =
existing_calculation
|> Map.update!(:opts, fn opts ->
Keyword.update(
opts,
:query,
further,
&merge_query_load(
&1,
further,
relationship.domain || domain,
calc_path,
calc_name,
calc_load,
relationship_path ++ [relationship.name],
initial_data,
strict_loads?,
reuse_values?
)
)
end)
|> Map.update!(:name, fn {:__calc_dep__, paths} ->
{:__calc_dep__,
[{calc_path, {:rel, relationship.name}, calc_name, calc_load} | paths]}
end)
query
|> rename_and_replace_calculation(key, new_calculation)
|> add_calculation_dependency(calc_name, new_calculation.name)
end
end
defp rename_and_replace_calculation(query, current_key, new_calc) do
new_calculations =
query.calculations
|> Map.delete(current_key)
|> Map.put(new_calc.name, new_calc)
new_load_through =
if query.load_through[:calculation][current_key] do
Map.update!(query.load_through, :calculation, fn load_through_calcs ->
load_through_calcs
|> Map.delete(current_key)
|> Map.put(new_calc, load_through_calcs[current_key])
end)
else
query.load_through
end
%{query | calculations: new_calculations, load_through: new_load_through}
|> Map.update!(:context, fn context ->
Map.update(context, :calculation_dependencies, %{}, fn calculation_dependencies ->
Map.new(calculation_dependencies, fn {key, value} ->
new_key =
if key == current_key do
new_calc.name
else
key
end
{new_key,
MapSet.new(value, fn depends_on ->
if depends_on == current_key do
new_calc.name
else
depends_on
end
end)}
end)
end)
end)
end
defp load_depended_on_calc(
query,
calculation,
further,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
can_expression_calculation?,
checked_calculations,
initial_data,
strict_loads?,
reuse_values?,
authorize?
) do
if loaded_and_reusable?(initial_data, relationship_path, calculation, reuse_values?) do
do_merge_load_through(
query,
further,
:calculation,
calculation.name,
calculation.type,
calculation.constraints,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
initial_data,
strict_loads?,
reuse_values?,
authorize?
)
else
case find_equivalent_calculation(query, calculation, authorize?) do
{:ok, equivalent_calculation} ->
if equivalent_calculation.load == calculation.load and
equivalent_calculation.name == calculation.name do
query =
do_merge_load_through(
query,
further,
:calculation,
equivalent_calculation.name,
equivalent_calculation.type,
equivalent_calculation.constraints,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
initial_data,
strict_loads?,
reuse_values?,
authorize?
)
add_calculation_dependency(query, calc_name, equivalent_calculation.name)
else
query =
do_merge_load_through(
query,
further,
:calculation,
calculation.name,
calculation.type,
calculation.constraints,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
initial_data,
strict_loads?,
reuse_values?,
authorize?
)
new_calc_name =
{:__calc_dep__,
[
{calc_path, {:calc, equivalent_calculation.name, equivalent_calculation.load},
calc_name, calc_load}
]}
Ash.Query.calculate(
query,
new_calc_name,
equivalent_calculation.type,
{Ash.Resource.Calculation.FetchCalc,
load: equivalent_calculation.name, name: equivalent_calculation.load},
equivalent_calculation.context.arguments,
equivalent_calculation.constraints,
equivalent_calculation.context
)
|> add_calculation_dependency(calc_name, new_calc_name)
|> add_calculation_dependency(new_calc_name, equivalent_calculation.name)
end
:error ->
new_calculation =
if query.calculations[calculation.name] do
%{
calculation
| name:
{:__calc_dep__,
[
{calc_path, {:calc, calculation.name, calculation.load}, calc_name,
calc_load}
]},
load: nil
}
else
calculation
end
query =
Ash.Query.load(query, new_calculation)
new_calculation =
if should_be_in_expression?(new_calculation, query) do
new_calculation
else
query.calculations[new_calculation.name]
end
domain
|> load_calculation_requirements(
query,
new_calculation,
can_expression_calculation?,
initial_data,
reuse_values?,
authorize?,
calc_path,
relationship_path,
checked_calculations
)
|> add_calculation_dependency(calc_name, new_calculation.name)
end
end
end
defp do_merge_load_through(
query,
further,
load_type,
name,
type,
constraints,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
initial_data,
strict_loads?,
reuse_values?,
authorize?
) do
case Map.fetch(
query.load_through[load_type] || %{},
name
) do
:error ->
if further in [nil, []] do
query
else
load_through =
query.load_through
|> Map.put_new(load_type, %{})
|> Map.update!(
load_type,
&Map.put(&1, name, further)
)
%{query | load_through: load_through}
end
{:ok, value} ->
load_through =
Map.update!(query.load_through, load_type, fn type_load_through ->
Map.put(
type_load_through,
name,
merge_load_through(
value || [],
further || [],
type,
constraints,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
initial_data,
strict_loads?,
reuse_values?,
authorize?
)
)
end)
%{
query
| load_through: load_through
}
end
end
defp loaded_and_reusable?({:ok, initial_data}, relationship_path, calculation, true) do
Ash.Resource.loaded?(initial_data, relationship_path ++ [calculation], type: :request)
end
defp loaded_and_reusable?(_initial_data, _relationship_path, _calculation, false) do
false
end
defp add_calculation_dependency(query, source, dest) do
%{
query
| context:
query.context
|> Map.put_new(:calculation_dependencies, %{})
|> Map.update!(:calculation_dependencies, fn deps ->
Map.update(deps, source, MapSet.new([dest]), &MapSet.put(&1, dest))
end)
}
end
defp find_equivalent_calculation(query, calculation, authorize?) do
reusable? =
if authorize? && calculation.module.has_expression?() do
calculation.module.expression(calculation.opts, calculation.context)
|> Ash.Filter.list_refs(false, false, true, true)
|> Enum.any?(&(&1.relationship_path != []))
|> Kernel.not()
else
true
end
if reusable? do
Enum.find_value(query.calculations, :error, fn {_, other_calc} ->
if other_calc.module == calculation.module and other_calc.opts == calculation.opts and
other_calc.context.arguments == calculation.context.arguments do
{:ok, other_calc}
end
end)
else
:error
end
end
defp find_equivalent_aggregate(query, agg, authorize?) do
if !authorize? || match?({:__calc_dep__, _}, agg.name) do
Enum.find_value(query.aggregates, :error, fn {_, other_agg} ->
if other_agg == agg do
{:ok, other_agg}
end
end)
else
:error
end
end
defp compatible_relationships?(left, right) do
# use this?
# Ash.Query.equivalent_to?(left, right.filter) and
left.resource == right.resource and
left.filter == right.filter and
left.sort == right.sort
end
defp merge_load_through(
old,
new,
type,
constraints,
domain,
calc_name,
calc_load,
calc_path,
relationship_path,
initial_data,
strict_loads?,
reuse_values?,
authorize?
) do
case Ash.Type.merge_load(type, old, new, constraints, %{
domain: domain,
calc_name: calc_name,
calc_load: calc_load,
calc_path: calc_path,
relationship_path: relationship_path,
initial_data: initial_data,
reuse_values?: reuse_values?,
authorize?: authorize?,
strict_loads?: strict_loads?
}) do
{:ok, result} ->
result
:error ->
raise """
Encountered a type #{inspect(type)} which cannot merge loads, and cannot be used in calculation dependencies.
"""
{:error, error} ->
raise """
Encountered an error loading through type type #{inspect(type)}.
Error: #{inspect(error)}
"""
end
end
def merge_query_load(
left,
right,
domain,
calc_path,
calc_name,
calc_load,
relationship_path,
initial_data \\ :error,
strict_loads? \\ false,
reuse_values? \\ false,
authorize? \\ true
) do
can_expression_calculation? =
Ash.DataLayer.data_layer_can?(left.resource, :expression_calculation)
do_load_calculation_requirements(
right.load ++
Map.values(right.calculations) ++
Map.values(right.aggregates) ++ query_select(right, strict_loads?),
domain,
left,
calc_name,
calc_load,
calc_path,
strict_loads?,
relationship_path,
can_expression_calculation?,
[],
initial_data,
reuse_values?,
authorize?
)
end
defp query_select(%{resource: resource, select: nil}, false) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.map(& &1.name)
end
defp query_select(%{select: select}, _), do: List.wrap(select)
defp to_loaded_query(resource, %Ash.Query{resource: resource} = query, strict_loads?) do
if strict_loads? do
Ash.Query.select(query, [])
else
query
end
end
defp to_loaded_query(resource, loads, strict_loads?) do
if strict_loads? do
resource
|> Ash.Query.select([])
|> Ash.Query.load(apply_strict(resource, loads, strict_loads?))
else
resource
|> Ash.Query.load(loads)
end
end
defp apply_strict(resource, loads, strict_loads?) do
loads
|> List.wrap()
|> Enum.map(fn
{key, value} ->
if relationship = Ash.Resource.Info.relationship(resource, key) do
{key, to_loaded_query(relationship.destination, value, strict_loads?)}
else
{key, value}
end
key ->
if relationship = Ash.Resource.Info.relationship(resource, key) do
{key, to_loaded_query(relationship.destination, [], strict_loads?)}
else
key
end
end)
end
# Deselect fields that we know statically cannot be seen
# The field may be reselected later as a calculation dependency
# this is an optimization not a guarantee
def deselect_known_forbidden_fields(
ash_query,
calculations_at_runtime,
calculations_in_query,
skip \\ []
) do
depended_on_fields = ash_query.context[:private][:depended_on_fields] || []
calculations_at_runtime
|> Enum.concat(calculations_in_query)
|> Enum.reduce([], fn
%{
name: {:__ash_fields_are_visible__, fields},
module: module,
opts: opts
},
deselect_fields
when module in [
Ash.Resource.Calculation.Expression,
Ash.Resource.Calculation.RuntimeExpression,
Ash.Resource.Calculation.Literal
] ->
value =
if Keyword.has_key?(opts, :expr) do
# expression
opts[:expr]
else
# literal
opts[:value]
end
if value == false do
deselect_fields ++ fields
else
deselect_fields
end
_, deselect_fields ->
deselect_fields
end)
|> Enum.uniq()
|> Kernel.--(depended_on_fields)
|> Kernel.--(skip)
|> then(
&unload_forbidden_fields(ash_query, &1, calculations_at_runtime, calculations_in_query)
)
end
defp unload_forbidden_fields(ash_query, fields, calculations_at_runtime, calculations_in_query) do
fields
|> Enum.group_by(fn field ->
cond do
Ash.Resource.Info.attribute(ash_query.resource, field) ->
:attribute
Ash.Resource.Info.aggregate(ash_query.resource, field) ->
:aggregate
Ash.Resource.Info.calculation(ash_query.resource, field) ->
:calculation
end
end)
|> Enum.reduce({ash_query, calculations_at_runtime, calculations_in_query}, fn
{:attribute, fields}, {ash_query, calculations_at_runtime, calculations_in_query} ->
{ash_query
|> Ash.Query.deselect(fields)
|> unload_attribute_calculations(fields), calculations_at_runtime, calculations_in_query}
{:aggregate, fields}, {ash_query, calculations_at_runtime, calculations_in_query} ->
{unload_aggregates(ash_query, fields), calculations_at_runtime, calculations_in_query}
{:calculation, fields}, {ash_query, calculations_at_runtime, calculations_in_query} ->
unload_calculations(ash_query, fields, calculations_at_runtime, calculations_in_query)
end)
end
defp unload_aggregates(ash_query, fields) do
drop =
ash_query.aggregates
|> Enum.flat_map(fn {name, %{agg_name: agg_name}} ->
if agg_name in fields do
[name]
else
[]
end
end)
%{ash_query | aggregates: Map.drop(ash_query.aggregates, drop)}
end
defp unload_attribute_calculations(ash_query, fields) do
drop =
ash_query.calculations
|> Enum.flat_map(fn
{name, %{module: Ash.Resource.Calculation.LoadAttribute, opts: opts}} ->
if opts[:attribute] in fields do
[name]
else
[]
end
_ ->
[]
end)
%{ash_query | calculations: Map.drop(ash_query.calculations, drop)}
end
defp unload_calculations(ash_query, fields, calculations_at_runtime, calculations_in_query) do
drop =
ash_query.calculations
|> Enum.flat_map(fn
{name, %{calc_name: calc_name}} ->
if calc_name in fields do
[name]
else
[]
end
end)
Enum.reduce(
drop,
{ash_query, calculations_at_runtime, calculations_in_query},
fn drop, {ash_query, calculations_at_runtime, calculations_in_query} ->
if Enum.any?(ash_query.context[:calculation_dependencies] || [], fn {_source, dest} ->
drop in dest
end) do
{%{ash_query | calculations: Map.delete(ash_query.calculations, drop)},
calculations_at_runtime, calculations_in_query}
else
{%{ash_query | calculations: Map.delete(ash_query.calculations, drop)},
Enum.reject(calculations_at_runtime, &(&1.name == drop)),
Enum.reject(calculations_in_query, &(&1 == drop))}
end
end
)
|> remove_unreferenced_calculations()
end
defp remove_unreferenced_calculations(
{ash_query, calculations_at_runtime, calculations_in_query}
) do
{ash_query, Enum.filter(calculations_at_runtime, &used?(ash_query, &1.name)),
Enum.filter(calculations_in_query, &used?(ash_query, &1.name))}
end
defp used?(_ash_query, {:__ash_fields_are_visible__, _}), do: true
defp used?(ash_query, name) do
Map.has_key?(ash_query.calculations, name) ||
Enum.any?(ash_query.context[:calculation_dependencies] || [], fn {_source, dest} ->
name in dest
end)
end
end