Repository: elixir-ecto/ecto
Pull request #4392 (2024-03-19 10:08:04)

FIX: Allow for HOT updates in replace_all
Hi, 

This is a small tweak for `:replace_all`. Currently `replace_all` will generate a query that will also replace the conflict targets with the same value:
```sql
insert into users (name, age) values ('elixir', 18) on conflict (name) do update
  SET name = EXCLUDED.name, age = EXCLUDED.age;
```

but this will prevent Postgres from using its [HOT optimization](https://www.postgresql.org/docs/current/storage-hot.html) because it modifies the name column. We can enable possible HOT optimization by not replacing the on conflict targets: 
```sql
insert into users (name, age) values ('elixir', 18) on conflict (name) do update SET age = EXCLUDED.age;
```

This can already be achieved by using `replace_all_except` but I was wondering if this should by default be the case for `replace_all` because I couldn't think of any reason/use case why someone would want to replace the on conflict columns with the same values.

I couldn't run the integration tests locally. I tried `earthly -P --ci --build-arg ELIXIR_BASE=1.15.6-erlang-26.1.2-alpine-3.16.7 +integration-test` but it failed for waiting that any DB comes along. I didn't have time to debug this further.



Patch:
diff --git a/lib/ecto/changeset.ex b/lib/ecto/changeset.ex
index 258dc247..ea44adf5 100644
--- a/lib/ecto/changeset.ex
+++ b/lib/ecto/changeset.ex
@@ -1077,7 +1077,7 @@ defmodule Ecto.Changeset do
   of `cast_assoc/3`. This opens up the possibility to work on a subset of the data,
   instead of all associations in the database.
 
-  Taking the initial example of users having addresses imagine those addresses
+  Taking the initial example of users having addresses, imagine those addresses
   are set up to belong to a country. If you want to allow users to bulk edit all
   addresses that belong to a single country, you can do so by changing the preload
   query:
@@ -4173,8 +4173,9 @@ defimpl Inspect, for: Ecto.Changeset do
   import Inspect.Algebra
 
   def inspect(%Ecto.Changeset{data: data} = changeset, opts) do
+    # The trailing element is skipped later on
     list =
-      for attr <- [:action, :changes, :errors, :data, :valid?] do
+      for attr <- [:action, :changes, :errors, :data, :valid?, :action] do
         {attr, Map.get(changeset, attr)}
       end
 
@@ -4191,20 +4192,20 @@ defimpl Inspect, for: Ecto.Changeset do
           []
       end
 
-    container_doc("#Ecto.Changeset<", list, ">", opts, fn
-      {:action, action}, opts ->
+    container_doc("#Ecto.Changeset<", list, ">", %{limit: 5}, fn
+      {:action, action}, _opts ->
         concat("action: ", to_doc(action, opts))
 
-      {:changes, changes}, opts ->
+      {:changes, changes}, _opts ->
         concat("changes: ", changes |> filter(redacted_fields) |> to_doc(opts))
 
       {:data, data}, _opts ->
         concat("data: ", to_struct(data, opts))
 
-      {:errors, errors}, opts ->
+      {:errors, errors}, _opts ->
         concat("errors: ", to_doc(errors, opts))
 
-      {:valid?, valid?}, opts ->
+      {:valid?, valid?}, _opts ->
         concat("valid?: ", to_doc(valid?, opts))
     end)
   end
diff --git a/lib/ecto/repo/schema.ex b/lib/ecto/repo/schema.ex
index 19b91842..2b1583e3 100644
--- a/lib/ecto/repo/schema.ex
+++ b/lib/ecto/repo/schema.ex
@@ -727,7 +727,11 @@ defmodule Ecto.Repo.Schema do
         {{replace_fields!(dumper, keys), [], conflict_target}, []}
 
       :replace_all ->
-        {{replace_all_fields!(:replace_all, schema, []), [], conflict_target}, []}
+        # Remove the conflict targets from the replacing fields
+        # since the values don't change and this allows postgres to 
+        # possibly perform a HOT optimization: https://www.postgresql.org/docs/current/storage-hot.html
+        to_remove = List.wrap(conflict_target)
+        {{replace_all_fields!(:replace_all, schema, to_remove), [], conflict_target}, []}
 
       {:replace_all_except, fields} ->
         {{replace_all_fields!(:replace_all_except, schema, fields), [], conflict_target}, []}


Test patch:
diff --git a/test/ecto/changeset_test.exs b/test/ecto/changeset_test.exs
index 67ec5145..531857c4 100644
--- a/test/ecto/changeset_test.exs
+++ b/test/ecto/changeset_test.exs
@@ -121,31 +121,35 @@ defmodule Ecto.ChangesetTest do
   end
 
   defp changeset(schema \\ %Post{}, params) do
-    cast(schema, params, ~w(id token title author_email body upvotes decimal color topics seo_metadata virtual read_only)a)
+    cast(
+      schema,
+      params,
+      ~w(id token title author_email body upvotes decimal color topics seo_metadata virtual read_only)a
+    )
   end
 
   defmodule CustomError do
     use Ecto.Type
-    def type,      do: :any
-    def cast(_),   do: {:error, message: "custom error message", reason: :foobar}
-    def load(_),   do: :error
-    def dump(_),   do: :error
+    def type, do: :any
+    def cast(_), do: {:error, message: "custom error message", reason: :foobar}
+    def load(_), do: :error
+    def dump(_), do: :error
   end
 
   defmodule CustomErrorWithType do
     use Ecto.Type
-    def type,      do: :any
-    def cast(_),   do: {:error, message: "custom error message", reason: :foobar, type: :some_type}
-    def load(_),   do: :error
-    def dump(_),   do: :error
+    def type, do: :any
+    def cast(_), do: {:error, message: "custom error message", reason: :foobar, type: :some_type}
+    def load(_), do: :error
+    def dump(_), do: :error
   end
 
   defmodule CustomErrorWithoutMessage do
     use Ecto.Type
-    def type,      do: :any
-    def cast(_),   do: {:error, reason: :foobar}
-    def load(_),   do: :error
-    def dump(_),   do: :error
+    def type, do: :any
+    def cast(_), do: {:error, reason: :foobar}
+    def load(_), do: :error
+    def dump(_), do: :error
   end
 
   defmodule CustomErrorTest do
@@ -167,7 +171,7 @@ defmodule Ecto.ChangesetTest do
 
     changeset = cast(struct, params, ~w(title body)a)
     assert changeset.params == params
-    assert changeset.data  == struct
+    assert changeset.data == struct
     assert changeset.changes == %{title: "hello", body: "world"}
     assert changeset.errors == []
     assert validations(changeset) == []
@@ -181,7 +185,7 @@ defmodule Ecto.ChangesetTest do
 
     changeset = cast(struct, params, ~w(title body)a)
     assert changeset.params == %{"title" => "hello", "body" => "world"}
-    assert changeset.data  == struct
+    assert changeset.data == struct
     assert changeset.changes == %{title: "hello", body: "world"}
     assert changeset.errors == []
     assert validations(changeset) == []
@@ -229,17 +233,22 @@ defmodule Ecto.ChangesetTest do
     changeset = cast(struct, params, ~w(title body)a, force_changes: true)
     assert changeset.changes == %{title: "", body: nil}
 
-    changeset = cast(struct, %{"title" => "not empty", "body" => "empty"}, ~w(title body)a, force_changes: true, empty_values: ["empty"])
+    changeset =
+      cast(struct, %{"title" => "not empty", "body" => "empty"}, ~w(title body)a,
+        force_changes: true,
+        empty_values: ["empty"]
+      )
+
     assert changeset.changes == %{title: "not empty", body: nil}
   end
 
   test "cast/4: with data and types" do
-    data   = {%{title: "hello"}, %{title: :string, upvotes: :integer}}
+    data = {%{title: "hello"}, %{title: :string, upvotes: :integer}}
     params = %{"title" => "world", "upvotes" => "0"}
 
     changeset = cast(data, params, ~w(title upvotes)a)
     assert changeset.params == params
-    assert changeset.data  == %{title: "hello"}
+    assert changeset.data == %{title: "hello"}
     assert changeset.changes == %{title: "world", upvotes: 0}
     assert changeset.errors == []
     assert changeset.valid?
@@ -247,12 +256,12 @@ defmodule Ecto.ChangesetTest do
   end
 
   test "cast/4: with data struct and types" do
-    data   = {%NoSchemaPost{title: "hello"}, %{title: :string, upvotes: :integer}}
+    data = {%NoSchemaPost{title: "hello"}, %{title: :string, upvotes: :integer}}
     params = %{"title" => "world", "upvotes" => "0"}
 
     changeset = cast(data, params, ~w(title upvotes)a)
     assert changeset.params == params
-    assert changeset.data  == %NoSchemaPost{title: "hello"}
+    assert changeset.data == %NoSchemaPost{title: "hello"}
     assert changeset.changes == %{title: "world", upvotes: 0}
     assert changeset.errors == []
     assert changeset.valid?
@@ -281,7 +290,10 @@ defmodule Ecto.ChangesetTest do
       }
     }
 
-    params = %{"title" => "world", "source" => %{"origin" => "facebook", "url" => "http://example.com/social"}}
+    params = %{
+      "title" => "world",
+      "source" => %{"origin" => "facebook", "url" => "http://example.com/social"}
+    }
 
     changeset =
       data
@@ -289,24 +301,32 @@ defmodule Ecto.ChangesetTest do
       |> cast_embed(:source, required: true)
 
     assert changeset.params == params
-    assert changeset.data  == %{title: "hello"}
+    assert changeset.data == %{title: "hello"}
     assert %{title: "world", source: %Ecto.Changeset{}} = changeset.changes
     assert changeset.errors == []
     assert changeset.valid?
+
     assert apply_changes(changeset) ==
-      %{title: "world", source: %Ecto.ChangesetTest.SocialSource{origin: "facebook", url: "http://example.com/social"}}
+             %{
+               title: "world",
+               source: %Ecto.ChangesetTest.SocialSource{
+                 origin: "facebook",
+                 url: "http://example.com/social"
+               }
+             }
   end
 
   test "cast/4: with changeset" do
-    base_changeset = cast(%Post{title: "valid"}, %{}, ~w(title)a)
-                     |> validate_required(:title)
-                     |> validate_length(:title, min: 3)
-                     |> unique_constraint(:title)
+    base_changeset =
+      cast(%Post{title: "valid"}, %{}, ~w(title)a)
+      |> validate_required(:title)
+      |> validate_length(:title, min: 3)
+      |> unique_constraint(:title)
 
     # No changes
     changeset = cast(base_changeset, %{}, ~w())
     assert changeset.valid?
-    assert changeset.changes  == %{}
+    assert changeset.changes == %{}
     assert changeset.required == [:title]
     assert length(validations(changeset)) == 1
     assert length(constraints(changeset)) == 1
@@ -314,7 +334,7 @@ defmodule Ecto.ChangesetTest do
     # Value changes
     changeset = cast(changeset, %{body: "new body"}, ~w(body)a)
     assert changeset.valid?
-    assert changeset.changes  == %{body: "new body"}
+    assert changeset.changes == %{body: "new body"}
     assert changeset.required == [:title]
     assert length(validations(changeset)) == 1
     assert length(constraints(changeset)) == 1
@@ -322,7 +342,7 @@ defmodule Ecto.ChangesetTest do
     # Nil changes
     changeset = cast(changeset, %{body: nil}, ~w(body)a)
     assert changeset.valid?
-    assert changeset.changes  == %{body: nil}
+    assert changeset.changes == %{body: nil}
     assert changeset.required == [:title]
     assert length(validations(changeset)) == 1
     assert length(constraints(changeset)) == 1
@@ -364,7 +384,13 @@ defmodule Ecto.ChangesetTest do
     struct = %CustomErrorTest{}
 
     changeset = cast(struct, params, ~w(custom_error)a)
-    assert changeset.errors == [custom_error: {"custom error message", [type: Ecto.ChangesetTest.CustomError, validation: :cast, reason: :foobar]}]
+
+    assert changeset.errors == [
+             custom_error:
+               {"custom error message",
+                [type: Ecto.ChangesetTest.CustomError, validation: :cast, reason: :foobar]}
+           ]
+
     refute changeset.valid?
   end
 
@@ -392,7 +418,12 @@ defmodule Ecto.ChangesetTest do
 
     changeset = cast(struct, params, ~w(custom_error)a, message: msg_func)
 
-    assert changeset.errors == [custom_error: {"user-defined error", [type: Ecto.ChangesetTest.CustomError, validation: :cast, reason: :foobar]}]
+    assert changeset.errors == [
+             custom_error:
+               {"user-defined error",
+                [type: Ecto.ChangesetTest.CustomError, validation: :cast, reason: :foobar]}
+           ]
+
     refute changeset.valid?
   end
 
@@ -414,7 +445,13 @@ defmodule Ecto.ChangesetTest do
     struct = %CustomErrorTest{}
 
     changeset = cast(struct, params, ~w(custom_error_with_type)a)
-    assert changeset.errors == [custom_error_with_type: {"custom error message", [type: Ecto.ChangesetTest.CustomErrorWithType, validation: :cast, reason: :foobar]}]
+
+    assert changeset.errors == [
+             custom_error_with_type:
+               {"custom error message",
+                [type: Ecto.ChangesetTest.CustomErrorWithType, validation: :cast, reason: :foobar]}
+           ]
+
     refute changeset.valid?
   end
 
@@ -423,7 +460,17 @@ defmodule Ecto.ChangesetTest do
     struct = %CustomErrorTest{}
 
     changeset = cast(struct, params, ~w(custom_error_without_message)a)
-    assert changeset.errors == [custom_error_without_message: {"is invalid", [type: Ecto.ChangesetTest.CustomErrorWithoutMessage, validation: :cast, reason: :foobar]}]
+
+    assert changeset.errors == [
+             custom_error_without_message:
+               {"is invalid",
+                [
+                  type: Ecto.ChangesetTest.CustomErrorWithoutMessage,
+                  validation: :cast,
+                  reason: :foobar
+                ]}
+           ]
+
     refute changeset.valid?
   end
 
@@ -454,15 +501,15 @@ defmodule Ecto.ChangesetTest do
     changeset = cast(struct, params, ~w(map_custom_error)a)
 
     assert changeset.errors == [
-      map_custom_error:
-        {"custom error message",
-         [
-           type: {:map, Ecto.ChangesetTest.CustomError},
-           validation: :cast,
-           reason: :foobar,
-           source: [:foo]
-         ]}
-    ]
+             map_custom_error:
+               {"custom error message",
+                [
+                  type: {:map, Ecto.ChangesetTest.CustomError},
+                  validation: :cast,
+                  reason: :foobar,
+                  source: [:foo]
+                ]}
+           ]
 
     refute changeset.valid?
   end
@@ -505,8 +552,9 @@ defmodule Ecto.ChangesetTest do
     params = %{"body" => :world}
     struct = %Post{}
 
-    changeset = cast(struct, params, [:body])
-                |> validate_required([:body])
+    changeset =
+      cast(struct, params, [:body])
+      |> validate_required([:body])
 
     assert changeset.changes == %{}
     assert changeset.errors == [body: {"is invalid", [type: :string, validation: :cast]}]
@@ -519,7 +567,7 @@ defmodule Ecto.ChangesetTest do
 
     changeset = cast(struct, params, ~w(title decimal)a)
     assert changeset.params == %{}
-    assert changeset.data  == struct
+    assert changeset.data == struct
     assert changeset.changes == %{}
     assert changeset.errors == []
     assert validations(changeset) == []
@@ -568,15 +616,22 @@ defmodule Ecto.ChangesetTest do
     cs2 = cast(%Post{}, %{}, ~w(title body)a) |> validate_required([:title, :body])
     changeset = merge(cs1, cs2)
     refute changeset.valid?
+
     assert changeset.errors ==
-           [title: {"can't be blank", [validation: :required]}, body: {"can't be blank", [validation: :required]}]
+             [
+               title: {"can't be blank", [validation: :required]},
+               body: {"can't be blank", [validation: :required]}
+             ]
   end
 
   test "merge/2: merges validations" do
-    cs1 = cast(%Post{}, %{title: "Title"}, ~w(title)a)
-                |> validate_length(:title, min: 1, max: 10)
-    cs2 = cast(%Post{}, %{body: "Body"}, ~w(body)a)
-                |> validate_format(:body, ~r/B/)
+    cs1 =
+      cast(%Post{}, %{title: "Title"}, ~w(title)a)
+      |> validate_length(:title, min: 1, max: 10)
+
+    cs2 =
+      cast(%Post{}, %{body: "Body"}, ~w(body)a)
+      |> validate_format(:body, ~r/B/)
 
     changeset = merge(cs1, cs2)
     assert changeset.valid?
@@ -586,17 +641,20 @@ defmodule Ecto.ChangesetTest do
   end
 
   test "merge/2: repo opts" do
-    cs1 = %Post{} |> change() |> Map.put(:repo_opts, [a: 1, b: 2])
-    cs2 = %Post{} |> change() |> Map.put(:repo_opts, [b: 3, c: 4])
+    cs1 = %Post{} |> change() |> Map.put(:repo_opts, a: 1, b: 2)
+    cs2 = %Post{} |> change() |> Map.put(:repo_opts, b: 3, c: 4)
     changeset = merge(cs1, cs2)
     assert changeset.repo_opts == [a: 1, b: 3, c: 4]
   end
 
   test "merge/2: merges constraints" do
-    cs1 = cast(%Post{}, %{title: "Title"}, ~w(title)a)
-                |> unique_constraint(:title)
-    cs2 = cast(%Post{}, %{body: "Body"}, ~w(body)a)
-                |> unique_constraint(:body)
+    cs1 =
+      cast(%Post{}, %{title: "Title"}, ~w(title)a)
+      |> unique_constraint(:title)
+
+    cs2 =
+      cast(%Post{}, %{body: "Body"}, ~w(body)a)
+      |> unique_constraint(:body)
 
     changeset = merge(cs1, cs2)
     assert changeset.valid?
@@ -605,8 +663,8 @@ defmodule Ecto.ChangesetTest do
 
   test "merge/2: merges parameters" do
     empty = cast(%Post{}, %{}, ~w(title)a)
-    cs1   = cast(%Post{}, %{body: "foo"}, ~w(body)a)
-    cs2   = cast(%Post{}, %{body: "bar"}, ~w(body)a)
+    cs1 = cast(%Post{}, %{body: "foo"}, ~w(body)a)
+    cs2 = cast(%Post{}, %{body: "bar"}, ~w(body)a)
     assert merge(cs1, cs2).params == %{"body" => "bar"}
 
     assert merge(cs1, empty).params == %{"body" => "foo"}
@@ -652,13 +710,17 @@ defmodule Ecto.ChangesetTest do
       merge(cs1, cs2)
     end
 
-    assert_raise ArgumentError, "different repos (`:foo` and `:bar`) when merging changesets", fn ->
-      merge(%Ecto.Changeset{repo: :foo}, %Ecto.Changeset{repo: :bar})
-    end
+    assert_raise ArgumentError,
+                 "different repos (`:foo` and `:bar`) when merging changesets",
+                 fn ->
+                   merge(%Ecto.Changeset{repo: :foo}, %Ecto.Changeset{repo: :bar})
+                 end
 
-    assert_raise ArgumentError, "different actions (`:insert` and `:update`) when merging changesets", fn ->
-      merge(%Ecto.Changeset{action: :insert}, %Ecto.Changeset{action: :update})
-    end
+    assert_raise ArgumentError,
+                 "different actions (`:insert` and `:update`) when merging changesets",
+                 fn ->
+                   merge(%Ecto.Changeset{action: :insert}, %Ecto.Changeset{action: :update})
+                 end
   end
 
   test "change/2 with a struct" do
@@ -763,12 +825,13 @@ defmodule Ecto.ChangesetTest do
   test "changed?/3 " do
     post = %Post{}
 
-    changeset = change(post,
-      title: "title",
-      upvotes: nil,
-      author_email: "johndoe@example.com",
-      comment: %Comment{}
-    )
+    changeset =
+      change(post,
+        title: "title",
+        upvotes: nil,
+        author_email: "johndoe@example.com",
+        comment: %Comment{}
+      )
 
     assert changed?(changeset, :title)
     assert changed?(changeset, :upvotes)
@@ -856,7 +919,10 @@ defmodule Ecto.ChangesetTest do
     comment_changeset = change(%Comment{}, %{id: 123})
     base_changesset = change(post) |> put_assoc(:comments, [comment_changeset])
 
-    assert get_assoc(base_changesset, :comments, :changeset) == [%{comment_changeset | action: :update}]
+    assert get_assoc(base_changesset, :comments, :changeset) == [
+             %{comment_changeset | action: :update}
+           ]
+
     assert get_assoc(base_changesset, :comments, :struct) == [%Comment{id: 123}]
   end
 
@@ -893,22 +959,26 @@ defmodule Ecto.ChangesetTest do
   test "update_change/3" do
     changeset =
       changeset(%{"title" => "foo"})
-      |> update_change(:title, & &1 <> "bar")
+      |> update_change(:title, &(&1 <> "bar"))
+
     assert changeset.changes.title == "foobar"
 
     changeset =
       changeset(%{"upvotes" => nil})
-      |> update_change(:upvotes, & &1 || 10)
+      |> update_change(:upvotes, &(&1 || 10))
+
     assert changeset.changes.upvotes == 10
 
     changeset =
       changeset(%{})
-      |> update_change(:title, & &1 || "bar")
+      |> update_change(:title, &(&1 || "bar"))
+
     assert changeset.changes == %{}
 
     changeset =
       changeset(%Post{title: "mytitle"}, %{title: "MyTitle"})
       |> update_change(:title, &String.downcase/1)
+
     assert changeset.changes == %{}
   end
 
@@ -956,9 +1026,10 @@ defmodule Ecto.ChangesetTest do
 
     assert post.title == ""
 
-    changeset = post
-    |> changeset(%{"title" => "foo"})
-    |> put_assoc(:category, category)
+    changeset =
+      post
+      |> changeset(%{"title" => "foo"})
+      |> put_assoc(:category, category)
 
     changed_post = apply_changes(changeset)
 
@@ -966,9 +1037,10 @@ defmodule Ecto.ChangesetTest do
     assert changed_post.title == "foo"
     assert changed_post.category_id == category.category_id
 
-    changeset = post
-    |> changeset(%{"title" => "foo"})
-    |> put_assoc(:category, nil)
+    changeset =
+      post
+      |> changeset(%{"title" => "foo"})
+      |> put_assoc(:category, nil)
 
     changed_post = apply_changes(changeset)
 
@@ -1036,11 +1108,13 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{})
       |> add_error(:foo, "bar")
+
     assert changeset.errors == [foo: {"bar", []}]
 
     changeset =
       changeset(%{})
       |> add_error(:foo, "bar", additional: "information")
+
     assert changeset.errors == [foo: {"bar", [additional: "information"]}]
   end
 
@@ -1102,7 +1176,7 @@ defmodule Ecto.ChangesetTest do
     assert changeset.errors == []
 
     # When unknown field
-    assert_raise ArgumentError, ~r/unknown field :bad in/, fn  ->
+    assert_raise ArgumentError, ~r/unknown field :bad in/, fn ->
       changeset(%{"title" => "hello"})
       |> validate_change(:bad, fn _, _ -> [] end)
     end
@@ -1131,6 +1205,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "hello", "body" => "something"})
       |> validate_required(:title)
+
     assert changeset.valid?
     assert changeset.errors == []
 
@@ -1144,13 +1219,18 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{title: nil, body: "\n"})
       |> validate_required([:title, :body], message: "is blank")
+
     refute changeset.valid?
     assert changeset.required == [:title, :body]
     assert changeset.changes == %{}
-    assert changeset.errors == [title: {"is blank", [validation: :required]}, body: {"is blank", [validation: :required]}]
+
+    assert changeset.errors == [
+             title: {"is blank", [validation: :required]},
+             body: {"is blank", [validation: :required]}
+           ]
 
     # When unknown field
-    assert_raise ArgumentError, ~r/unknown field :bad in/, fn  ->
+    assert_raise ArgumentError, ~r/unknown field :bad in/, fn ->
       changeset(%{"title" => "hello", "body" => "something"})
       |> validate_required(:bad)
     end
@@ -1197,7 +1277,7 @@ defmodule Ecto.ChangesetTest do
     assert missing_fields == [:color]
 
     # When unknown field
-    assert_raise ArgumentError, ~r/unknown field :bad in/, fn  ->
+    assert_raise ArgumentError, ~r/unknown field :bad in/, fn ->
       changeset(%{"title" => "hello", "body" => "something"})
       |> field_missing?(:bad)
     end
@@ -1207,6 +1287,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "foo@bar"})
       |> validate_format(:title, ~r/@/)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [title: {:format, ~r/@/}]
@@ -1214,6 +1295,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "foobar"})
       |> validate_format(:title, ~r/@/)
+
     refute changeset.valid?
     assert changeset.errors == [title: {"has invalid format", [validation: :format]}]
     assert validations(changeset) == [title: {:format, ~r/@/}]
@@ -1221,6 +1303,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "foobar"})
       |> validate_format(:title, ~r/@/, message: "yada")
+
     assert changeset.errors == [title: {"yada", [validation: :format]}]
 
     assert_raise ArgumentError, ~r/expects changes to be strings/, fn ->
@@ -1233,6 +1316,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "hello"})
       |> validate_inclusion(:title, ~w(hello))
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [title: {:inclusion, ~w(hello)}]
@@ -1240,6 +1324,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "hello"})
       |> validate_inclusion(:title, ~w(world))
+
     refute changeset.valid?
     assert changeset.errors == [title: {"is invalid", [validation: :inclusion, enum: ~w(world)]}]
     assert validations(changeset) == [title: {:inclusion, ~w(world)}]
@@ -1247,6 +1332,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "hello"})
       |> validate_inclusion(:title, ~w(world), message: "yada")
+
     assert changeset.errors == [title: {"yada", [validation: :inclusion, enum: ~w(world)]}]
   end
 
@@ -1273,6 +1359,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"topics" => ["cat", "dog"]})
       |> validate_subset(:topics, ~w(cat dog))
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [topics: {:subset, ~w(cat dog)}]
@@ -1280,13 +1367,19 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"topics" => ["cat", "laptop"]})
       |> validate_subset(:topics, ~w(cat dog))
+
     refute changeset.valid?
