diff --git a/lib/predicate_converter.ex b/lib/predicate_converter.ex index 6905357..6bfefee 100644 --- a/lib/predicate_converter.ex +++ b/lib/predicate_converter.ex @@ -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: @@ -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 diff --git a/test/predicates_test.exs b/test/predicates_test.exs index 4433148..9acab97 100644 --- a/test/predicates_test.exs +++ b/test/predicates_test.exs @@ -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 @@ -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 )