mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 21:13:10 +12:00
improvement: prune calculations made unnecessary by field policies
closes #1356
This commit is contained in:
parent
dc3f921d33
commit
5a4864650b
3 changed files with 132 additions and 65 deletions
|
@ -1731,7 +1731,17 @@ defmodule Ash.Actions.Read.Calculations do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp find_equivalent_calculation(query, calculation, authorize?) do
|
defp find_equivalent_calculation(query, calculation, authorize?) do
|
||||||
if !authorize? || match?({:__calc_dep__, _}, calculation.name) 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} ->
|
Enum.find_value(query.calculations, :error, fn {_, other_calc} ->
|
||||||
if other_calc.module == calculation.module and other_calc.opts == calculation.opts and
|
if other_calc.module == calculation.module and other_calc.opts == calculation.opts and
|
||||||
other_calc.context.arguments == calculation.context.arguments do
|
other_calc.context.arguments == calculation.context.arguments do
|
||||||
|
@ -1891,10 +1901,11 @@ defmodule Ash.Actions.Read.Calculations do
|
||||||
# Deselect fields that we know statically cannot be seen
|
# Deselect fields that we know statically cannot be seen
|
||||||
# The field may be reselected later as a calculation dependency
|
# The field may be reselected later as a calculation dependency
|
||||||
# this is an optimization not a guarantee
|
# this is an optimization not a guarantee
|
||||||
def deselect_known_forbidden_fields(ash_query, calculations_at_runtime) do
|
def deselect_known_forbidden_fields(ash_query, calculations_at_runtime, calculations_in_query) do
|
||||||
depended_on_fields = ash_query.context[:private][:depended_on_fields] || []
|
depended_on_fields = ash_query.context[:private][:depended_on_fields] || []
|
||||||
|
|
||||||
calculations_at_runtime
|
calculations_at_runtime
|
||||||
|
|> Enum.concat(calculations_in_query)
|
||||||
|> Enum.reduce([], fn
|
|> Enum.reduce([], fn
|
||||||
%{
|
%{
|
||||||
name: {:__ash_fields_are_visible__, fields},
|
name: {:__ash_fields_are_visible__, fields},
|
||||||
|
@ -1927,10 +1938,12 @@ defmodule Ash.Actions.Read.Calculations do
|
||||||
end)
|
end)
|
||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
|> Kernel.--(depended_on_fields)
|
|> Kernel.--(depended_on_fields)
|
||||||
|> then(&unload_forbidden_fields(ash_query, &1))
|
|> then(
|
||||||
|
&unload_forbidden_fields(ash_query, &1, calculations_at_runtime, calculations_in_query)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp unload_forbidden_fields(ash_query, fields) do
|
defp unload_forbidden_fields(ash_query, fields, calculations_at_runtime, calculations_in_query) do
|
||||||
fields
|
fields
|
||||||
|> Enum.group_by(fn field ->
|
|> Enum.group_by(fn field ->
|
||||||
cond do
|
cond do
|
||||||
|
@ -1944,17 +1957,17 @@ defmodule Ash.Actions.Read.Calculations do
|
||||||
:calculation
|
:calculation
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> Enum.reduce(ash_query, fn
|
|> Enum.reduce({ash_query, calculations_at_runtime, calculations_in_query}, fn
|
||||||
{:attribute, fields}, ash_query ->
|
{:attribute, fields}, {ash_query, calculations_at_runtime, calculations_in_query} ->
|
||||||
ash_query
|
{ash_query
|
||||||
|> Ash.Query.deselect(fields)
|
|> Ash.Query.deselect(fields)
|
||||||
|> unload_attribute_calculations(fields)
|
|> unload_attribute_calculations(fields), calculations_at_runtime, calculations_in_query}
|
||||||
|
|
||||||
{:aggregate, fields}, ash_query ->
|
{:aggregate, fields}, {ash_query, calculations_at_runtime, calculations_in_query} ->
|
||||||
unload_aggregates(ash_query, fields)
|
{unload_aggregates(ash_query, fields), calculations_at_runtime, calculations_in_query}
|
||||||
|
|
||||||
{:calculation, fields}, ash_query ->
|
{:calculation, fields}, {ash_query, calculations_at_runtime, calculations_in_query} ->
|
||||||
unload_calculations(ash_query, fields)
|
unload_calculations(ash_query, fields, calculations_at_runtime, calculations_in_query)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1990,7 +2003,7 @@ defmodule Ash.Actions.Read.Calculations do
|
||||||
%{ash_query | calculations: Map.drop(ash_query.calculations, drop)}
|
%{ash_query | calculations: Map.drop(ash_query.calculations, drop)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp unload_calculations(ash_query, fields) do
|
defp unload_calculations(ash_query, fields, calculations_at_runtime, calculations_in_query) do
|
||||||
drop =
|
drop =
|
||||||
ash_query.calculations
|
ash_query.calculations
|
||||||
|> Enum.flat_map(fn
|
|> Enum.flat_map(fn
|
||||||
|
@ -2002,6 +2015,36 @@ defmodule Ash.Actions.Read.Calculations do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
%{ash_query | calculations: Map.drop(ash_query.calculations, drop)}
|
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, name) do
|
||||||
|
Map.has_key?(ash_query, name) ||
|
||||||
|
Enum.any?(ash_query.context[:calculation_dependencies], fn {_source, dest} ->
|
||||||
|
name in dest
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -251,14 +251,19 @@ defmodule Ash.Actions.Read do
|
||||||
|
|
||||||
{data_result, query_ran} =
|
{data_result, query_ran} =
|
||||||
case data_result do
|
case data_result do
|
||||||
{:ok, result, query} -> {{:ok, result, nil}, query}
|
{:ok, _result, _count, _calculations_at_runtime, _calculations_in_query, query} =
|
||||||
{:ok, result, count, query} -> {{:ok, result, count}, query}
|
data_result ->
|
||||||
{{:error, _, _} = error, query} -> {error, query}
|
{data_result, query}
|
||||||
{{:error, _} = error, query} -> {error, query}
|
|
||||||
other -> {other, query}
|
{{:error, _} = data_result, query} ->
|
||||||
|
{data_result, query}
|
||||||
|
|
||||||
|
data_result ->
|
||||||
|
{data_result, query}
|
||||||
end
|
end
|
||||||
|
|
||||||
with {:ok, data, count} <- data_result,
|
with {:ok, data, count, calculations_at_runtime, calculations_in_query, _query} <-
|
||||||
|
data_result,
|
||||||
data = add_tenant(data, query),
|
data = add_tenant(data, query),
|
||||||
{:ok, data} <-
|
{:ok, data} <-
|
||||||
load_through_attributes(
|
load_through_attributes(
|
||||||
|
@ -358,12 +363,13 @@ defmodule Ash.Actions.Read do
|
||||||
},
|
},
|
||||||
pre_authorization_query <- query,
|
pre_authorization_query <- query,
|
||||||
{:ok, query} <- authorize_query(query, opts) do
|
{:ok, query} <- authorize_query(query, opts) do
|
||||||
maybe_in_transaction(query, opts, fn ->
|
maybe_in_transaction(query, opts, fn notify_callback ->
|
||||||
with query_before_pagination <- query,
|
with query_before_pagination <- query,
|
||||||
query <-
|
{query, calculations_at_runtime, calculations_in_query} <-
|
||||||
Ash.Actions.Read.Calculations.deselect_known_forbidden_fields(
|
Ash.Actions.Read.Calculations.deselect_known_forbidden_fields(
|
||||||
query,
|
query,
|
||||||
calculations_at_runtime ++ calculations_in_query
|
calculations_at_runtime,
|
||||||
|
calculations_in_query
|
||||||
),
|
),
|
||||||
{:ok, data_layer_calculations} <- hydrate_calculations(query, calculations_in_query),
|
{:ok, data_layer_calculations} <- hydrate_calculations(query, calculations_in_query),
|
||||||
{:ok, query} <-
|
{:ok, query} <-
|
||||||
|
@ -480,10 +486,12 @@ defmodule Ash.Actions.Read do
|
||||||
{:ok, results} <- run_authorize_results(query, results),
|
{:ok, results} <- run_authorize_results(query, results),
|
||||||
{:ok, results, after_notifications} <- run_after_action(query, results),
|
{:ok, results, after_notifications} <- run_after_action(query, results),
|
||||||
{:ok, count} <- maybe_await(count) do
|
{:ok, count} <- maybe_await(count) do
|
||||||
{:ok, results, count, query, before_notifications ++ after_notifications}
|
notify_callback.(query, before_notifications ++ after_notifications)
|
||||||
|
{:ok, results, count, calculations_at_runtime, calculations_in_query, query}
|
||||||
else
|
else
|
||||||
{%{valid?: false} = query, before_notifications} ->
|
{%{valid?: false} = query, before_notifications} ->
|
||||||
{{:error, query, before_notifications}, query}
|
notify_callback.(before_notifications)
|
||||||
|
{{:error, query}, query}
|
||||||
|
|
||||||
{{:error, %Ash.Query{} = query}, query} ->
|
{{:error, %Ash.Query{} = query}, query} ->
|
||||||
{:error, query}
|
{:error, query}
|
||||||
|
@ -790,7 +798,9 @@ defmodule Ash.Actions.Read do
|
||||||
query.action.transaction? ->
|
query.action.transaction? ->
|
||||||
Ash.DataLayer.transaction(
|
Ash.DataLayer.transaction(
|
||||||
[query.resource | query.action.touches_resources],
|
[query.resource | query.action.touches_resources],
|
||||||
func,
|
fn ->
|
||||||
|
func.(¬ify_or_store(&1, &2, notify?))
|
||||||
|
end,
|
||||||
query.timeout,
|
query.timeout,
|
||||||
%{
|
%{
|
||||||
type: :read,
|
type: :read,
|
||||||
|
@ -802,36 +812,25 @@ defmodule Ash.Actions.Read do
|
||||||
data_layer_context: query.context[:data_layer]
|
data_layer_context: query.context[:data_layer]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|> case do
|
||||||
|
{:error, {:error, error}} -> {:error, error}
|
||||||
|
{:error, error} -> {:error, error}
|
||||||
|
{:ok, result} -> {:ok, result}
|
||||||
|
end
|
||||||
|
|
||||||
query.timeout ->
|
query.timeout ->
|
||||||
{:ok,
|
Ash.ProcessHelpers.task_with_timeout(
|
||||||
Ash.ProcessHelpers.task_with_timeout(
|
fn ->
|
||||||
func,
|
func.(¬ify_or_store(&1, &2, notify?))
|
||||||
query.resource,
|
end,
|
||||||
query.timeout,
|
query.resource,
|
||||||
"#{inspect(query.resource)}.#{query.action.name}",
|
query.timeout,
|
||||||
opts[:tracer]
|
"#{inspect(query.resource)}.#{query.action.name}",
|
||||||
)}
|
opts[:tracer]
|
||||||
|
)
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{:ok, func.()}
|
func.(¬ify_or_store(&1, &2, notify?))
|
||||||
end
|
|
||||||
|> case do
|
|
||||||
{:ok, {:ok, result, count, query_ran, notifications}} ->
|
|
||||||
notify_or_store(query, notifications, notify?)
|
|
||||||
|
|
||||||
{:ok, result, count, query_ran}
|
|
||||||
|
|
||||||
{:ok, {:error, error, notifications}} ->
|
|
||||||
notify_or_store(query, notifications, notify?)
|
|
||||||
|
|
||||||
{:error, error}
|
|
||||||
|
|
||||||
{:ok, value} ->
|
|
||||||
value
|
|
||||||
|
|
||||||
other ->
|
|
||||||
other
|
|
||||||
end
|
end
|
||||||
after
|
after
|
||||||
if notify? do
|
if notify? do
|
||||||
|
@ -879,22 +878,24 @@ defmodule Ash.Actions.Read do
|
||||||
) do
|
) do
|
||||||
must_be_reselected = List.wrap(query.select) -- Ash.Resource.Info.primary_key(query.resource)
|
must_be_reselected = List.wrap(query.select) -- Ash.Resource.Info.primary_key(query.resource)
|
||||||
|
|
||||||
query =
|
{query, calculations_at_runtime, calculations_in_query} =
|
||||||
Ash.Actions.Read.Calculations.deselect_known_forbidden_fields(
|
Ash.Actions.Read.Calculations.deselect_known_forbidden_fields(
|
||||||
query,
|
query,
|
||||||
calculations_at_runtime ++ calculations_in_query
|
calculations_at_runtime,
|
||||||
|
calculations_in_query
|
||||||
)
|
)
|
||||||
|
|
||||||
if missing_pkeys? ||
|
if missing_pkeys? ||
|
||||||
(Enum.empty?(must_be_reselected) && Enum.empty?(query.aggregates) &&
|
(Enum.empty?(must_be_reselected) && Enum.empty?(query.aggregates) &&
|
||||||
Enum.empty?(calculations_in_query)) do
|
Enum.empty?(calculations_in_query)) do
|
||||||
{:ok, initial_data, query}
|
{:ok, initial_data, 0, calculations_at_runtime, calculations_in_query, query}
|
||||||
else
|
else
|
||||||
reselect_and_load(
|
reselect_and_load(
|
||||||
initial_data,
|
initial_data,
|
||||||
query,
|
query,
|
||||||
must_be_reselected,
|
must_be_reselected,
|
||||||
calculations_in_query,
|
calculations_in_query,
|
||||||
|
calculations_at_runtime,
|
||||||
opts
|
opts
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -905,6 +906,7 @@ defmodule Ash.Actions.Read do
|
||||||
query,
|
query,
|
||||||
must_be_reselected,
|
must_be_reselected,
|
||||||
calculations_in_query,
|
calculations_in_query,
|
||||||
|
calculations_at_runtime,
|
||||||
opts
|
opts
|
||||||
) do
|
) do
|
||||||
primary_key = Ash.Resource.Info.primary_key(query.resource)
|
primary_key = Ash.Resource.Info.primary_key(query.resource)
|
||||||
|
@ -1004,7 +1006,7 @@ defmodule Ash.Actions.Read do
|
||||||
|> compute_expression_at_runtime_for_missing_records(query, data_layer_calculations)
|
|> compute_expression_at_runtime_for_missing_records(query, data_layer_calculations)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, result} ->
|
{:ok, result} ->
|
||||||
{:ok, result, 0, query}
|
{:ok, result, 0, calculations_at_runtime, calculations_in_query, query}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:error, error}
|
{:error, error}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
defmodule Ash.Test.Policy.FieldPolicyTest do
|
defmodule Ash.Test.Policy.FieldPolicyPruningTest do
|
||||||
@doc false
|
@doc false
|
||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
|
|
||||||
defmodule App.Core.TestResource do
|
defmodule TestResource do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: App.Core,
|
domain: Ash.Test.Domain,
|
||||||
|
data_layer: Ash.DataLayer.Ets,
|
||||||
authorizers: [Ash.Policy.Authorizer]
|
authorizers: [Ash.Policy.Authorizer]
|
||||||
|
|
||||||
|
ets do
|
||||||
|
private? true
|
||||||
|
end
|
||||||
|
|
||||||
policies do
|
policies do
|
||||||
policy always() do
|
policy always() do
|
||||||
authorize_if always()
|
authorize_if always()
|
||||||
|
@ -18,7 +23,11 @@ defmodule Ash.Test.Policy.FieldPolicyTest do
|
||||||
authorize_if always()
|
authorize_if always()
|
||||||
end
|
end
|
||||||
|
|
||||||
field_policy :graphs do
|
field_policy :calc do
|
||||||
|
forbid_if always()
|
||||||
|
end
|
||||||
|
|
||||||
|
field_policy :calc2 do
|
||||||
forbid_if always()
|
forbid_if always()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -28,13 +37,21 @@ defmodule Ash.Test.Policy.FieldPolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
read :read do
|
defaults [:create, :read, :update, :destroy]
|
||||||
primary? true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
calculations do
|
calculations do
|
||||||
calculate :graphs, :map do
|
calculate :calc2, :map do
|
||||||
|
public? true
|
||||||
|
|
||||||
|
calculation fn records, _ ->
|
||||||
|
Enum.map(records, & &1.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
calculate :calc, :map do
|
||||||
|
load :calc2
|
||||||
|
|
||||||
calculation fn records, _ ->
|
calculation fn records, _ ->
|
||||||
raise "shouldn't get here!"
|
raise "shouldn't get here!"
|
||||||
end
|
end
|
||||||
|
@ -45,5 +62,10 @@ defmodule Ash.Test.Policy.FieldPolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "field policies prune unnecessary calculations" do
|
test "field policies prune unnecessary calculations" do
|
||||||
|
Ash.create!(TestResource, %{}, authorize?: false)
|
||||||
|
|
||||||
|
TestResource
|
||||||
|
|> Ash.Query.load([:calc, :calc2])
|
||||||
|
|> Ash.read!()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue