Skip to content

Commit 5ab355a

Browse files
authored
Permit outer joins when using update_all with PostreSQL (#457)
The PostgreSQL UPDATE command permits specifying multiple 'from_item' expressions to allow columns from other tables to appear in the update expression. In particular, the documentation explains (cf. https://www.postgresql.org/docs/current/sql-update.html): > This uses the same syntax as the FROM clause of a SELECT statement Thus, a statement such as UPDATE x SET a = z.a FROM y LEFT_JOIN z ON z.i = y.i is legal. However, executing query like this this via Ecto would fail, raising an error: ** (Ecto.QueryError) PostgreSQL supports only inner joins on update_all, got: `left` in query: from x0 in "x", join: y1 in "y", on: true, left_join: z2 in "z", on: true, update: [set: [a: 1]] This PR fixes the issue by adding a `Postgres.Connection.using_join/4` clause for the :update_all case: in this scenario, at least one inner join has to be specified, but additional other joins are permissible.
1 parent ddf4fb5 commit 5ab355a

File tree

2 files changed

+38
-0
lines changed

2 files changed

+38
-0
lines changed

lib/ecto/adapters/postgres/connection.ex

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,31 @@ if Code.ensure_loaded?(Postgrex) do
453453
end
454454

455455
defp using_join(%{joins: []}, _kind, _prefix, _sources), do: {[], []}
456+
457+
defp using_join(%{joins: joins} = query, :update_all, prefix, sources) do
458+
{inner_joins, other_joins} = Enum.split_while(joins, & &1.qual == :inner)
459+
460+
if inner_joins == [] and other_joins != [] do
461+
error!(query, "Need at least one inner join at the beginning to use other joins with update_all")
462+
end
463+
464+
froms =
465+
intersperse_map(inner_joins, ", ", fn
466+
%JoinExpr{qual: :inner, ix: ix, source: source} ->
467+
{join, name} = get_source(query, sources, ix, source)
468+
[join, " AS " | [name]]
469+
end)
470+
471+
join_clauses = join(%{query | joins: other_joins}, sources)
472+
473+
wheres =
474+
for %JoinExpr{on: %QueryExpr{expr: value} = expr} <- inner_joins,
475+
value != true,
476+
do: expr |> Map.put(:__struct__, BooleanExpr) |> Map.put(:op, :and)
477+
478+
{[?\s, prefix, ?\s, froms | join_clauses], wheres}
479+
end
480+
456481
defp using_join(%{joins: joins} = query, kind, prefix, sources) do
457482
froms =
458483
intersperse_map(joins, ", ", fn

test/ecto/adapters/postgres_test.exs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,19 @@ defmodule Ecto.Adapters.PostgresTest do
920920
~s{UPDATE "first"."schema" AS s0 SET "x" = 0}
921921
end
922922

923+
test "update all with left join" do
924+
query = from(m in Schema, join: x in assoc(m, :comments), left_join: p in assoc(m, :permalink), update: [set: [w: m.list2]]) |> plan(:update_all)
925+
assert update_all(query) ==
926+
~s{UPDATE "schema" AS s0 SET "w" = s0."list2" FROM "schema2" AS s1 LEFT OUTER JOIN "schema3" AS s2 ON s2."id" = s0."y" WHERE (s1."z" = s0."x")}
927+
end
928+
929+
test "update all with left join but no inner join" do
930+
query = from(m in Schema, left_join: p in assoc(m, :permalink), left_join: x in assoc(m, :permalink), update: [set: [w: m.list2]]) |> plan(:update_all)
931+
assert_raise Ecto.QueryError, ~r/Need at least one inner join at the beginning to use other joins with update_all/, fn ->
932+
update_all(query)
933+
end
934+
end
935+
923936
test "delete all" do
924937
query = Schema |> Queryable.to_query |> plan()
925938
assert delete_all(query) == ~s{DELETE FROM "schema" AS s0}

0 commit comments

Comments
 (0)