improvement: handle nils in memory the same way sql would have

This commit is contained in:
Zach Daniel 2023-06-05 16:50:11 -04:00
parent 04fb305f10
commit 8e7815388e
7 changed files with 87 additions and 55 deletions

View file

@ -373,11 +373,15 @@ defmodule Ash.DataLayer.Ets do
case Ash.Expr.eval_hydrated(expression, record: record, resource: resource) do case Ash.Expr.eval_hydrated(expression, record: record, resource: resource) do
{:ok, value} -> {:ok, value} ->
if calculation.load do if calculation.load do
{:cont, {:ok, Map.put(record, calculation.load, value)}} {:cont, {:ok, Map.put(record, calculation.load, value || calculation.default)}}
else else
{:cont, {:cont,
{:ok, {:ok,
Map.update!(record, :calculations, &Map.put(&1, calculation.name, value))}} Map.update!(
record,
:calculations,
&Map.put(&1, calculation.name, value || calculation.default)
)}}
end end
:unknown -> :unknown ->
@ -385,7 +389,12 @@ defmodule Ash.DataLayer.Ets do
{:cont, {:ok, Map.put(record, calculation.load, nil)}} {:cont, {:ok, Map.put(record, calculation.load, nil)}}
else else
{:cont, {:cont,
{:ok, Map.update!(record, :calculations, &Map.put(&1, calculation.name, nil))}} {:ok,
Map.update!(
record,
:calculations,
&Map.put(&1, calculation.name, calculation.default)
)}}
end end
{:error, error} -> {:error, error} ->

View file

