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