-    assert changeset.errors == [topics: {"has an invalid entry", [validation: :subset, enum: ~w(cat dog)]}]
+
+    assert changeset.errors == [
+             topics: {"has an invalid entry", [validation: :subset, enum: ~w(cat dog)]}
+           ]
+
     assert validations(changeset) == [topics: {:subset, ~w(cat dog)}]
 
     changeset =
       changeset(%{"topics" => ["laptop"]})
       |> validate_subset(:topics, ~w(cat dog), message: "yada")
+
     assert changeset.errors == [topics: {"yada", [validation: :subset, enum: ~w(cat dog)]}]
   end
 
@@ -1313,6 +1406,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "world"})
       |> validate_exclusion(:title, ~w(hello))
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [title: {:exclusion, ~w(hello)}]
@@ -1320,6 +1414,7 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "world"})
       |> validate_exclusion(:title, ~w(world))
+
     refute changeset.valid?
     assert changeset.errors == [title: {"is reserved", [validation: :exclusion, enum: ~w(world)]}]
     assert validations(changeset) == [title: {:exclusion, ~w(world)}]
@@ -1327,16 +1422,19 @@ defmodule Ecto.ChangesetTest do
     changeset =
       changeset(%{"title" => "world"})
       |> validate_exclusion(:title, ~w(world), message: "yada")
+
     assert changeset.errors == [title: {"yada", [validation: :exclusion, enum: ~w(world)]}]
   end
 
   test "validate_exclusion/3 with decimal does semantic comparison" do
     decimals = Enum.map([0.0, 0.2], &Decimal.from_float/1)
+
     changeset =
       {%{}, %{value: :decimal}}
       |> Ecto.Changeset.cast(%{value: 0}, [:value])
       |> validate_exclusion(:value, decimals)
-    assert changeset.errors ==  [value: {"is reserved", [validation: :exclusion, enum: decimals]}]
+
+    assert changeset.errors == [value: {"is reserved", [validation: :exclusion, enum: decimals]}]
   end
 
   test "validate_length/3 with string" do
@@ -1353,25 +1451,52 @@ defmodule Ecto.ChangesetTest do
 
     changeset = changeset(%{"title" => "world"}) |> validate_length(:title, min: 6)
     refute changeset.valid?
-    assert changeset.errors == [title: {"should be at least %{count} character(s)", count: 6, validation: :length, kind: :min, type: :string}]
+
+    assert changeset.errors == [
+             title:
+               {"should be at least %{count} character(s)",
+                count: 6, validation: :length, kind: :min, type: :string}
+           ]
 
     changeset = changeset(%{"title" => "world"}) |> validate_length(:title, max: 4)
     refute changeset.valid?
-    assert changeset.errors == [title: {"should be at most %{count} character(s)", count: 4, validation: :length, kind: :max, type: :string}]
+
+    assert changeset.errors == [
+             title:
+               {"should be at most %{count} character(s)",
+                count: 4, validation: :length, kind: :max, type: :string}
+           ]
 
     changeset = changeset(%{"title" => "world"}) |> validate_length(:title, is: 10)
     refute changeset.valid?
-    assert changeset.errors == [title: {"should be %{count} character(s)", count: 10, validation: :length, kind: :is, type: :string}]
 
-    changeset = changeset(%{"title" => "world"}) |> validate_length(:title, is: 10, message: "yada")
-    assert changeset.errors == [title: {"yada", count: 10, validation: :length, kind: :is, type: :string}]
+    assert changeset.errors == [
+             title:
+               {"should be %{count} character(s)",
+                count: 10, validation: :length, kind: :is, type: :string}
+           ]
+
+    changeset =
+      changeset(%{"title" => "world"}) |> validate_length(:title, is: 10, message: "yada")
+
+    assert changeset.errors == [
+             title: {"yada", count: 10, validation: :length, kind: :is, type: :string}
+           ]
 
     changeset = changeset(%{"title" => "\u0065\u0301"}) |> validate_length(:title, max: 1)
     assert changeset.valid?
 
-    changeset = changeset(%{"title" => "\u0065\u0301"}) |> validate_length(:title, max: 1, count: :codepoints)
+    changeset =
+      changeset(%{"title" => "\u0065\u0301"})
+      |> validate_length(:title, max: 1, count: :codepoints)
+
     refute changeset.valid?
-    assert changeset.errors == [title: {"should be at most %{count} character(s)", count: 1, validation: :length, kind: :max, type: :string}]
+
+    assert changeset.errors == [
+             title:
+               {"should be at most %{count} character(s)",
+                count: 1, validation: :length, kind: :max, type: :string}
+           ]
   end
 
   test "validate_length/3 with binary" do
@@ -1401,7 +1526,8 @@ defmodule Ecto.ChangesetTest do
 
     assert changeset.errors == [
              body:
-               {"should be at least %{count} byte(s)", count: 6, validation: :length, kind: :min, type: :binary}
+               {"should be at least %{count} byte(s)",
+                count: 6, validation: :length, kind: :min, type: :binary}
            ]
 
     changeset =
@@ -1410,7 +1536,9 @@ defmodule Ecto.ChangesetTest do
     refute changeset.valid?
 
     assert changeset.errors == [
-             body: {"should be at most %{count} byte(s)", count: 4, validation: :length, kind: :max, type: :binary}
+             body:
+               {"should be at most %{count} byte(s)",
+                count: 4, validation: :length, kind: :max, type: :binary}
            ]
 
     changeset =
@@ -1419,257 +1547,471 @@ defmodule Ecto.ChangesetTest do
     refute changeset.valid?
 
     assert changeset.errors == [
-             body: {"should be %{count} byte(s)", count: 10, validation: :length, kind: :is, type: :binary}
+             body:
+               {"should be %{count} byte(s)",
+                count: 10, validation: :length, kind: :is, type: :binary}
            ]
 
     changeset =
       changeset(%{"body" => <<0, 1, 2, 3, 4>>})
       |> validate_length(:body, count: :bytes, is: 10, message: "yada")
 
-    assert changeset.errors == [body: {"yada", count: 10, validation: :length, kind: :is, type: :binary}]
+    assert changeset.errors == [
+             body: {"yada", count: 10, validation: :length, kind: :is, type: :binary}
+           ]
   end
 
   test "validate_length/3 with list" do
-    changeset = changeset(%{"topics" => ["Politics", "Security", "Economy", "Elections"]}) |> validate_length(:topics, min: 3, max: 7)
+    changeset =
+      changeset(%{"topics" => ["Politics", "Security", "Economy", "Elections"]})
+      |> validate_length(:topics, min: 3, max: 7)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [topics: {:length, [min: 3, max: 7]}]
 
-    changeset = changeset(%{"topics" => ["Politics", "Security"]}) |> validate_length(:topics, min: 2, max: 2)
+    changeset =
+      changeset(%{"topics" => ["Politics", "Security"]})
+      |> validate_length(:topics, min: 2, max: 2)
+
     assert changeset.valid?
 
-    changeset = changeset(%{"topics" => ["Politics", "Security", "Economy"]}) |> validate_length(:topics, is: 3)
+    changeset =
+      changeset(%{"topics" => ["Politics", "Security", "Economy"]})
+      |> validate_length(:topics, is: 3)
+
     assert changeset.valid?
 
-    changeset = changeset(%{"topics" => ["Politics", "Security"]}) |> validate_length(:topics, min: 6, foo: true)
+    changeset =
+      changeset(%{"topics" => ["Politics", "Security"]})
+      |> validate_length(:topics, min: 6, foo: true)
+
     refute changeset.valid?
-    assert changeset.errors == [topics: {"should have at least %{count} item(s)", count: 6, validation: :length, kind: :min, type: :list}]
 
-    changeset = changeset(%{"topics" => ["Politics", "Security", "Economy"]}) |> validate_length(:topics, max: 2)
+    assert changeset.errors == [
+             topics:
+               {"should have at least %{count} item(s)",
+                count: 6, validation: :length, kind: :min, type: :list}
+           ]
+
+    changeset =
+      changeset(%{"topics" => ["Politics", "Security", "Economy"]})
+      |> validate_length(:topics, max: 2)
+
     refute changeset.valid?
-    assert changeset.errors == [topics: {"should have at most %{count} item(s)", count: 2, validation: :length, kind: :max, type: :list}]
 
-    changeset = changeset(%{"topics" => ["Politics", "Security"]}) |> validate_length(:topics, is: 10)
+    assert changeset.errors == [
+             topics:
+               {"should have at most %{count} item(s)",
+                count: 2, validation: :length, kind: :max, type: :list}
+           ]
+
+    changeset =
+      changeset(%{"topics" => ["Politics", "Security"]}) |> validate_length(:topics, is: 10)
+
     refute changeset.valid?
-    assert changeset.errors == [topics: {"should have %{count} item(s)", count: 10, validation: :length, kind: :is, type: :list}]
 
-    changeset = changeset(%{"topics" => ["Politics", "Security"]}) |> validate_length(:topics, is: 10, message: "yada")
-    assert changeset.errors == [topics: {"yada", count: 10, validation: :length, kind: :is, type: :list}]
+    assert changeset.errors == [
+             topics:
+               {"should have %{count} item(s)",
+                count: 10, validation: :length, kind: :is, type: :list}
+           ]
+
+    changeset =
+      changeset(%{"topics" => ["Politics", "Security"]})
+      |> validate_length(:topics, is: 10, message: "yada")
+
+    assert changeset.errors == [
+             topics: {"yada", count: 10, validation: :length, kind: :is, type: :list}
+           ]
   end
 
   test "validate_length/3 with map" do
-    changeset = changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-1"}}) |> validate_length(:seo_metadata, min: 2, max: 3)
+    changeset =
+      changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-1"}})
+      |> validate_length(:seo_metadata, min: 2, max: 3)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [seo_metadata: {:length, [min: 2, max: 3]}]
 
-    changeset = changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-2"}}) |> validate_length(:seo_metadata, min: 2, max: 2)
+    changeset =
+      changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-2"}})
+      |> validate_length(:seo_metadata, min: 2, max: 2)
+
     assert changeset.valid?
 
-    changeset = changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-3"}}) |> validate_length(:seo_metadata, is: 2)
+    changeset =
+      changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-3"}})
+      |> validate_length(:seo_metadata, is: 2)
+
     assert changeset.valid?
 
-    changeset = changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-4"}}) |> validate_length(:seo_metadata, min: 3)
+    changeset =
+      changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-4"}})
+      |> validate_length(:seo_metadata, min: 3)
+
     refute changeset.valid?
-    assert changeset.errors == [seo_metadata: {"should have at least %{count} item(s)", count: 3, validation: :length, kind: :min, type: :map}]
 
-    changeset = changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-5", "is_indexed" => false}}) |> validate_length(:seo_metadata, max: 2)
+    assert changeset.errors == [
+             seo_metadata:
+               {"should have at least %{count} item(s)",
+                count: 3, validation: :length, kind: :min, type: :map}
+           ]
+
+    changeset =
+      changeset(%{
+        "seo_metadata" => %{
+          "keywords" => ["foo", "bar"],
+          "slug" => "my-post-5",
+          "is_indexed" => false
+        }
+      })
+      |> validate_length(:seo_metadata, max: 2)
+
     refute changeset.valid?
-    assert changeset.errors == [seo_metadata: {"should have at most %{count} item(s)", count: 2, validation: :length, kind: :max, type: :map}]
 
-    changeset = changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-6", "is_indexed" => false}}) |> validate_length(:seo_metadata, is: 10)
+    assert changeset.errors == [
+             seo_metadata:
+               {"should have at most %{count} item(s)",
+                count: 2, validation: :length, kind: :max, type: :map}
+           ]
+
+    changeset =
+      changeset(%{
+        "seo_metadata" => %{
+          "keywords" => ["foo", "bar"],
+          "slug" => "my-post-6",
+          "is_indexed" => false
+        }
+      })
+      |> validate_length(:seo_metadata, is: 10)
+
     refute changeset.valid?
-    assert changeset.errors == [seo_metadata: {"should have %{count} item(s)", count: 10, validation: :length, kind: :is, type: :map}]
 
-    changeset = changeset(%{"seo_metadata" => %{"keywords" => ["foo", "bar"], "slug" => "my-post-7", "is_indexed" => false}}) |> validate_length(:seo_metadata, is: 10, message: "yada")
-    assert changeset.errors == [seo_metadata: {"yada", count: 10, validation: :length, kind: :is, type: :map}]
+    assert changeset.errors == [
+             seo_metadata:
+               {"should have %{count} item(s)",
+                count: 10, validation: :length, kind: :is, type: :map}
+           ]
+
+    changeset =
+      changeset(%{
+        "seo_metadata" => %{
+          "keywords" => ["foo", "bar"],
+          "slug" => "my-post-7",
+          "is_indexed" => false
+        }
+      })
+      |> validate_length(:seo_metadata, is: 10, message: "yada")
+
+    assert changeset.errors == [
+             seo_metadata: {"yada", count: 10, validation: :length, kind: :is, type: :map}
+           ]
   end
 
   test "validate_length/3 with associations" do
     post = %Post{comments: [%Comment{id: 1}]}
     changeset = change(post) |> put_assoc(:comments, []) |> validate_length(:comments, min: 1)
