Skip to content

Commit 3560bad

Browse files
Add replace_changed option to inserts (#4700)
1 parent 682c07d commit 3560bad

File tree

3 files changed

+42
-7
lines changed

3 files changed

+42
-7
lines changed

lib/ecto/repo.ex

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1675,6 +1675,11 @@ defmodule Ecto.Repo do
16751675
`{:unsafe_fragment, "(coalesce(firstname, ''), coalesce(lastname, '')) WHERE middlename IS NULL"}` for
16761676
`ON CONFLICT (coalesce(firstname, ''), coalesce(lastname, '')) WHERE middlename IS NULL` SQL query.
16771677
1678+
* `:replace_changed` - Whether to include `:conflict_target` fields when `:on_conflict`
1679+
is `:replace_all` or `{:replace_all_except, fields}`. If `true`, the conflict target
1680+
fields are not updated in order to enable optimizations such as HOT updates in PostgreSQL.
1681+
Defaults to `true`.
1682+
16781683
* `:placeholders` - A map with placeholders. This feature is not supported
16791684
by all databases. See the ["Placeholders" section](#c:insert_all/3-placeholders) for more information.
16801685
@@ -1718,7 +1723,9 @@ defmodule Ecto.Repo do
17181723
such as IDs and autogenerated timestamps (`inserted_at` and `updated_at`).
17191724
Do not use this option if you have auto-incrementing primary keys, as they
17201725
will also be replaced. You most likely want to use `{:replace_all_except, [:id]}`
1721-
or `{:replace, fields}` explicitly instead. This option requires a schema
1726+
or `{:replace, fields}` explicitly instead. This option requires a schema. Fields
1727+
specified by `:conflict_target` will be ignored unless `:replace_changed` is
1728+
configured to be `false`
17221729
17231730
* `{:replace_all_except, fields}` - same as above except the given fields
17241731
(and the ones given as conflict target) are not replaced. This option
@@ -1842,6 +1849,11 @@ defmodule Ecto.Repo do
18421849
`{:unsafe_fragment, "(coalesce(firstname, ""), coalesce(lastname, "")) WHERE middlename IS NULL"}` for
18431850
`ON CONFLICT (coalesce(firstname, ""), coalesce(lastname, "")) WHERE middlename IS NULL` SQL query.
18441851
1852+
* `:replace_changed` - Whether to include `:conflict_target` fields when `:on_conflict`
1853+
is `:replace_all` or `{:replace_all_except, fields}`. If `true`, the conflict fields
1854+
are not updated in order to enable optimizations such as HOT updates in PostgreSQL.
1855+
Defaults to `true`.
1856+
18451857
* `:stale_error_field` - The field where stale errors will be added in
18461858
the returning changeset. This option can be used to avoid raising
18471859
`Ecto.StaleEntryError`.
@@ -1880,7 +1892,9 @@ defmodule Ecto.Repo do
18801892
such as IDs and autogenerated timestamps (`inserted_at` and `updated_at`).
18811893
Do not use this option if you have auto-incrementing primary keys, as they
18821894
will also be replaced. You most likely want to use `{:replace_all_except, [:id]}`
1883-
or `{:replace, fields}` explicitly instead. This option requires a schema
1895+
or `{:replace, fields}` explicitly instead. This option requires a schema. Fields
1896+
specified by `:conflict_target` will be ignored unless `:replace_changed` is
1897+
configured to be `false`
18841898
18851899
* `{:replace_all_except, fields}` - same as above except the given fields are
18861900
not replaced. This option requires a schema

lib/ecto/repo/schema.ex

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ defmodule Ecto.Repo.Schema do
6969
on_conflict = Keyword.get(opts, :on_conflict, :raise)
7070
conflict_target = Keyword.get(opts, :conflict_target, [])
7171
conflict_target = conflict_target(conflict_target, dumper)
72+
replace_changed? = Keyword.get(opts, :replace_changed, true)
7273

7374
{on_conflict, conflict_cast_params} =
74-
on_conflict(on_conflict, conflict_target, schema_meta, counter, dumper, adapter)
75+
on_conflict(on_conflict, conflict_target, replace_changed?, schema_meta, counter, dumper, adapter)
7576

7677
opts =
7778
Keyword.put(
@@ -445,6 +446,7 @@ defmodule Ecto.Repo.Schema do
445446
on_conflict = Keyword.get(opts, :on_conflict, :raise)
446447
conflict_target = Keyword.get(opts, :conflict_target, [])
447448
conflict_target = conflict_target(conflict_target, dumper)
449+
replace_changed? = Keyword.get(opts, :replace_changed, true)
448450

449451
# On insert, we always merge the whole struct into the
450452
# changeset as changes, except the primary key if it is nil.
@@ -479,6 +481,7 @@ defmodule Ecto.Repo.Schema do
479481
on_conflict(
480482
on_conflict,
481483
conflict_target,
484+
replace_changed?,
482485
schema_meta,
483486
fn -> length(dump_changes) end,
484487
dumper,
@@ -889,7 +892,7 @@ defmodule Ecto.Repo.Schema do
889892
end
890893
end
891894

892-
defp on_conflict(on_conflict, conflict_target, schema_meta, counter_fun, dumper, adapter) do
895+
defp on_conflict(on_conflict, conflict_target, replace_changed?, schema_meta, counter_fun, dumper, adapter) do
893896
%{source: source, schema: schema, prefix: prefix} = schema_meta
894897

895898
case on_conflict do
@@ -913,15 +916,15 @@ defmodule Ecto.Repo.Schema do
913916
# Remove the conflict targets from the replacing fields
914917
# since the values don't change and this allows postgres to
915918
# possibly perform a HOT optimization: https://www.postgresql.org/docs/current/storage-hot.html
916-
to_remove = List.wrap(conflict_target)
919+
to_remove = if replace_changed?, do: List.wrap(conflict_target), else: []
917920
replace = replace_all_fields!(:replace_all, schema, to_remove)
918921

919922
if replace == [], do: raise(ArgumentError, "empty list of fields to update, use the `:replace` option instead")
920923

921924
{{replace, [], conflict_target}, []}
922925

923926
{:replace_all_except, fields} ->
924-
to_remove = List.wrap(conflict_target) ++ fields
927+
to_remove = if replace_changed?, do: List.wrap(conflict_target) ++ fields, else: fields
925928
replace = replace_all_fields!(:replace_all_except, schema, to_remove)
926929

927930
if replace == [], do: raise(ArgumentError, "empty list of fields to update, use the `:replace` option instead")

test/ecto/repo_test.exs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1928,7 +1928,7 @@ defmodule Ecto.RepoTest do
19281928
assert_received {:insert, %{source: "my_schema", on_conflict: {^fields, [], []}}}
19291929
end
19301930

1931-
test "includes conflict target in the field list given to :replace_all_except" do
1931+
test "does not pass conflict target to :replace_all_except" do
19321932
fields = [:map, :z, :yyy, :x]
19331933

19341934
TestRepo.insert(%MySchema{id: 1},
@@ -1939,6 +1939,18 @@ defmodule Ecto.RepoTest do
19391939
assert_received {:insert, %{source: "my_schema", on_conflict: {^fields, [], [:id]}}}
19401940
end
19411941

1942+
test "passes conflict target to :replace_all_except when replace_changed is false" do
1943+
fields = [:map, :z, :yyy, :x, :id]
1944+
1945+
TestRepo.insert(%MySchema{id: 1},
1946+
on_conflict: {:replace_all_except, [:array]},
1947+
conflict_target: [:id],
1948+
replace_changed: false
1949+
)
1950+
1951+
assert_received {:insert, %{source: "my_schema", on_conflict: {^fields, [], [:id]}}}
1952+
end
1953+
19421954
test "raises on empty-list of fields to update when :replace_all_except is given" do
19431955
msg = "empty list of fields to update, use the `:replace` option instead"
19441956

@@ -1956,6 +1968,12 @@ defmodule Ecto.RepoTest do
19561968
assert_received {:insert, %{source: "my_schema", on_conflict: {^fields, [], [:id]}}}
19571969
end
19581970

1971+
test "includes conflict target in :replace_all when replace_changed is false" do
1972+
fields = [:map, :array, :z, :yyy, :x, :id]
1973+
TestRepo.insert(%MySchema{id: 1}, on_conflict: :replace_all, conflict_target: [:id], replace_changed: false)
1974+
assert_received {:insert, %{source: "my_schema", on_conflict: {^fields, [], [:id]}}}
1975+
end
1976+
19591977
test "raises on empty-list of fields to update when :replace_all is given" do
19601978
msg = "empty list of fields to update, use the `:replace` option instead"
19611979

0 commit comments

Comments
 (0)