mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
improvement: add allow_async?
to calculations, default to false
improvement: add elixir evaluation step to expression calculations
This commit is contained in:
parent
e17daf6b02
commit
05d8eb1741
9 changed files with 112 additions and 38 deletions
|
@ -3,6 +3,7 @@
|
||||||
locals_without_parens = [
|
locals_without_parens = [
|
||||||
accept: 1,
|
accept: 1,
|
||||||
action: 1,
|
action: 1,
|
||||||
|
allow_async?: 1,
|
||||||
allow_nil?: 1,
|
allow_nil?: 1,
|
||||||
allow_nil_input: 1,
|
allow_nil_input: 1,
|
||||||
allow_unregistered?: 1,
|
allow_unregistered?: 1,
|
||||||
|
|
|
@ -362,7 +362,8 @@ defmodule Ash.Actions.Read do
|
||||||
Enum.split_with(ash_query.calculations, fn {_name, calculation} ->
|
Enum.split_with(ash_query.calculations, fn {_name, calculation} ->
|
||||||
Enum.find(used_calculations, &(&1.name == calculation.name)) ||
|
Enum.find(used_calculations, &(&1.name == calculation.name)) ||
|
||||||
calculation.name in Enum.map(ash_query.sort || [], &elem(&1, 0)) ||
|
calculation.name in Enum.map(ash_query.sort || [], &elem(&1, 0)) ||
|
||||||
!:erlang.function_exported(calculation.module, :calculate, 3)
|
(!:erlang.function_exported(calculation.module, :calculate, 3) &&
|
||||||
|
calculation.allow_async?)
|
||||||
end)
|
end)
|
||||||
else
|
else
|
||||||
{[], ash_query.calculations}
|
{[], ash_query.calculations}
|
||||||
|
@ -826,21 +827,24 @@ defmodule Ash.Actions.Read do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
can_be_runtime
|
can_be_runtime
|
||||||
|> Enum.reduce_while({:ok, %{}}, fn calculation, {:ok, calculation_results} ->
|
|> Enum.reduce_while({:ok, %{}, require_query}, fn calculation,
|
||||||
|
{:ok, calculation_results, require_query} ->
|
||||||
case calculation.module.calculate(results, calculation.opts, calculation.context) do
|
case calculation.module.calculate(results, calculation.opts, calculation.context) do
|
||||||
results when is_list(results) ->
|
results when is_list(results) ->
|
||||||
{:cont, {:ok, Map.put(calculation_results, calculation, results)}}
|
{:cont, {:ok, Map.put(calculation_results, calculation, results), require_query}}
|
||||||
|
|
||||||
|
:unknown ->
|
||||||
|
{:cont, {:ok, calculation_results, [calculation | require_query]}}
|
||||||
|
|
||||||
{:ok, results} ->
|
{:ok, results} ->
|
||||||
{:cont, {:ok, Map.put(calculation_results, calculation, results)}}
|
{:cont, {:ok, Map.put(calculation_results, calculation, results), require_query}}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:halt, {:error, error}}
|
{:halt, {:error, error}}
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, calculation_results} ->
|
{:ok, calculation_results, require_query} ->
|
||||||
{:ok,
|
|
||||||
Enum.reduce(calculation_results, results, fn {calculation, values}, records ->
|
Enum.reduce(calculation_results, results, fn {calculation, values}, records ->
|
||||||
if calculation.load do
|
if calculation.load do
|
||||||
:lists.zipwith(
|
:lists.zipwith(
|
||||||
|
@ -857,17 +861,17 @@ defmodule Ash.Actions.Read do
|
||||||
values
|
values
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end)}
|
end)
|
||||||
|
|> run_calculation_query(require_query, query)
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:error, error}
|
{:error, error}
|
||||||
end
|
end
|
||||||
|> run_calculation_query(require_query, query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp run_calculation_query({:ok, results}, [], _), do: {:ok, results}
|
defp run_calculation_query(results, [], _), do: {:ok, results}
|
||||||
|
|
||||||
defp run_calculation_query({:ok, results}, calculations, query) do
|
defp run_calculation_query(results, calculations, query) do
|
||||||
pkey = Ash.Resource.Info.primary_key(query.resource)
|
pkey = Ash.Resource.Info.primary_key(query.resource)
|
||||||
|
|
||||||
pkey_filter =
|
pkey_filter =
|
||||||
|
@ -918,10 +922,6 @@ defmodule Ash.Actions.Read do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp run_calculation_query({:error, error}, _, _) do
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp add_aggregate_values(results, aggregates, resource, aggregate_values, aggregates_in_query) do
|
defp add_aggregate_values(results, aggregates, resource, aggregate_values, aggregates_in_query) do
|
||||||
keys_to_aggregates =
|
keys_to_aggregates =
|
||||||
Enum.reduce(aggregate_values, %{}, fn {_name, keys_to_values}, acc ->
|
Enum.reduce(aggregate_values, %{}, fn {_name, keys_to_values}, acc ->
|
||||||
|
|
|
@ -26,7 +26,7 @@ defmodule Ash.Calculation do
|
||||||
@callback init(Keyword.t()) :: {:ok, Keyword.t()} | {:error, term}
|
@callback init(Keyword.t()) :: {:ok, Keyword.t()} | {:error, term}
|
||||||
@callback describe(Keyword.t()) :: String.t()
|
@callback describe(Keyword.t()) :: String.t()
|
||||||
@callback calculate([Ash.Resource.record()], Keyword.t(), map) ::
|
@callback calculate([Ash.Resource.record()], Keyword.t(), map) ::
|
||||||
{:ok, [term]} | [term] | {:error, term}
|
{:ok, [term]} | [term] | {:error, term} | :unknown
|
||||||
@callback expression(Keyword.t(), map) :: any
|
@callback expression(Keyword.t(), map) :: any
|
||||||
@callback load(Ash.Query.t(), Keyword.t(), map) :: Ash.Query.t()
|
@callback load(Ash.Query.t(), Keyword.t(), map) :: Ash.Query.t()
|
||||||
@callback select(Ash.Query.t(), Keyword.t(), map) :: list(atom)
|
@callback select(Ash.Query.t(), Keyword.t(), map) :: list(atom)
|
||||||
|
|
|
@ -147,9 +147,6 @@ defmodule Ash.Filter.Runtime do
|
||||||
nil ->
|
nil ->
|
||||||
{:ok, true}
|
{:ok, true}
|
||||||
|
|
||||||
boolean when is_boolean(boolean) ->
|
|
||||||
{:ok, boolean}
|
|
||||||
|
|
||||||
%op{__operator__?: true, left: left, right: right} = operator ->
|
%op{__operator__?: true, left: left, right: right} = operator ->
|
||||||
with {:ok, [left, right]} <-
|
with {:ok, [left, right]} <-
|
||||||
resolve_exprs([left, right], record),
|
resolve_exprs([left, right], record),
|
||||||
|
@ -195,6 +192,9 @@ defmodule Ash.Filter.Runtime do
|
||||||
|
|
||||||
%BooleanExpression{op: op, left: left, right: right} ->
|
%BooleanExpression{op: op, left: left, right: right} ->
|
||||||
expression_matches(op, left, right, record)
|
expression_matches(op, left, right, record)
|
||||||
|
|
||||||
|
other ->
|
||||||
|
{:ok, other}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
defmodule Ash.Query.Calculation do
|
defmodule Ash.Query.Calculation do
|
||||||
@moduledoc "Represents a calculated attribute requested on a query"
|
@moduledoc "Represents a calculated attribute requested on a query"
|
||||||
|
|
||||||
defstruct [:name, :module, :opts, :load, :type, context: %{}, select: [], sequence: 0]
|
defstruct [
|
||||||
|
:name,
|
||||||
|
:module,
|
||||||
|
:opts,
|
||||||
|
:load,
|
||||||
|
:type,
|
||||||
|
context: %{},
|
||||||
|
select: [],
|
||||||
|
sequence: 0,
|
||||||
|
allow_async?: false
|
||||||
|
]
|
||||||
|
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
|
|
|
@ -731,7 +731,12 @@ defmodule Ash.Query do
|
||||||
|> Kernel.||([])
|
|> Kernel.||([])
|
||||||
|> Enum.concat(module.select(query, opts, calculation.context) || [])
|
|> Enum.concat(module.select(query, opts, calculation.context) || [])
|
||||||
|
|
||||||
calculation = %{calculation | load: field, select: fields_to_select}
|
calculation = %{
|
||||||
|
calculation
|
||||||
|
| load: field,
|
||||||
|
select: fields_to_select,
|
||||||
|
allow_async?: resource_calculation.allow_async?
|
||||||
|
}
|
||||||
|
|
||||||
query =
|
query =
|
||||||
query
|
query
|
||||||
|
|
|
@ -10,7 +10,8 @@ defmodule Ash.Resource.Calculation do
|
||||||
:private?,
|
:private?,
|
||||||
:allow_nil?,
|
:allow_nil?,
|
||||||
:select,
|
:select,
|
||||||
:load
|
:load,
|
||||||
|
:allow_async?
|
||||||
]
|
]
|
||||||
|
|
||||||
@schema [
|
@schema [
|
||||||
|
@ -28,6 +29,20 @@ defmodule Ash.Resource.Calculation do
|
||||||
default: [],
|
default: [],
|
||||||
doc: "Constraints to provide to the type."
|
doc: "Constraints to provide to the type."
|
||||||
],
|
],
|
||||||
|
allow_async?: [
|
||||||
|
type: :boolean,
|
||||||
|
default: false,
|
||||||
|
doc: """
|
||||||
|
If set to `true`, then the calculation may be run after the main query.
|
||||||
|
|
||||||
|
This is useful for calculations that are very expensive, especially when combined with complex filters/join
|
||||||
|
scenarios. By adding this, we will rerun a trimmed down version of the main query, using the primary keys for
|
||||||
|
fast access. This will be done asynchronously for each calculation that has `allow_async?: true`.
|
||||||
|
|
||||||
|
Keep in mind that if the calculation is used in a filter or sort, it cannot be done asynchrnously,
|
||||||
|
and *must* be done in the main query.
|
||||||
|
"""
|
||||||
|
],
|
||||||
calculation: [
|
calculation: [
|
||||||
type: {:custom, __MODULE__, :calculation, []},
|
type: {:custom, __MODULE__, :calculation, []},
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
@ -9,6 +9,47 @@ defmodule Ash.Resource.Calculation.Expression do
|
||||||
Ash.Filter.build_filter_from_template(expr, nil, context, context[:context] || %{})
|
Ash.Filter.build_filter_from_template(expr, nil, context, context[:context] || %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def calculate([], _, _), do: []
|
||||||
|
|
||||||
|
def calculate([%resource{} | _] = records, opts, context) do
|
||||||
|
expression =
|
||||||
|
Ash.Filter.build_filter_from_template(opts[:expr], nil, context, context[:context] || %{})
|
||||||
|
|
||||||
|
Enum.reduce_while(records, {:ok, []}, fn record, {:ok, values} ->
|
||||||
|
case Ash.Filter.hydrate_refs(expression, %{
|
||||||
|
resource: resource,
|
||||||
|
aggregates: %{},
|
||||||
|
calculations: %{},
|
||||||
|
public?: false
|
||||||
|
}) do
|
||||||
|
{:ok, expression} ->
|
||||||
|
case Ash.Filter.Runtime.do_match(record, expression) do
|
||||||
|
{:ok, value} ->
|
||||||
|
{:cont, {:ok, [value | values]}}
|
||||||
|
|
||||||
|
:unknown ->
|
||||||
|
{:halt, :unknown}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:halt, {:error, error}}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:halt, {:error, error}}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> case do
|
||||||
|
{:ok, values} ->
|
||||||
|
{:ok, Enum.reverse(values)}
|
||||||
|
|
||||||
|
:unknown ->
|
||||||
|
:unknown
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def load(query, opts, context) do
|
def load(query, opts, context) do
|
||||||
expr =
|
expr =
|
||||||
Ash.Filter.build_filter_from_template(opts[:expr], nil, context, context[:context] || %{})
|
Ash.Filter.build_filter_from_template(opts[:expr], nil, context, context[:context] || %{})
|
||||||
|
|
|
@ -53,7 +53,9 @@ defmodule Ash.Test.CalculationTest do
|
||||||
constraints: [allow_empty?: true, trim?: false]
|
constraints: [allow_empty?: true, trim?: false]
|
||||||
end
|
end
|
||||||
|
|
||||||
calculate :expr_full_name, :string, expr(first_name <> " " <> last_name)
|
calculate :expr_full_name, :string, expr(first_name <> " " <> last_name) do
|
||||||
|
allow_async? true
|
||||||
|
end
|
||||||
|
|
||||||
calculate :conditional_full_name,
|
calculate :conditional_full_name,
|
||||||
:string,
|
:string,
|
||||||
|
|
Loading…
Reference in a new issue