@ -193,7 +193,7 @@ defmodule Ash.Filter do
use Ash.Api use Ash.Api
resources do resources do
allow_unregistered? true allow_unregistered?(true)
end end
end end
@ -1717,7 +1717,7 @@ defmodule Ash.Filter do
end end
defp records_to_expression(records, relationship, path) do defp records_to_expression(records, relationship, path) do
Enum.reduce_while(records, {:ok, nil}, fn record, {:ok, expression} -> Enum.reduce_while(records, {:ok, true}, fn record, {:ok, expression} ->
case records_to_expression([record], relationship, path) do case records_to_expression([record], relationship, path) do
{:ok, operator} -> {:ok, operator} ->
{:cont, {:ok, BooleanExpression.optimized_new(:and, expression, operator)}} {:cont, {:ok, BooleanExpression.optimized_new(:and, expression, operator)}}
@ -2074,7 +2074,7 @@ defmodule Ash.Filter do
do: {:ok, move_to_relationship_path(expression, context[:relationship_path] || [])} do: {:ok, move_to_relationship_path(expression, context[:relationship_path] || [])}
defp parse_expression(statement, context) when is_list(statement) do defp parse_expression(statement, context) when is_list(statement) do
Enum.reduce_while(statement, {:ok, nil}, fn expression_part, {:ok, expression} -> Enum.reduce_while(statement, {:ok, true}, fn expression_part, {:ok, expression} ->
case add_expression_part(expression_part, context, expression) do case add_expression_part(expression_part, context, expression) do
{:ok, new_expression} -> {:ok, new_expression} ->
{:cont, {:ok, new_expression}} {:cont, {:ok, new_expression}}
@ -2089,8 +2089,13 @@ defmodule Ash.Filter do
parse_expression([statement], context) parse_expression([statement], context)
end end
defp add_expression_part(boolean, _context, expression) when is_boolean(boolean), defp add_expression_part(boolean, context, nil) do
do: {:ok, BooleanExpression.optimized_new(:and, expression, boolean)} add_expression_part(boolean, context, true)
end
defp add_expression_part(boolean, _context, expression) when is_boolean(boolean) do
{:ok, BooleanExpression.optimized_new(:and, expression, boolean)}
end
defp add_expression_part(%__MODULE__{expression: adding_expression}, context, expression) do defp add_expression_part(%__MODULE__{expression: adding_expression}, context, expression) do
{:ok, {:ok,
@ -2544,7 +2549,7 @@ defmodule Ash.Filter do
value value
|> Map.to_list() |> Map.to_list()
|> Enum.reduce_while({:ok, nil}, fn {key, value}, {:ok, expression} -> |> Enum.reduce_while({:ok, true}, fn {key, value}, {:ok, expression} ->
case add_expression_part({key, value}, context, expression) do case add_expression_part({key, value}, context, expression) do
{:ok, new_expression} -> {:ok, new_expression} ->
{:cont, {:ok, new_expression}} {:cont, {:ok, new_expression}}
@ -3240,16 +3245,22 @@ defmodule Ash.Filter do
defp add_to_ref_path(other, _), do: other defp add_to_ref_path(other, _), do: other
defp parse_and_join(statements, op, context) do defp parse_and_join([statement | statements], op, context) do
Enum.reduce_while(statements, {:ok, nil}, fn statement, {:ok, expression} -> case parse_expression(statement, context) do
case parse_expression(statement, context) do {:ok, nested_expression} ->
{:ok, nested_expression} -> Enum.reduce_while(statements, {:ok, nested_expression}, fn statement, {:ok, expression} ->
{:cont, {:ok, BooleanExpression.optimized_new(op, expression, nested_expression)}} case parse_expression(statement, context) do
{:ok, nested_expression} ->
{:cont, {:ok, BooleanExpression.optimized_new(op, expression, nested_expression)}}
{:error, error} -> {:error, error} ->
{:halt, {:error, error}} {:halt, {:error, error}}
end end
end) end)
{:error, error} ->
{:halt, {:error, error}}
end
end end
defp parse_predicates(value, field, context) when not is_list(value) and not is_map(value) do defp parse_predicates(value, field, context) when not is_list(value) and not is_map(value) do
@ -3266,7 +3277,7 @@ defmodule Ash.Filter do
parse_predicates([eq: values], attr, context) parse_predicates([eq: values], attr, context)
else else
if is_map(values) || Keyword.keyword?(values) do if is_map(values) || Keyword.keyword?(values) do
Enum.reduce_while(values, {:ok, nil}, fn Enum.reduce_while(values, {:ok, true}, fn
{:not, value}, {:ok, expression} -> {:not, value}, {:ok, expression} ->
case parse_predicates(List.wrap(value), attr, context) do case parse_predicates(List.wrap(value), attr, context) do
{:ok, not_expression} -> {:ok, not_expression} ->

View file

@ -279,11 +279,17 @@ defmodule Ash.Filter.Runtime do
:unknown :unknown
end end
%Not{expression: nil} ->
{:ok, nil}
%Not{expression: expression} -> %Not{expression: expression} ->
case do_match(record, expression, parent, resource) do case do_match(record, expression, parent, resource) do
:unknown -> :unknown ->
:unknown :unknown
{:ok, nil} ->
{:ok, nil}
{:ok, match?} -> {:ok, match?} ->
{:ok, !match?} {:ok, !match?}
@ -744,7 +750,7 @@ defmodule Ash.Filter.Runtime do
{:ok, false} {:ok, false}
{:ok, nil} -> {:ok, nil} ->
{:ok, false} {:ok, nil}
{:ok, true} -> {:ok, true} ->
case do_match(record, right, parent) do case do_match(record, right, parent) do
@ -752,7 +758,7 @@ defmodule Ash.Filter.Runtime do
{:ok, false} {:ok, false}
{:ok, nil} -> {:ok, nil} ->
{:ok, false} {:ok, nil}
{:ok, _} -> {:ok, _} ->
{:ok, true} {:ok, true}

View file

@ -8,8 +8,10 @@ defmodule Ash.Query.BooleanExpression do
alias Ash.Query.Ref alias Ash.Query.Ref
def new(_, nil, nil), do: nil def new(_, nil, nil), do: nil
def new(_, left, nil), do: left def new(:or, left, nil), do: left
def new(_, nil, right), do: right def new(:or, nil, right), do: right
def new(:and, _, nil), do: nil
def new(:and, nil, _), do: nil
def new(op, left, right) do def new(op, left, right) do
%__MODULE__{op: op, left: left, right: right} %__MODULE__{op: op, left: left, right: right}
@ -27,8 +29,12 @@ defmodule Ash.Query.BooleanExpression do
def optimized_new(:and, _, false), do: false def optimized_new(:and, _, false), do: false
def optimized_new(:or, true, _), do: true def optimized_new(:or, true, _), do: true
def optimized_new(:or, _, true), do: true def optimized_new(:or, _, true), do: true
def optimized_new(_, nil, right), do: right def optimized_new(:or, nil, right), do: right
def optimized_new(_, left, nil), do: left def optimized_new(:or, left, nil), do: left
def optimized_new(:and, true, right), do: right
def optimized_new(:and, left, true), do: left
def optimized_new(:and, nil, _), do: nil
def optimized_new(:and, _, nil), do: nil
def optimized_new( def optimized_new(
op, op,

View file

@ -29,16 +29,16 @@ defmodule Ash.Test.Actions.ReadTest do
end end
actions do actions do
defaults [:read, :create, :update, :destroy] defaults([:read, :create, :update, :destroy])
end end
attributes do attributes do
uuid_primary_key :id uuid_primary_key(:id)
attribute :name, :string attribute(:name, :string)
end end
relationships do relationships do
has_many :posts, Ash.Test.Actions.ReadTest.Post, destination_attribute: :author1_id has_many(:posts, Ash.Test.Actions.ReadTest.Post, destination_attribute: :author1_id)
end end
end end
@ -47,39 +47,39 @@ defmodule Ash.Test.Actions.ReadTest do
use Ash.Resource, data_layer: Ash.DataLayer.Ets use Ash.Resource, data_layer: Ash.DataLayer.Ets
identities do identities do
identity :backup_id, [:uuid], pre_check_with: Api identity(:backup_id, [:uuid], pre_check_with: Api)
end end
ets do ets do
private? true private?(true)
end end
actions do actions do
defaults [:read, :create, :update, :destroy] defaults([:read, :create, :update, :destroy])
read :read_with_after_action do read :read_with_after_action do
prepare PostPreparation prepare(PostPreparation)
end end
read :get_by_id do read :get_by_id do
get_by :id get_by(:id)
end end
read :get_by_id_and_uuid do read :get_by_id_and_uuid do
get_by [:id, :uuid] get_by([:id, :uuid])
end end
end end
attributes do attributes do
uuid_primary_key :id uuid_primary_key(:id)
attribute :uuid, :uuid, default: &Ash.UUID.generate/0 attribute(:uuid, :uuid, default: &Ash.UUID.generate/0)
attribute :title, :string attribute(:title, :string)
attribute :contents, :string attribute(:contents, :string)
end end
relationships do relationships do
belongs_to :author1, Ash.Test.Actions.ReadTest.Author belongs_to(:author1, Ash.Test.Actions.ReadTest.Author)
belongs_to :author2, Ash.Test.Actions.ReadTest.Author belongs_to(:author2, Ash.Test.Actions.ReadTest.Author)
end end
end end

View file

@ -29,7 +29,7 @@ defmodule Ash.Test.Flow.Flows.Branching do
end end
end end
branch :inner_branch_alt, expr(not (^arg(:do_inner_branch))) do branch :inner_branch_alt, expr(not (^arg(:do_inner_branch) || false)) do
custom :inner_branch_alt_return, Ash.Test.Flow.Steps.SimpleReturn do custom :inner_branch_alt_return, Ash.Test.Flow.Steps.SimpleReturn do
input %{return: "inner_branch didn't happen"} input %{return: "inner_branch didn't happen"}
end end

View file

@ -4,31 +4,31 @@ defmodule Ash.Test.Flow.Flows.Halting do
flow do flow do
argument :on_step, :atom do argument :on_step, :atom do
constraints one_of: [:a, :b, :c] constraints(one_of: [:a, :b, :c])
end end
returns :c returns(:c)
end end
steps do steps do
custom :a, Ash.Test.Flow.Steps.SimpleReturn do custom :a, Ash.Test.Flow.Steps.SimpleReturn do
input %{return: "a"} input(%{return: "a"})
halt_if expr(not (^arg(:on_step) == :a)) halt_if(expr(not (^arg(:on_step) == :a || false)))
halt_reason :not_on_step_a halt_reason(:not_on_step_a)
end end
custom :b, Ash.Test.Flow.Steps.SimpleReturn do custom :b, Ash.Test.Flow.Steps.SimpleReturn do
input %{return: "b"} input(%{return: "b"})
halt_if expr(not (^arg(:on_step) == :b)) halt_if(expr(not (^arg(:on_step) == :b || false)))
wait_for result(:a) wait_for(result(:a))
halt_reason :not_on_step_b halt_reason(:not_on_step_b)
end end
custom :c, Ash.Test.Flow.Steps.SimpleReturn do custom :c, Ash.Test.Flow.Steps.SimpleReturn do
input %{return: "c"} input(%{return: "c"})
halt_if expr(not (^arg(:on_step) == :c)) halt_if(expr(not (^arg(:on_step) == :c || false)))
wait_for result(:b) wait_for(result(:b))
halt_reason :not_on_step_c halt_reason(:not_on_step_c)
end end
end end
end end