Skip to content

Commit 6d82579

Browse files
committed
fix: rework the update and destroy query builder to support multiple kinds of joining
1 parent e597c39 commit 6d82579

File tree

2 files changed

+133
-50
lines changed

2 files changed

+133
-50
lines changed

lib/data_layer.ex

Lines changed: 94 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,7 +1310,98 @@ defmodule AshPostgres.DataLayer do
13101310
end
13111311
end
13121312

1313-
defp bulk_updatable_query(query, resource, atomics, calculations, context) do
1313+
defp bulk_updatable_query(query, resource, atomics, calculations, context, type \\ :update) do
1314+
requires_adding_inner_join? =
1315+
case type do
1316+
:update ->
1317+
# could potentially optimize this to avoid the subquery by shuffling free
1318+
# inner joins to the top of the query
1319+
has_inner_join_to_start? =
1320+
case Enum.at(query.joins, 0) do
1321+
nil ->
1322+
false
1323+
1324+
%{qual: :inner} ->
1325+
true
1326+
1327+
_ ->
1328+
false
1329+
end
1330+
1331+
cond do
1332+
has_inner_join_to_start? ->
1333+
false
1334+
1335+
Enum.any?(query.joins, &(&1.qual != :inner)) ->
1336+
true
1337+
1338+
Enum.any?(atomics ++ calculations, fn {_, expr} ->
1339+
Ash.Filter.list_refs(expr) |> Enum.any?(&(&1.relationship_path != []))
1340+
end) ->
1341+
true
1342+
1343+
true ->
1344+
false
1345+
end
1346+
1347+
:destroy ->
1348+
Enum.any?(query.joins, &(&1.qual != :inner)) ||
1349+
Enum.any?(atomics ++ calculations, fn {_, expr} ->
1350+
expr |> Ash.Filter.list_refs() |> Enum.any?(&(&1.relationship_path != []))
1351+
end)
1352+
end
1353+
1354+
needs_to_join? =
1355+
requires_adding_inner_join? ||
1356+
query.limit || query.offset
1357+
1358+
query =
1359+
if needs_to_join? do
1360+
root_query = Ecto.Query.exclude(query, :select)
1361+
1362+
root_query =
1363+
cond do
1364+
query.limit || query.offset ->
1365+
from(row in Ecto.Query.subquery(root_query), [])
1366+
1367+
!Enum.empty?(query.joins) ->
1368+
from(row in Ecto.Query.subquery(Ecto.Query.exclude(root_query, :order_by)), [])
1369+
1370+
true ->
1371+
Ecto.Query.exclude(root_query, :order_by)
1372+
end
1373+
1374+
dynamic =
1375+
Enum.reduce(Ash.Resource.Info.primary_key(resource), nil, fn pkey, dynamic ->
1376+
if dynamic do
1377+
Ecto.Query.dynamic(
1378+
[row, joining],
1379+
field(row, ^pkey) == field(joining, ^pkey) and ^dynamic
1380+
)
1381+
else
1382+
Ecto.Query.dynamic([row, joining], field(row, ^pkey) == field(joining, ^pkey))
1383+
end
1384+
end)
1385+
1386+
faked_query =
1387+
from(row in query.from.source,
1388+
inner_join: limiter in ^root_query,
1389+
as: ^0,
1390+
on: ^dynamic
1391+
)
1392+
|> AshSql.Bindings.default_bindings(
1393+
query.__ash_bindings__.resource,
1394+
AshPostgres.SqlImplementation,
1395+
context
1396+
)
1397+
1398+
faked_query
1399+
else
1400+
query
1401+
|> Ecto.Query.exclude(:select)
1402+
|> Ecto.Query.exclude(:order_by)
1403+
end
1404+
13141405
Enum.reduce_while(atomics ++ calculations, {:ok, query}, fn {_, expr}, {:ok, query} ->
13151406
used_aggregates =
13161407
Ash.Filter.used_aggregates(expr, [])
@@ -1332,53 +1423,6 @@ defmodule AshPostgres.DataLayer do
13321423
{:halt, {:error, error}}
13331424
end
13341425
end)
1335-
|> case do
1336-
{:ok, query} ->
1337-
needs_to_join? =
1338-
Enum.any?(query.joins, &(&1.qual != :inner)) || query.limit || query.offset
1339-
1340-
if needs_to_join? do
1341-
root_query = Ecto.Query.exclude(query, :select)
1342-
1343-
root_query =
1344-
if query.limit || query.offset do
1345-
Map.put(root_query, :order_bys, query.order_bys)
1346-
else
1347-
Ecto.Query.exclude(root_query, :order_by)
1348-
end
1349-
1350-
dynamic =
1351-
Enum.reduce(Ash.Resource.Info.primary_key(resource), nil, fn pkey, dynamic ->
1352-
if dynamic do
1353-
Ecto.Query.dynamic(
1354-
[row, joining],
1355-
field(row, ^pkey) == field(joining, ^pkey) and ^dynamic
1356-
)
1357-
else
1358-
Ecto.Query.dynamic([row, joining], field(row, ^pkey) == field(joining, ^pkey))
1359-
end
1360-
end)
1361-
1362-
faked_query =
1363-
from(row in query.from.source,
1364-
inner_join: limiter in ^subquery(root_query),
1365-
as: ^0,
1366-
on: ^dynamic
1367-
)
1368-
|> Map.put(:__ash_bindings__, query.__ash_bindings__)
1369-
1370-
{:ok, faked_query}
1371-
else
1372-
{:ok,
1373-
query
1374-
|> AshSql.Bindings.default_bindings(resource, AshPostgres.SqlImplementation, context)
1375-
|> Ecto.Query.exclude(:select)
1376-
|> Ecto.Query.exclude(:order_by)}
1377-
end
1378-
1379-
{:error, error} ->
1380-
{:error, error}
1381-
end
13821426
end
13831427

