improvement: support latest ash exists/2 expr

This commit is contained in:
Zach Daniel 2022-09-06 18:33:17 -04:00
parent 16134be8a7
commit 170b9fc9e7
5 changed files with 193 additions and 8 deletions

View file

@ -613,7 +613,7 @@ defmodule AshPostgres.DataLayer do
@impl true
def set_tenant(_resource, query, tenant) do
{:ok, Ecto.Query.put_query_prefix(query, to_string(tenant))}
{:ok, Map.put(Ecto.Query.put_query_prefix(query, to_string(tenant)), :__tenant__, tenant)}
end
@impl true

View file

@ -2,7 +2,7 @@ defmodule AshPostgres.Expr do
@moduledoc false
alias Ash.Filter
alias Ash.Query.{BooleanExpression, Not, Ref}
alias Ash.Query.{BooleanExpression, Exists, Not, Ref}
alias Ash.Query.Operator.IsNil
alias Ash.Query.Function.{Ago, Contains, GetPath, If}
alias AshPostgres.Functions.{Fragment, TrigramSimilarity, Type}
@ -687,6 +687,78 @@ defmodule AshPostgres.Expr do
end
end
defp do_dynamic_expr(
query,
%Exists{path: [first | rest], expr: expr},
bindings,
_embedded?,
_type
) do
resource = query.__ash_bindings__.resource
first_relationship = Ash.Resource.Info.relationship(resource, first)
filter = %Ash.Filter{expression: expr, resource: first_relationship.destination}
{:ok, source} =
AshPostgres.Join.maybe_get_resource_query(
first_relationship.destination,
first_relationship,
query
)
{:ok, filtered} =
AshPostgres.DataLayer.filter(
source,
Ash.Filter.move_to_relationship_path(filter, rest),
first_relationship.destination
)
source_ref =
ref_binding(
%Ref{
attribute: Ash.Resource.Info.attribute(resource, first_relationship.source_attribute),
relationship_path: [],
resource: resource
},
bindings
)
exists_query =
if first_relationship.type == :many_to_many do
through_relationship =
Ash.Resource.Info.relationship(resource, first_relationship.join_relationship)
{:ok, through} =
AshPostgres.Join.maybe_get_resource_query(
first_relationship.through,
through_relationship,
query
)
Ecto.Query.from(destination in filtered,
join: through in ^through,
on:
field(through, ^first_relationship.destination_attribute_on_join_resource) ==
field(destination, ^first_relationship.destination_attribute),
on:
field(parent_as(^source_ref), ^first_relationship.source_attribute) ==
field(through, ^first_relationship.source_attribute_on_join_resource),
select: 1
)
# )
else
Ecto.Query.from(destination in filtered,
select: [1],
where:
field(parent_as(^source_ref), ^first_relationship.source_attribute) ==
field(destination, ^first_relationship.destination_attribute)
)
end
Ecto.Query.dynamic(exists(exists_query))
end
defp do_dynamic_expr(
_query,
%Ref{attribute: %Ash.Resource.Attribute{name: name}} = ref,

View file

@ -138,7 +138,7 @@ defmodule AshPostgres.MixProject do
{:ecto, "~> 3.8"},
{:jason, "~> 1.0"},
{:postgrex, ">= 0.0.0"},
{:ash, ash_version("~> 2.0.0-rc.0")},
{:ash, ash_version("~> 2.0.0-rc.3")},
{:git_ops, "~> 2.4.5", only: :dev},
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:ex_check, "~> 0.14", only: :dev},

View file

