Skip to content

Commit aed4621

Browse files
committed
Mark more fields in relation as optional
1 parent c2e7e2d commit aed4621

File tree

2 files changed

+49
-42
lines changed

2 files changed

+49
-42
lines changed

lib/ecto/association.ex

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,6 @@ defmodule Ecto.Association.Has do
751751
unique: true,
752752
defaults: [],
753753
relationship: :child,
754-
ordered: false,
755754
preload_order: []
756755
]
757756

@@ -1018,8 +1017,7 @@ defmodule Ecto.Association.HasThrough do
10181017
:through,
10191018
:on_cast,
10201019
relationship: :child,
1021-
unique: true,
1022-
ordered: false
1020+
unique: true
10231021
]
10241022

10251023
@impl true
@@ -1121,8 +1119,7 @@ defmodule Ecto.Association.BelongsTo do
11211119
defaults: [],
11221120
cardinality: :one,
11231121
relationship: :parent,
1124-
unique: true,
1125-
ordered: false
1122+
unique: true
11261123
]
11271124

11281125
@impl true
@@ -1322,7 +1319,6 @@ defmodule Ecto.Association.ManyToMany do
13221319
relationship: :child,
13231320
cardinality: :many,
13241321
unique: false,
1325-
ordered: false,
13261322
preload_order: []
13271323
]
13281324

lib/ecto/changeset/relation.ex

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ defmodule Ecto.Changeset.Relation do
99
required(:__struct__) => atom(),
1010
required(:cardinality) => :one | :many,
1111
required(:on_replace) => :raise | :mark_as_invalid | atom,
12-
required(:relationship) => :parent | :child,
13-
required(:ordered) => boolean,
14-
required(:owner) => atom,
15-
required(:related) => atom,
1612
required(:field) => atom,
13+
optional(:unique) => boolean,
14+
optional(:ordered) => boolean,
15+
optional(:owner) => atom,
16+
optional(:related) => atom,
1717
optional(atom()) => any()
1818
}
1919

@@ -106,7 +106,7 @@ defmodule Ecto.Changeset.Relation do
106106
end
107107
end
108108

109-
def cast(%{related: mod} = relation, owner, params, current, on_cast) do
109+
def cast(relation, owner, params, current, on_cast) do
110110
on_cast =
111111
case on_cast do
112112
{module, fun, args} ->
@@ -119,16 +119,16 @@ defmodule Ecto.Changeset.Relation do
119119
end
120120

121121
nil ->
122-
on_cast_default(mod)
122+
on_cast_default(relation)
123123

124124
fun when is_function(fun) ->
125125
fun
126126
end
127127

128-
pks = mod.__schema__(:primary_key)
128+
pks = primary_keys(relation)
129129
fun = &do_cast(relation, owner, &1, &2, &3, &4, on_cast)
130130
data_pk = data_pk(pks)
131-
param_pk = param_pk(mod, pks)
131+
param_pk = param_pk(relation, pks)
132132

133133
with :error <- cast_or_change(relation, params, current, data_pk, param_pk, fun) do
134134
{:error, {"is invalid", [type: expected_type(relation)]}}
@@ -181,8 +181,9 @@ defmodule Ecto.Changeset.Relation do
181181
end
182182
end
183183

184-
def change(%{related: mod} = relation, value, current) do
185-
get_pks = data_pk(mod.__schema__(:primary_key))
184+
def change(relation, value, current) do
185+
pks = primary_keys(relation)
186+
get_pks = data_pk(pks)
186187
fun = &do_change(relation, &1, &2, &3, &4)
187188

188189
with :error <- cast_or_change(relation, value, current, get_pks, get_pks, fun) do
@@ -253,23 +254,27 @@ defmodule Ecto.Changeset.Relation do
253254
raise ArgumentError, "expected changeset data to be a #{mod} struct, got: #{inspect(data)}"
254255
end
255256

257+
defp assert_changeset_struct!(changeset, _relation) do
258+
# For relations without a related module, any data type is accepted
259+
changeset
260+
end
261+
256262
@doc """
257263
Handles the changeset or struct when being replaced.
258264
"""
259265
def on_replace(%{on_replace: :mark_as_invalid}, _changeset_or_struct) do
260266
:error
261267
end
262268