-    assert changeset.errors == [comments: {"should have at least %{count} item(s)", count: 1, validation: :length, kind: :min, type: :list}]
 
-    changeset = change(post) |> put_assoc(:comments, [%Comment{id: 2}, %Comment{id: 3}]) |> validate_length(:comments, max: 2)
+    assert changeset.errors == [
+             comments:
+               {"should have at least %{count} item(s)",
+                count: 1, validation: :length, kind: :min, type: :list}
+           ]
+
+    changeset =
+      change(post)
+      |> put_assoc(:comments, [%Comment{id: 2}, %Comment{id: 3}])
+      |> validate_length(:comments, max: 2)
+
     assert changeset.valid?
   end
 
   test "validate_number/3" do
-    changeset = changeset(%{"upvotes" => 3})
-                |> validate_number(:upvotes, greater_than: 0)
+    changeset =
+      changeset(%{"upvotes" => 3})
+      |> validate_number(:upvotes, greater_than: 0)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [upvotes: {:number, [greater_than: 0]}]
 
     # Single error
-    changeset = changeset(%{"upvotes" => -1})
-                |> validate_number(:upvotes, greater_than: 0)
+    changeset =
+      changeset(%{"upvotes" => -1})
+      |> validate_number(:upvotes, greater_than: 0)
+
     refute changeset.valid?
-    assert changeset.errors == [upvotes: {"must be greater than %{number}", validation: :number, kind: :greater_than, number: 0}]
+
+    assert changeset.errors == [
+             upvotes:
+               {"must be greater than %{number}",
+                validation: :number, kind: :greater_than, number: 0}
+           ]
+
     assert validations(changeset) == [upvotes: {:number, [greater_than: 0]}]
 
     # Non equality error
-    changeset = changeset(%{"upvotes" => 1})
-                |> validate_number(:upvotes, not_equal_to: 1)
+    changeset =
+      changeset(%{"upvotes" => 1})
+      |> validate_number(:upvotes, not_equal_to: 1)
+
     refute changeset.valid?
-    assert changeset.errors == [upvotes: {"must be not equal to %{number}", validation: :number, kind: :not_equal_to, number: 1}]
+
+    assert changeset.errors == [
+             upvotes:
+               {"must be not equal to %{number}",
+                validation: :number, kind: :not_equal_to, number: 1}
+           ]
+
     assert validations(changeset) == [upvotes: {:number, [not_equal_to: 1]}]
 
     # Multiple validations
-    changeset = changeset(%{"upvotes" => 3})
-                |> validate_number(:upvotes, greater_than: 0, less_than: 100)
+    changeset =
+      changeset(%{"upvotes" => 3})
+      |> validate_number(:upvotes, greater_than: 0, less_than: 100)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [upvotes: {:number, [greater_than: 0, less_than: 100]}]
 
     # Multiple validations with multiple errors
-    changeset = changeset(%{"upvotes" => 3})
-                |> validate_number(:upvotes, greater_than: 100, less_than: 0)
+    changeset =
+      changeset(%{"upvotes" => 3})
+      |> validate_number(:upvotes, greater_than: 100, less_than: 0)
+
     refute changeset.valid?
-    assert changeset.errors == [upvotes: {"must be greater than %{number}", validation: :number, kind: :greater_than, number: 100}]
+
+    assert changeset.errors == [
+             upvotes:
+               {"must be greater than %{number}",
+                validation: :number, kind: :greater_than, number: 100}
+           ]
 
     # Multiple validations with custom message errors
-    changeset = changeset(%{"upvotes" => 3})
-                |> validate_number(:upvotes, greater_than: 100, less_than: 0, message: "yada")
-    assert changeset.errors == [upvotes: {"yada", validation: :number, kind: :greater_than, number: 100}]
+    changeset =
+      changeset(%{"upvotes" => 3})
+      |> validate_number(:upvotes, greater_than: 100, less_than: 0, message: "yada")
+
+    assert changeset.errors == [
+             upvotes: {"yada", validation: :number, kind: :greater_than, number: 100}
+           ]
   end
 
   test "validate_number/3 with decimal" do
-    changeset = changeset(%{"decimal" => Decimal.new(1)})
-                |> validate_number(:decimal, greater_than: Decimal.new(-3))
+    changeset =
+      changeset(%{"decimal" => Decimal.new(1)})
+      |> validate_number(:decimal, greater_than: Decimal.new(-3))
+
     assert changeset.valid?
 
-    changeset = changeset(%{"decimal" => Decimal.new(-3)})
-                |> validate_number(:decimal, less_than: Decimal.new(1))
+    changeset =
+      changeset(%{"decimal" => Decimal.new(-3)})
+      |> validate_number(:decimal, less_than: Decimal.new(1))
+
     assert changeset.valid?
 
-    changeset = changeset(%{"decimal" => Decimal.new(-1)})
-                |> validate_number(:decimal, equal_to: Decimal.new(-1))
+    changeset =
+      changeset(%{"decimal" => Decimal.new(-1)})
+      |> validate_number(:decimal, equal_to: Decimal.new(-1))
+
     assert changeset.valid?
 
-    changeset = changeset(%{"decimal" => Decimal.new(0)})
-                |> validate_number(:decimal, not_equal_to: Decimal.new(-1))
+    changeset =
+      changeset(%{"decimal" => Decimal.new(0)})
+      |> validate_number(:decimal, not_equal_to: Decimal.new(-1))
+
     assert changeset.valid?
 
-    changeset = changeset(%{"decimal" => Decimal.new(-3)})
-                |> validate_number(:decimal, less_than_or_equal_to: Decimal.new(-1))
+    changeset =
+      changeset(%{"decimal" => Decimal.new(-3)})
+      |> validate_number(:decimal, less_than_or_equal_to: Decimal.new(-1))
+
     assert changeset.valid?
-    changeset = changeset(%{"decimal" => Decimal.new(-3)})
-                |> validate_number(:decimal, less_than_or_equal_to: Decimal.new(-3))
+
+    changeset =
+      changeset(%{"decimal" => Decimal.new(-3)})
+      |> validate_number(:decimal, less_than_or_equal_to: Decimal.new(-3))
+
     assert changeset.valid?
 
-    changeset = changeset(%{"decimal" => Decimal.new(-1)})
-                |> validate_number(:decimal, greater_than_or_equal_to: Decimal.new("-1.5"))
+    changeset =
+      changeset(%{"decimal" => Decimal.new(-1)})
+      |> validate_number(:decimal, greater_than_or_equal_to: Decimal.new("-1.5"))
+
     assert changeset.valid?
-    changeset = changeset(%{"decimal" => Decimal.new("1.5")})
-                |> validate_number(:decimal, greater_than_or_equal_to: Decimal.new("1.5"))
+
+    changeset =
+      changeset(%{"decimal" => Decimal.new("1.5")})
+      |> validate_number(:decimal, greater_than_or_equal_to: Decimal.new("1.5"))
+
     assert changeset.valid?
 
-    changeset = changeset(%{"decimal" => Decimal.new("4.9")})
-                |> validate_number(:decimal, greater_than_or_equal_to: 4.9)
+    changeset =
+      changeset(%{"decimal" => Decimal.new("4.9")})
+      |> validate_number(:decimal, greater_than_or_equal_to: 4.9)
+
     assert changeset.valid?
 
-    changeset = changeset(%{"decimal" => Decimal.new(5)})
-                |> validate_number(:decimal, less_than: 4)
+    changeset =
+      changeset(%{"decimal" => Decimal.new(5)})
+      |> validate_number(:decimal, less_than: 4)
+
     refute changeset.valid?
   end
 
   test "validate_number/3 with bad options" do
-    assert_raise ArgumentError, ~r"unknown option :min given to validate_number/3", fn  ->
+    assert_raise ArgumentError, ~r"unknown option :min given to validate_number/3", fn ->
       validate_number(changeset(%{"upvotes" => 1}), :upvotes, min: Decimal.new("1.5"))
     end
   end
 
   test "validate_number/3 with bad value" do
-    assert_raise ArgumentError, "expected field `virtual` to be a decimal, integer, or float, got: \"Oops\"", fn ->
-      validate_number(changeset(%{"virtual" => "Oops"}), :virtual, greater_than: 0)
-    end
+    assert_raise ArgumentError,
+                 "expected field `virtual` to be a decimal, integer, or float, got: \"Oops\"",
+                 fn ->
+                   validate_number(changeset(%{"virtual" => "Oops"}), :virtual, greater_than: 0)
+                 end
   end
 
   test "validate_number/3 with bad target" do
     # Number value
-    assert_raise ArgumentError, "expected option `greater_than` to be a decimal, integer, or float, got: 0..10", fn ->
-      validate_number(changeset(%{"upvotes" => 11}), :upvotes, greater_than: 0..10)
-    end
+    assert_raise ArgumentError,
+                 "expected option `greater_than` to be a decimal, integer, or float, got: 0..10",
+                 fn ->
+                   validate_number(changeset(%{"upvotes" => 11}), :upvotes, greater_than: 0..10)
+                 end
 
     # Decimal value
-    assert_raise ArgumentError, "expected option `greater_than` to be a decimal, integer, or float, got: 0..10", fn ->
-      validate_number(changeset(%{"decimal" => Decimal.new(11)}), :decimal, greater_than: 0..10)
-    end
+    assert_raise ArgumentError,
+                 "expected option `greater_than` to be a decimal, integer, or float, got: 0..10",
+                 fn ->
+                   validate_number(changeset(%{"decimal" => Decimal.new(11)}), :decimal,
+                     greater_than: 0..10
+                   )
+                 end
   end
 
   test "validate_confirmation/3" do
-    changeset = changeset(%{"title" => "title", "title_confirmation" => "title"})
-                |> validate_confirmation(:title)
+    changeset =
+      changeset(%{"title" => "title", "title_confirmation" => "title"})
+      |> validate_confirmation(:title)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [{:title, {:confirmation, []}}]
 
-    changeset = changeset(%{"title" => "title"})
-                |> validate_confirmation(:title)
+    changeset =
+      changeset(%{"title" => "title"})
+      |> validate_confirmation(:title)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [{:title, {:confirmation, []}}]
 
-    changeset = changeset(%{"title" => "title"})
-                |> validate_confirmation(:title, required: false)
+    changeset =
+      changeset(%{"title" => "title"})
+      |> validate_confirmation(:title, required: false)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [{:title, {:confirmation, [required: false]}}]
 
-    changeset = changeset(%{"title" => "title"})
-                |> validate_confirmation(:title, required: true)
+    changeset =
+      changeset(%{"title" => "title"})
+      |> validate_confirmation(:title, required: true)
+
     refute changeset.valid?
     assert changeset.errors == [title_confirmation: {"can't be blank", [validation: :required]}]
     assert validations(changeset) == [{:title, {:confirmation, [required: true]}}]
 
-    changeset = changeset(%{"title" => "title", "title_confirmation" => nil})
-                |> validate_confirmation(:title)
+    changeset =
+      changeset(%{"title" => "title", "title_confirmation" => nil})
+      |> validate_confirmation(:title)
+
     refute changeset.valid?
-    assert changeset.errors == [title_confirmation: {"does not match confirmation", [validation: :confirmation]}]
+
+    assert changeset.errors == [
+             title_confirmation: {"does not match confirmation", [validation: :confirmation]}
+           ]
+
     assert validations(changeset) == [{:title, {:confirmation, []}}]
 
-    changeset = changeset(%{"title" => "title", "title_confirmation" => "not title"})
-                |> validate_confirmation(:title)
+    changeset =
+      changeset(%{"title" => "title", "title_confirmation" => "not title"})
+      |> validate_confirmation(:title)
+
     refute changeset.valid?
-    assert changeset.errors == [title_confirmation: {"does not match confirmation", [validation: :confirmation]}]
+
+    assert changeset.errors == [
+             title_confirmation: {"does not match confirmation", [validation: :confirmation]}
+           ]
+
     assert validations(changeset) == [{:title, {:confirmation, []}}]
 
-    changeset = changeset(%{"title" => "title", "title_confirmation" => "not title"})
-                |> validate_confirmation(:title, message: "doesn't match field below")
+    changeset =
+      changeset(%{"title" => "title", "title_confirmation" => "not title"})
+      |> validate_confirmation(:title, message: "doesn't match field below")
+
     refute changeset.valid?
-    assert changeset.errors == [title_confirmation: {"doesn't match field below", [validation: :confirmation]}]
-    assert validations(changeset) == [{:title, {:confirmation, [message: "doesn't match field below"]}}]
+
+    assert changeset.errors == [
+             title_confirmation: {"doesn't match field below", [validation: :confirmation]}
+           ]
+
+    assert validations(changeset) == [
+             {:title, {:confirmation, [message: "doesn't match field below"]}}
+           ]
 
     # Skip when no parameter
-    changeset = changeset(%{"title" => "title"})
-                |> validate_confirmation(:title, message: "password doesn't match")
+    changeset =
+      changeset(%{"title" => "title"})
+      |> validate_confirmation(:title, message: "password doesn't match")
+
     assert changeset.valid?
     assert changeset.errors == []
-    assert validations(changeset) == [{:title, {:confirmation, [message: "password doesn't match"]}}]
+
+    assert validations(changeset) == [
+             {:title, {:confirmation, [message: "password doesn't match"]}}
+           ]
 
     # With casting
