fix: various filter & expression fixes

This commit is contained in:
Zach Daniel 2023-01-05 18:18:59 -05:00
parent 936dac8627
commit a234f0b6bf
11 changed files with 121 additions and 70 deletions

View file

@ -274,7 +274,7 @@ defmodule Ash.Actions.Sort do
public?: false
}) do
{:ok, expression} ->
case Ash.Expr.eval(expression, record: record) do
case Ash.Expr.eval_hydrated(expression, record: record) do
{:ok, value} ->
{:ok, value}

View file

@ -355,7 +355,7 @@ defmodule Ash.DataLayer.Ets do
public?: false
}) do
{:ok, expression} ->
case Ash.Expr.eval(expression, record: record) do
case Ash.Expr.eval_hydrated(expression, record: record) do
{:ok, value} ->
if calculation.load do
{:cont, {:ok, Map.put(record, calculation.load, value)}}

View file

@ -5,7 +5,36 @@ defmodule Ash.Expr do
@type t :: any
@pass_through_funcs [:where, :or_where, :expr, :@]
@doc """
Evaluate an expression. See `eval/2` for more.
"""
def eval!(expression, opts \\ []) do
case eval(expression, opts) do
{:ok, result} ->
result
{:error, error} ->
raise Ash.Error.to_ash_error(error)
end
end
@doc """
Evaluate an expression. This function only works if you have no references, or if you provide the `record` option.
"""
def eval(expression, opts \\ []) do
expression
|> Ash.Filter.hydrate_refs(%{})
|> case do
{:ok, hydrated} ->
eval_hydrated(hydrated, opts)
{:error, error} ->
{:error, error}
end
end
@doc false
def eval_hydrated(expression, opts \\ []) do
Ash.Filter.Runtime.do_match(opts[:record], expression, opts[:parent])
end

View file