263-
def on_replace(%{on_replace: :raise, field: name, owner: owner}, _) do
269+
def on_replace(%{on_replace: :raise, field: name} = relation, _) do
264270
raise """
265-
you are attempting to change relation #{inspect(name)} of
266-
#{inspect(owner)} but the `:on_replace` option of this relation
267-
is set to `:raise`.
271+
you are attempting to change relation #{pretty_relation(relation)}
272+
but the `:on_replace` option of this relation is set to `:raise`.
268273
269274
By default it is not possible to replace or delete embeds and
270275
associations during `cast`. Therefore Ecto requires the parameters
271276
given to `cast` to have IDs matching the data currently associated
272-
to #{inspect(owner)}. Failing to do so results in this error message.
277+
in #{inspect(name)}. Failing to do so results in this error message.
273278
274279
If you want to replace data or automatically delete any data
275280
not sent to `cast`, please set the appropriate `:on_replace`
@@ -293,9 +298,9 @@ defmodule Ecto.Changeset.Relation do
293298
{:ok, Changeset.change(changeset_or_struct) |> put_new_action(:replace)}
294299
end
295300

296-
defp raise_if_updating_with_struct!(%{field: name, owner: owner}, %{__struct__: _} = new) do
301+
defp raise_if_updating_with_struct!(%{field: name} = relation, %{__struct__: _} = new) do
297302
raise """
298-
you have set that the relation #{inspect(name)} of #{inspect(owner)}
303+
you have set that the relation #{pretty_relation(relation)}
299304
has `:on_replace` set to `:update` but you are giving it a struct/
300305
changeset to put_assoc/put_change.
301306
@@ -316,6 +321,9 @@ defmodule Ecto.Changeset.Relation do
316321
true
317322
end
318323

324+
defp pretty_relation(%{field: name, owner: owner}), do: "#{inspect(name)} of #{inspect(owner)}"
325+
defp pretty_relation(%{field: name}), do: inspect(name)
326+
319327
defp cast_or_change(
320328
%{cardinality: :one} = relation,
321329
value,
@@ -342,12 +350,11 @@ defmodule Ecto.Changeset.Relation do
342350
)
343351
when is_list(value) do
344352
{current_pks, current_map} = process_current(current, current_pks_fun, relation)
345-
%{unique: unique, ordered: ordered, related: mod} = relation
346-
change_pks_fun = change_pk(mod.__schema__(:primary_key))
347-
ordered = if ordered, do: current_pks, else: []
353+
change_pks_fun = change_pk(primary_keys(relation))
354+
ordered = if Map.get(relation, :ordered, false), do: current_pks, else: []
355+
unique = if Map.get(relation, :unique, false), do: %{}, else: nil
348356
funs = {new_pks_fun, change_pks_fun, fun}
349-
350-
map_changes(value, current_map, [], true, true, unique && %{}, 0, ordered, funs)
357+
map_changes(value, current_map, [], true, true, unique, 0, ordered, funs)
351358
end
352359

353360
defp cast_or_change(_, _, _, _, _, _), do: :error
@@ -405,18 +412,7 @@ defmodule Ecto.Changeset.Relation do
405412
case fun.(changes, struct, allowed_actions, idx) do
406413
{:ok, %{action: :ignore}} ->
407414
ordered = pop_ordered(pk_values, ordered)
408-
409-
map_changes(
410-
rest,
411-
current,
412-
acc,
413-
valid?,
414-
skip?,
415-
unique,
416-
idx + 1,
417-
ordered,
418-
funs
419-
)
415+
map_changes(rest, current, acc, valid?, skip?, unique, idx + 1, ordered, funs)
420416

421417
{:ok, changeset} ->
422418
pk_values = change_pks.(changeset)
@@ -485,7 +481,10 @@ defmodule Ecto.Changeset.Relation do
485481

486482
# helpers
487483

488-
defp on_cast_default(module) do
484+
defp primary_keys(%{related: mod}), do: mod.__schema__(:primary_key)
485+
defp primary_keys(_relation), do: []
486+
487+
defp on_cast_default(%{related: module}) do
489488
fn struct, params ->
490489
try do
491490
module.changeset(struct, params)
@@ -514,6 +513,14 @@ defmodule Ecto.Changeset.Relation do
514513
end
515514
end
516515

516+
defp on_cast_default(%{field: field}) do
517+
raise ArgumentError, """
518+
the relation `#{field}` does not have a schema module and requires an explicit :with option.
519+
520+
Please pass a changeset function of arity 2 (or arity 3 for cardinality :many) using the :with option.
521+
"""
522+
end
523+
517524
defp check_action!(changeset, allowed_actions) do
518525
action = changeset.action
519526

@@ -582,7 +589,7 @@ defmodule Ecto.Changeset.Relation do
582589
end
583590
end
584591

585-
defp param_pk(mod, pks) do
592+
defp param_pk(%{related: mod}, pks) do
586593
pks = Enum.map(pks, &{&1, Atom.to_string(&1), mod.__schema__(:type, &1)})
587594

588595
fn params ->
@@ -597,6 +604,10 @@ defmodule Ecto.Changeset.Relation do
597604
end
598605
end
599606

607+
defp param_pk(_relation, []) do
608+
fn _params -> [] end
609+
end
610+
600611
defp change_pk(pks) do
601612
fn %Changeset{} = cs ->
602613
Enum.map(pks, fn pk ->

0 commit comments

Comments
 (0)