@@ -1214,6 +1214,85 @@ defmodule Ecto.Changeset do
12141214 cast_relation ( :assoc , changeset , name , opts )
12151215 end
12161216
1217+ @ doc """
1218+ Reorders the changes for a given association.
1219+
1220+ This function should be used when wanting to re-order the list of changes
1221+ for an association with cardinality `:many` before writing to the database.
1222+ The 2-arity version of this function sorts the changes in a way that is safe
1223+ for use with unique constraints.
1224+
1225+ For example, if you have a unique constraint on the field `:name` and your list
1226+ of changes might introduce conflicts, you can use this function to sort changes
1227+ by deletes, then updates and then inserts. The `:on_replace` behavour will be
1228+ handled automatically.
1229+
1230+ Using this function is preferable to relying on deferred constraints because the
1231+ resulting error cannot be mapped back into the correct changeset and your transaction
1232+ will simply raise.
1233+
1234+ See `reorder_assoc/3` if you would like to use your own custom sorting function.
1235+ """
1236+ @ spec cast_assoc ( t , atom ( ) ) :: t
1237+ def reorder_assoc ( % Changeset { } = changeset , name ) when is_atom ( name ) do
1238+ reorder_assoc ( changeset , name , & unique_safe_sort / 3 )
1239+ end
1240+
1241+ @ doc """
1242+ Reorders the changes for a given association using a custom sorting function.
1243+
1244+ This function behaves similarly to `reorder_assoc/2` except it allows the user
1245+ to define their own sorting function. The function must be of arity 3 where the
1246+ first argument is the reflection struct of the association, such as `Ecto.Association.Has`.
1247+ The next two arguments are the changesets to be compared for sorting. You must return
1248+ a `true` if the first changeset precedes or is in the same place as the second changeset
1249+ and `false` otherwise.
1250+ """
1251+ @ spec cast_assoc ( t , atom ( ) , ( term ( ) , t , t -> boolean ( ) ) ) :: t
1252+ def reorder_assoc ( % Changeset { } = changeset , name , sort_fn )
1253+ when is_atom ( name ) and is_function ( sort_fn , 3 ) do
1254+ refl =
1255+ case changeset do
1256+ % { data: % { __struct__: schema } } ->
1257+ schema . __schema__ ( :association , name ) ||
1258+ raise ArgumentError ,
1259+ "schema #{ inspect ( schema ) } does not have association `#{ name } `"
1260+
1261+ _ ->
1262+ raise ArgumentError , "cannot reorder association without data"
1263+ end
1264+
1265+ assoc_changes =
1266+ case changeset . changes do
1267+ % { ^ name => changes } when is_list ( changes ) ->
1268+ changes
1269+
1270+ _ ->
1271+ raise ArgumentError ,
1272+ "`reorder_assoc/3` requires an association with `:many` cardinality and a list of associated changes"
1273+ end
1274+
1275+ sorted_assoc_changes = Enum . sort ( assoc_changes , & sort_fn . ( refl , & 1 , & 2 ) )
1276+ updated_changes = Map . put ( changeset . changes , name , sorted_assoc_changes )
1277+ % { changeset | changes: updated_changes }
1278+ end
1279+
1280+ defp unique_safe_sort ( refl , changeset1 , changeset2 ) do
1281+ action_sort_rank ( changeset1 . action , refl ) <= action_sort_rank ( changeset2 . action , refl )
1282+ end
1283+
1284+ defp action_sort_rank ( action , refl ) do
1285+ case action do
1286+ :delete -> 0
1287+ :replace when refl . on_replace == :delete -> 0
1288+ :update -> 1
1289+ :replace when refl . on_replace == :nilify -> 1
1290+ :insert -> 2
1291+ # For things like `:ignore` we lump them at the end
1292+ _ -> 3
1293+ end
1294+ end
1295+
12171296 @ doc """
12181297 Casts the given embed with the changeset parameters.
12191298
0 commit comments