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 public?: false
}) do }) do
{:ok, expression} -> {:ok, expression} ->
case Ash.Expr.eval(expression, record: record) do case Ash.Expr.eval_hydrated(expression, record: record) do
{:ok, value} -> {:ok, value} ->
{:ok, value} {:ok, value}

View file

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

View file

@ -5,7 +5,36 @@ defmodule Ash.Expr do
@type t :: any @type t :: any
@pass_through_funcs [:where, :or_where, :expr, :@] @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 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]) Ash.Filter.Runtime.do_match(opts[:record], expression, opts[:parent])
end end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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