13841428
@impl true
@@ -1399,7 +1443,8 @@ defmodule AshPostgres.DataLayer do
13991443
resource,
14001444
changeset.atomics,
14011445
options[:calculations] || [],
1402-
changeset.context
1446+
changeset.context,
1447+
:destroy
14031448
) do
14041449
{:error, error} ->
14051450
{:error, error}

test/atomics_test.exs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule AshPostgres.AtomicsTest do
44
alias AshPostgres.Test.Post
55

66
import Ash.Expr
7+
require Ash.Query
78

89
test "atomics work on upserts" do
910
id = Ash.UUID.generate()
@@ -103,7 +104,7 @@ defmodule AshPostgres.AtomicsTest do
103104
assert Post.increment_score!(post, 2).score == 4
104105
end
105106

106-
test "use rel in atomic update" do
107+
test "relationships can be used in atomic update" do
107108
author =
108109
Author
109110
|> Ash.Changeset.for_create(:create, %{first_name: "John", last_name: "Doe"})
@@ -121,4 +122,41 @@ defmodule AshPostgres.AtomicsTest do
121122

122123
assert post.title == "John"
123124
end
125+
126+
test "relationships can be used in atomic update and in an atomic update filter" do
127+
author =
128+
Author
129+
|> Ash.Changeset.for_create(:create, %{first_name: "John", last_name: "Doe"})
130+
|> Ash.create!()
131+
132+
Post
133+
|> Ash.Changeset.for_create(:create, %{price: 1, author_id: author.id})
134+
|> Ash.create!()
135+
136+
post =
137+
Post
138+
|> Ash.Query.filter(author.last_name == "Doe")
139+
|> Ash.bulk_update!(:set_title_from_author, %{}, return_records?: true)
140+
|> Map.get(:records)
141+
|> List.first()
142+
143+
assert post.title == "John"
144+
end
145+
146+
test "relationships can be used in atomic update and in an atomic update filter when first join is a left join" do
147+
author =
148+
Author
149+
|> Ash.Changeset.for_create(:create, %{first_name: "John", last_name: "Doe"})
150+
|> Ash.create!()
151+
152+
Post
153+
|> Ash.Changeset.for_create(:create, %{price: 1, author_id: author.id})
154+
|> Ash.create!()
155+
156+
assert [] =
157+
Post
158+
|> Ash.Query.filter(is_nil(author.last_name))
159+
|> Ash.bulk_update!(:set_title_from_author, %{}, return_records?: true)
160+
|> Map.get(:records)
161+
end
124162
end

0 commit comments

Comments
 (0)