-    changeset = changeset(%{"upvotes" => "1", "upvotes_confirmation" => "1"})
-                |> validate_confirmation(:upvotes)
+    changeset =
+      changeset(%{"upvotes" => "1", "upvotes_confirmation" => "1"})
+      |> validate_confirmation(:upvotes)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [{:upvotes, {:confirmation, []}}]
 
     # With blank change
-    changeset = changeset(%{"password" => "", "password_confirmation" => "password"})
-                |> validate_confirmation(:password)
+    changeset =
+      changeset(%{"password" => "", "password_confirmation" => "password"})
+      |> validate_confirmation(:password)
+
     refute changeset.valid?
-    assert changeset.errors == [password_confirmation: {"does not match confirmation", [validation: :confirmation]}]
+
+    assert changeset.errors == [
+             password_confirmation: {"does not match confirmation", [validation: :confirmation]}
+           ]
 
     # With missing change
-    changeset = changeset(%{"password_confirmation" => "password"})
-                |> validate_confirmation(:password)
+    changeset =
+      changeset(%{"password_confirmation" => "password"})
+      |> validate_confirmation(:password)
+
     refute changeset.valid?
-    assert changeset.errors == [password_confirmation: {"does not match confirmation", [validation: :confirmation]}]
+
+    assert changeset.errors == [
+             password_confirmation: {"does not match confirmation", [validation: :confirmation]}
+           ]
 
     # invalid params
-    changeset = changeset(:invalid)
-                |> validate_confirmation(:password)
+    changeset =
+      changeset(:invalid)
+      |> validate_confirmation(:password)
+
     refute changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == []
@@ -1677,45 +2019,60 @@ defmodule Ecto.ChangesetTest do
 
   test "validate_acceptance/3" do
     # accepted
-    changeset = changeset(%{"terms_of_service" => "true"})
-                |> validate_acceptance(:terms_of_service)
+    changeset =
+      changeset(%{"terms_of_service" => "true"})
+      |> validate_acceptance(:terms_of_service)
+
     assert changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [terms_of_service: {:acceptance, []}]
 
     # not accepted
-    changeset = changeset(%{"terms_of_service" => "false"})
-                |> validate_acceptance(:terms_of_service)
+    changeset =
+      changeset(%{"terms_of_service" => "false"})
+      |> validate_acceptance(:terms_of_service)
+
     refute changeset.valid?
     assert changeset.errors == [terms_of_service: {"must be accepted", [validation: :acceptance]}]
     assert validations(changeset) == [terms_of_service: {:acceptance, []}]
 
-    changeset = changeset(%{"terms_of_service" => "other"})
-                |> validate_acceptance(:terms_of_service)
+    changeset =
+      changeset(%{"terms_of_service" => "other"})
+      |> validate_acceptance(:terms_of_service)
+
     refute changeset.valid?
     assert changeset.errors == [terms_of_service: {"must be accepted", [validation: :acceptance]}]
     assert validations(changeset) == [terms_of_service: {:acceptance, []}]
 
     # empty params
-    changeset = changeset(%{})
-                |> validate_acceptance(:terms_of_service)
+    changeset =
+      changeset(%{})
+      |> validate_acceptance(:terms_of_service)
+
     refute changeset.valid?
     assert changeset.errors == [terms_of_service: {"must be accepted", [validation: :acceptance]}]
     assert validations(changeset) == [terms_of_service: {:acceptance, []}]
 
     # invalid params
-    changeset = changeset(:invalid)
-                |> validate_acceptance(:terms_of_service)
+    changeset =
+      changeset(:invalid)
+      |> validate_acceptance(:terms_of_service)
+
     refute changeset.valid?
     assert changeset.errors == []
     assert validations(changeset) == [terms_of_service: {:acceptance, []}]
 
     # custom message
-    changeset = changeset(%{})
-                |> validate_acceptance(:terms_of_service, message: "must be abided")
+    changeset =
+      changeset(%{})
+      |> validate_acceptance(:terms_of_service, message: "must be abided")
+
     refute changeset.valid?
     assert changeset.errors == [terms_of_service: {"must be abided", [validation: :acceptance]}]
-    assert validations(changeset) == [terms_of_service: {:acceptance, [message: "must be abided"]}]
+
+    assert validations(changeset) == [
+             terms_of_service: {:acceptance, [message: "must be abided"]}
+           ]
   end
 
   alias Ecto.TestRepo
@@ -1771,24 +2128,37 @@ defmodule Ecto.ChangesetTest do
 
     test "does not validate uniqueness if there is any prior error on a field", context do
       Process.put(:test_repo_all_results, context.dup_result)
+
       changeset =
         context.base_changeset
         |> validate_length(:title, max: 3)
         |> unsafe_validate_unique(:title, TestRepo)
 
       refute changeset.valid?
-      assert changeset.errors == [title: {"should be at most %{count} character(s)", [count: 3, validation: :length, kind: :max, type: :string]}]
+
+      assert changeset.errors == [
+               title:
+                 {"should be at most %{count} character(s)",
+                  [count: 3, validation: :length, kind: :max, type: :string]}
+             ]
     end
 
-    test "does not validate uniqueness if there is any prior error on a combination of fields", context do
+    test "does not validate uniqueness if there is any prior error on a combination of fields",
+         context do
       Process.put(:test_repo_all_results, context.dup_result)
+
       changeset =
         context.base_changeset
         |> validate_length(:title, max: 3)
         |> unsafe_validate_unique([:title, :body], TestRepo)
 
       refute changeset.valid?
-      assert changeset.errors == [title: {"should be at most %{count} character(s)", [count: 3, validation: :length, kind: :max, type: :string]}]
+
+      assert changeset.errors == [
+               title:
+                 {"should be at most %{count} character(s)",
+                  [count: 3, validation: :length, kind: :max, type: :string]}
+             ]
     end
 
     test "allows setting a custom error message", context do
@@ -1805,7 +2175,10 @@ defmodule Ecto.ChangesetTest do
       Process.put(:test_repo_all_results, context.dup_result)
 
       changeset =
-        unsafe_validate_unique(context.base_changeset, [:title], TestRepo, message: "is taken", error_key: :foo)
+        unsafe_validate_unique(context.base_changeset, [:title], TestRepo,
+          message: "is taken",
+          error_key: :foo
+        )
 
       assert changeset.errors ==
                [foo: {"is taken", validation: :unsafe_unique, fields: [:title]}]
@@ -1814,7 +2187,13 @@ defmodule Ecto.ChangesetTest do
     test "accepts a prefix option" do
       body_change = changeset(%Post{title: "Hello World", body: "hi"}, %{body: "ho"})
       unsafe_validate_unique(body_change, :body, MockRepo, prefix: "my_prefix")
-      assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{prefix: "my_prefix"}, opts: []]
+
+      assert_receive [
+        MockRepo,
+        function: :exists?,
+        query: %Ecto.Query{prefix: "my_prefix"},
+        opts: []
+      ]
     end
 
     test "accepts repo options" do
@@ -1825,7 +2204,11 @@ defmodule Ecto.ChangesetTest do
 
     test "accepts query options" do
       body_change = changeset(%Post{title: "Hello World", body: "hi"}, %{body: "ho"})
-      unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in Post, where: is_nil(p.published_at)))
+
+      unsafe_validate_unique(body_change, :body, MockRepo,
+        query: Ecto.Query.from(p in Post, where: is_nil(p.published_at))
+      )
+
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: check_expr}] = wheres
 
@@ -1839,7 +2222,8 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: check_expr}] = wheres
 
-      assert Macro.to_string(check_expr) == "true and is_nil(&0.body()) and is_nil(&0.title()) and &0.color() == ^0"
+      assert Macro.to_string(check_expr) ==
+               "true and is_nil(&0.body()) and is_nil(&0.title()) and &0.color() == ^0"
     end
 
     test "generates correct where clause for single primary key without query option for loaded schema" do
@@ -1852,8 +2236,8 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(pk_expr,  "not (&0.id() == ^0)", "not(&0.id() == ^0)")
-      assert_eq_macro_to_string(check_expr,  "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(pk_expr, "not (&0.id() == ^0)", "not(&0.id() == ^0)")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
     end
 
     test "generates correct where clause for composite primary keys without query option for loaded schema" do
@@ -1866,8 +2250,13 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: pk_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(pk_expr,  "not (&0.id() == ^0 and &0.token() == ^1)", "not(&0.id() == ^0 and &0.token() == ^1)")
-      assert_eq_macro_to_string(check_expr,  "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(
+        pk_expr,
+        "not (&0.id() == ^0 and &0.token() == ^1)",
+        "not(&0.id() == ^0 and &0.token() == ^1)"
+      )
+
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
     end
 
     test "generates correct where clause for single primary key with query option for loaded schema" do
@@ -1876,13 +2265,21 @@ defmodule Ecto.ChangesetTest do
         |> Ecto.put_meta(state: :loaded)
         |> cast(%{body: "ho"}, [:body])
 
-      unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in SinglePkSchema, where: is_nil(p.published_at)))
+      unsafe_validate_unique(body_change, :body, MockRepo,
+        query: Ecto.Query.from(p in SinglePkSchema, where: is_nil(p.published_at))
+      )
+
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(query_expr,  "is_nil(&0.published_at())", "is_nil(&0.published_at())")
-      assert_eq_macro_to_string(pk_expr,  "not (&0.id() == ^0)", "not(&0.id() == ^0)")
-      assert_eq_macro_to_string(check_expr,  "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(
+        query_expr,
+        "is_nil(&0.published_at())",
+        "is_nil(&0.published_at())"
+      )
+
+      assert_eq_macro_to_string(pk_expr, "not (&0.id() == ^0)", "not(&0.id() == ^0)")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
     end
 
     test "generates correct where clause for composite primary keys with query option for loaded schema" do
@@ -1891,13 +2288,26 @@ defmodule Ecto.ChangesetTest do
         |> Ecto.put_meta(state: :loaded)
         |> changeset(%{body: "ho"})
 
-      unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in Post, where: is_nil(p.published_at)))
+      unsafe_validate_unique(body_change, :body, MockRepo,
+        query: Ecto.Query.from(p in Post, where: is_nil(p.published_at))
+      )
+
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: pk_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(query_expr,  "is_nil(&0.published_at())", "is_nil(&0.published_at())")
-      assert_eq_macro_to_string(pk_expr,  "not (&0.id() == ^0 and &0.token() == ^1)", "not(&0.id() == ^0 and &0.token() == ^1)")
-      assert_eq_macro_to_string(check_expr,  "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(
+        query_expr,
+        "is_nil(&0.published_at())",
+        "is_nil(&0.published_at())"
+      )
+
+      assert_eq_macro_to_string(
+        pk_expr,
+        "not (&0.id() == ^0 and &0.token() == ^1)",
+        "not(&0.id() == ^0 and &0.token() == ^1)"
+      )
+
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
     end
 
     test "generates correct where clause for single primary key without query option when schema wasn't loaded" do
@@ -1907,7 +2317,7 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(check_expr,  "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
     end
 
     test "generates correct where clause for composite primary keys without query option when schema wasn't loaded" do
@@ -1917,29 +2327,45 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(check_expr,  "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
     end
 
     test "generates correct where clause for single primary key with query option when schema wasn't loaded" do
       body_change = cast(%SinglePkSchema{id: 0, body: "hi"}, %{body: "ho"}, [:body])
 
-      unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in SinglePkSchema, where: is_nil(p.published_at)))
+      unsafe_validate_unique(body_change, :body, MockRepo,
+        query: Ecto.Query.from(p in SinglePkSchema, where: is_nil(p.published_at))
+      )
+
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(query_expr,  "is_nil(&0.published_at())", "is_nil(&0.published_at())")
-      assert_eq_macro_to_string(check_expr,  "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(
+        query_expr,
+        "is_nil(&0.published_at())",
+        "is_nil(&0.published_at())"
+      )
+
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
     end
 
     test "generates correct where clause for composite primary keys with query option when schema wasn't loaded" do
       body_change = changeset(%Post{id: 0, token: 1, body: "hi"}, %{body: "ho"})
 
