mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
fix: various runtime expression evaluation fixes
the issue is that the expression parser didn't support the fact that some operators accept `nil` values and other operators do not.
This commit is contained in:
parent
0ae14bdf71
commit
08a72acc6b
23 changed files with 476 additions and 336 deletions
|
@ -1119,7 +1119,8 @@ defmodule Ash.Actions.Read do
|
|||
{:ok, result} <-
|
||||
Ash.Expr.eval(
|
||||
expression,
|
||||
resource: ash_query.resource
|
||||
resource: ash_query.resource,
|
||||
unknown_on_unknown_refs?: true
|
||||
) do
|
||||
{in_query,
|
||||
[
|
||||
|
@ -2374,7 +2375,8 @@ defmodule Ash.Actions.Read do
|
|||
}
|
||||
end
|
||||
|
||||
defp add_calc_context(calc, actor, authorize?, tenant, tracer) do
|
||||
@doc false
|
||||
def add_calc_context(calc, actor, authorize?, tenant, tracer) do
|
||||
%{
|
||||
calc
|
||||
| context:
|
||||
|
@ -2390,6 +2392,21 @@ defmodule Ash.Actions.Read do
|
|||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_calc_context(calc, map) do
|
||||
%{
|
||||
calc
|
||||
| context:
|
||||
Map.merge(
|
||||
Map.take(
|
||||
map,
|
||||
[:actor, :authorize?, :tenant, :tracer]
|
||||
),
|
||||
calc.context
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
# TODO: Make more generic?
|
||||
defp query_unique_for_calc(query) do
|
||||
query
|
||||
|
|
|
@ -94,6 +94,7 @@ defmodule Ash.DataLayer do
|
|||
| :offset
|
||||
| :transact
|
||||
| :filter
|
||||
| :composite_type
|
||||
| {:lock, lock_type()}
|
||||
| {:filter_expr, struct}
|
||||
| {:filter_relationship, Ash.Resource.Relationships.relationship()}
|
||||
|
|
|
@ -358,6 +358,15 @@ defmodule Ash.DataLayer.Ets do
|
|||
_resource,
|
||||
parent \\ nil
|
||||
) do
|
||||
used_aggregates =
|
||||
calculations
|
||||
|> List.wrap()
|
||||
|> Enum.flat_map(fn {calc, expr} ->
|
||||
expr
|
||||
|> Ash.Filter.used_aggregates(:all)
|
||||
|> Enum.map(&Ash.Actions.Read.add_calc_context(&1, calc.context))
|
||||
end)
|
||||
|
||||
with {:ok, records} <- get_records(resource, tenant),
|
||||
{:ok, records} <-
|
||||
filter_matches(records, filter, api, parent),
|
||||
|
@ -366,7 +375,8 @@ defmodule Ash.DataLayer.Ets do
|
|||
records <- Sort.runtime_sort(records, sort, api: api),
|
||||
records <- Enum.drop(records, offset || []),
|
||||
records <- do_limit(records, limit),
|
||||
{:ok, records} <- do_add_aggregates(records, api, resource, aggregates),
|
||||
{:ok, records} <-
|
||||
do_add_aggregates(records, api, resource, aggregates ++ used_aggregates),
|
||||
{:ok, records} <-
|
||||
do_add_calculations(records, resource, calculations, api) do
|
||||
{:ok, records}
|
||||
|
@ -596,61 +606,6 @@ defmodule Ash.DataLayer.Ets do
|
|||
end
|
||||
end
|
||||
|
||||
# def do_add_calculations(records, _resource, [], _api), do: {:ok, records}
|
||||
|
||||
# def do_add_calculations(records, resource, calculations, api) do
|
||||
# Enum.reduce_while(records, {:ok, []}, fn record, {:ok, records} ->
|
||||
# calculations
|
||||
# |> IO.inspect()
|
||||
# |> Enum.reduce_while({:ok, record}, fn {calculation, expression}, {:ok, record} ->
|
||||
# case Ash.Expr.eval_hydrated(expression, record: record, resource: resource, api: api) do
|
||||
# {:ok, value} ->
|
||||
# if calculation.load do
|
||||
# {:cont, {:ok, Map.put(record, calculation.load, value)}}
|
||||
# else
|
||||
# {:cont,
|
||||
# {:ok,
|
||||
# Map.update!(
|
||||
# record,
|
||||
# :calculations,
|
||||
# &Map.put(&1, calculation.name, value)
|
||||
# )}}
|
||||
# end
|
||||
|
||||
# :unknown ->
|
||||
# if calculation.load do
|
||||
# {:cont, {:ok, Map.put(record, calculation.load, nil)}}
|
||||
# else
|
||||
# {:cont,
|
||||
# {:ok,
|
||||
# Map.update!(
|
||||
# record,
|
||||
# :calculations,
|
||||
# &Map.put(&1, calculation.name, nil)
|
||||
# )}}
|
||||
# end
|
||||
|
||||
# {:error, error} ->
|
||||
# {:halt, {:error, error}}
|
||||
# end
|
||||
# end)
|
||||
# |> case do
|
||||
# {:ok, record} ->
|
||||
# {:cont, {:ok, [record | records]}}
|
||||
|
||||
# {:error, error} ->
|
||||
# {:halt, {:error, error}}
|
||||
# end
|
||||
# end)
|
||||
# |> case do
|
||||
# {:ok, records} ->
|
||||
# {:ok, Enum.reverse(records)}
|
||||
|
||||
# {:error, error} ->
|
||||
# {:error, Ash.Error.to_ash_error(error)}
|
||||
# end
|
||||
# end
|
||||
|
||||
@doc false
|
||||
def do_add_aggregates(records, _api, _resource, []), do: {:ok, records}
|
||||
|
||||
|
@ -689,7 +644,7 @@ defmodule Ash.DataLayer.Ets do
|
|||
field = field || Enum.at(Ash.Resource.Info.primary_key(query.resource), 0)
|
||||
|
||||
value =
|
||||
aggregate_value(sorted, kind, field, uniq?, default_value)
|
||||
aggregate_value(sorted, kind, field, uniq?, default_value) |> IO.inspect()
|
||||
|
||||
if load do
|
||||
{:cont, {:ok, Map.put(record, load, value)}}
|
||||
|
|
|
@ -231,6 +231,15 @@ defmodule Ash.DataLayer.Mnesia do
|
|||
},
|
||||
_resource
|
||||
) do
|
||||
used_aggregates =
|
||||
calculations
|
||||
|> List.wrap()
|
||||
|> Enum.flat_map(fn {calc, expr} ->
|
||||
expr
|
||||
|> Ash.Filter.used_aggregates(:all)
|
||||
|> Enum.map(&Ash.Actions.Read.add_calc_context(&1, calc.context))
|
||||
end)
|
||||
|
||||
with {:atomic, records} <-
|
||||
Mnesia.transaction(fn ->
|
||||
Mnesia.select(table(resource), [{:_, [], [:"$_"]}])
|
||||
|
@ -242,9 +251,19 @@ defmodule Ash.DataLayer.Mnesia do
|
|||
filtered |> Sort.runtime_sort(sort, api: api) |> Enum.drop(offset || 0),
|
||||
limited_records <- do_limit(offset_records, limit),
|
||||
{:ok, records} <-
|
||||
Ash.DataLayer.Ets.do_add_aggregates(limited_records, api, resource, aggregates),
|
||||
Ash.DataLayer.Ets.do_add_aggregates(
|
||||
limited_records,
|
||||
api,
|
||||
resource,
|
||||
aggregates ++ used_aggregates
|
||||
),
|
||||
{:ok, records} <-
|
||||
Ash.DataLayer.Ets.do_add_calculations(records, resource, calculations, api) do
|
||||
Ash.DataLayer.Ets.do_add_calculations(
|
||||
records,
|
||||
resource,
|
||||
calculations,
|
||||
api
|
||||
) do
|
||||
{:ok, records}
|
||||
else
|
||||
{:error, error} ->
|
||||
|
|
|
@ -45,7 +45,8 @@ defmodule Ash.Expr do
|
|||
expression,
|
||||
opts[:parent],
|
||||
opts[:resource],
|
||||
opts[:api]
|
||||
opts[:api],
|
||||
opts[:unknown_on_unknown_refs?]
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -197,7 +197,6 @@ defmodule Ash.Filter do
|
|||
def builtins, do: @builtins
|
||||
def builtin_functions, do: @functions
|
||||
def builtin_operators, do: @operators
|
||||
def builtin_predicate_operators, do: Enum.filter(@operators, & &1.predicate?())
|
||||
|
||||
defmodule Simple do
|
||||
@moduledoc "Represents a simplified filter, with a simple list of predicates"
|
||||
|
|
|
@ -13,7 +13,7 @@ defmodule Ash.Filter.Runtime do
|
|||
layer like `ash_postgres`, certain expressions will behave unpredictably.
|
||||
"""
|
||||
|
||||
alias Ash.Query.{BooleanExpression, Call, Not, Ref}
|
||||
alias Ash.Query.{BooleanExpression, Not, Ref}
|
||||
|
||||
@doc """
|
||||
Removes any records that don't match the filter. Automatically loads
|
||||
|
@ -218,7 +218,14 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
|
||||
@doc false
|
||||
def load_and_eval(record, expression, parent \\ nil, resource \\ nil, api \\ nil) do
|
||||
def load_and_eval(
|
||||
record,
|
||||
expression,
|
||||
parent \\ nil,
|
||||
resource \\ nil,
|
||||
api \\ nil,
|
||||
unknown_on_unknown_refs? \\ false
|
||||
) do
|
||||
if api && record do
|
||||
{refs_to_load, refs} =
|
||||
expression
|
||||
|
@ -261,7 +268,7 @@ defmodule Ash.Filter.Runtime do
|
|||
|> Enum.map(&path_to_load(resource, &1, refs))
|
||||
|> case do
|
||||
[] ->
|
||||
do_match(record, expression, parent, resource)
|
||||
do_match(record, expression, parent, resource, unknown_on_unknown_refs?)
|
||||
|
||||
need_to_load ->
|
||||
query =
|
||||
|
@ -271,28 +278,40 @@ defmodule Ash.Filter.Runtime do
|
|||
|
||||
case api.load(record, query) do
|
||||
{:ok, loaded} ->
|
||||
do_match(loaded, expression, parent, resource)
|
||||
do_match(loaded, expression, parent, resource, unknown_on_unknown_refs?)
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
else
|
||||
do_match(record, expression, parent, resource)
|
||||
do_match(record, expression, parent, resource, unknown_on_unknown_refs?)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def do_match(record, expression, parent \\ nil, resource \\ nil)
|
||||
def do_match(
|
||||
record,
|
||||
expression,
|
||||
parent \\ nil,
|
||||
resource \\ nil,
|
||||
unknown_on_unknown_refs? \\ false
|
||||
)
|
||||
|
||||
def do_match(record, %Ash.Filter.Simple{predicates: predicates}, parent, resource) do
|
||||
def do_match(
|
||||
record,
|
||||
%Ash.Filter.Simple{predicates: predicates},
|
||||
parent,
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
{:ok,
|
||||
Enum.all?(predicates, fn predicate ->
|
||||
do_match(record, predicate, parent, resource) == {:ok, true}
|
||||
do_match(record, predicate, parent, resource, unknown_on_unknown_refs?) == {:ok, true}
|
||||
end)}
|
||||
end
|
||||
|
||||
def do_match(record, expression, parent, resource) do
|
||||
def do_match(record, expression, parent, resource, unknown_on_unknown_refs?) do
|
||||
hydrated =
|
||||
case record do
|
||||
%resource{} ->
|
||||
|
@ -314,97 +333,21 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
case hydrated do
|
||||
{:ok, expression} ->
|
||||
case expression do
|
||||
%Ash.Filter{expression: expression} ->
|
||||
do_match(record, expression, parent, resource)
|
||||
|
||||
%op{__operator__?: true, left: left, right: right} ->
|
||||
with {:ok, [left, right]} <-
|
||||
resolve_exprs([left, right], record, parent, resource),
|
||||
{:op, {:ok, %op{} = new_operator}} <-
|
||||
{:op, Ash.Query.Operator.try_cast_with_ref(op, left, right)},
|
||||
{:known, val} <-
|
||||
op.evaluate(new_operator) do
|
||||
{:ok, val}
|
||||
else
|
||||
{:op, {:error, error}} ->
|
||||
{:error, error}
|
||||
|
||||
{:op, {:ok, expr}} ->
|
||||
do_match(record, expr, parent, resource)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
:unknown ->
|
||||
:unknown
|
||||
|
||||
_value ->
|
||||
:unknown
|
||||
end
|
||||
|
||||
%func{__function__?: true, arguments: arguments} = function ->
|
||||
with {:ok, args} <- resolve_exprs(arguments, record, parent, resource),
|
||||
{:args, args} when not is_nil(args) <-
|
||||
{:args, try_cast_arguments(func.args(), args)},
|
||||
{:known, val} <- func.evaluate(%{function | arguments: args}) do
|
||||
{:ok, val}
|
||||
else
|
||||
{:args, nil} ->
|
||||
{:error,
|
||||
"Could not cast function arguments for #{func.name()}/#{Enum.count(arguments)}"}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
:unknown ->
|
||||
:unknown
|
||||
|
||||
_ ->
|
||||
:unknown
|
||||
end
|
||||
|
||||
%Not{expression: nil} ->
|
||||
with {:ok, hydrated} <- hydrated do
|
||||
case resolve_expr(hydrated, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
:unknown ->
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
%Not{expression: expression} ->
|
||||
case do_match(record, expression, parent, resource) do
|
||||
:unknown ->
|
||||
:unknown
|
||||
{:ok, value} ->
|
||||
{:ok, value}
|
||||
|
||||
{:ok, nil} ->
|
||||
{:ok, nil}
|
||||
|
||||
{:ok, match?} ->
|
||||
{:ok, !match?}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
|
||||
%Ash.Query.Exists{} = expr ->
|
||||
resolve_expr(expr, record, parent, resource)
|
||||
|
||||
%Ash.Query.Parent{} = expr ->
|
||||
resolve_expr(expr, parent, nil, resource)
|
||||
|
||||
%BooleanExpression{op: op, left: left, right: right} ->
|
||||
expression_matches(op, left, right, record, parent)
|
||||
|
||||
%Call{} = call ->
|
||||
raise "Unresolvable filter component: #{inspect(call)}"
|
||||
|
||||
%Ref{} = ref ->
|
||||
resolve_expr(ref, record, parent, resource)
|
||||
|
||||
other ->
|
||||
resolve_expr(other, record, parent, resource)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -423,13 +366,18 @@ defmodule Ash.Filter.Runtime do
|
|||
)
|
||||
end
|
||||
|
||||
defp resolve_exprs(exprs, record, parent, resource) do
|
||||
defp resolve_exprs(exprs, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
exprs
|
||||
|> Enum.reduce_while({:ok, []}, fn expr, {:ok, exprs} ->
|
||||
case resolve_expr(expr, record, parent, resource) do
|
||||
{:ok, resolved} -> {:cont, {:ok, [resolved | exprs]}}
|
||||
{:error, error} -> {:halt, {:error, error}}
|
||||
:unknown -> {:halt, :unknown}
|
||||
case resolve_expr(expr, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
{:ok, resolved} ->
|
||||
{:cont, {:ok, [resolved | exprs]}}
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
|
||||
:unknown ->
|
||||
{:halt, :unknown}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
|
@ -439,8 +387,19 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr({key, value}, record, parent, resource) when is_atom(key) do
|
||||
case resolve_expr(value, record, parent, resource) do
|
||||
defp resolve_expr(
|
||||
%Ash.Filter{expression: expression},
|
||||
record,
|
||||
parent,
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
resolve_expr(expression, record, parent, resource, unknown_on_unknown_refs?)
|
||||
end
|
||||
|
||||
defp resolve_expr({key, value}, record, parent, resource, unknown_on_unknown_refs?)
|
||||
when is_atom(key) do
|
||||
case resolve_expr(value, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
{:ok, resolved} ->
|
||||
{:ok, {key, resolved}}
|
||||
|
||||
|
@ -449,22 +408,30 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%Ref{} = ref, record, parent, resource) do
|
||||
resolve_ref(ref, record, parent, resource)
|
||||
defp resolve_expr(%Ref{} = ref, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
resolve_ref(ref, record, parent, resource, unknown_on_unknown_refs?)
|
||||
end
|
||||
|
||||
defp resolve_expr(
|
||||
%BooleanExpression{op: :and, left: left, right: right},
|
||||
record,
|
||||
parent,
|
||||
resource
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
with {:ok, left_resolved} <- resolve_expr(left, record, parent, resource),
|
||||
{:ok, right_resolved} <- resolve_expr(right, record, parent, resource) do
|
||||
if is_nil(left_resolved) || is_nil(right_resolved) do
|
||||
{:ok, nil}
|
||||
else
|
||||
{:ok, !!left_resolved and !!right_resolved}
|
||||
with {:ok, left_resolved} <-
|
||||
resolve_expr(left, record, parent, resource, unknown_on_unknown_refs?),
|
||||
{:ok, right_resolved} <-
|
||||
resolve_expr(right, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
cond do
|
||||
is_nil(left_resolved) ->
|
||||
{:ok, nil}
|
||||
|
||||
is_nil(right_resolved) ->
|
||||
{:ok, nil}
|
||||
|
||||
true ->
|
||||
{:ok, !!left_resolved and !!right_resolved}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -473,41 +440,67 @@ defmodule Ash.Filter.Runtime do
|
|||
%BooleanExpression{op: :or, left: left, right: right},
|
||||
record,
|
||||
parent,
|
||||
resource
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
with {:ok, left_resolved} <- resolve_expr(left, record, parent, resource),
|
||||
{:ok, right_resolved} <- resolve_expr(right, record, parent, resource) do
|
||||
if is_nil(left_resolved) || is_nil(right_resolved) do
|
||||
{:ok, nil}
|
||||
else
|
||||
{:ok, !!left_resolved or !!right_resolved}
|
||||
with {:ok, left_resolved} <-
|
||||
resolve_expr(left, record, parent, resource, unknown_on_unknown_refs?),
|
||||
{:ok, right_resolved} <-
|
||||
resolve_expr(right, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
cond do
|
||||
left_resolved ->
|
||||
{:ok, !!left_resolved}
|
||||
|
||||
is_nil(right_resolved) ->
|
||||
{:ok, right_resolved}
|
||||
|
||||
true ->
|
||||
{:ok, !!right_resolved}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%Not{expression: expression}, record, parent, resource) do
|
||||
case resolve_expr(expression, record, parent, resource) do
|
||||
defp resolve_expr(
|
||||
%Not{expression: expression},
|
||||
record,
|
||||
parent,
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
case resolve_expr(expression, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
{:ok, nil} -> {:ok, nil}
|
||||
{:ok, resolved} -> {:ok, !resolved}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%Ash.Query.Parent{expr: expr}, _, parent, resource) do
|
||||
resolve_expr(expr, parent, nil, resource)
|
||||
defp resolve_expr(%Ash.Query.Parent{expr: expr}, _, parent, resource, unknown_on_unknown_refs?) do
|
||||
resolve_expr(expr, parent, nil, resource, unknown_on_unknown_refs?)
|
||||
end
|
||||
|
||||
defp resolve_expr(%Ash.Query.Exists{}, nil, _parent, _resource), do: :unknown
|
||||
defp resolve_expr(%Ash.Query.Exists{}, nil, _parent, _resource, unknown_on_unknown_refs?) do
|
||||
if is_nil(unknown_on_unknown_refs?) do
|
||||
raise "WHAT"
|
||||
end
|
||||
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(
|
||||
%Ash.Query.Exists{at_path: [], path: path, expr: expr},
|
||||
record,
|
||||
_parent,
|
||||
resource
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
record
|
||||
|> flatten_relationships([path])
|
||||
|> load_unflattened(path)
|
||||
|> get_related(path)
|
||||
|> get_related(path, unknown_on_unknown_refs?)
|
||||
|> case do
|
||||
:unknown ->
|
||||
:unknown
|
||||
|
@ -516,7 +509,7 @@ defmodule Ash.Filter.Runtime do
|
|||
related
|
||||
|> List.wrap()
|
||||
|> Enum.reduce_while({:ok, false}, fn related, {:ok, false} ->
|
||||
case resolve_expr(expr, related, record, resource) do
|
||||
case resolve_expr(expr, related, record, resource, unknown_on_unknown_refs?) do
|
||||
{:ok, falsy} when falsy in [nil, false] ->
|
||||
{:cont, {:ok, false}}
|
||||
|
||||
|
@ -530,10 +523,16 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%Ash.Query.Exists{at_path: at_path} = exists, record, parent, resource) do
|
||||
defp resolve_expr(
|
||||
%Ash.Query.Exists{at_path: at_path} = exists,
|
||||
record,
|
||||
parent,
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
record
|
||||
|> flatten_relationships([at_path])
|
||||
|> get_related(at_path)
|
||||
|> get_related(at_path, unknown_on_unknown_refs?)
|
||||
|> case do
|
||||
:unknown ->
|
||||
:unknown
|
||||
|
@ -541,13 +540,22 @@ defmodule Ash.Filter.Runtime do
|
|||
related ->
|
||||
related
|
||||
|> Enum.reduce_while({:ok, false}, fn related, {:ok, false} ->
|
||||
case resolve_expr(%{exists | at_path: []}, related, parent, resource) do
|
||||
case resolve_expr(
|
||||
%{exists | at_path: []},
|
||||
related,
|
||||
parent,
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
{:ok, true} ->
|
||||
{:halt, {:ok, true}}
|
||||
|
||||
{:ok, _} ->
|
||||
{:cont, {:ok, false}}
|
||||
|
||||
:unknown ->
|
||||
{:halt, :unknown}
|
||||
|
||||
other ->
|
||||
{:halt, other}
|
||||
end
|
||||
|
@ -555,22 +563,33 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%mod{__predicate__?: _, left: left, right: right}, record, parent, resource) do
|
||||
with {:ok, [left, right]} <- resolve_exprs([left, right], record, parent, resource),
|
||||
{:op, {:ok, %mod{} = new_pred}} <-
|
||||
defp resolve_expr(
|
||||
%mod{__predicate__?: _, left: left, right: right},
|
||||
record,
|
||||
parent,
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
with {:ok, [left, right]} <-
|
||||
resolve_exprs([left, right], record, parent, resource, unknown_on_unknown_refs?),
|
||||
{:op, {:ok, new_pred}} <-
|
||||
{:op, Ash.Query.Operator.try_cast_with_ref(mod, left, right)},
|
||||
{:known, val} <- mod.evaluate(new_pred) do
|
||||
{:known, val} <-
|
||||
evaluate(new_pred, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
{:ok, val}
|
||||
else
|
||||
{:op, {:error, error}} ->
|
||||
{:error, error}
|
||||
|
||||
{:op, {:ok, expr}} ->
|
||||
resolve_expr(expr, record, parent, resource)
|
||||
resolve_expr(expr, record, parent, resource, unknown_on_unknown_refs?)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
{:op, :unknown} ->
|
||||
:unknown
|
||||
|
||||
:unknown ->
|
||||
:unknown
|
||||
|
||||
|
@ -579,11 +598,18 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(%mod{__predicate__?: _, arguments: args} = pred, record, parent, resource) do
|
||||
with {:ok, args} <- resolve_exprs(args, record, parent, resource),
|
||||
defp resolve_expr(
|
||||
%mod{__predicate__?: _, arguments: args} = pred,
|
||||
record,
|
||||
parent,
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
with {:ok, args} <- resolve_exprs(args, record, parent, resource, unknown_on_unknown_refs?),
|
||||
{:args, args} when not is_nil(args) <-
|
||||
{:args, try_cast_arguments(mod.args(), args)},
|
||||
{:known, val} <- mod.evaluate(%{pred | arguments: args}) do
|
||||
{:known, val} <-
|
||||
evaluate(%{pred | arguments: args}, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
{:ok, val}
|
||||
else
|
||||
{:args, nil} ->
|
||||
|
@ -600,10 +626,11 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(list, record, parent, resource) when is_list(list) do
|
||||
defp resolve_expr(list, record, parent, resource, unknown_on_unknown_refs?)
|
||||
when is_list(list) do
|
||||
list
|
||||
|> Enum.reduce_while({:ok, []}, fn item, {:ok, acc} ->
|
||||
case resolve_expr(item, record, parent, resource) do
|
||||
case resolve_expr(item, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
{:ok, result} ->
|
||||
{:cont, {:ok, [result | acc]}}
|
||||
|
||||
|
@ -620,10 +647,11 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
defp resolve_expr(map, record, parent, resource) when is_map(map) and not is_struct(map) do
|
||||
defp resolve_expr(map, record, parent, resource, unknown_on_unknown_refs?)
|
||||
when is_map(map) and not is_struct(map) do
|
||||
Enum.reduce_while(map, {:ok, %{}}, fn {key, value}, {:ok, acc} ->
|
||||
with {:ok, key} <- resolve_expr(key, record, parent, resource),
|
||||
{:ok, value} <- resolve_expr(value, record, parent, resource) do
|
||||
with {:ok, key} <- resolve_expr(key, record, parent, resource, unknown_on_unknown_refs?),
|
||||
{:ok, value} <- resolve_expr(value, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
{:cont, {:ok, Map.put(acc, key, value)}}
|
||||
else
|
||||
other ->
|
||||
|
@ -632,7 +660,7 @@ defmodule Ash.Filter.Runtime do
|
|||
end)
|
||||
end
|
||||
|
||||
defp resolve_expr(other, _, _, _), do: {:ok, other}
|
||||
defp resolve_expr(other, _, _, _, _), do: {:ok, other}
|
||||
|
||||
defp try_cast_arguments(:var_args, args) do
|
||||
Enum.map(args, fn _ -> :any end)
|
||||
|
@ -659,7 +687,8 @@ defmodule Ash.Filter.Runtime do
|
|||
},
|
||||
record,
|
||||
parent,
|
||||
resource
|
||||
resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
if function_exported?(module, :expression, 2) do
|
||||
expression = module.expression(opts, context)
|
||||
|
@ -688,7 +717,7 @@ defmodule Ash.Filter.Runtime do
|
|||
with {:ok, hydrated} <- hydrated do
|
||||
hydrated
|
||||
|> Ash.Filter.prefix_refs(relationship_path)
|
||||
|> resolve_expr(record, parent, resource)
|
||||
|> resolve_expr(record, parent, resource, unknown_on_unknown_refs?)
|
||||
end
|
||||
else
|
||||
# We need to rewrite this
|
||||
|
@ -704,17 +733,29 @@ defmodule Ash.Filter.Runtime do
|
|||
{:ok, [result]} ->
|
||||
{:ok, result}
|
||||
|
||||
:unknown when unknown_on_unknown_refs? ->
|
||||
:unknown
|
||||
|
||||
_ ->
|
||||
{:ok, nil}
|
||||
end
|
||||
else
|
||||
:unknown
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
raise "WHAT"
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_ref(_ref, nil, _, _resource) do
|
||||
:unknown
|
||||
defp resolve_ref(_ref, nil, _, _resource, unknown_on_unknown_refs?) do
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_ref(
|
||||
|
@ -727,12 +768,17 @@ defmodule Ash.Filter.Runtime do
|
|||
},
|
||||
record,
|
||||
_parent,
|
||||
_resource
|
||||
_resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
if load do
|
||||
case Map.get(record, load) do
|
||||
%Ash.NotLoaded{} ->
|
||||
:unknown
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
other ->
|
||||
{:ok, other}
|
||||
|
@ -743,7 +789,11 @@ defmodule Ash.Filter.Runtime do
|
|||
{:ok, value}
|
||||
|
||||
:error ->
|
||||
:unknown
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -752,7 +802,8 @@ defmodule Ash.Filter.Runtime do
|
|||
%Ref{attribute: attribute, relationship_path: path},
|
||||
record,
|
||||
_parent,
|
||||
_resource
|
||||
_resource,
|
||||
unknown_on_unknown_refs?
|
||||
) do
|
||||
name =
|
||||
case attribute do
|
||||
|
@ -761,62 +812,56 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
|
||||
record
|
||||
|> get_related(path)
|
||||
|> get_related(path, unknown_on_unknown_refs?)
|
||||
|> case do
|
||||
:unknown ->
|
||||
:unknown
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
[] ->
|
||||
{:ok, nil}
|
||||
|
||||
[%struct{} = record] ->
|
||||
[%struct{} = record | _] ->
|
||||
if Spark.Dsl.is?(struct, Ash.Resource) do
|
||||
if Ash.Resource.Info.attribute(struct, name) do
|
||||
if Ash.Resource.selected?(record, name) do
|
||||
{:ok, Map.get(record, name)}
|
||||
else
|
||||
:unknown
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
else
|
||||
if Ash.Resource.loaded?(record, name) do
|
||||
{:ok, Map.get(record, name)}
|
||||
else
|
||||
:unknown
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
{:ok, Map.get(record, name)}
|
||||
end
|
||||
|
||||
[record] ->
|
||||
[record | _] ->
|
||||
{:ok, Map.get(record, name)}
|
||||
|
||||
%struct{} = record ->
|
||||
if Spark.Dsl.is?(struct, Ash.Resource) do
|
||||
if Ash.Resource.Info.attribute(struct, name) do
|
||||
if Ash.Resource.selected?(record, name) do
|
||||
{:ok, Map.get(record, name)}
|
||||
else
|
||||
:unknown
|
||||
end
|
||||
else
|
||||
if Ash.Resource.loaded?(record, name) do
|
||||
{:ok, Map.get(record, name)}
|
||||
else
|
||||
:unknown
|
||||
end
|
||||
end
|
||||
else
|
||||
{:ok, Map.get(record, name)}
|
||||
end
|
||||
|
||||
record ->
|
||||
{:ok, Map.get(record, name)}
|
||||
_ ->
|
||||
{:ok, nil}
|
||||
end
|
||||
|> or_default(attribute)
|
||||
end
|
||||
|
||||
defp resolve_ref(_value, _record, _, _), do: :unknown
|
||||
defp resolve_ref(_value, _record, _, _, true), do: :unknown
|
||||
defp resolve_ref(_value, _record, _, _, _), do: {:ok, nil}
|
||||
|
||||
defp or_default({:ok, nil}, %Ash.Resource.Aggregate{default: default})
|
||||
when not is_nil(default) do
|
||||
|
@ -887,77 +932,39 @@ defmodule Ash.Filter.Runtime do
|
|||
{first, [path_to_load(related, rest, further_refs)] ++ to_load}
|
||||
end
|
||||
|
||||
defp expression_matches(:and, left, right, record, parent) do
|
||||
case do_match(record, left, parent) do
|
||||
{:ok, false} ->
|
||||
{:ok, false}
|
||||
|
||||
{:ok, nil} ->
|
||||
{:ok, nil}
|
||||
|
||||
{:ok, true} ->
|
||||
case do_match(record, right, parent) do
|
||||
{:ok, false} ->
|
||||
{:ok, false}
|
||||
|
||||
{:ok, nil} ->
|
||||
{:ok, nil}
|
||||
|
||||
{:ok, _} ->
|
||||
{:ok, true}
|
||||
|
||||
:unknown ->
|
||||
:unknown
|
||||
end
|
||||
|
||||
:unknown ->
|
||||
:unknown
|
||||
end
|
||||
end
|
||||
|
||||
defp expression_matches(:or, left, right, record, parent) do
|
||||
case do_match(record, left, parent) do
|
||||
{:ok, falsy} when falsy in [nil, false] ->
|
||||
case do_match(record, right, parent) do
|
||||
{:ok, falsy} when falsy in [nil, false] ->
|
||||
{:ok, false}
|
||||
|
||||
{:ok, _} ->
|
||||
{:ok, true}
|
||||
|
||||
:unknown ->
|
||||
:unknown
|
||||
end
|
||||
|
||||
{:ok, _} ->
|
||||
{:ok, true}
|
||||
|
||||
:unknown ->
|
||||
:unknown
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def get_related(nil, _), do: []
|
||||
def get_related(source, path, unknown_on_unknown_refs? \\ false)
|
||||
|
||||
def get_related(%Ash.NotLoaded{}, []) do
|
||||
:unknown
|
||||
def get_related(nil, _, unknown_on_unknown_refs?) do
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def get_related(record, []) do
|
||||
record
|
||||
def get_related(%Ash.NotLoaded{}, [], unknown_on_unknown_refs?) do
|
||||
if unknown_on_unknown_refs? do
|
||||
:unknown
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def get_related(records, paths) when is_list(records) do
|
||||
def get_related(record, [], _) do
|
||||
List.wrap(record)
|
||||
end
|
||||
|
||||
def get_related(records, paths, unknown_on_unknown_refs?) when is_list(records) do
|
||||
records
|
||||
|> Enum.reduce_while([], fn
|
||||
:unknown, _records ->
|
||||
{:halt, :unknown}
|
||||
:unknown, records ->
|
||||
{:cont, records}
|
||||
|
||||
record, records ->
|
||||
case get_related(record, paths) do
|
||||
case get_related(record, paths, unknown_on_unknown_refs?) do
|
||||
:unknown ->
|
||||
{:halt, :unknown}
|
||||
{:cont, records}
|
||||
|
||||
related ->
|
||||
{:cont, [related | records]}
|
||||
|
@ -974,13 +981,13 @@ defmodule Ash.Filter.Runtime do
|
|||
end
|
||||
end
|
||||
|
||||
def get_related(record, [key | rest]) do
|
||||
def get_related(record, [key | rest], unknown_on_unknown_refs?) do
|
||||
case Map.get(record, key) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
value ->
|
||||
case get_related(value, rest) do
|
||||
case get_related(value, rest, unknown_on_unknown_refs?) do
|
||||
:unknown ->
|
||||
:unknown
|
||||
|
||||
|
@ -992,4 +999,29 @@ defmodule Ash.Filter.Runtime do
|
|||
|
||||
defp parent_stack(nil), do: []
|
||||
defp parent_stack(%resource{}), do: [resource]
|
||||
|
||||
defp evaluate(
|
||||
%{__function__?: true} = func,
|
||||
_record,
|
||||
_parent,
|
||||
_resource,
|
||||
_unknown_on_unknown_refs?
|
||||
),
|
||||
do: Ash.Query.Function.evaluate(func)
|
||||
|
||||
defp evaluate(
|
||||
%{__operator__?: true} = op,
|
||||
_record,
|
||||
_parent,
|
||||
_resource,
|
||||
_unknown_on_unknown_refs?
|
||||
),
|
||||
do: Ash.Query.Operator.evaluate(op)
|
||||
|
||||
defp evaluate(other, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
case resolve_expr(other, record, parent, resource, unknown_on_unknown_refs?) do
|
||||
{:ok, value} -> {:known, value}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -89,7 +89,10 @@ defmodule Ash.Policy.FilterCheck do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: query.resource)
|
||||
Ash.Expr.eval_hydrated(hydrated,
|
||||
resource: query.resource,
|
||||
unknown_on_unknown_refs?: true
|
||||
)
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
|
@ -107,7 +110,7 @@ defmodule Ash.Policy.FilterCheck do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -135,7 +138,11 @@ defmodule Ash.Policy.FilterCheck do
|
|||
nil
|
||||
end
|
||||
|
||||
Ash.Expr.eval_hydrated(hydrated, record: data, resource: resource)
|
||||
Ash.Expr.eval_hydrated(hydrated,
|
||||
record: data,
|
||||
resource: resource,
|
||||
unknown_on_unknown_refs?: true
|
||||
)
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
|
@ -150,7 +157,11 @@ defmodule Ash.Policy.FilterCheck do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource, resource: resource)
|
||||
Ash.Expr.eval_hydrated(hydrated,
|
||||
resource: resource,
|
||||
resource: resource,
|
||||
unknown_on_unknown_refs?: true
|
||||
)
|
||||
|
||||
{:error, error} ->
|
||||
{:halt, {:error, error}}
|
||||
|
|
|
@ -74,7 +74,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -92,7 +92,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -120,7 +120,11 @@ defmodule Ash.Policy.FilterCheckWithContext do
|
|||
nil
|
||||
end
|
||||
|
||||
Ash.Expr.eval_hydrated(hydrated, record: data, resource: resource)
|
||||
Ash.Expr.eval_hydrated(hydrated,
|
||||
record: data,
|
||||
resource: resource,
|
||||
unknown_on_unknown_refs?: true
|
||||
)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
@ -135,7 +139,7 @@ defmodule Ash.Policy.FilterCheckWithContext do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, hydrated} ->
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource)
|
||||
Ash.Expr.eval_hydrated(hydrated, resource: resource, unknown_on_unknown_refs?: true)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
|
|
@ -13,13 +13,31 @@ defmodule Ash.Query.Function do
|
|||
The number and types of arguments supported.
|
||||
"""
|
||||
@callback args() :: [arg]
|
||||
@callback name() :: atom
|
||||
@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 eager_evaluate?() :: boolean()
|
||||
@callback private?() :: boolean
|
||||
|
||||
@doc """
|
||||
If `true`, will be allowed to evaluate `nil` inputs.
|
||||
|
||||
If `false` (the default), any `nil` inputs will cause a `nil` return.
|
||||
"""
|
||||
@callback evaluate_nil_inputs?() :: boolean()
|
||||
|
||||
@optional_callbacks partial_evaluate: 1
|
||||
|
||||
@doc "Evaluate the operator with provided inputs"
|
||||
def evaluate(%mod{arguments: arguments} = func) do
|
||||
if Enum.any?(arguments, &is_nil/1) && !mod.evaluate_nil_inputs?() do
|
||||
{:known, nil}
|
||||
else
|
||||
mod.evaluate(func)
|
||||
end
|
||||
end
|
||||
|
||||
def new(mod, args) do
|
||||
args = List.wrap(args)
|
||||
|
||||
|
@ -163,7 +181,10 @@ defmodule Ash.Query.Function do
|
|||
|
||||
defmacro __using__(opts) do
|
||||
quote do
|
||||
@behaviour Ash.Filter.Predicate
|
||||
@behaviour Ash.Query.Function
|
||||
if unquote(opts[:predicate?] || false) do
|
||||
@behaviour Ash.Filter.Predicate
|
||||
end
|
||||
|
||||
alias Ash.Query.Ref
|
||||
|
||||
|
@ -175,19 +196,25 @@ defmodule Ash.Query.Function do
|
|||
__predicate__?: unquote(opts[:predicate?] || false)
|
||||
]
|
||||
|
||||
@impl Ash.Query.Function
|
||||
def name, do: unquote(opts[:name])
|
||||
|
||||
@impl Ash.Query.Function
|
||||
def new(args), do: {:ok, struct(__MODULE__, arguments: args)}
|
||||
|
||||
@impl Ash.Query.Function
|
||||
def evaluate(_), do: :unknown
|
||||
|
||||
def predicate?, do: unquote(opts[:predicate?] || false)
|
||||
|
||||
@impl Ash.Query.Function
|
||||
def eager_evaluate?, do: unquote(Keyword.get(opts, :eager_evaluate?, true))
|
||||
|
||||
@impl Ash.Query.Function
|
||||
def evaluate_nil_inputs?, do: false
|
||||
|
||||
@impl Ash.Query.Function
|
||||
def private?, do: false
|
||||
|
||||
defoverridable new: 1, evaluate: 1, private?: 0
|
||||
defoverridable new: 1, evaluate: 1, private?: 0, evaluate_nil_inputs?: 0
|
||||
|
||||
unless unquote(opts[:no_inspect?]) do
|
||||
defimpl Inspect do
|
||||
|
|
|
@ -7,6 +7,8 @@ defmodule Ash.Query.Function.If do
|
|||
|
||||
def args, do: [[:boolean, :any], [:boolean, :any, :any]]
|
||||
|
||||
def evaluate_nil_inputs?, do: true
|
||||
|
||||
def new([condition, block]) do
|
||||
args =
|
||||
if Keyword.keyword?(block) && Keyword.has_key?(block, :do) do
|
||||
|
@ -24,6 +26,7 @@ defmodule Ash.Query.Function.If do
|
|||
|
||||
def new([true, block, _else_block]), do: {:ok, block}
|
||||
def new([false, _block, else_block]), do: {:ok, else_block}
|
||||
def new([nil, _block, else_block]), do: {:ok, else_block}
|
||||
|
||||
def new([condition, block, else_block]) do
|
||||
super([condition, block, else_block])
|
||||
|
|
|
@ -6,6 +6,8 @@ defmodule Ash.Query.Function.IsNil do
|
|||
|
||||
def args, do: [[:any]]
|
||||
|
||||
def evaluate_nil_inputs?, do: true
|
||||
|
||||
def new([arg]) do
|
||||
Ash.Query.Operator.new(Ash.Query.Operator.IsNil, arg, true)
|
||||
end
|
||||
|
|
|
@ -55,6 +55,7 @@ defmodule Ash.Query.Operator.Basic do
|
|||
types: unquote(opts[:types] || [:same, :any])
|
||||
|
||||
if unquote(opts[:no_nils]) do
|
||||
@impl Ash.Query.Operator
|
||||
def evaluate(%{left: left, right: right}) do
|
||||
if is_nil(left) || is_nil(right) do
|
||||
{:known, nil}
|
||||
|
@ -64,12 +65,19 @@ defmodule Ash.Query.Operator.Basic do
|
|||
do_evaluate(unquote(opts[:symbol]), left, right)
|
||||
end
|
||||
end
|
||||
|
||||
@impl Ash.Query.Operator
|
||||
def evaluate_nil_inputs?, do: false
|
||||
else
|
||||
@impl Ash.Query.Operator
|
||||
def evaluate(%{left: left, right: right}) do
|
||||
# delegate to function to avoid dialyzer warning
|
||||
# that this can only ever be one value (for each module we define)
|
||||
do_evaluate(unquote(opts[:symbol]), left, right)
|
||||
end
|
||||
|
||||
@impl Ash.Query.Operator
|
||||
def evaluate_nil_inputs?, do: true
|
||||
end
|
||||
|
||||
defp do_evaluate(:<>, %Ash.CiString{string: left}, %Ash.CiString{string: right}) do
|
||||
|
|
|
@ -24,6 +24,7 @@ defmodule Ash.Query.Operator.Eq do
|
|||
{:known, Comp.equal?(left, right)}
|
||||
end
|
||||
|
||||
@impl Ash.Filter.Predicate
|
||||
def bulk_compare(predicates) do
|
||||
predicates
|
||||
|> Enum.filter(&match?(%struct{} when struct == __MODULE__, &1))
|
||||
|
|
|
@ -17,6 +17,7 @@ defmodule Ash.Query.Operator.GreaterThan do
|
|||
{:known, Comp.greater_than?(left, right)}
|
||||
end
|
||||
|
||||
@impl Ash.Filter.Predicate
|
||||
def simplify(%__MODULE__{left: %Ref{} = ref, right: %Date{} = value}) do
|
||||
{:ok, op} = Ash.Query.Operator.new(Ash.Query.Operator.LessThan, ref, Date.add(value, 1))
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ defmodule Ash.Query.Operator.GreaterThanOrEqual do
|
|||
def evaluate(%{left: left, right: right}),
|
||||
do: {:known, Comp.greater_or_equal?(left, right)}
|
||||
|
||||
@impl Ash.Filter.Predicate
|
||||
def simplify(%__MODULE__{left: %Ref{} = ref, right: value}) do
|
||||
{:ok, op} = Ash.Query.Operator.new(Ash.Query.Operator.LessThan, ref, value)
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ defmodule Ash.Query.Operator.In do
|
|||
{:known, Enum.any?(right, &Comp.equal?(&1, left))}
|
||||
end
|
||||
|
||||
@impl Ash.Filter.Predicate
|
||||
def compare(%__MODULE__{left: left, right: %MapSet{} = left_right}, %__MODULE__{
|
||||
left: left,
|
||||
right: %MapSet{} = right_right
|
||||
|
|
|
@ -17,6 +17,10 @@ defmodule Ash.Query.Operator.IsNil do
|
|||
super(left, right)
|
||||
end
|
||||
|
||||
@impl Ash.Query.Operator
|
||||
def evaluate_nil_inputs?, do: true
|
||||
|
||||
@impl Ash.Query.Operator
|
||||
def evaluate(%{right: nil}), do: {:known, nil}
|
||||
|
||||
def evaluate(%{left: left, right: is_nil?}) do
|
||||
|
@ -26,19 +30,30 @@ defmodule Ash.Query.Operator.IsNil do
|
|||
def to_string(%{left: left, right: right}, opts) do
|
||||
import Inspect.Algebra
|
||||
|
||||
text =
|
||||
if right do
|
||||
" is nil"
|
||||
else
|
||||
" is not nil"
|
||||
end
|
||||
cond do
|
||||
right == true ->
|
||||
concat([
|
||||
to_doc(left, opts),
|
||||
" is nil"
|
||||
])
|
||||
|
||||
concat([
|
||||
to_doc(left, opts),
|
||||
text
|
||||
])
|
||||
right == false ->
|
||||
concat([
|
||||
to_doc(left, opts),
|
||||
" is not nil"
|
||||
])
|
||||
|
||||
true ->
|
||||
concat([
|
||||
" is_nil(",
|
||||
to_doc(left, opts),
|
||||
") == ",
|
||||
to_doc(right, opts)
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
@impl Ash.Filter.Predicate
|
||||
def compare(%__MODULE__{left: %Ref{} = same_ref, right: true}, %Ash.Query.Operator.Eq{
|
||||
left: %Ref{} = same_ref,
|
||||
right: nil
|
||||
|
|
|
@ -28,6 +28,7 @@ defmodule Ash.Query.Operator.LessThan do
|
|||
{:known, Comp.less_than?(left, right)}
|
||||
end
|
||||
|
||||
@impl Ash.Filter.Predicate
|
||||
def bulk_compare(all_predicates) do
|
||||
all_predicates
|
||||
|> Enum.group_by(& &1.left)
|
||||
|
|
|
@ -17,6 +17,7 @@ defmodule Ash.Query.Operator.LessThanOrEqual do
|
|||
{:known, Comp.less_or_equal?(left, right)}
|
||||
end
|
||||
|
||||
@impl Ash.Filter.Predicate
|
||||
def simplify(%__MODULE__{left: %Ref{} = same_ref, right: %Date{} = value}) do
|
||||
{:ok, op} = Ash.Query.Operator.new(Ash.Query.Operator.LessThan, same_ref, Date.add(value, 1))
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ defmodule Ash.Query.Operator.NotEq do
|
|||
{:known, Comp.not_equal?(left, right)}
|
||||
end
|
||||
|
||||
@impl Ash.Filter.Predicate
|
||||
def simplify(%__MODULE__{left: left, right: right}) do
|
||||
%Not{expression: %Eq{left: left, right: right}}
|
||||
end
|
||||
|
|
|
@ -27,6 +27,38 @@ defmodule Ash.Query.Operator do
|
|||
"""
|
||||
@callback to_string(struct, Inspect.Opts.t()) :: term
|
||||
|
||||
@doc """
|
||||
Evaluates the operator in Elixir
|
||||
"""
|
||||
@callback evaluate(term) :: term
|
||||
|
||||
@doc """
|
||||
If `true`, will be allowed to evaluate `nil` inputs.
|
||||
|
||||
If `false` (the default), any `nil` inputs will cause a `nil` return.
|
||||
"""
|
||||
@callback evaluate_nil_inputs?() :: boolean()
|
||||
|
||||
@doc """
|
||||
The types accepted by the operator. Defaults to `[:same, :any]`, which is any values of the same type.
|
||||
"""
|
||||
@callback types() :: [
|
||||
:any | :same | [Ash.Type.t() | {Ash.Type.t(), constraints :: Keyword.t()}]
|
||||
]
|
||||
|
||||
@doc "Evaluate the operator with provided inputs"
|
||||
def evaluate(%mod{left: left, right: right} = op) when is_nil(left) or is_nil(right) do
|
||||
if mod.evaluate_nil_inputs?() do
|
||||
mod.evaluate(op)
|
||||
else
|
||||
{:known, nil}
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate(%mod{} = op) do
|
||||
mod.evaluate(op)
|
||||
end
|
||||
|
||||
@doc "Create a new operator. Pass the module and the left and right values"
|
||||
def new(mod, %Ref{} = left, right) do
|
||||
try_cast_with_ref(mod, left, right)
|
||||
|
@ -289,23 +321,26 @@ defmodule Ash.Query.Operator do
|
|||
@behaviour Ash.Filter.Predicate
|
||||
end
|
||||
|
||||
@behaviour Ash.Query.Operator
|
||||
|
||||
alias Ash.Query.Ref
|
||||
import Inspect.Algebra
|
||||
|
||||
def operator, do: unquote(opts[:operator])
|
||||
def name, do: unquote(opts[:name] || opts[:operator])
|
||||
|
||||
def predicate? do
|
||||
unquote(opts[:predicate?])
|
||||
end
|
||||
|
||||
@impl Ash.Query.Operator
|
||||
def types do
|
||||
unquote(opts[:types] || [:same, :any])
|
||||
end
|
||||
|
||||
@impl Ash.Query.Operator
|
||||
def new(left, right), do: {:ok, struct(__MODULE__, left: left, right: right)}
|
||||
|
||||
import Inspect.Algebra
|
||||
@impl Ash.Query.Operator
|
||||
def evaluate_nil_inputs?, do: false
|
||||
|
||||
@impl Ash.Query.Operator
|
||||
def to_string(%{left: left, right: right, operator: operator}, opts) do
|
||||
concat([
|
||||
to_doc(left, opts),
|
||||
|
@ -316,7 +351,7 @@ defmodule Ash.Query.Operator do
|
|||
])
|
||||
end
|
||||
|
||||
defoverridable to_string: 2, new: 2
|
||||
defoverridable to_string: 2, new: 2, evaluate_nil_inputs?: 0
|
||||
|
||||
defimpl Inspect do
|
||||
def inspect(%mod{} = op, opts) do
|
||||
|
|
|
@ -39,7 +39,11 @@ defmodule Ash.Resource.Calculation.Expression do
|
|||
public?: false
|
||||
}) do
|
||||
{:ok, expression} ->
|
||||
case Ash.Expr.eval_hydrated(expression, record: record, resource: resource) do
|
||||
case Ash.Expr.eval_hydrated(expression,
|
||||
record: record,
|
||||
resource: resource,
|
||||
unknown_on_unknown_refs?: true
|
||||
) do
|
||||
{:ok, value} ->
|
||||
value = try_cast_stored(value, context[:ash][:type], context[:ash][:constraints])
|
||||
{:cont, {:ok, [value | values]}}
|
||||
|
|
Loading…
Reference in a new issue