@ -890,7 +890,7 @@ defmodule Ash.Filter do
|> Enum.flat_map(fn calculation ->
expression = calculation.module.expression(calculation.opts, calculation.context)
case Ash.Filter.hydrate_refs(expression, %{
case hydrate_refs(expression, %{
resource: resource,
aggregates: aggregates,
calculations: calculations,
@ -1979,33 +1979,43 @@ defmodule Ash.Filter do
end
end
defp attribute(%{public?: true, resource: resource}, attribute),
defp attribute(%{public?: true, resource: resource}, attribute) when not is_nil(resource),
do: Ash.Resource.Info.public_attribute(resource, attribute)
defp attribute(%{public?: false, resource: resource}, attribute) do
defp attribute(%{public?: false, resource: resource}, attribute) when not is_nil(resource) do
Ash.Resource.Info.attribute(resource, attribute)
end
defp aggregate(%{public?: true, resource: resource}, aggregate),
defp attribute(_, _), do: nil
defp aggregate(%{public?: true, resource: resource}, aggregate) when not is_nil(resource),
do: Ash.Resource.Info.public_aggregate(resource, aggregate)
defp aggregate(%{public?: false, resource: resource}, aggregate),
defp aggregate(%{public?: false, resource: resource}, aggregate) when not is_nil(resource),
do: Ash.Resource.Info.aggregate(resource, aggregate)
defp calculation(%{public?: true, resource: resource}, calculation),
defp aggregate(_, _), do: nil
defp calculation(%{public?: true, resource: resource}, calculation) when not is_nil(resource),
do: Ash.Resource.Info.public_calculation(resource, calculation)
defp calculation(%{public?: false, resource: resource}, calculation),
defp calculation(%{public?: false, resource: resource}, calculation) when not is_nil(resource),
do: Ash.Resource.Info.calculation(resource, calculation)
defp relationship(%{public?: true, resource: resource}, relationship) do
defp calculation(_, _), do: nil
defp relationship(%{public?: true, resource: resource}, relationship)
when not is_nil(resource) do
Ash.Resource.Info.public_relationship(resource, relationship)
end
defp relationship(%{public?: false, resource: resource}, relationship) do
defp relationship(%{public?: false, resource: resource}, relationship)
when not is_nil(resource) do
Ash.Resource.Info.relationship(resource, relationship)
end
defp relationship(_, _), do: nil
defp related(context, relationship) when not is_list(relationship) do
related(context, [relationship])
end
@ -2262,10 +2272,17 @@ defmodule Ash.Filter do
if is_boolean(function) do
{:ok, BooleanExpression.optimized_new(:and, expression, function)}
else
if Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, function}) do
if context.resource &&
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, function}) do
{:ok, BooleanExpression.optimized_new(:and, expression, function)}
else
{:error, "data layer does not support the function #{inspect(function)}"}
case function_module.evaluate(function) do
{:known, result} ->
{:ok, result}
_ ->
{:error, "data layer does not support the function #{inspect(function)}"}
end
end
end
end
@ -2725,10 +2742,17 @@ defmodule Ash.Filter do
if is_boolean(function) do
{:ok, function}
else
if Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, function}) do
if context.resource &&
Ash.DataLayer.data_layer_can?(context.resource, {:filter_expr, function}) do
{:ok, function}
else
{:error, "data layer does not support the function #{inspect(function)}"}
case function_module.evaluate(function) do
{:known, result} ->
{:ok, result}
_ ->
{:error, "data layer does not support the function #{inspect(function)}"}
end
end
end
else
@ -2767,10 +2791,18 @@ defmodule Ash.Filter do
end
end
def hydrate_refs({key, value}, context) when is_atom(key) do
context = Map.put_new(context, :root_resource, context[:resource])
def hydrate_refs(value, context) do
context =
context
|> Map.put_new(:resource, nil)
|> Map.put_new(:root_resource, context[:resource])
|> Map.put_new(:public?, false)
case hydrate_refs(value, context) do
do_hydrate_refs(value, context)
end
def do_hydrate_refs({key, value}, context) when is_atom(key) do
case do_hydrate_refs(value, context) do
{:ok, hydrated} ->
{:ok, {key, hydrated}}
@ -2779,13 +2811,11 @@ defmodule Ash.Filter do
end
end
def hydrate_refs(
def do_hydrate_refs(
%Ref{attribute: attribute} = ref,
%{aggregates: aggregates, calculations: calculations} = context
)
when is_atom(attribute) do
context = Map.put_new(context, :root_resource, context[:resource])
case related(context, ref.relationship_path) do
nil ->
{:error,
@ -2876,16 +2906,13 @@ defmodule Ash.Filter do
end
end
def hydrate_refs(%Ref{relationship_path: relationship_path, resource: nil} = ref, context) do
context = Map.put_new(context, :root_resource, context[:resource])
def do_hydrate_refs(%Ref{relationship_path: relationship_path, resource: nil} = ref, context) do
{:ok, %{ref | resource: Ash.Resource.Info.related(context.resource, relationship_path)}}
end
def hydrate_refs(%BooleanExpression{left: left, right: right} = expr, context) do
context = Map.put_new(context, :root_resource, context[:resource])
with {:ok, left} <- hydrate_refs(left, context),
{:ok, right} <- hydrate_refs(right, context) do
def do_hydrate_refs(%BooleanExpression{left: left, right: right} = expr, context) do
with {:ok, left} <- do_hydrate_refs(left, context),
{:ok, right} <- do_hydrate_refs(right, context) do
{:ok, %{expr | left: left, right: right}}
else
other ->
@ -2893,24 +2920,19 @@ defmodule Ash.Filter do
end
end
def hydrate_refs(%Not{expression: expression} = expr, context) do
context = Map.put_new(context, :root_resource, context[:resource])
with {:ok, expression} <- hydrate_refs(expression, context) do
def do_hydrate_refs(%Not{expression: expression} = expr, context) do
with {:ok, expression} <- do_hydrate_refs(expression, context) do
{:ok, %{expr | expression: expression}}
end
end
def hydrate_refs(%Call{} = call, context) do
context = Map.put_new(context, :root_resource, context[:resource])
def do_hydrate_refs(%Call{} = call, context) do
resolve_call(call, context)
end
def hydrate_refs(%{__predicate__?: _, left: left, right: right} = expr, context) do
context = Map.put_new(context, :root_resource, context[:resource])
with {:ok, left} <- hydrate_refs(left, context),
{:ok, right} <- hydrate_refs(right, context) do
def do_hydrate_refs(%{__predicate__?: _, left: left, right: right} = expr, context) do
with {:ok, left} <- do_hydrate_refs(left, context),
{:ok, right} <- do_hydrate_refs(right, context) do
{:ok, %{expr | left: left, right: right}}
else
other ->
@ -2918,10 +2940,8 @@ defmodule Ash.Filter do
end
end
def hydrate_refs(%{__predicate__?: _, arguments: arguments} = expr, context) do
context = Map.put_new(context, :root_resource, context[:resource])
case hydrate_refs(arguments, context) do
def do_hydrate_refs(%{__predicate__?: _, arguments: arguments} = expr, context) do
case do_hydrate_refs(arguments, context) do
{:ok, args} ->
{:ok, %{expr | arguments: args}}
@ -2930,7 +2950,7 @@ defmodule Ash.Filter do
end
end
def hydrate_refs(%Ash.Query.Parent{expr: expr} = this, context) do
def do_hydrate_refs(%Ash.Query.Parent{expr: expr} = this, context) do
context =
%{
context
@ -2940,7 +2960,7 @@ defmodule Ash.Filter do
}
|> Map.put(:relationship_path, [])
case hydrate_refs(expr, context) do
case do_hydrate_refs(expr, context) do
{:ok, expr} ->
{:ok, %{this | expr: expr}}
@ -2949,7 +2969,10 @@ defmodule Ash.Filter do
end
end
def hydrate_refs(%Ash.Query.Exists{expr: expr, at_path: at_path, path: path} = exists, context) do
def do_hydrate_refs(
%Ash.Query.Exists{expr: expr, at_path: at_path, path: path} = exists,
context
) do
new_resource = Ash.Resource.Info.related(context[:resource], at_path ++ path)
context = %{
@ -2963,7 +2986,7 @@ defmodule Ash.Filter do
data_layer: Ash.DataLayer.data_layer(new_resource)
}
case hydrate_refs(expr, context) do
case do_hydrate_refs(expr, context) do
{:ok, expr} ->
{:ok, %{exists | expr: expr}}
@ -2972,12 +2995,10 @@ defmodule Ash.Filter do
end
end
def hydrate_refs(list, context) when is_list(list) do
context = Map.put_new(context, :root_resource, context[:resource])
def do_hydrate_refs(list, context) when is_list(list) do
list
|> Enum.reduce_while({:ok, []}, fn val, {:ok, acc} ->
case hydrate_refs(val, context) do
case do_hydrate_refs(val, context) do
{:ok, value} ->
{:cont, {:ok, [value | acc]}}
@ -2991,7 +3012,7 @@ defmodule Ash.Filter do
end
end
def hydrate_refs(val, _context) do
def do_hydrate_refs(val, _context) do
{:ok, val}
end

View file

@ -461,8 +461,9 @@ defmodule Ash.Filter.Runtime do
defp resolve_ref(%Ash.Query.Ref{attribute: attribute}, nil, _),
do: :unknown |> or_default(attribute)
defp resolve_ref(_, nil, _),
do: :unknown
defp resolve_ref(_ref, nil, _) do
:unknown
end
defp resolve_ref(
%Ash.Query.Ref{

View file

@ -320,7 +320,7 @@ defmodule Ash.Flow do
public?: false
}) do
{:ok, hydrated} ->
case Ash.Expr.eval(hydrated) do
case Ash.Expr.eval_hydrated(hydrated) do
{:ok, result} ->
result

View file

@ -89,7 +89,7 @@ defmodule Ash.Policy.FilterCheck do
public?: false
}) do
{:ok, hydrated} ->
Ash.Expr.eval(hydrated)
Ash.Expr.eval_hydrated(hydrated)
{:error, error} ->
{:halt, {:error, error}}
@ -107,7 +107,7 @@ defmodule Ash.Policy.FilterCheck do
public?: false
}) do
{:ok, hydrated} ->
Ash.Expr.eval(hydrated)
Ash.Expr.eval_hydrated(hydrated)
{:error, error} ->
{:error, error}
@ -135,7 +135,7 @@ defmodule Ash.Policy.FilterCheck do
nil
end
Ash.Expr.eval(hydrated, record: data)
Ash.Expr.eval_hydrated(hydrated, record: data)
{:error, error} ->
{:halt, {:error, error}}
@ -150,7 +150,7 @@ defmodule Ash.Policy.FilterCheck do
public?: false
}) do
{:ok, hydrated} ->
Ash.Expr.eval(hydrated)
Ash.Expr.eval_hydrated(hydrated)
{:error, error} ->
{:halt, {:error, error}}