-      unsafe_validate_unique(body_change, :body, MockRepo, query: Ecto.Query.from(p in Post, where: is_nil(p.published_at)))
+      unsafe_validate_unique(body_change, :body, MockRepo,
+        query: Ecto.Query.from(p in Post, where: is_nil(p.published_at))
+      )
+
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: query_expr}, %{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(query_expr,  "is_nil(&0.published_at())", "is_nil(&0.published_at())")
-      assert_eq_macro_to_string(check_expr,  "&0.body() == ^0", "&0.body() == ^0")
+      assert_eq_macro_to_string(
+        query_expr,
+        "is_nil(&0.published_at())",
+        "is_nil(&0.published_at())"
+      )
+
+      assert_eq_macro_to_string(check_expr, "&0.body() == ^0", "&0.body() == ^0")
     end
 
     test "only queries the db when necessary" do
@@ -1961,11 +2387,17 @@ defmodule Ecto.ChangesetTest do
 
       # struct
       changeset = Ecto.Changeset.cast({%NoSchemaPost{}, types}, %{title: "hi"}, Map.keys(types))
-      assert_raise ArgumentError, msg, fn -> unsafe_validate_unique(changeset, [:title], TestRepo) end
+
+      assert_raise ArgumentError, msg, fn ->
+        unsafe_validate_unique(changeset, [:title], TestRepo)
+      end
 
       # map
       changeset = Ecto.Changeset.cast({%{}, types}, %{title: "hi"}, Map.keys(types))
-      assert_raise ArgumentError, msg, fn -> unsafe_validate_unique(changeset, [:title], TestRepo) end
+
+      assert_raise ArgumentError, msg, fn ->
+        unsafe_validate_unique(changeset, [:title], TestRepo)
+      end
     end
 
     test "raise with embedded schema without base query" do
@@ -1987,7 +2419,7 @@ defmodule Ecto.ChangesetTest do
       assert_receive [MockRepo, function: :exists?, query: %Ecto.Query{wheres: wheres}, opts: []]
       assert [%{expr: check_expr}] = wheres
 
-      assert_eq_macro_to_string(check_expr,  "&0.origin() == ^0", "&0.origin() == ^0")
+      assert_eq_macro_to_string(check_expr, "&0.origin() == ^0", "&0.origin() == ^0")
     end
   end
 
@@ -2019,11 +2451,14 @@ defmodule Ecto.ChangesetTest do
     assert changeset.changes == %{}
     assert prepared_changes(changeset) == %{upvotes: 1}
 
-    changeset = changeset(%Post{upvotes: 2_147_483_647}, %{upvotes: 2_147_483_648}) |> optimistic_lock(:upvotes)
+    changeset =
+      changeset(%Post{upvotes: 2_147_483_647}, %{upvotes: 2_147_483_648})
+      |> optimistic_lock(:upvotes)
+
     assert changeset.filters == %{upvotes: 2_147_483_648}
     assert changeset.changes == %{upvotes: 2_147_483_648}
     assert prepared_changes(changeset) == %{upvotes: 1}
- end
+  end
 
   test "optimistic_lock/3 with struct" do
     changeset = %Post{} |> optimistic_lock(:upvotes)
@@ -2042,33 +2477,124 @@ defmodule Ecto.ChangesetTest do
   ## Constraints
   test "check_constraint/3" do
     changeset = change(%Post{}) |> check_constraint(:title, name: :title_must_be_short)
+
     assert constraints(changeset) ==
-           [%{type: :check, field: :title, constraint: "title_must_be_short", match: :exact,
-              error_message: "is invalid", error_type: :check}]
+             [
+               %{
+                 type: :check,
+                 field: :title,
+                 constraint: "title_must_be_short",
+                 match: :exact,
+                 error_message: "is invalid",
+                 error_type: :check
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> check_constraint(:title,
+        name: :title_must_be_short,
+        message: "cannot be more than 15 characters"
+      )
 
-    changeset = change(%Post{}) |> check_constraint(:title, name: :title_must_be_short, message: "cannot be more than 15 characters")
     assert constraints(changeset) ==
-           [%{type: :check, field: :title, constraint: "title_must_be_short", match: :exact,
-              error_message: "cannot be more than 15 characters", error_type: :check}]
+             [
+               %{
+                 type: :check,
+                 field: :title,
+                 constraint: "title_must_be_short",
+                 match: :exact,
+                 error_message: "cannot be more than 15 characters",
+                 error_type: :check
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> check_constraint(:title,
+        name: :title_must_be_short,
+        match: :exact,
+        message: "cannot be more than 15 characters"
+      )
 
-    changeset = change(%Post{}) |> check_constraint(:title, name: :title_must_be_short, match: :exact, message: "cannot be more than 15 characters")
     assert constraints(changeset) ==
-            [%{type: :check, field: :title, constraint: "title_must_be_short", match: :exact, error_message: "cannot be more than 15 characters", error_type: :check}]
+             [
+               %{
+                 type: :check,
+                 field: :title,
+                 constraint: "title_must_be_short",
+                 match: :exact,
+                 error_message: "cannot be more than 15 characters",
+                 error_type: :check
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> check_constraint(:title,
+        name: :title_must_be_short,
+        match: :suffix,
+        message: "cannot be more than 15 characters"
+      )
 
-    changeset = change(%Post{}) |> check_constraint(:title, name: :title_must_be_short, match: :suffix, message: "cannot be more than 15 characters")
     assert constraints(changeset) ==
-          [%{type: :check, field: :title, constraint: "title_must_be_short", match: :suffix, error_message: "cannot be more than 15 characters", error_type: :check}]
+             [
+               %{
+                 type: :check,
+                 field: :title,
+                 constraint: "title_must_be_short",
+                 match: :suffix,
+                 error_message: "cannot be more than 15 characters",
+                 error_type: :check
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> check_constraint(:title,
+        name: :title_must_be_short,
+        match: :prefix,
+        message: "cannot be more than 15 characters"
+      )
 
-    changeset = change(%Post{}) |> check_constraint(:title, name: :title_must_be_short, match: :prefix, message: "cannot be more than 15 characters")
     assert constraints(changeset) ==
-          [%{type: :check, field: :title, constraint: "title_must_be_short", match: :prefix, error_message: "cannot be more than 15 characters", error_type: :check}]
+             [
+               %{
+                 type: :check,
+                 field: :title,
+                 constraint: "title_must_be_short",
+                 match: :prefix,
+                 error_message: "cannot be more than 15 characters",
+                 error_type: :check
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> check_constraint(:title,
+        name: ~r/title_must_be_short\d+/,
+        message: "cannot be more than 15 characters"
+      )
 
-    changeset = change(%Post{}) |> check_constraint(:title, name: ~r/title_must_be_short\d+/, message: "cannot be more than 15 characters")
     assert constraints(changeset) ==
-          [%{type: :check, field: :title, constraint: ~r/title_must_be_short\d+/, match: :exact, error_message: "cannot be more than 15 characters", error_type: :check}]
+             [
+               %{
+                 type: :check,
+                 field: :title,
+                 constraint: ~r/title_must_be_short\d+/,
+                 match: :exact,
+                 error_message: "cannot be more than 15 characters",
+                 error_type: :check
+               }
+             ]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
-      change(%Post{}) |> check_constraint(:title, name: :title_must_be_short, match: :invalid, message: "match is invalid")
+      change(%Post{})
+      |> check_constraint(:title,
+        name: :title_must_be_short,
+        match: :invalid,
+        message: "match is invalid"
+      )
     end
 
     assert_raise ArgumentError, ~r/supply the name/, fn ->
@@ -2080,27 +2606,83 @@ defmodule Ecto.ChangesetTest do
     changeset = change(%Post{}) |> unique_constraint(:title)
 
     assert constraints(changeset) ==
-           [%{type: :unique, field: :title, constraint: "posts_title_index", match: :exact,
-              error_message: "has already been taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :title,
+                 constraint: "posts_title_index",
+                 match: :exact,
+                 error_message: "has already been taken",
+                 error_type: :unique
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> unique_constraint(:title, name: :whatever, match: :exact, message: "is taken")
 
-    changeset = change(%Post{}) |> unique_constraint(:title, name: :whatever, match: :exact, message: "is taken")
     assert constraints(changeset) ==
-           [%{type: :unique, field: :title, constraint: "whatever", match: :exact, error_message: "is taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :title,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is taken",
+                 error_type: :unique
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> unique_constraint(:title, name: :whatever, match: :suffix, message: "is taken")
 
-    changeset = change(%Post{}) |> unique_constraint(:title, name: :whatever, match: :suffix, message: "is taken")
     assert constraints(changeset) ==
-           [%{type: :unique, field: :title, constraint: "whatever", match: :suffix, error_message: "is taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :title,
+                 constraint: "whatever",
+                 match: :suffix,
+                 error_message: "is taken",
+                 error_type: :unique
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> unique_constraint(:title, name: :whatever, match: :prefix, message: "is taken")
 
-    changeset = change(%Post{}) |> unique_constraint(:title, name: :whatever, match: :prefix, message: "is taken")
     assert constraints(changeset) ==
-           [%{type: :unique, field: :title, constraint: "whatever", match: :prefix, error_message: "is taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :title,
+                 constraint: "whatever",
+                 match: :prefix,
+                 error_message: "is taken",
+                 error_type: :unique
+               }
+             ]
+
+    changeset =
+      change(%Post{}) |> unique_constraint(:title, name: ~r/whatever\d+/, message: "is taken")
 
-    changeset = change(%Post{}) |> unique_constraint(:title, name: ~r/whatever\d+/, message: "is taken")
     assert constraints(changeset) ==
-          [%{type: :unique, field: :title, constraint: ~r/whatever\d+/, match: :exact, error_message: "is taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :title,
+                 constraint: ~r/whatever\d+/,
+                 match: :exact,
+                 error_message: "is taken",
+                 error_type: :unique
+               }
+             ]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
-      change(%Post{}) |> unique_constraint(:title, name: :whatever, match: :invalid, message: "match is invalid")
+      change(%Post{})
+      |> unique_constraint(:title, name: :whatever, match: :invalid, message: "match is invalid")
     end
   end
 
@@ -2108,23 +2690,66 @@ defmodule Ecto.ChangesetTest do
     changeset = change(%Post{}) |> unique_constraint(:permalink)
 
     assert constraints(changeset) ==
-           [%{type: :unique, field: :permalink, constraint: "posts_url_index", match: :exact,
-              error_message: "has already been taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :permalink,
+                 constraint: "posts_url_index",
+                 match: :exact,
+                 error_message: "has already been taken",
+                 error_type: :unique
+               }
+             ]
+
+    changeset =
+      change(%Post{}) |> unique_constraint(:permalink, name: :whatever, message: "is taken")
 
-    changeset = change(%Post{}) |> unique_constraint(:permalink, name: :whatever, message: "is taken")
     assert constraints(changeset) ==
-           [%{type: :unique, field: :permalink, constraint: "whatever", match: :exact, error_message: "is taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :permalink,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is taken",
+                 error_type: :unique
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> unique_constraint(:permalink, name: :whatever, match: :suffix, message: "is taken")
 
-    changeset = change(%Post{}) |> unique_constraint(:permalink, name: :whatever, match: :suffix, message: "is taken")
     assert constraints(changeset) ==
-           [%{type: :unique, field: :permalink, constraint: "whatever", match: :suffix, error_message: "is taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :permalink,
+                 constraint: "whatever",
+                 match: :suffix,
+                 error_message: "is taken",
+                 error_type: :unique
+               }
+             ]
+
+    changeset =
+      change(%Post{}) |> unique_constraint(:permalink, name: ~r/whatever\d+/, message: "is taken")
 
-    changeset = change(%Post{}) |> unique_constraint(:permalink, name: ~r/whatever\d+/, message: "is taken")
     assert constraints(changeset) ==
-          [%{type: :unique, field: :permalink, constraint: ~r/whatever\d+/, match: :exact, error_message: "is taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :permalink,
+                 constraint: ~r/whatever\d+/,
+                 match: :exact,
+                 error_message: "is taken",
+                 error_type: :unique
+               }
+             ]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
-      change(%Post{}) |> unique_constraint(:permalink, name: :whatever, match: :invalid, message: "is taken")
+      change(%Post{})
+      |> unique_constraint(:permalink, name: :whatever, match: :invalid, message: "is taken")
     end
   end
 
@@ -2132,161 +2757,470 @@ defmodule Ecto.ChangesetTest do
     changeset = change(%Post{}) |> unique_constraint([:permalink, :color])
 
     assert constraints(changeset) ==
-           [%{type: :unique, field: :permalink, constraint: "posts_url_color_index", match: :exact,
-              error_message: "has already been taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :permalink,
+                 constraint: "posts_url_color_index",
+                 match: :exact,
+                 error_message: "has already been taken",
+                 error_type: :unique
+               }
+             ]
 
     changeset = change(%Post{}) |> unique_constraint([:permalink, :color], error_key: :color)
 
     assert constraints(changeset) ==
-           [%{type: :unique, field: :color, constraint: "posts_url_color_index", match: :exact,
-              error_message: "has already been taken", error_type: :unique}]
+             [
+               %{
+                 type: :unique,
+                 field: :color,
+                 constraint: "posts_url_color_index",
+                 match: :exact,
+                 error_message: "has already been taken",
+                 error_type: :unique
+               }
+             ]
   end
 
   test "foreign_key_constraint/3" do
     changeset = change(%Comment{}) |> foreign_key_constraint(:post_id)
+
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :post_id, constraint: "comments_post_id_fkey", match: :exact,
-              error_message: "does not exist", error_type: :foreign}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post_id,
+                 constraint: "comments_post_id_fkey",
+                 match: :exact,
+                 error_message: "does not exist",
+                 error_type: :foreign
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> foreign_key_constraint(:post_id, name: :whatever, message: "is not available")
 
-    changeset = change(%Comment{}) |> foreign_key_constraint(:post_id, name: :whatever, message: "is not available")
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :post_id, constraint: "whatever", match: :exact, error_message: "is not available", error_type: :foreign}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post_id,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is not available",
+                 error_type: :foreign
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> foreign_key_constraint(:post,
+        name: :whatever,
+        match: :exact,
+        message: "is not available"
+      )
 
-    changeset = change(%Comment{}) |> foreign_key_constraint(:post, name: :whatever, match: :exact, message: "is not available")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :post, constraint: "whatever", match: :exact, error_message: "is not available", error_type: :foreign}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is not available",
+                 error_type: :foreign
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> foreign_key_constraint(:post,
+        name: :whatever,
+        match: :suffix,
+        message: "is not available"
+      )
 
-    changeset = change(%Comment{}) |> foreign_key_constraint(:post, name: :whatever, match: :suffix, message: "is not available")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :post, constraint: "whatever", match: :suffix, error_message: "is not available", error_type: :foreign}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: "whatever",
+                 match: :suffix,
+                 error_message: "is not available",
+                 error_type: :foreign
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> foreign_key_constraint(:post,
+        name: :whatever,
+        match: :prefix,
+        message: "is not available"
+      )
 
-    changeset = change(%Comment{}) |> foreign_key_constraint(:post, name: :whatever, match: :prefix, message: "is not available")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :post, constraint: "whatever", match: :prefix, error_message: "is not available", error_type: :foreign}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: "whatever",
+                 match: :prefix,
+                 error_message: "is not available",
+                 error_type: :foreign
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> foreign_key_constraint(:post, name: ~r/whatever\d+/, message: "is not available")
 
-    changeset = change(%Comment{}) |> foreign_key_constraint(:post, name: ~r/whatever\d+/, message: "is not available")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :post, constraint: ~r/whatever\d+/, match: :exact, error_message: "is not available", error_type: :foreign}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: ~r/whatever\d+/,
+                 match: :exact,
+                 error_message: "is not available",
+                 error_type: :foreign
+               }
+             ]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
-      change(%Comment{}) |> foreign_key_constraint(:post, name: :whatever, match: :invalid, message: "match is invalid")
+      change(%Comment{})
+      |> foreign_key_constraint(:post,
+        name: :whatever,
+        match: :invalid,
+        message: "match is invalid"
+      )
     end
   end
 
   test "foreign_key_constraint/3 on field with :source" do
     changeset = change(%Post{}) |> foreign_key_constraint(:permalink)
+
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :permalink, constraint: "posts_url_fkey", match: :exact,
-              error_message: "does not exist", error_type: :foreign}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :permalink,
+                 constraint: "posts_url_fkey",
+                 match: :exact,
+                 error_message: "does not exist",
+                 error_type: :foreign
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> foreign_key_constraint(:permalink, name: :whatever, message: "is not available")
 
-    changeset = change(%Post{}) |> foreign_key_constraint(:permalink, name: :whatever, message: "is not available")
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :permalink, constraint: "whatever", match: :exact, error_message: "is not available", error_type: :foreign}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :permalink,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is not available",
+                 error_type: :foreign
+               }
+             ]
   end
 
   test "assoc_constraint/3" do
     changeset = change(%Comment{}) |> assoc_constraint(:post)
+
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :post, constraint: "comments_post_id_fkey", match: :exact,
-              error_message: "does not exist", error_type: :assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: "comments_post_id_fkey",
+                 match: :exact,
+                 error_message: "does not exist",
+                 error_type: :assoc
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> assoc_constraint(:post, name: :whatever, match: :exact, message: "is not available")
 
-    changeset = change(%Comment{}) |> assoc_constraint(:post, name: :whatever, match: :exact, message: "is not available")
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :post, constraint: "whatever", match: :exact, error_message: "is not available", error_type: :assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is not available",
+                 error_type: :assoc
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> assoc_constraint(:post, name: :whatever, match: :suffix, message: "is not available")
 
-    changeset = change(%Comment{}) |> assoc_constraint(:post, name: :whatever, match: :suffix, message: "is not available")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :post, constraint: "whatever", match: :suffix, error_message: "is not available", error_type: :assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: "whatever",
+                 match: :suffix,
+                 error_message: "is not available",
+                 error_type: :assoc
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> assoc_constraint(:post, name: :whatever, match: :prefix, message: "is not available")
 
-    changeset = change(%Comment{}) |> assoc_constraint(:post, name: :whatever, match: :prefix, message: "is not available")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :post, constraint: "whatever", match: :prefix, error_message: "is not available", error_type: :assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: "whatever",
+                 match: :prefix,
+                 error_message: "is not available",
+                 error_type: :assoc
+               }
+             ]
+
+    changeset =
+      change(%Comment{})
+      |> assoc_constraint(:post, name: ~r/whatever\d+/, message: "is not available")
 
-    changeset = change(%Comment{}) |> assoc_constraint(:post, name: ~r/whatever\d+/, message: "is not available")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :post, constraint: ~r/whatever\d+/, match: :exact, error_message: "is not available", error_type: :assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :post,
+                 constraint: ~r/whatever\d+/,
+                 match: :exact,
+                 error_message: "is not available",
+                 error_type: :assoc
+               }
+             ]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
-      change(%Comment{}) |> assoc_constraint(:post, name: :whatever, match: :invalid, message: "match is invalid")
+      change(%Comment{})
+      |> assoc_constraint(:post, name: :whatever, match: :invalid, message: "match is invalid")
     end
   end
 
   test "assoc_constraint/3 on field with :source" do
     changeset = change(%Post{}) |> assoc_constraint(:category)
+
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :category, constraint: "posts_category_id_fkey", match: :exact,
-              error_message: "does not exist", error_type: :assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :category,
+                 constraint: "posts_category_id_fkey",
+                 match: :exact,
+                 error_message: "does not exist",
+                 error_type: :assoc
+               }
+             ]
+
+    changeset =
+      change(%Post{}) |> assoc_constraint(:category, name: :whatever, message: "is not available")
 
-    changeset = change(%Post{}) |> assoc_constraint(:category, name: :whatever, message: "is not available")
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :category, constraint: "whatever", match: :exact, error_message: "is not available", error_type: :assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :category,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is not available",
+                 error_type: :assoc
+               }
+             ]
   end
 
   test "assoc_constraint/3 with errors" do