@ -1,5 +1,5 @@
%{
"ash": {:hex, :ash, "2.0.0-rc.0", "3ea236906afebef5ffd6a2cbb249d1787822ddd0048a5e976938cf9d1d26eae2", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:spark, "~> 0.1 and >= 0.1.9", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bef8a7cb7d1d60666ca4853aa82880f26a06b169826480e75cd24a23c9b79a79"},
"ash": {:hex, :ash, "2.0.0-rc.3", "0ee29ff744430c5bc4f3fb9f443624ef5ccfad1185afc0ce923b49693d77084b", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:spark, "~> 0.1 and >= 0.1.9", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "95bf582a19226e1615bb2f1cab32ab8c158e4cd29ae12deaa062a3934f89eff7"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
@ -10,7 +10,7 @@
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"},
"ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"},
"ecto_sql": {:hex, :ecto_sql, "3.8.0", "b00d2080e523f2aff6100f22305c1c748016570b9cebf5b29cc61d4924b038c2", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8a273619e05c4924b225d526810641e0bae8b0aa114b9cbe3cc5c70cf9e5d607"},
"ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.8.1", "8ff9bcda5682b98493f8878fc9dbd990e48d566cba8cce59f7c2a78130da29ea", [:mix], [], "hexpm", "6be41b50adb5bc5c43626f25ea2d0af1f4a242fb3fad8d53f0c67c20b78915cc"},
@ -23,7 +23,6 @@
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
"libgraph": {:hex, :libgraph, "0.13.3", "20732b7bafb933dcf7351c479e03076ebd14a85fd3202c67a1c197f4f7c2466b", [:mix], [], "hexpm", "78f2576eef615440b46f10060b1de1c86640441422832052686df53dc3c148c6"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
@ -33,10 +32,10 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.1", "407dcb90755167fd9e3311b60565ff32ed0d234010363406c07cdb4175b95bc5", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "68f4bdb2ac3b594209e54625d3d58c9e2e98b90f2ec8e03235f66e88c9eda5fe"},
"postgrex": {:hex, :postgrex, "0.16.2", "0f83198d0e73a36e8d716b90f45f3bde75b5eebf4ade4f43fa1f88c90a812f74", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a9ea589754d9d4d076121090662b7afe155b374897a6550eb288f11d755acfa0"},
"postgrex": {:hex, :postgrex, "0.16.4", "26d998467b4a22252285e728a29d341e08403d084e44674784975bb1cd00d2cb", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3234d1a70cb7b1e0c95d2e242785ec2a7a94a092bbcef4472320b950cfd64c5f"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"sourceror": {:hex, :sourceror, "0.11.2", "549ce48be666421ac60cfb7f59c8752e0d393baa0b14d06271d3f6a8c1b027ab", [:mix], [], "hexpm", "9ab659118896a36be6eec68ff7b0674cba372fc8e210b1e9dc8cf2b55bb70dfb"},
"spark": {:hex, :spark, "0.1.18", "2cf48ea8f695da7e413ff5e16e83c0dbd6e9810a61a67fc6601b702199e9baf3", [:mix], [{:libgraph, "~> 0.13.3", [hex: :libgraph, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "e4e7bd47de30a2830cc5d1f12c6db5d8ead9c5ba336e5d8f2ff26495d4e3f41d"},
"spark": {:hex, :spark, "0.1.19", "f4093755f74423c4dfc64f308d16ac00c7c4e69fc186329aae9ffd3b4b062ffa", [:mix], [{:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "5a34859c8a8aa36a1633270117f64ecabd92596cf68943650306c31af0013e58"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},

View file

@ -464,6 +464,120 @@ defmodule AshPostgres.FilterTest do
end
end
describe "exists/2" do
test "it works with single relationships" do
post =
Post
|> Ash.Changeset.new(%{title: "match"})
|> Api.create!()
Comment
|> Ash.Changeset.new(%{title: "abba"})
|> Ash.Changeset.replace_relationship(:post, post)
|> Api.create!()
post2 =
Post
|> Ash.Changeset.new(%{title: "no_match"})
|> Api.create!()
Comment
|> Ash.Changeset.new(%{title: "acca"})
|> Ash.Changeset.replace_relationship(:post, post2)
|> Api.create!()
assert [%{title: "match"}] =
Post
|> Ash.Query.filter(exists(comments, title == ^"abba"))
|> Api.read!()
end
test "it works with many to many relationships" do
post =
Post
|> Ash.Changeset.new(%{title: "a"})
|> Api.create!()
Post
|> Ash.Changeset.new(%{title: "b"})
|> Ash.Changeset.replace_relationship(:linked_posts, [post])
|> Api.create!()
assert [%{title: "b"}] =
Post
|> Ash.Query.filter(exists(linked_posts, title == ^"a"))
|> Api.read!()
end
test "it works with nested relationships as the path" do
post =
Post
|> Ash.Changeset.new(%{title: "a"})
|> Api.create!()
Comment
|> Ash.Changeset.new(%{title: "comment"})
|> Ash.Changeset.replace_relationship(:post, post)
|> Api.create!()
Post
|> Ash.Changeset.new(%{title: "b"})
|> Ash.Changeset.replace_relationship(:linked_posts, [post])
|> Api.create!()
assert [%{title: "b"}] =
Post
|> Ash.Query.filter(exists(linked_posts.comments, title == ^"comment"))
|> Api.read!()
end
test "it works with nested relationships inside of exists" do
post =
Post
|> Ash.Changeset.new(%{title: "a"})
|> Api.create!()
Comment
|> Ash.Changeset.new(%{title: "comment"})
|> Ash.Changeset.replace_relationship(:post, post)
|> Api.create!()
Post
|> Ash.Changeset.new(%{title: "b"})
|> Ash.Changeset.replace_relationship(:linked_posts, [post])
|> Api.create!()
assert [%{title: "b"}] =
Post
|> Ash.Query.filter(exists(linked_posts, comments.title == ^"comment"))
|> Api.read!()
end
test "it works with aggregates inside of exists" do
post =
Post
|> Ash.Changeset.new(%{title: "a", category: "foo"})
|> Api.create!()
Comment
|> Ash.Changeset.new(%{title: "comment"})
|> Ash.Changeset.replace_relationship(:post, post)
|> Api.create!()
Post
|> Ash.Changeset.new(%{title: "b"})
|> Ash.Changeset.replace_relationship(:linked_posts, [post])
|> Api.create!()
assert [%{title: "b"}] =
Post
|> Ash.Query.filter(
exists(linked_posts.comments, title == ^"comment" and post_category == "foo")
)
|> Api.read!()
end
end
describe "filtering on enum types" do
test "it allows simple filtering" do
Post