improvement: support partial eager evaluation of functions

This commit is contained in:
Zach Daniel 2023-06-27 19:11:03 -04:00
parent 5635c311fe
commit 5d20af82c3
6 changed files with 72 additions and 18 deletions

View file

@ -37,6 +37,7 @@ spark_locals_without_parens = [
broadcast_type: 1,
bypass: 1,
bypass: 2,
calculate: 2,
calculate: 3,
calculate: 4,
calculation: 1,

View file

@ -661,7 +661,16 @@ defmodule Ash.Engine.Request do
Map.take(request, keys)
) do
{:ok, new_filter} ->
{:ok, %{request | query: %{request.query | filter: new_filter}}}
case Ash.Filter.hydrate_refs(new_filter, %{
resource: request.query.resource,
public?: false
}) do
{:ok, result} ->
{:ok, %{request | query: %{request.query | filter: result}}}
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}

View file

@ -2325,21 +2325,11 @@ defmodule Ash.Filter do
function_module,
args
) do
if is_boolean(function) do
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, function}) do
{:ok, BooleanExpression.optimized_new(:and, expression, function)}
else
if is_nil(context.resource) ||
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, function}) do
{:ok, BooleanExpression.optimized_new(:and, expression, function)}
else
case function_module.evaluate(function) do
{:known, result} ->
{:ok, result}
_ ->
{:error, "data layer does not support the function #{inspect(function)}"}
end
end
{:error, "data layer does not support the function #{inspect(function)}"}
end
end
end
@ -3066,6 +3056,16 @@ defmodule Ash.Filter do
end
end
def do_hydrate_refs(%__MODULE__{expression: expression} = filter, context) do
case do_hydrate_refs(expression, context) do
{:ok, expression} ->
{:ok, %{filter | expression: expression}}
{:error, error} ->
{:error, error}
end
end
def do_hydrate_refs(list, context) when is_list(list) do
list
|> Enum.reduce_while({:ok, []}, fn val, {:ok, acc} ->

View file

@ -15,8 +15,11 @@ defmodule Ash.Query.Function do
@callback args() :: [arg]
@callback new(list(term)) :: {:ok, term} | {:error, String.t() | Exception.t()}
@callback evaluate(func :: map) :: :unknown | {:known, term}
@callback partial_evaluate(func) :: func when func: map
@callback private?() :: boolean
@optional_callbacks partial_evaluate: 1
def new(mod, args) do
args = List.wrap(args)
@ -45,10 +48,14 @@ defmodule Ash.Query.Function do
case mod.new(casted) do
{:ok, function} ->
if Enum.any?(casted, &expr?/1) do
{:ok, function}
if function_exported?(mod, :partial_evaluate, 1) && match?(%^mod{}, function) do
{:ok, mod.partial_evaluate(function)}
else
{:ok, function}
end
else
case function do
%mod{__predicate__?: _} ->
%^mod{__predicate__?: _} ->
if mod.eager_evaluate?() do
case mod.evaluate(function) do
{:known, result} ->

View file

@ -3,6 +3,7 @@ defmodule Ash.Query.Function.If do
If predicate is truthy, then the second argument is returned, otherwise the third.
"""
use Ash.Query.Function, name: :if
import Ash.Filter.TemplateHelpers, only: [expr?: 1]
def args, do: [[:boolean, :any], [:boolean, :any, :any]]
@ -38,4 +39,22 @@ defmodule Ash.Query.Function.If do
do: {:known, when_false}
def evaluate(%{arguments: [_, when_true, _]}), do: {:known, when_true}
def partial_evaluate(%{arguments: [false, _, when_false]}),
do: when_false
def partial_evaluate(%{arguments: [nil, _, when_false]}),
do: when_false
def partial_evaluate(%{arguments: [condition, when_true, _]} = fun) do
if expr?(condition) do
fun
else
when_true
end
end
def partial_evaluate(other) do
raise inspect(other)
end
end

View file

@ -2146,7 +2146,16 @@ defmodule Ash.Query do
case new_filter do
{:ok, filter} ->
%{query | filter: filter}
case Ash.Filter.hydrate_refs(filter, %{
resource: query.resource,
public?: false
}) do
{:ok, result} ->
%{query | filter: result}
{:error, error} ->
add_error(query, :filter, error)
end
{:error, error} ->
add_error(query, :filter, error)
@ -2183,7 +2192,16 @@ defmodule Ash.Query do
case filter do
{:ok, filter} ->
Map.put(query, :filter, filter)
case Ash.Filter.hydrate_refs(filter, %{
resource: query.resource,
public?: false
}) do
{:ok, result} ->
%{query | filter: result}
{:error, error} ->
add_error(query, :filter, error)
end
{:error, error} ->
add_error(query, :filter, error)