Skip to content
Closed
29 changes: 24 additions & 5 deletions lib/predicate_converter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,11 @@ defmodule Predicates.PredicateConverter do
)

defp convert_not_eq({:single, field}, value),
do: dynamic([q], field(q, ^field) != ^value or is_nil(field(q, ^field)))
do:
dynamic(
[q],
field(q, ^field) != ^value or is_nil(field(q, ^field))
)

defp convert_not_eq({:virtual, field, type, json_path}, value),
do:
Expand Down Expand Up @@ -461,17 +465,32 @@ defmodule Predicates.PredicateConverter do

sub_schema = get_schema(sub_association)

# define the subquery to execute the given predicates against the defined association
subquery =
association =
from(s in sub_schema,
select: 1,
where:
field(s, ^sub_association.related_key) ==
field(parent_as(^parent_table_name), ^sub_association.owner_key)
)
|> build_sub_query(sub_predicate, meta)

dynamic(exists(subquery))
subquery = build_sub_query(association, sub_predicate, meta)

cond do
sub_predicate["op"] == "not_eq" and not is_nil(sub_predicate["arg"]) ->
dynamic(exists(subquery) or not exists(association))

sub_predicate["op"] == "eq" and is_nil(sub_predicate["arg"]) ->
dynamic(exists(subquery) or not exists(association))

sub_predicate["op"] == "in" and Enum.any?(sub_predicate["arg"], &is_nil(&1)) ->
dynamic(exists(subquery) or not exists(association))

sub_predicate["op"] == "not_in" and not Enum.any?(sub_predicate["arg"], &is_nil(&1)) ->
dynamic(exists(subquery) or not exists(association))

true ->
dynamic(exists(subquery))
end
end
end

Expand Down
85 changes: 84 additions & 1 deletion test/predicates_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,87 @@ defmodule PredicatesTest do
})
|> Predicates.Repo.one()
end

test "nil handling in associations" do
{3, [goethe, schiller, lessing]} =
Predicates.Repo.insert_all(
Author,
[%{name: "Goethe"}, %{name: "Schiller"}, %{name: "Lessing"}],
returning: true
)

# logic test
Predicates.Repo.insert_all(Post, [
%{name: "Faust", author_id: goethe.id},
%{name: nil, author_id: schiller.id}
])

# eq and not_eq tests
assert [schiller, lessing] =
Converter.build_query(Author, %{
"op" => "eq",
"path" => "posts.name",
"arg" => nil
})
|> Predicates.Repo.all()

assert [goethe] =
Converter.build_query(Author, %{
"op" => "not_eq",
"path" => "posts.name",
"arg" => nil
})
|> Predicates.Repo.all()

assert [goethe] =
Converter.build_query(Author, %{
"op" => "eq",
"path" => "posts.name",
"arg" => "Faust"
})
|> Predicates.Repo.all()

assert [schiller, lessing] =
Converter.build_query(Author, %{
"op" => "not_eq",
"path" => "posts.name",
"arg" => "Faust"
})
|> Predicates.Repo.all()

# in and not_in tests
assert [schiller, lessing] =
Converter.build_query(Author, %{
"op" => "in",
"path" => "posts.name",
"arg" => [nil]
})
|> Predicates.Repo.all()

assert [goethe] =
Converter.build_query(Author, %{
"op" => "not_in",
"path" => "posts.name",
"arg" => [nil]
})
|> Predicates.Repo.all()

assert [goethe] =
Converter.build_query(Author, %{
"op" => "in",
"path" => "posts.name",
"arg" => ["Faust"]
})
|> Predicates.Repo.all()

assert [schiller, lessing] =
Converter.build_query(Author, %{
"op" => "not_in",
"path" => "posts.name",
"arg" => ["Faust"]
})
|> Predicates.Repo.all()
end
end

test "supports shorthand true/false expressions" do
Expand Down Expand Up @@ -756,7 +837,9 @@ defmodule PredicatesTest do

test "walks association" do
{2, [goethe, schiller]} =
Predicates.Repo.insert_all(Author, [%{name: "Goethe"}, %{name: "Schiller"}],
Predicates.Repo.insert_all(
Author,
[%{name: "Goethe"}, %{name: "Schiller"}],
returning: true
)

Expand Down