-    message = ~r"cannot add constraint to changeset because association `unknown` does not exist. Did you mean one of `category`, `comment`, `comments`?"
+    message =
+      ~r"cannot add constraint to changeset because association `unknown` does not exist. Did you mean one of `category`, `comment`, `comments`?"
+
     assert_raise ArgumentError, message, fn ->
       change(%Post{}) |> assoc_constraint(:unknown)
     end
 
     message = ~r"assoc_constraint can only be added to belongs to associations"
+
     assert_raise ArgumentError, message, fn ->
       change(%Post{}) |> assoc_constraint(:comments)
     end
   end
 
   test "no_assoc_constraint/3" do
-    changeset = change(%Post{}) |> no_assoc_constraint(:comments, name: :comments_post_id_fkey, match: :exact, message: "exists")
+    changeset =
+      change(%Post{})
+      |> no_assoc_constraint(:comments,
+        name: :comments_post_id_fkey,
+        match: :exact,
+        message: "exists"
+      )
+
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :comments, constraint: "comments_post_id_fkey", match: :exact, error_message: "exists", error_type: :no_assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :comments,
+                 constraint: "comments_post_id_fkey",
+                 match: :exact,
+                 error_message: "exists",
+                 error_type: :no_assoc
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> no_assoc_constraint(:comments,
+        name: :comments_post_id_fkey,
+        match: :suffix,
+        message: "exists"
+      )
 
-    changeset = change(%Post{}) |> no_assoc_constraint(:comments, name: :comments_post_id_fkey, match: :suffix, message: "exists")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :comments, constraint: "comments_post_id_fkey", match: :suffix, error_message: "exists", error_type: :no_assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :comments,
+                 constraint: "comments_post_id_fkey",
+                 match: :suffix,
+                 error_message: "exists",
+                 error_type: :no_assoc
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> no_assoc_constraint(:comments,
+        name: :comments_post_id_fkey,
+        match: :prefix,
+        message: "exists"
+      )
 
-    changeset = change(%Post{}) |> no_assoc_constraint(:comments, name: :comments_post_id_fkey, match: :prefix, message: "exists")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :comments, constraint: "comments_post_id_fkey", match: :prefix, error_message: "exists", error_type: :no_assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :comments,
+                 constraint: "comments_post_id_fkey",
+                 match: :prefix,
+                 error_message: "exists",
+                 error_type: :no_assoc
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> no_assoc_constraint(:comments, name: ~r/comments_post_id_fkey\d+/, message: "exists")
 
-    changeset = change(%Post{}) |> no_assoc_constraint(:comments, name: ~r/comments_post_id_fkey\d+/, message: "exists")
     assert constraints(changeset) ==
-          [%{type: :foreign_key, field: :comments, constraint: ~r/comments_post_id_fkey\d+/, match: :exact, error_message: "exists", error_type: :no_assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :comments,
+                 constraint: ~r/comments_post_id_fkey\d+/,
+                 match: :exact,
+                 error_message: "exists",
+                 error_type: :no_assoc
+               }
+             ]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
-      change(%Post{}) |> no_assoc_constraint(:comments, name: :comments_post_id_fkey, match: :invalid, message: "is taken")
+      change(%Post{})
+      |> no_assoc_constraint(:comments,
+        name: :comments_post_id_fkey,
+        match: :invalid,
+        message: "is taken"
+      )
     end
   end
 
   test "no_assoc_constraint/3 with has_many" do
     changeset = change(%Post{}) |> no_assoc_constraint(:comments)
+
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :comments, constraint: "comments_post_id_fkey", match: :exact,
-              error_message: "are still associated with this entry", error_type: :no_assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :comments,
+                 constraint: "comments_post_id_fkey",
+                 match: :exact,
+                 error_message: "are still associated with this entry",
+                 error_type: :no_assoc
+               }
+             ]
+
+    changeset =
+      change(%Post{}) |> no_assoc_constraint(:comments, name: :whatever, message: "exists")
 
-    changeset = change(%Post{}) |> no_assoc_constraint(:comments, name: :whatever, message: "exists")
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :comments, constraint: "whatever", match: :exact,
-              error_message: "exists", error_type: :no_assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :comments,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "exists",
+                 error_type: :no_assoc
+               }
+             ]
   end
 
   test "no_assoc_constraint/3 with has_one" do
     changeset = change(%Post{}) |> no_assoc_constraint(:comment)
+
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :comment, constraint: "comments_post_id_fkey", match: :exact,
-              error_message: "is still associated with this entry", error_type: :no_assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :comment,
+                 constraint: "comments_post_id_fkey",
+                 match: :exact,
+                 error_message: "is still associated with this entry",
+                 error_type: :no_assoc
+               }
+             ]
+
+    changeset =
+      change(%Post{}) |> no_assoc_constraint(:comment, name: :whatever, message: "exists")
 
-    changeset = change(%Post{}) |> no_assoc_constraint(:comment, name: :whatever, message: "exists")
     assert constraints(changeset) ==
-           [%{type: :foreign_key, field: :comment, constraint: "whatever", match: :exact,
-              error_message: "exists", error_type: :no_assoc}]
+             [
+               %{
+                 type: :foreign_key,
+                 field: :comment,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "exists",
+                 error_type: :no_assoc
+               }
+             ]
   end
 
   test "no_assoc_constraint/3 with errors" do
     message = ~r"cannot add constraint to changeset because association `unknown` does not exist"
+
     assert_raise ArgumentError, message, fn ->
       change(%Post{}) |> no_assoc_constraint(:unknown)
     end
 
     message = ~r"no_assoc_constraint can only be added to has one/many associations"
+
     assert_raise ArgumentError, message, fn ->
       change(%Comment{}) |> no_assoc_constraint(:post)
     end
@@ -2294,33 +3228,105 @@ defmodule Ecto.ChangesetTest do
 
   test "exclusion_constraint/3" do
     changeset = change(%Post{}) |> exclusion_constraint(:title)
+
     assert constraints(changeset) ==
-           [%{type: :exclusion, field: :title, constraint: "posts_title_exclusion", match: :exact,
-              error_message: "violates an exclusion constraint", error_type: :exclusion}]
+             [
+               %{
+                 type: :exclusion,
+                 field: :title,
+                 constraint: "posts_title_exclusion",
+                 match: :exact,
+                 error_message: "violates an exclusion constraint",
+                 error_type: :exclusion
+               }
+             ]
+
+    changeset =
+      change(%Post{}) |> exclusion_constraint(:title, name: :whatever, message: "is invalid")
 
-    changeset = change(%Post{}) |> exclusion_constraint(:title, name: :whatever, message: "is invalid")
     assert constraints(changeset) ==
