Skip to content

Commit 4ad8491

Browse files
add on_join_table_conflict to repo.insert
1 parent 270aceb commit 4ad8491

4 files changed

Lines changed: 59 additions & 3 deletions

File tree

lib/ecto/association.ex

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1554,8 +1554,9 @@ defmodule Ecto.Association.ManyToMany do
15541554
owner_value = dump!(:insert, join_through, owner, owner_key, adapter)
15551555
related_value = dump!(:insert, join_through, related, related_key, adapter)
15561556
data = %{join_owner_key => owner_value, join_related_key => related_value}
1557+
join_table_opts = put_join_table_on_conflict!(opts)
15571558

1558-
case insert_join(join_through, refl, parent_changeset, data, opts) do
1559+
case insert_join(join_through, refl, parent_changeset, data, join_table_opts) do
15591560
{:error, join_changeset} ->
15601561
{:error,
15611562
%{
@@ -1592,6 +1593,20 @@ defmodule Ecto.Association.ManyToMany do
15921593
"an atom (representing a schema) or a string (representing a table)"
15931594
end
15941595

1596+
defp put_join_table_on_conflict!(opts) do
1597+
case Keyword.fetch(opts, :on_join_table_conflict) do
1598+
{:ok, on_conflict} when on_conflict in [:raise, :nothing] ->
1599+
Keyword.put(opts, :on_conflict, on_conflict)
1600+
1601+
:error ->
1602+
opts
1603+
1604+
{:ok, other} ->
1605+
raise ArgumentError,
1606+
"expected `:on_join_table_conflict` to be one of `:raise` or `:nothing`, got: `#{inspect(other)}`"
1607+
end
1608+
end
1609+
15951610
defp insert_join?(%{action: :insert}, _, _field, _related_key), do: true
15961611
defp insert_join?(_, %{action: :insert}, _field, _related_key), do: true
15971612

lib/ecto/repo.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1905,6 +1905,12 @@ defmodule Ecto.Repo do
19051905
are not updated in order to enable optimizations such as HOT updates in PostgreSQL.
19061906
Defaults to `true`.
19071907
1908+
* `:on_join_table_conflict` - If a many-to-many association is part of the insert,
1909+
Ecto will automatically try to create the corresponding entry in the association's
1910+
`:join_through` table. This option allows you to configure the conflict resolution
1911+
behaviour when the record already exists. The allowed values are `:raise` (the default)
1912+
or `:nothing`.
1913+
19081914
* `:stale_error_field` - The field where stale errors will be added in
19091915
the returning changeset. This option can be used to avoid raising
19101916
`Ecto.StaleEntryError`.

lib/ecto/repo/schema.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,7 @@ defmodule Ecto.Repo.Schema do
11631163
defp assoc_opts([], _opts), do: []
11641164

11651165
defp assoc_opts(_assocs, opts) do
1166-
Keyword.take(opts, [:timeout, :log, :telemetry_event, :prefix, :allow_stale])
1166+
Keyword.take(opts, [:timeout, :log, :telemetry_event, :prefix, :allow_stale, :on_join_table_conflict])
11671167
end
11681168

11691169
defp process_parents(changeset, user_changeset, assocs, reset_assocs, adapter, opts) do

test/ecto/repo/many_to_many_test.exs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,45 @@ defmodule Ecto.Repo.ManyToManyTest do
107107
assert assoc.inserted_at
108108
assert_received {:insert, _}
109109

110-
assert_received {:insert_all, %{source: "schemas_assocs"},
110+
assert_received {:insert_all, %{source: "schemas_assocs", on_conflict: {:raise, [], []}},
111+
[[my_assoc_id: 1, my_schema_id: 1]]}
112+
end
113+
114+
test "handles assocs on insert with on_join_table_conflict" do
115+
sample = %MyAssoc{x: "xyz"}
116+
117+
changeset =
118+
%MySchema{}
119+
|> Ecto.Changeset.change()
120+
|> Ecto.Changeset.put_assoc(:assocs, [sample])
121+
122+
schema = TestRepo.insert!(changeset, on_join_table_conflict: :nothing)
123+
[assoc] = schema.assocs
124+
assert assoc.id
125+
assert assoc.x == "xyz"
126+
assert assoc.inserted_at
127+
assert_received {:insert, _}
128+
129+
assert_received {:insert_all, %{source: "schemas_assocs", on_conflict: {:nothing, [], []}},
111130
[[my_assoc_id: 1, my_schema_id: 1]]}
112131
end
113132

133+
test "on_join_table_conflict only accepts :raise or :nothing" do
134+
sample = %MyAssoc{x: "xyz"}
135+
136+
changeset =
137+
%MySchema{}
138+
|> Ecto.Changeset.change()
139+
|> Ecto.Changeset.put_assoc(:assocs, [sample])
140+
141+
msg =
142+
"expected `:on_join_table_conflict` to be one of `:raise` or `:nothing`, got: `:replace_all`"
143+
144+
assert_raise ArgumentError, msg, fn ->
145+
TestRepo.insert!(changeset, on_join_table_conflict: :replace_all)
146+
end
147+
end
148+
114149
test "handles assocs on insert preserving parent schema prefix" do
115150
sample = %MyAssoc{x: "xyz"}
116151

0 commit comments

Comments
 (0)