mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
fix: treat empty string as nil
in manage_relationship
fix: be more conservative (and more correct) when optimizing predicates
This commit is contained in:
parent
e10b273ce4
commit
e60e5bf281
5 changed files with 186 additions and 69 deletions
|
@ -71,7 +71,8 @@
|
||||||
{Credo.Check.Consistency.ExceptionNames, []},
|
{Credo.Check.Consistency.ExceptionNames, []},
|
||||||
{Credo.Check.Consistency.LineEndings, []},
|
{Credo.Check.Consistency.LineEndings, []},
|
||||||
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||||
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
# This check was erroring on sigils so I had to disable it
|
||||||
|
{Credo.Check.Consistency.SpaceAroundOperators, false},
|
||||||
{Credo.Check.Consistency.SpaceInParentheses, []},
|
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||||
{Credo.Check.Consistency.TabsOrSpaces, []},
|
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||||
|
|
||||||
|
|
|
@ -1028,7 +1028,13 @@ defmodule Ash.Changeset do
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
def manage_relationship(changeset, relationship, input, opts \\ []) do
|
def manage_relationship(changeset, relationship, input, opts \\ [])
|
||||||
|
|
||||||
|
def manage_relationship(changeset, relationship, "", opts) do
|
||||||
|
manage_relationship(changeset, relationship, nil, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def manage_relationship(changeset, relationship, input, opts) do
|
||||||
opts = Ash.OptionsHelpers.validate!(opts, @manage_opts)
|
opts = Ash.OptionsHelpers.validate!(opts, @manage_opts)
|
||||||
|
|
||||||
case Ash.Resource.Info.relationship(changeset.resource, relationship) do
|
case Ash.Resource.Info.relationship(changeset.resource, relationship) do
|
||||||
|
|
|
@ -15,6 +15,13 @@ defmodule Ash.Query.BooleanExpression do
|
||||||
%__MODULE__{op: op, left: left, right: right}
|
%__MODULE__{op: op, left: left, right: right}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# In many cases we could actually just return `true/false` directly because we know
|
||||||
|
# statements are contradictions. However, that would likely confuse users. For example:
|
||||||
|
# `Ash.Query.filter(Resource, x == 1 and x in [2, 3])` becomes `#Ash.Query<filter: false>`
|
||||||
|
# We may want to go down this route some day, but for now we simply use this to combine
|
||||||
|
# statements where possible, which helps with authorization logic that leverages the query
|
||||||
|
# For example, `x in [1, 2] or x == 3` becomes `x in [1, 2, 3]`, and `x in [1, 2, 3] and x != 1`
|
||||||
|
# becomes `x in [2, 3]`
|
||||||
def optimized_new(op, left, right, current_op \\ :and)
|
def optimized_new(op, left, right, current_op \\ :and)
|
||||||
def optimized_new(_, nil, nil, _), do: nil
|
def optimized_new(_, nil, nil, _), do: nil
|
||||||
def optimized_new(:and, false, _, _), do: false
|
def optimized_new(:and, false, _, _), do: false
|
||||||
|
@ -71,51 +78,68 @@ defmodule Ash.Query.BooleanExpression do
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:or,
|
:or,
|
||||||
%Eq{left: left, right: value},
|
%Eq{left: left, right: value} = left_op,
|
||||||
%In{left: left, right: %{__struct__: MapSet} = mapset} = right,
|
%In{left: left, right: %{__struct__: MapSet} = mapset} = right,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
%{right | right: MapSet.put(mapset, value)}
|
if can_optimize?(value) do
|
||||||
|
%{right | right: MapSet.put(mapset, value)}
|
||||||
|
else
|
||||||
|
do_new(:or, left_op, right)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:or,
|
:or,
|
||||||
%NotEq{left: left, right: value},
|
%NotEq{left: left, right: value} = left_op,
|
||||||
%In{left: left, right: %{__struct__: MapSet} = mapset} = right,
|
%In{left: left, right: %{__struct__: MapSet} = mapset} = right,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
without = MapSet.delete(mapset, value)
|
if can_optimize?(value) do
|
||||||
|
without = MapSet.delete(mapset, value)
|
||||||
|
|
||||||
case MapSet.size(without) do
|
case MapSet.size(without) do
|
||||||
0 -> false
|
0 ->
|
||||||
1 -> %Eq{left: left, right: Enum.at(without, 0)}
|
do_new(:or, left_op, right)
|
||||||
_ -> %{right | right: without}
|
|
||||||
|
1 ->
|
||||||
|
%Eq{left: left, right: Enum.at(without, 0)}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
%{right | right: without}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
do_new(:or, left_op, right)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:and,
|
:and,
|
||||||
%Eq{left: left, right: value} = left_expr,
|
%Eq{left: left, right: value} = left_expr,
|
||||||
%In{left: left, right: %{__struct__: MapSet} = mapset},
|
%In{left: left, right: %{__struct__: MapSet} = mapset} = right,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
if MapSet.member?(mapset, value) do
|
if can_optimize?(value) do
|
||||||
left_expr
|
if MapSet.member?(mapset, value) do
|
||||||
|
left_expr
|
||||||
|
else
|
||||||
|
do_new(:or, left_expr, right)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
false
|
do_new(:or, left_expr, right)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:and,
|
:and,
|
||||||
%NotEq{left: left, right: value},
|
%NotEq{left: left, right: value} = left_op,
|
||||||
%In{left: left, right: %{__struct__: MapSet} = mapset} = right_expr,
|
%In{left: left, right: %{__struct__: MapSet} = mapset} = right_expr,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
if MapSet.member?(mapset, value) do
|
if can_optimize?(value) do
|
||||||
false
|
%{right_expr | right: MapSet.delete(mapset, value)}
|
||||||
else
|
else
|
||||||
right_expr
|
do_new(:or, left_op, right_expr)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -130,77 +154,68 @@ defmodule Ash.Query.BooleanExpression do
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:or,
|
:or,
|
||||||
%Eq{left: left, right: left_value},
|
%Eq{left: left, right: left_value} = left_expr,
|
||||||
%Eq{left: left, right: right_value},
|
|
||||||
_
|
|
||||||
) do
|
|
||||||
%In{left: left, right: MapSet.new([left_value, right_value])}
|
|
||||||
end
|
|
||||||
|
|
||||||
def optimized_new(
|
|
||||||
:or,
|
|
||||||
%NotEq{left: left, right: left_value},
|
|
||||||
%Eq{left: left, right: right_value} = right_expr,
|
%Eq{left: left, right: right_value} = right_expr,
|
||||||
_
|
_
|
||||||
)
|
) do
|
||||||
when left_value != right_value do
|
if can_optimize?(left_value) && can_optimize?(right_value) do
|
||||||
right_expr
|
%In{left: left, right: MapSet.new([left_value, right_value])}
|
||||||
end
|
else
|
||||||
|
do_new(:or, left_expr, right_expr)
|
||||||
def optimized_new(
|
end
|
||||||
:or,
|
|
||||||
%NotEq{left: left, right: left_value},
|
|
||||||
%Eq{left: left, right: right_value},
|
|
||||||
_
|
|
||||||
)
|
|
||||||
when left_value == right_value do
|
|
||||||
true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:and,
|
:and,
|
||||||
%Eq{left: left, right: left_value} = left_expr,
|
%Eq{left: left, right: left_value} = left_expr,
|
||||||
%Eq{left: left, right: right_value},
|
%Eq{left: left, right: right_value} = right_expr,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
if left_value == right_value do
|
if can_optimize?(left_value) && can_optimize?(right_value) && left_value == right_value do
|
||||||
left_expr
|
left_expr
|
||||||
else
|
else
|
||||||
false
|
do_new(:and, left_expr, right_expr)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:and,
|
:and,
|
||||||
%NotEq{left: left, right: left_value} = left_expr,
|
%NotEq{left: left, right: left_value} = left_expr,
|
||||||
%NotEq{left: left, right: right_value},
|
%NotEq{left: left, right: right_value} = right_expr,
|
||||||
_
|
_
|
||||||
)
|
) do
|
||||||
when left_value == right_value do
|
if can_optimize?(left_value) && can_optimize?(right_value) && left_value == right_value do
|
||||||
left_expr
|
left_value
|
||||||
|
else
|
||||||
|
do_new(:and, left_expr, right_expr)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:or,
|
:or,
|
||||||
%In{left: left, right: left_values},
|
%In{left: left, right: %{__struct__: MapSet} = left_values},
|
||||||
%In{left: left, right: right_values} = right,
|
%In{left: left, right: %{__struct__: MapSet} = right_values} = right_expr,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
%{right | right: MapSet.union(left_values, right_values)}
|
%{right_expr | right: MapSet.union(left_values, right_values)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def optimized_new(
|
def optimized_new(
|
||||||
:and,
|
:and,
|
||||||
%In{left: left, right: left_values},
|
%In{left: left, right: left_values} = left_expr,
|
||||||
%In{left: left, right: right_values} = right,
|
%In{left: left, right: right_values} = right_expr,
|
||||||
_
|
_
|
||||||
) do
|
) do
|
||||||
intersection = MapSet.intersection(left_values, right_values)
|
if can_optimize?(left_values) && can_optimize?(right_values) do
|
||||||
|
intersection = MapSet.intersection(left_values, right_values)
|
||||||
|
|
||||||
case MapSet.size(intersection) do
|
case MapSet.size(intersection) do
|
||||||
0 -> false
|
0 -> do_new(:and, left_expr, right_expr)
|
||||||
1 -> %Eq{left: left, right: Enum.at(intersection, 0)}
|
1 -> %Eq{left: left, right: Enum.at(intersection, 0)}
|
||||||
_ -> %{right | right: intersection}
|
_ -> %{right_expr | right: intersection}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
do_new(:and, left_expr, right_expr)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -253,32 +268,61 @@ defmodule Ash.Query.BooleanExpression do
|
||||||
|
|
||||||
defp simplify?(%Eq{right: %Ref{}}, _), do: false
|
defp simplify?(%Eq{right: %Ref{}}, _), do: false
|
||||||
defp simplify?(_, %Eq{right: %Ref{}}), do: false
|
defp simplify?(_, %Eq{right: %Ref{}}), do: false
|
||||||
defp simplify?(%Eq{left: left}, %Eq{left: left}), do: true
|
|
||||||
|
defp simplify?(%Eq{left: left, right: left_right}, %Eq{left: left, right: right_right}) do
|
||||||
|
can_optimize?(left_right) && can_optimize?(right_right)
|
||||||
|
end
|
||||||
|
|
||||||
defp simplify?(%NotEq{right: %Ref{}}, _), do: false
|
defp simplify?(%NotEq{right: %Ref{}}, _), do: false
|
||||||
defp simplify?(_, %NotEq{right: %Ref{}}), do: false
|
defp simplify?(_, %NotEq{right: %Ref{}}), do: false
|
||||||
defp simplify?(%NotEq{left: left}, %NotEq{left: left}), do: true
|
|
||||||
|
defp simplify?(%NotEq{left: left, right: left_right}, %NotEq{left: left, right: right_right}) do
|
||||||
|
can_optimize?(left_right) && can_optimize?(right_right)
|
||||||
|
end
|
||||||
|
|
||||||
defp simplify?(
|
defp simplify?(
|
||||||
%Eq{left: left},
|
%Eq{left: left, right: left_right},
|
||||||
%In{left: left, right: %MapSet{}}
|
%In{left: left, right: %MapSet{}}
|
||||||
),
|
) do
|
||||||
do: true
|
can_optimize?(left_right)
|
||||||
|
end
|
||||||
|
|
||||||
defp simplify?(
|
defp simplify?(
|
||||||
%NotEq{left: left},
|
%NotEq{left: left, right: left_right},
|
||||||
%In{left: left, right: %MapSet{}}
|
%In{left: left, right: %MapSet{}}
|
||||||
),
|
) do
|
||||||
do: true
|
can_optimize?(left_right)
|
||||||
|
end
|
||||||
|
|
||||||
defp simplify?(
|
defp simplify?(
|
||||||
%Eq{left: left},
|
%Eq{left: left, right: left_right},
|
||||||
%NotEq{left: left, right: %MapSet{}}
|
%NotEq{left: left, right: %MapSet{}}
|
||||||
),
|
) do
|
||||||
do: true
|
can_optimize?(left_right)
|
||||||
|
end
|
||||||
|
|
||||||
defp simplify?(_, _), do: false
|
defp simplify?(_, _), do: false
|
||||||
|
|
||||||
|
defp can_optimize?(value) when is_list(value) do
|
||||||
|
Enum.all?(value, &can_optimize?/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp can_optimize?(map) when is_map(map) and not is_struct(map) do
|
||||||
|
Enum.all?(map, fn {key, val} ->
|
||||||
|
can_optimize?(key) && can_optimize?(val)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp can_optimize?(%{__struct__: MapSet} = mapset) do
|
||||||
|
Enum.all?(mapset, &can_optimize?/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp can_optimize?(value) when is_integer(value) or is_binary(value) do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
defp can_optimize?(_), do: false
|
||||||
|
|
||||||
defp do_new(op, left, right) do
|
defp do_new(op, left, right) do
|
||||||
if left == right do
|
if left == right do
|
||||||
left
|
left
|
||||||
|
|
|
@ -79,6 +79,14 @@ defmodule Ash.Query.Operator do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp try_cast(%{__predicate__?: _} = left, right, _) do
|
||||||
|
{:ok, left, right}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp try_cast(left, %{__predicate__?: _} = right, _) do
|
||||||
|
{:ok, left, right}
|
||||||
|
end
|
||||||
|
|
||||||
defp try_cast(%Ref{attribute: %{type: type}} = left, right, :same) do
|
defp try_cast(%Ref{attribute: %{type: type}} = left, right, :same) do
|
||||||
case Ash.Query.Type.try_cast(right, type) do
|
case Ash.Query.Type.try_cast(right, type) do
|
||||||
{:ok, new_right} ->
|
{:ok, new_right} ->
|
||||||
|
|
|
@ -171,6 +171,64 @@ defmodule Ash.Test.Filter.FilterTest do
|
||||||
|
|
||||||
import Ash.Changeset
|
import Ash.Changeset
|
||||||
|
|
||||||
|
describe "predicate optimization" do
|
||||||
|
# Testing against the stringified query may be a bad idea, but its a quick win and we
|
||||||
|
# can switch to actually checking the structure if this bites us
|
||||||
|
test "equality simplifies to `in`" do
|
||||||
|
stringified_query =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.filter(title == "foo" or title == "bar")
|
||||||
|
|> inspect()
|
||||||
|
|
||||||
|
assert stringified_query =~ ~S(title in ["bar", "foo"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "in with equality simplifies to `in`" do
|
||||||
|
stringified_query =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.filter(title in ["foo", "bar", "baz"] or title == "bar")
|
||||||
|
|> inspect()
|
||||||
|
|
||||||
|
assert stringified_query =~ ~S(title in ["bar", "baz", "foo"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "in with non-equality simplifies to `in`" do
|
||||||
|
stringified_query =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.filter(title in ["foo", "bar", "baz"] and title != "bar")
|
||||||
|
|> inspect()
|
||||||
|
|
||||||
|
assert stringified_query =~ ~S(title in ["baz", "foo"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "in with or-in simplifies to `in`" do
|
||||||
|
stringified_query =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.filter(title in ["foo", "bar"] or title in ["bar", "baz"])
|
||||||
|
|> inspect()
|
||||||
|
|
||||||
|
assert stringified_query =~ ~S(title in ["bar", "baz", "foo"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "in with and-in simplifies to `in` when multiple values overlap" do
|
||||||
|
stringified_query =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.filter(title in ["foo", "bar", "baz"] and title in ["bar", "baz", "bif"])
|
||||||
|
|> inspect()
|
||||||
|
|
||||||
|
assert stringified_query =~ ~S(title in ["bar", "baz"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "in with and-in simplifies to `eq` when one value overlaps" do
|
||||||
|
stringified_query =
|
||||||
|
Post
|
||||||
|
|> Ash.Query.filter(title in ["foo", "bar"] and title in ["bar", "baz", "bif"])
|
||||||
|
|> inspect()
|
||||||
|
|
||||||
|
assert stringified_query =~ ~S(title == "bar")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "simple attribute filters" do
|
describe "simple attribute filters" do
|
||||||
setup do
|
setup do
|
||||||
post1 =
|
post1 =
|
||||||
|
|
Loading…
Reference in a new issue