-           [%{type: :exclusion, field: :title, constraint: "whatever", match: :exact,
-              error_message: "is invalid", error_type: :exclusion}]
+             [
+               %{
+                 type: :exclusion,
+                 field: :title,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is invalid",
+                 error_type: :exclusion
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> exclusion_constraint(:title, name: :whatever, match: :exact, message: "is invalid")
 
-    changeset = change(%Post{}) |> exclusion_constraint(:title, name: :whatever, match: :exact, message: "is invalid")
     assert constraints(changeset) ==
-            [%{type: :exclusion, field: :title, constraint: "whatever", match: :exact, error_message: "is invalid", error_type: :exclusion}]
+             [
+               %{
+                 type: :exclusion,
+                 field: :title,
+                 constraint: "whatever",
+                 match: :exact,
+                 error_message: "is invalid",
+                 error_type: :exclusion
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> exclusion_constraint(:title, name: :whatever, match: :suffix, message: "is invalid")
 
-    changeset = change(%Post{}) |> exclusion_constraint(:title, name: :whatever, match: :suffix, message: "is invalid")
     assert constraints(changeset) ==
-          [%{type: :exclusion, field: :title, constraint: "whatever", match: :suffix, error_message: "is invalid", error_type: :exclusion}]
+             [
+               %{
+                 type: :exclusion,
+                 field: :title,
+                 constraint: "whatever",
+                 match: :suffix,
+                 error_message: "is invalid",
+                 error_type: :exclusion
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> exclusion_constraint(:title, name: :whatever, match: :prefix, message: "is invalid")
 
-    changeset = change(%Post{}) |> exclusion_constraint(:title, name: :whatever, match: :prefix, message: "is invalid")
     assert constraints(changeset) ==
-          [%{type: :exclusion, field: :title, constraint: "whatever", match: :prefix, error_message: "is invalid", error_type: :exclusion}]
+             [
+               %{
+                 type: :exclusion,
+                 field: :title,
+                 constraint: "whatever",
+                 match: :prefix,
+                 error_message: "is invalid",
+                 error_type: :exclusion
+               }
+             ]
+
+    changeset =
+      change(%Post{})
+      |> exclusion_constraint(:title, name: ~r/whatever\d+/, message: "is invalid")
 
-    changeset = change(%Post{}) |> exclusion_constraint(:title, name: ~r/whatever\d+/, message: "is invalid")
     assert constraints(changeset) ==
-          [%{type: :exclusion, field: :title, constraint: ~r/whatever\d+/, match: :exact, error_message: "is invalid", error_type: :exclusion}]
+             [
+               %{
+                 type: :exclusion,
+                 field: :title,
+                 constraint: ~r/whatever\d+/,
+                 match: :exact,
+                 error_message: "is invalid",
+                 error_type: :exclusion
+               }
+             ]
 
     assert_raise ArgumentError, ~r/invalid match type: :invalid/, fn ->
-      change(%Post{}) |> exclusion_constraint(:title, name: :whatever, match: :invalid, message: "match is invalid")
+      change(%Post{})
+      |> exclusion_constraint(:title,
+        name: :whatever,
+        match: :invalid,
+        message: "match is invalid"
+      )
     end
   end
 
@@ -2333,22 +3339,25 @@ defmodule Ecto.ChangesetTest do
       |> validate_format(:body, ~r/888/)
       |> add_error(:title, "is taken", name: "your title")
 
-    errors = traverse_errors(changeset, fn
-      {"is invalid", [type: type, validation: :cast]} ->
-        "expected to be #{inspect(type)}"
-      {"is taken", keys} ->
-        String.upcase("#{keys[:name]} is taken")
-      {msg, keys} ->
-        msg
-        |> String.replace("%{count}", to_string(keys[:count]))
-        |> String.upcase()
-    end)
+    errors =
+      traverse_errors(changeset, fn
+        {"is invalid", [type: type, validation: :cast]} ->
+          "expected to be #{inspect(type)}"
+
+        {"is taken", keys} ->
+          String.upcase("#{keys[:name]} is taken")
+
+        {msg, keys} ->
+          msg
+          |> String.replace("%{count}", to_string(keys[:count]))
+          |> String.upcase()
+      end)
 
     assert errors == %{
-      body: ["HAS INVALID FORMAT", "SHOULD BE AT LEAST 3 CHARACTER(S)"],
-      title: ["YOUR TITLE IS TAKEN"],
-      upvotes: ["expected to be :integer"],
-    }
+             body: ["HAS INVALID FORMAT", "SHOULD BE AT LEAST 3 CHARACTER(S)"],
+             title: ["YOUR TITLE IS TAKEN"],
+             upvotes: ["expected to be :integer"]
+           }
   end
 
   test "traverses changeset errors with field" do
@@ -2359,31 +3368,42 @@ defmodule Ecto.ChangesetTest do
       |> validate_inclusion(:body, ["hola", "bonjour", "hallo"])
       |> add_error(:title, "is taken", name: "your title")
 
-    errors = traverse_errors(changeset, fn
-      %Ecto.Changeset{}, field, {_, [type: type, validation: :cast]} ->
-        "expected #{field} to be #{inspect(type)}"
-      %Ecto.Changeset{}, field, {_, [name: "your title"]} ->
-        "value in #{field} is taken"
-        |> String.upcase()
-      %Ecto.Changeset{}, field, {_, [count: 3, validation: :length, kind: :min, type: :string] = keys} ->
-        "should be at least #{keys[:count]} character(s) in field #{field}"
-        |> String.upcase()
-      %Ecto.Changeset{validations: validations}, field, {_, [validation: :format]} ->
-        validation = Keyword.get_values(validations, field)
-        "field #{field} should match format #{inspect validation[:format]}"
-      %Ecto.Changeset{validations: validations}, field, {_, [validation: :inclusion, enum: _]} ->
-        validation = Keyword.get_values(validations, field)
-        values = Enum.join(validation[:inclusion], ", ")
-        "#{field} value should be in #{values}"
-    end)
+    errors =
+      traverse_errors(changeset, fn
+        %Ecto.Changeset{}, field, {_, [type: type, validation: :cast]} ->
+          "expected #{field} to be #{inspect(type)}"
+
+        %Ecto.Changeset{}, field, {_, [name: "your title"]} ->
+          "value in #{field} is taken"
+          |> String.upcase()
+
+        %Ecto.Changeset{},
+        field,
+        {_, [count: 3, validation: :length, kind: :min, type: :string] = keys} ->
+          "should be at least #{keys[:count]} character(s) in field #{field}"
+          |> String.upcase()
+
+        %Ecto.Changeset{validations: validations}, field, {_, [validation: :format]} ->
+          validation = Keyword.get_values(validations, field)
+          "field #{field} should match format #{inspect(validation[:format])}"
+
+        %Ecto.Changeset{validations: validations},
+        field,
+        {_, [validation: :inclusion, enum: _]} ->
+          validation = Keyword.get_values(validations, field)
+          values = Enum.join(validation[:inclusion], ", ")
+          "#{field} value should be in #{values}"
+      end)
 
     assert errors == %{
-      body: ["body value should be in hola, bonjour, hallo",
-             "field body should match format ~r/888/",
-             "SHOULD BE AT LEAST 3 CHARACTER(S) IN FIELD BODY"],
-      title: ["VALUE IN TITLE IS TAKEN"],
-      upvotes: ["expected upvotes to be :integer"],
-    }
+             body: [
+               "body value should be in hola, bonjour, hallo",
+               "field body should match format ~r/888/",
+               "SHOULD BE AT LEAST 3 CHARACTER(S) IN FIELD BODY"
+             ],
+             title: ["VALUE IN TITLE IS TAKEN"],
+             upvotes: ["expected upvotes to be :integer"]
+           }
   end
 
   ## traverse_validations
@@ -2395,17 +3415,25 @@ defmodule Ecto.ChangesetTest do
       |> validate_format(:body, ~r/888/)
       |> validate_inclusion(:upvotes, [:good, :bad])
 
-    validations = traverse_validations(changeset, fn
-      {:length, opts} -> {:length, "#{Keyword.get(opts, :min, 0)}-#{Keyword.get(opts, :max, 32)}"}
-      {:format, %Regex{source: source}} -> {:format, "/#{source}/"}
-      {:inclusion, enum} -> {:inclusion, Enum.join(enum, ", ")}
-      {other, opts} -> {other, inspect(opts)}
-    end)
+    validations =
+      traverse_validations(changeset, fn
+        {:length, opts} ->
+          {:length, "#{Keyword.get(opts, :min, 0)}-#{Keyword.get(opts, :max, 32)}"}
+
+        {:format, %Regex{source: source}} ->
+          {:format, "/#{source}/"}
+
+        {:inclusion, enum} ->
+          {:inclusion, Enum.join(enum, ", ")}
+
+        {other, opts} ->
+          {other, inspect(opts)}
+      end)
 
     assert validations == %{
-      body: [format: "/888/", length: "3-32"],
-      upvotes: [inclusion: "good, bad"],
-    }
+             body: [format: "/888/", length: "3-32"],
+             upvotes: [inclusion: "good, bad"]
+           }
   end
 
   test "traverses changeset validations with field" do
@@ -2415,19 +3443,22 @@ defmodule Ecto.ChangesetTest do
       |> validate_format(:body, ~r/888/)
       |> validate_inclusion(:upvotes, [:good, :bad])
 
-    validations = traverse_validations(changeset, fn
-      %Ecto.Changeset{}, field, {:length, opts} ->
-        "#{field} must be #{Keyword.get(opts, :min, 0)}-#{Keyword.get(opts, :max, 32)} long"
-      %Ecto.Changeset{}, field, {:format, %Regex{source: source}} ->
-        "#{field} must match /#{source}/"
-      %Ecto.Changeset{}, field, {:inclusion, enum} ->
-        "#{field} must be one of: #{Enum.join(enum, ", ")}"
-    end)
+    validations =
+      traverse_validations(changeset, fn
+        %Ecto.Changeset{}, field, {:length, opts} ->
+          "#{field} must be #{Keyword.get(opts, :min, 0)}-#{Keyword.get(opts, :max, 32)} long"
+
+        %Ecto.Changeset{}, field, {:format, %Regex{source: source}} ->
+          "#{field} must match /#{source}/"
+
+        %Ecto.Changeset{}, field, {:inclusion, enum} ->
+          "#{field} must be one of: #{Enum.join(enum, ", ")}"
+      end)
 
     assert validations == %{
-      body: ["body must match /888/", "body must be 3-32 long"],
-      upvotes: ["upvotes must be one of: good, bad"],
-    }
+             body: ["body must match /888/", "body must be 3-32 long"],
+             upvotes: ["upvotes must be one of: good, bad"]
+           }
   end
 
   ## inspect
@@ -2458,7 +3489,7 @@ defmodule Ecto.ChangesetTest do
   describe "inspect" do
     test "reveals relevant data" do
       assert inspect(%Ecto.Changeset{}) ==
-            "#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: nil, valid?: false>"
+               "#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: nil, valid?: false, ...>"
 
       changes = %{"title" => "title", "body" => "hi"}
 
@@ -2468,15 +3499,15 @@ defmodule Ecto.ChangesetTest do
         |> Enum.into(%{})
 
       assert inspect(changeset(changes)) ==
-            "#Ecto.Changeset<action: nil, changes: #{inspect normalized_changes}, " <>
-            "errors: [], data: #Ecto.ChangesetTest.Post<>, valid?: true>"
+               "#Ecto.Changeset<action: nil, changes: #{inspect(normalized_changes)}, " <>
+                 "errors: [], data: #Ecto.ChangesetTest.Post<>, valid?: true, ...>"
 
-      data   = {%NoSchemaPost{title: "hello"}, %{title: :string, upvotes: :integer}}
+      data = {%NoSchemaPost{title: "hello"}, %{title: :string, upvotes: :integer}}
       params = %{"title" => "world", "upvotes" => "0"}
 
       assert inspect(cast(data, params, ~w(title upvotes)a)) ==
                "#Ecto.Changeset<action: nil, changes: %{title: \"world\", upvotes: 0}, " <>
-               "errors: [], data: #Ecto.ChangesetTest.NoSchemaPost<>, valid?: true>"
+                 "errors: [], data: #Ecto.ChangesetTest.NoSchemaPost<>, valid?: true, ...>"
     end
 
     test "redacts fields marked redact: true" do
@@ -2484,19 +3515,25 @@ defmodule Ecto.ChangesetTest do
       refute inspect(changeset) =~ "hunter2"
       assert inspect(changeset) =~ "**redacted**"
 
-      changeset = Ecto.Changeset.cast(%RedactedEmbeddedSchema{}, %{password: "hunter2"}, [:password])
+      changeset =
+        Ecto.Changeset.cast(%RedactedEmbeddedSchema{}, %{password: "hunter2"}, [:password])
+
       refute inspect(changeset) =~ "hunter2"
       assert inspect(changeset) =~ "**redacted**"
     end
 
     test "redacts virtual fields marked redact: true" do
-      changeset = Ecto.Changeset.cast(%RedactedSchema{}, %{virtual_pass: "hunter2"}, [:virtual_pass])
+      changeset =
+        Ecto.Changeset.cast(%RedactedSchema{}, %{virtual_pass: "hunter2"}, [:virtual_pass])
+
       refute inspect(changeset) =~ "hunter2"
       assert inspect(changeset) =~ "**redacted**"
     end
 
     test "redacts read only fields marked redact: true" do
-      changeset = Ecto.Changeset.cast(%RedactedSchema{}, %{read_only_pass: "hunter2"}, [:read_only_pass])
+      changeset =
+        Ecto.Changeset.cast(%RedactedSchema{}, %{read_only_pass: "hunter2"}, [:read_only_pass])
+
       refute inspect(changeset) =~ "hunter2"
       assert inspect(changeset) =~ "**redacted**"
     end
@@ -2508,7 +3545,9 @@ defmodule Ecto.ChangesetTest do
     end
 
     test "doesn't redact fields marked redact: false" do
-      changeset = Ecto.Changeset.cast(%RedactedSchema{}, %{display_name: "hunter2"}, [:display_name])
+      changeset =
+        Ecto.Changeset.cast(%RedactedSchema{}, %{display_name: "hunter2"}, [:display_name])
+
       assert inspect(changeset) =~ "hunter2"
       refute inspect(changeset) =~ "**redacted**"
     end