View file

@ -73,7 +73,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
public?: false
}) do
{:ok, hydrated} ->
Ash.Expr.eval(hydrated)
Ash.Expr.eval_hydrated(hydrated)
{:error, error} ->
{:error, error}
@ -91,7 +91,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
public?: false
}) do
{:ok, hydrated} ->
Ash.Expr.eval(hydrated)
Ash.Expr.eval_hydrated(hydrated)
{:error, error} ->
{:error, error}
@ -119,7 +119,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
nil
end
Ash.Expr.eval(hydrated, record: data)
Ash.Expr.eval_hydrated(hydrated, record: data)
{:error, error} ->
{:error, error}
@ -134,7 +134,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
public?: false
}) do
{:ok, hydrated} ->
Ash.Expr.eval(hydrated)
Ash.Expr.eval_hydrated(hydrated)
{:error, error} ->
{:error, error}

View file

@ -112,10 +112,10 @@ defmodule Ash.Query.Operator.Basic do
defp do_evaluate(op, left, right) do
if Decimal.is_decimal(left) || Decimal.is_decimal(right) do
case op do
:+ -> Decimal.add(to_decimal(left), to_decimal(right))
:* -> Decimal.mult(to_decimal(left), to_decimal(right))
:- -> Decimal.sub(to_decimal(left), to_decimal(right))
:/ -> Decimal.div(to_decimal(left), to_decimal(right))
:+ -> {:known, Decimal.add(to_decimal(left), to_decimal(right))}
:* -> {:known, Decimal.mult(to_decimal(left), to_decimal(right))}
:- -> {:known, Decimal.sub(to_decimal(left), to_decimal(right))}
:/ -> {:known, Decimal.div(to_decimal(left), to_decimal(right))}
end
else
{:known, apply(Kernel, unquote(opts[:symbol]), [left, right])}

View file

@ -2007,6 +2007,7 @@ defmodule Ash.Query do
),
{:ok, query} <-
add_tenant(query, ash_query),
{:ok, query} <- Ash.DataLayer.select(query, ash_query.select, ash_query.resource),
{:ok, query} <-
add_aggregates(query, ash_query, aggregates),
{:ok, query} <-
@ -2016,8 +2017,7 @@ defmodule Ash.Query do
{:ok, query} <-
Ash.DataLayer.limit(query, ash_query.limit, resource),
{:ok, query} <-
Ash.DataLayer.offset(query, ash_query.offset, resource),
{:ok, query} <- Ash.DataLayer.select(query, ash_query.select, ash_query.resource) do
Ash.DataLayer.offset(query, ash_query.offset, resource) do
if opts[:no_modify?] || !ash_query.action || !ash_query.action.modify_query do
{:ok, query}
else

View file

@ -23,7 +23,7 @@ defmodule Ash.Resource.Calculation.Expression do
public?: false
}) do
{:ok, expression} ->
case Ash.Expr.eval(expression, record: record) do
case Ash.Expr.eval_hydrated(expression, record: record) do
{:ok, value} ->
{:cont, {:ok, [value | values]}}