Repository: phoenixframework/phoenix
Pull request #4218 (2021-02-24 23:44:45)

Update ecto.md
Changed integer to bingint in Section *Using the schema and migration generator*


Patch:
diff --git a/guides/ecto.md b/guides/ecto.md
index 71846c306..0346f7fc4 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -97,7 +97,7 @@ hello_dev=# \d users
 Table "public.users"
 Column         |            Type             | Modifiers
 ---------------+-----------------------------+----------------------------------------------------
-id             | integer                     | not null default nextval('users_id_seq'::regclass)
+id             | bigint                      | not null default nextval('users_id_seq'::regclass)
 name           | character varying(255)      |
 email          | character varying(255)      |
 bio            | character varying(255)      |
@@ -368,38 +368,63 @@ iex> alias Hello.{Repo, User}
 [Hello.Repo, Hello.User]
 
 iex> Repo.insert(%User{email: "user1@example.com"})
-[debug] QUERY OK db=4.6ms
-INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", {{2017, 5, 23}, {19, 6, 4, 822044}}, {{2017, 5, 23}, {19, 6, 4, 822055}}]
+[debug] QUERY OK db=6.5ms decode=2.0ms queue=0.5ms idle=1358.3ms
+INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", ~N[2021-02-25 01:58:55], ~N[2021-02-25 01:58:55]]
 {:ok,
- %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
-  bio: nil, email: "user1@example.com", id: 3,
-  inserted_at: ~N[2017-05-23 19:06:04.822044], name: nil, number_of_pets: nil,
-  updated_at: ~N[2017-05-23 19:06:04.822055]}}
-
+ %Hello.User{
+   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+   bio: nil,
+   email: "user1@example.com",
+   id: 1,
+   inserted_at: ~N[2021-02-25 01:58:55],
+   name: nil,
+   number_of_pets: nil,
+   updated_at: ~N[2021-02-25 01:58:55]
+ }}
 iex> Repo.insert(%User{email: "user2@example.com"})
-[debug] QUERY OK db=5.1ms
-INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user2@example.com", {{2017, 5, 23}, {19, 6, 8, 452545}}, {{2017, 5, 23}, {19, 6, 8, 452556}}]
+[debug] QUERY OK db=1.3ms idle=1402.7ms
+INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user2@example.com", ~N[2021-02-25 02:03:28], ~N[2021-02-25 02:03:28]]
 {:ok,
- %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
-  bio: nil, email: "user2@example.com", id: 4,
-  inserted_at: ~N[2017-05-23 19:06:08.452545], name: nil, number_of_pets: nil,
-  updated_at: ~N[2017-05-23 19:06:08.452556]}}
+ %Hello.User{
+   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+   bio: nil,
+   email: "user2@example.com",
+   id: 2,
+   inserted_at: ~N[2021-02-25 02:03:28],
+   name: nil,
+   number_of_pets: nil,
+   updated_at: ~N[2021-02-25 02:03:28]
+ }}
 ```
 
 We started by aliasing our `User` and `Repo` modules for easy access. Next, we called `Repo.insert/1` and passed a user struct. Since we're in the `dev` environment, we can see the debug logs for the query our Repo performed when inserting the underlying `%User{}` data. We received a 2-tuple back with `{:ok, %User{}}`, which lets us know the insertion was successful. With a couple of users inserted, let's fetch them back out of the repo.
 
 ```elixir
 iex> Repo.all(User)
-[debug] QUERY OK source="users" db=2.7ms
+[debug] QUERY OK source="users" db=5.8ms queue=1.4ms idle=1672.0ms
 SELECT u0."id", u0."bio", u0."email", u0."name", u0."number_of_pets", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
-[%Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
-  bio: nil, email: "user1@example.com", id: 3,
-  inserted_at: ~N[2017-05-23 19:06:04.822044], name: nil, number_of_pets: nil,
-  updated_at: ~N[2017-05-23 19:06:04.822055]},
- %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
-  bio: nil, email: "user2@example.com", id: 4,
-  inserted_at: ~N[2017-05-23 19:06:08.452545], name: nil, number_of_pets: nil,
-  updated_at: ~N[2017-05-23 19:06:08.452556]}]
+[
+  %Hello.User{
+    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+    bio: nil,
+    email: "user1@example.com",
+    id: 1,
+    inserted_at: ~N[2021-02-25 01:58:55],
+    name: nil,
+    number_of_pets: nil,
+    updated_at: ~N[2021-02-25 01:58:55]
+  },
+  %Hello.User{
+    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+    bio: nil,
+    email: "user2@example.com",
+    id: 2,
+    inserted_at: ~N[2021-02-25 02:03:28],
+    name: nil,
+    number_of_pets: nil,
+    updated_at: ~N[2021-02-25 02:03:28]
+  }
+]
 ```
 
 That was easy! `Repo.all/1` takes a data source, our `User` schema in this case, and translates that to an underlying SQL query against our database. After it fetches the data, the Repo then uses our Ecto schema to map the database values back into Elixir data-structures according to our `User` schema. We're not just limited to basic querying – Ecto includes a full-fledged query DSL for advanced SQL generation. In addition to a natural Elixir DSL, Ecto's query engine gives us multiple great features, such as SQL injection protection and compile-time optimization of queries. Let's try it out.
@@ -409,7 +434,7 @@ iex> import Ecto.Query
 Ecto.Query
 
 iex> Repo.all(from u in User, select: u.email)
-[debug] QUERY OK source="users" db=2.4ms
+[debug] QUERY OK source="users" db=0.8ms queue=0.9ms idle=1634.0ms
 SELECT u0."email" FROM "users" AS u0 []
 ["user1@example.com", "user2@example.com"]
 ```
diff --git a/guides/plug.md b/guides/plug.md
index abcab2003..0403c2360 100644
--- a/guides/plug.md
+++ b/guides/plug.md
@@ -113,7 +113,7 @@ defmodule HelloWeb.Router do
   ...
 ```
 
-In the [`init/1`] callback, we pass a default locale to use if none is present in the params. We also use pattern matching to define multiple [`call/2`] function heads to validate the locale in the params, and fall back to `"en"` if there is no match.
+In the [`init/1`] callback, we pass a default locale to use if none is present in the params. We also use pattern matching to define multiple [`call/2`] function heads to validate the locale in the params, and fall back to `"en"` if there is no match. The [`assign/3`] is a part of the `Plug.Conn` module and it's how we store values in the `conn` data structure.
 
 To see the assign in action, go to the layout in `lib/hello_web/templates/layout/app.html.eex` and add the following close to the main container:
 
diff --git a/installer/templates/phx_single/gitignore b/installer/templates/phx_single/gitignore
index 6c634d280..8eae1ec4d 100644
--- a/installer/templates/phx_single/gitignore
+++ b/installer/templates/phx_single/gitignore
@@ -32,3 +32,6 @@ npm-debug.log
 # we ignore priv/static. You may want to comment
 # this depending on your deployment strategy.
 /priv/static/
+
+# Temporary files, for example, from tests.
+/tmp
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index d2c2bbdcf..4baecafa7 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -42,7 +42,7 @@ defmodule <%= @app_module %>.MixProject do
       {:ecto_sql, "~> 3.5"},
       {<%= inspect @adapter_app %>, ">= 0.0.0"},<% end %><%= if @html do %><%= if @live do %>
       {:phoenix_live_view, "~> 0.15.0"},
-      {:floki, ">= 0.27.0", only: :test},<% end %>
+      {:floki, ">= 0.30.0", only: :test},<% end %>
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if @dashboard do %>
       {:phoenix_live_dashboard, "~> 0.4"},<% end %>
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
index ee8416b15..b709f43ed 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -40,7 +40,7 @@ defmodule <%= @web_namespace %>.MixProject do
       <%= @phoenix_dep %>,<%= if @ecto do %>
       {:phoenix_ecto, "~> 4.1"},<% end %><%= if @html do %><%= if @live do %>
       {:phoenix_live_view, "~> 0.15.0"},
-      {:floki, ">= 0.27.0", only: :test},<% end %>
+      {:floki, ">= 0.30.0", only: :test},<% end %>
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if @dashboard do %>
       {:phoenix_live_dashboard, "~> 0.4"},<% end %>
diff --git a/installer/templates/phx_umbrella/gitignore b/installer/templates/phx_umbrella/gitignore
index 12179ea3c..ffa1e1923 100644
--- a/installer/templates/phx_umbrella/gitignore
+++ b/installer/templates/phx_umbrella/gitignore
@@ -18,3 +18,6 @@ erl_crash.dump
 
 # Also ignore archive artifacts (built via "mix archive.build").
 *.ez
+
+# Temporary files, for example, from tests.
+/tmp
diff --git a/lib/mix/phoenix.ex b/lib/mix/phoenix.ex
index c5b5662bf..41293a386 100644
--- a/lib/mix/phoenix.ex
+++ b/lib/mix/phoenix.ex
@@ -355,4 +355,8 @@ defmodule Mix.Phoenix do
   def to_text(data) do
     inspect data, limit: :infinity, printable_limit: :infinity
   end
+
+  def prepend_newline(string) do
+    "\n" <> string
+  end
 end
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index aa731014f..cadc130e6 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -33,7 +33,9 @@ defmodule Mix.Phoenix.Schema do
             web_namespace: nil,
             context_app: nil,
             route_helper: nil,
-            migration_module: nil
+            migration_module: nil,
+            fixture_unique_functions: %{},
+            fixture_params: %{}
 
   @valid_types [
     :integer,
@@ -95,6 +97,7 @@ defmodule Mix.Phoenix.Schema do
         {key, _} -> key
         nil -> :some_field
       end
+    fixture_unique_functions = fixture_unique_functions(singular, uniques, attrs)
 
     %Schema{
       opts: opts,
@@ -131,7 +134,9 @@ defmodule Mix.Phoenix.Schema do
       sample_id: sample_id(opts),
       context_app: ctx_app,
       generate?: generate?,
-      migration_module: migration_module()}
+      migration_module: migration_module(),
+      fixture_unique_functions: fixture_unique_functions,
+      fixture_params: fixture_params(attrs, fixture_unique_functions)}
   end
 
   @doc """
@@ -240,7 +245,7 @@ defmodule Mix.Phoenix.Schema do
     if schema.params.create |> Map.values() |> Enum.any?(&date_value?/1) do
       "is invalid"
     else
-      "can&apos;t be blank"
+      "can&#39;t be blank"
     end
   end
 
@@ -470,4 +475,60 @@ defmodule Mix.Phoenix.Schema do
       other -> Mix.raise "Expected :migration_module to be a module, got: #{inspect(other)}"
     end
   end
+
+  defp fixture_unique_functions(singular, uniques, attrs) do
+    uniques
+    |> Enum.filter(&Keyword.has_key?(attrs, &1))
+    |> Enum.into(%{}, fn attr ->
+      function_name = "unique_#{singular}_#{attr}"
+
+      {function_def, needs_impl?} =
+        case Keyword.fetch!(attrs, attr) do
+          :integer ->
+            function_def =
+              """
+                def #{function_name}, do: System.unique_integer([:positive])
+              """
+
+            {function_def, false}
+
+          type when type in [:string, :text] ->
+            function_def =
+              """
+                def #{function_name}, do: "some #{attr}\#{System.unique_integer([:positive])}"
+              """
+
+            {function_def, false}
+
+          _ ->
+            function_def =
+              """
+                def #{function_name} do
+                  raise "implement the logic to generate a unique #{singular} #{attr}"
+                end
+              """
+
+            {function_def, true}
+        end
+
+
+      {attr, {function_name, function_def, needs_impl?}}
+    end)
+  end
+
+  defp fixture_params(attrs, fixture_unique_functions) do
+    attrs
+    |> Enum.reject(fn
+      {_, {:references, _}} -> true
+      {_, _} -> false
+    end)
+    |> Enum.into(%{}, fn {attr, type} ->
+      case Map.fetch(fixture_unique_functions, attr) do
+        {:ok, {function_name, _function_def, _needs_impl?}} ->
+          {attr, "#{function_name}()"}
+        :error ->
+          {attr, inspect(type_to_default(attr, type, :create))}
+      end
+    end)
+  end
 end
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 60ebc6401..c4b19f750 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -197,7 +197,52 @@ defmodule Mix.Tasks.Phx.Gen.Context do
 
     paths
     |> Mix.Phoenix.eval_from("priv/templates/phx.gen.context/fixtures.ex", binding)
+    |> Mix.Phoenix.prepend_newline()
     |> inject_eex_before_final_end(test_fixtures_file, binding)
+
+    maybe_print_unimplemented_fixture_functions(context)
+  end
+
+  defp maybe_print_unimplemented_fixture_functions(%Context{} = context) do
+    fixture_functions_needing_implementations =
+      Enum.flat_map(
+        context.schema.fixture_unique_functions,
+        fn
+          {_field, {_function_name, function_def, true}} -> [function_def]
+          {_field, {_function_name, _function_def, false}} -> []
+        end
+      )
+
+    if Enum.any?(fixture_functions_needing_implementations) do
+      Mix.shell.info(
+        """
+
+        Some of the generated database columns are unique. Please provide
+        unique implementations for the following fixture function(s) in
+        #{context.test_fixtures_file}:
+
+        #{
+          fixture_functions_needing_implementations
+          |> Enum.map_join(&indent(&1, 2))
+          |> String.trim_trailing()
+        }
+        """
+      )
+    end
+  end
+
+  defp indent(string, spaces) do
+    indent_string = String.duplicate(" ", spaces)
+
+    string
+    |> String.split("\n")
+    |> Enum.map_join(fn line ->
+        if String.trim(line) == "" do
+          "\n"
+        else
+          indent_string <> line <> "\n"
+        end
+    end)
   end
 
   defp inject_eex_before_final_end(content_to_inject, file_path, binding) do
diff --git a/lib/phoenix/config.ex b/lib/phoenix/config.ex
index 685755b82..6927f91bb 100644
--- a/lib/phoenix/config.ex
+++ b/lib/phoenix/config.ex
@@ -46,7 +46,18 @@ defmodule Phoenix.Config do
   """
   @spec cache(module, term, (module -> {:cache | :nocache, term})) :: term
   def cache(module, key, fun) do
-    case :ets.lookup(module, key) do
+    try do
+      :ets.lookup(module, key)
+    rescue
+      e ->
+        case :ets.info(module) do
+          :undefined ->
+            raise "could not find ets table for endpoint #{inspect(module)}. Make sure your endpoint is started and note you cannot access endpoint functions at compile-time."
+
+          _ ->
+            reraise e, __STACKTRACE__
+        end
+    else
       [{^key, :cache, val}] ->
         val
 
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index ec1158f63..49ee33ffc 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -218,7 +218,7 @@ defmodule Phoenix.Endpoint do
       `c:broadcast_from/4`, `c:broadcast_from!/4`, `c:local_broadcast/3`,
       and `c:local_broadcast_from/4`
 
-    * for configuration: `c:start_link/0`, `c:config/2`, and `c:config_change/2`
+    * for configuration: `c:start_link/1`, `c:config/2`, and `c:config_change/2`
 
     * as required by the `Plug` behaviour: `c:Plug.init/1` and `c:Plug.call/2`
 
diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex
index e87bfa42b..39d35f8d3 100644
--- a/lib/phoenix/logger.ex
+++ b/lib/phoenix/logger.ex
@@ -201,7 +201,7 @@ defmodule Phoenix.Logger do
   ## Event: [:phoenix, :endpoint, *]
 
   defp phoenix_endpoint_start(_, _, %{conn: conn} = metadata, _) do
-    case log_level(metadata[:options][:log], conn) do
+    case log_level(metadata[:options][:log] || :info, conn) do
       false ->
         :ok
 
@@ -214,7 +214,7 @@ defmodule Phoenix.Logger do
   end
 
   defp phoenix_endpoint_stop(_, %{duration: duration}, %{conn: conn} = metadata, _) do
-    case log_level(metadata[:options][:log], conn) do
+    case log_level(metadata[:options][:log] || :info, conn) do
       false ->
         :ok
 
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 9ac2e5e23..080769be3 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -346,6 +346,9 @@ defmodule Phoenix.Router do
 
     case pipeline.(conn) do
       %Plug.Conn{halted: true} = halted_conn ->
+        measurements = %{duration: System.monotonic_time() - start}
+        metadata = %{metadata | conn: halted_conn}
+        :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
         halted_conn
       %Plug.Conn{} = piped_conn ->
         try do
@@ -425,7 +428,7 @@ defmodule Phoenix.Router do
     {matches, _} = Enum.map_reduce(routes_with_exprs, %{}, &build_match/2)
 
     checks =
-      for {%{line: line, plug: plug, plug_opts: plug_opts}, _} <- routes_with_exprs, into: %{} do
+      for %{line: line, plug: plug, plug_opts: plug_opts} <- routes, into: %{} do
         quote line: line do
           {unquote(plug).init(unquote(Macro.escape(plug_opts))), []}
         end
@@ -439,6 +442,9 @@ defmodule Phoenix.Router do
         end
       end
 
+    keys = [:verb, :path, :plug, :plug_opts, :helper, :metadata]
+    routes = Enum.map(routes, &Map.take(&1, keys))
+
     quote do
       @doc false
       def __routes__,  do: unquote(Macro.escape(routes))
@@ -625,6 +631,14 @@ defmodule Phoenix.Router do
   are appended to the ones previously given.
   """
   defmacro pipeline(plug, do: block) do
+    with true <- is_atom(plug),
+         imports = __CALLER__.macros ++ __CALLER__.functions,
+         {mod, _} <- Enum.find(imports, fn {_, imports} -> {plug, 2} in imports end) do
+      raise ArgumentError,
+            "cannot define pipeline named #{inspect(plug)} " <>
+              "because there is an import from #{inspect(mod)} with the same name"
+    end
+
     block =
       quote do
         plug = unquote(plug)
@@ -960,6 +974,13 @@ defmodule Phoenix.Router do
     end
   end
 
+  @doc """
+  Returns all routes information from the given router.
+  """
+  def routes(router) do
+    router.__routes__()
+  end
+
   @doc """
   Returns the compile-time route info and runtime path params for a request.
 
diff --git a/lib/phoenix/router/console_formatter.ex b/lib/phoenix/router/console_formatter.ex
index 0a75ab82b..5372e16b1 100644
--- a/lib/phoenix/router/console_formatter.ex
+++ b/lib/phoenix/router/console_formatter.ex
@@ -1,12 +1,11 @@
 defmodule Phoenix.Router.ConsoleFormatter do
   @moduledoc false
-  alias Phoenix.Router.Route
 
   @doc """
   Format the routes for printing.
   """
   def format(router, endpoint \\ nil) do
-    routes = router.__routes__()
+    routes = Phoenix.Router.routes(router)
     column_widths = calculate_column_widths(routes, endpoint)
 
     routes
@@ -63,7 +62,7 @@ defmodule Phoenix.Router.ConsoleFormatter do
 
     widths =
       Enum.reduce(routes, {0, 0, 0}, fn route, acc ->
-        %Route{verb: verb, path: path, helper: helper} = route
+        %{verb: verb, path: path, helper: helper} = route
         verb = verb_name(verb)
         {verb_len, path_len, route_name_len} = acc
         route_name = route_name(helper)
@@ -83,8 +82,7 @@ defmodule Phoenix.Router.ConsoleFormatter do
   end
 
   defp format_route(route, column_widths) do
-    %Route{verb: verb, path: path, plug: plug,
-           plug_opts: plug_opts, helper: helper} = route
+    %{verb: verb, path: path, plug: plug, plug_opts: plug_opts, helper: helper} = route
     verb = verb_name(verb)
     route_name = route_name(helper)
     {verb_len, path_len, route_name_len} = column_widths
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index cb5b0a3d7..332d88237 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -60,7 +60,7 @@ defmodule Phoenix.Socket.Transport do
           %{id: Task, start: {Task, :start_link, [fn -> :ok end]}, restart: :transient}
         end
 
-        def connect(map) do
+        def connect(state) do
           # Callback to retrieve relevant data from the connection.
           # The map contains options, params, transport and endpoint keys.
           {:ok, state}
diff --git a/priv/templates/phx.gen.context/fixtures.ex b/priv/templates/phx.gen.context/fixtures.ex
index e49a72e97..13e1f735c 100644
--- a/priv/templates/phx.gen.context/fixtures.ex
+++ b/priv/templates/phx.gen.context/fixtures.ex
@@ -1,9 +1,15 @@
-
+<%= for {attr, {_function_name, function_def, _needs_impl?}} <- schema.fixture_unique_functions do %>  @doc """
+  Generate a unique <%= schema.singular %> <%= attr %>.
+  """
+<%= function_def %>
+<% end %>  @doc """
+  Generate a <%= schema.singular %>.
+  """
   def <%= schema.singular %>_fixture(attrs \\ %{}) do
     {:ok, <%= schema.singular %>} =
       attrs
       |> Enum.into(%{
-<%= schema.params.create |> Enum.map(fn {key, val} -> "        #{key}: #{inspect(val)}" end) |> Enum.join(",\n") %>
+<%= schema.fixture_params |> Enum.map(fn {key, code} -> "        #{key}: #{code}" end) |> Enum.join(",\n") %>
       })
       |> <%= inspect context.module %>.create_<%= schema.singular %>()
 
diff --git a/priv/templates/phx.gen.html/show.html.eex b/priv/templates/phx.gen.html/show.html.eex
index c19a2c955..469fc4a13 100644
--- a/priv/templates/phx.gen.html/show.html.eex
+++ b/priv/templates/phx.gen.html/show.html.eex
@@ -9,5 +9,5 @@
 <% end %>
 </ul>
 
-<span><%%= link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, @<%= schema.singular %>) %></span>
+<span><%%= link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, @<%= schema.singular %>) %></span> |
 <span><%%= link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index) %></span>
diff --git a/priv/templates/phx.gen.live/show.html.leex b/priv/templates/phx.gen.live/show.html.leex
index 236f80e45..9de080b77 100644
--- a/priv/templates/phx.gen.live/show.html.leex
+++ b/priv/templates/phx.gen.live/show.html.leex
@@ -18,5 +18,5 @@
 <% end %>
 </ul>
 
-<span><%%= live_patch "Edit", to: Routes.<%= schema.route_helper %>_show_path(@socket, :edit, @<%= schema.singular %>), class: "button" %></span>
+<span><%%= live_patch "Edit", to: Routes.<%= schema.route_helper %>_show_path(@socket, :edit, @<%= schema.singular %>), class: "button" %></span> |
 <span><%%= live_redirect "Back", to: Routes.<%= schema.route_helper %>_index_path(@socket, :index) %></span>


Test patch:
diff --git a/integration_test/mix.exs b/integration_test/mix.exs
index 655a14e5c..09134b8d4 100644
--- a/integration_test/mix.exs
+++ b/integration_test/mix.exs
@@ -34,7 +34,7 @@ defmodule Phoenix.Integration.MixProject do
       {:myxql, ">= 0.0.0"},
       {:tds, ">= 0.0.0"},
       {:phoenix_live_view, "~> 0.15.0"},
-      {:floki, ">= 0.27.0"},
+      {:floki, ">= 0.30.0"},
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2"},
       {:phoenix_live_dashboard, "~> 0.4"},
diff --git a/integration_test/mix.lock b/integration_test/mix.lock
index 0b1138e24..c06488281 100644
--- a/integration_test/mix.lock
+++ b/integration_test/mix.lock
@@ -1,36 +1,37 @@
 %{
-  "argon2_elixir": {:hex, :argon2_elixir, "2.3.0", "e251bdafd69308e8c1263e111600e6d68bd44f23d2cccbe43fcb1a417a76bc8e", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "28ccb63bff213aecec1f7f3dde9648418b031f822499973281d8f494b9d5a3b3"},
-  "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.2.0", "3df902b81ce7fa8867a2ae30d20a1da6877a2c056bfb116fd0bc8a5f0190cea4", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "762be3fcb779f08207531bc6612cca480a338e4b4357abb49f5ce00240a77d1e"},
-  "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
-  "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
+  "argon2_elixir": {:hex, :argon2_elixir, "2.4.0", "2a22ea06e979f524c53b42b598fc6ba38cdcbc977a155e33e057732cfb1fb311", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "4ea82e183cf8e7f66dab1f767fedcfe6a195e140357ef2b0423146b72e0a551d"},
+  "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.0", "6cb662d5c1b0a8858801cf20997bd006e7016aa8c52959c9ef80e0f34fb60b7a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2c81d61d4f6ed0e5cf7bf27a9109b791ff216a1034b3d541327484f46dd43769"},
+  "comeonin": {:hex, :comeonin, "5.3.2", "5c2f893d05c56ae3f5e24c1b983c2d5dfb88c6d979c9287a76a7feb1e1d8d646", [:mix], [], "hexpm", "d0993402844c49539aeadb3fe46a3c9bd190f1ecf86b6f9ebd71957534c95f04"},
+  "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
   "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
+  "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
   "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
-  "db_connection": {:hex, :db_connection, "2.3.0", "d56ef906956a37959bcb385704fc04035f4f43c0f560dd23e00740daf8028c49", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "dcc082b8f723de9a630451b49fdbd7a59b065c4b38176fb147aaf773574d4520"},
+  "db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"},
   "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
-  "ecto": {:hex, :ecto, "3.5.4", "73ee115deb10769c73fd2d27e19e36bc4af7c56711ad063616a86aec44f80f6f", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7f13f9c9c071bd2ca04652373ff3edd1d686364de573255096872a4abc471807"},
-  "ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"},
-  "elixir_make": {:hex, :elixir_make, "0.6.1", "8faa29a5597faba999aeeb72bbb9c91694ef8068f0131192fb199f98d32994ef", [:mix], [], "hexpm", "35d33270680f8d839a4003c3e9f43afb595310a592405a00afc12de4c7f55a18"},
-  "file_system": {:hex, :file_system, "0.2.9", "545b9c9d502e8bfa71a5315fac2a923bd060fd9acb797fe6595f54b0f975fd32", [:mix], [], "hexpm", "3cf87a377fe1d93043adeec4889feacf594957226b4f19d5897096d6f61345d8"},
-  "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"},
+  "ecto": {:hex, :ecto, "3.5.8", "8ebf12be6016cb99313348ba7bb4612f4114b9a506d6da79a2adc7ef449340bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea0be182ea8922eb7742e3ae8e71b67ee00ae177de1bf76210299a5f16ba4c77"},
+  "ecto_sql": {:hex, :ecto_sql, "3.5.4", "a9e292c40bd79fff88885f95f1ecd7b2516e09aa99c7dd0201aa84c54d2358e4", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1fff1a28a898d7bbef263f1f3ea425b04ba9f33816d843238c84eff883347343"},
+  "elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
+  "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+  "floki": {:hex, :floki, "0.30.0", "22ebbe681a5d3777cdd830ca091b1b806d33c3449c26312eadca7f7be685c0c8", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "a9e128a4ca9bb71f11affa315b6768a9ad326d5996ff1e92acf1d7a01a10076a"},
   "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
-  "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
+  "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
   "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
   "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
-  "myxql": {:hex, :myxql, "0.4.4", "b60707bc4ea73db8d6591c5e8016251b35fdc15bc2bc5a51f9212c160f97b9e9", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.3", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "028c44611efc193e809fa711d16b2ff34921fae9ff97c0d5b8eeaa9a3701e919"},
-  "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
+  "myxql": {:hex, :myxql, "0.4.5", "49784e6a3e4fc33088cc9004948ef255ee698b0d7b533fb1fa453cc99a3f9972", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.3", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "40a6166ab0a54f44a6e2c437aed6360ce51ce7f779557ae30d1cc4c4b4e7ad13"},
+  "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.3.0", "929f34678198844a9f948157a5030b8279d5e3e6f672a38065f4ba3a793263bc", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "1e06f1aa09185e6b371a502adff72c28ecdc6b16d192cd6df148ce1cbd9b4abd"},
   "phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
-  "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
+  "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
   "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
-  "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.4", "940c0344b1d66a2e46eef02af3a70e0c5bb45a4db0bf47917add271b76cd3914", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "38f9308357dea4cc77f247e216da99fcb0224e05ada1469167520bed4cb8cccd"},
+  "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
   "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.0", "1bb9b0c230a047cddc32b1708ec4cad4f91aaab0b4688c09702fcf78d7e2df94", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d42ad9b5fe9e874df3a12ea0a806e3f650db99774d597285c07df43f33cab486"},
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
-  "phoenix_view": {:git, "https://github.com/phoenixframework/phoenix_view.git", "d453f7e527a04645652947b9755ad0ece3330e55", []},
+  "phoenix_view": {:git, "https://github.com/phoenixframework/phoenix_view.git", "720e2fa6e9f21346fe8ee3f58fcee7d569ccf214", []},
   "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
-  "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"},
-  "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
-  "postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"},
+  "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"},
+  "plug_crypto": {:hex, :plug_crypto, "1.2.1", "5c854427528bf61d159855cedddffc0625e2228b5f30eff76d5a4de42d896ef4", [:mix], [], "hexpm", "6961c0e17febd9d0bfa89632d391d2545d2e0eb73768f5f50305a23961d8782c"},
+  "postgrex": {:hex, :postgrex, "0.15.8", "f5e782bbe5e8fa178d5e3cd1999c857dc48eda95f0a4d7f7bd92a50e84a0d491", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "698fbfacea34c4cf22c8281abeb5cf68d99628d541874f085520ab3b53d356fe"},
   "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
-  "tds": {:hex, :tds, "2.1.1", "b6163ea716d74ed90a3a83668db2e7c74c1e722fd3538ef5758e0a084fde8d60", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6b28e1f06a57867eb6b1a957ae5d872b09214ba771ef08cf5ca9d52d6d372876"},
+  "tds": {:hex, :tds, "2.1.3", "d4e9b1134cd19b6fe7365ea3cc9da433723c9e479579bdc484251957fbc212c2", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "374f279f238811025b531c971626fc1266ba3aea039f646674a920d8372e71da"},
   "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
   "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
   "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
diff --git a/integration_test/test/code_generation/app_with_defaults_test.exs b/integration_test/test/code_generation/app_with_defaults_test.exs
index 5a997f4ee..3bf568fef 100644
--- a/integration_test/test/code_generation/app_with_defaults_test.exs
+++ b/integration_test/test/code_generation/app_with_defaults_test.exs
@@ -50,7 +50,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithDefaultsTest do
       with_installer_tmp("app_with_defaults", fn tmp_dir ->
         {app_root_path, _} = generate_phoenix_app(tmp_dir, "phx_blog")
 
-        mix_run!(~w(phx.gen.html Blog Post posts title body:string status:enum:unpublished:published:deleted), app_root_path)
+        mix_run!(~w(phx.gen.html Blog Post posts title:unique body:string status:enum:unpublished:published:deleted order:integer:unique), app_root_path)
 
         modify_file(Path.join(app_root_path, "lib/phx_blog_web/router.ex"), fn file ->
           inject_before_final_end(file, """
@@ -86,7 +86,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithDefaultsTest do
       with_installer_tmp("app_with_defaults", fn tmp_dir ->
         {app_root_path, _} = generate_phoenix_app(tmp_dir, "phx_blog")
 
-        mix_run!(~w(phx.gen.json Blog Post posts title body:string status:enum:unpublished:published:deleted), app_root_path)
+        mix_run!(~w(phx.gen.json Blog Post posts title:unique body:string status:enum:unpublished:published:deleted), app_root_path)
 
         modify_file(Path.join(app_root_path, "lib/phx_blog_web/router.ex"), fn file ->
           inject_before_final_end(file, """
diff --git a/test/mix/tasks/phx.gen.context_test.exs b/test/mix/tasks/phx.gen.context_test.exs
index a8bc0cf29..9158dba2f 100644
--- a/test/mix/tasks/phx.gen.context_test.exs
+++ b/test/mix/tasks/phx.gen.context_test.exs
@@ -211,6 +211,81 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
     end
   end
 
+  test "generates with unique fields", config do
+    in_tmp_project config.test, fn ->
+      Gen.Context.run(~w(Blog Post posts
+            slug:string:unique
+            subject:unique
+            body:text:unique
+            order:integer:unique
+            price:decimal:unique
+            published_at:utc_datetime:unique
+            author:references:users:unique
+            published?:boolean
+          ))
+
+      assert_received {:mix_shell, :info, ["""
+
+        Some of the generated database columns are unique. Please provide
+        unique implementations for the following fixture function(s) in
+        test/support/fixtures/blog_fixtures.ex:
+
+            def unique_post_price do
+              raise "implement the logic to generate a unique post price"
+            end
+
+            def unique_post_published_at do
+              raise "implement the logic to generate a unique post published_at"
+            end
+        """]}
+
+      assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+        assert file =~ ~S|def unique_post_order, do: System.unique_integer([:positive])|
+        assert file =~ ~S|def unique_post_slug, do: "some slug#{System.unique_integer([:positive])}"|
+        assert file =~ ~S|def unique_post_body, do: "some body#{System.unique_integer([:positive])}"|
+        assert file =~ ~S|def unique_post_subject, do: "some subject#{System.unique_integer([:positive])}"|
+        refute file =~ ~S|def unique_post_author|
+        assert file =~ """
+          def unique_post_price do
+            raise "implement the logic to generate a unique post price"
+          end
+        """
+
+        assert file =~ """
+                body: unique_post_body(),
+                order: unique_post_order(),
+                price: unique_post_price(),
+                published?: true,
+                published_at: unique_post_published_at(),
+                slug: unique_post_slug(),
+                subject: unique_post_subject()
+        """
+      end
+
+      assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
+      assert_file path, fn file ->
+        assert file =~ "create table(:posts)"
+        assert file =~ "create unique_index(:posts, [:order])"
+        assert file =~ "create unique_index(:posts, [:price])"
+        assert file =~ "create unique_index(:posts, [:slug])"
+        assert file =~ "create unique_index(:posts, [:subject])"
+      end
+    end
+  end
+
+  test "does not prompt on unimplemented functions with only string, text and integer unique fields", config do
+    in_tmp_project config.test, fn ->
+      Gen.Context.run(~w(Blog Post posts
+            slug:string:unique
+            subject:unique
+            body:text:unique
+            order:integer:unique
+          ))
+
+      refute_received {:mix_shell, :info, ["\nSome of the generated database columns are unique." <> _]}
+    end
+  end
+
   test "generates into existing context without prompt with --merge-with-existing-context", config do
     in_tmp_project config.test, fn ->
       Gen.Context.run(~w(Blog Post posts title))
diff --git a/test/phoenix/config_test.exs b/test/phoenix/config_test.exs
index e447010a9..6c0156d49 100644
--- a/test/phoenix/config_test.exs
+++ b/test/phoenix/config_test.exs
@@ -22,8 +22,15 @@ defmodule Phoenix.ConfigTest do
     assert :ets.lookup(meta.test, :custom)  == [custom: true]
   end
 
+  test "raises with warning about compile time when table not started" do
+    assert_raise RuntimeError,
+                 "could not find ets table for endpoint Fooz. Make sure your endpoint is started and note you cannot access endpoint functions at compile-time.",
+                 fn -> cache(Fooz, :foo, fn _ -> {:nocache, :bar} end) end
+  end
+
   test "can change configuration", meta do
-    {:ok, _pid} = start_link({meta.test, @all, @defaults, []})
+    {:ok, pid} = start_link({meta.test, @all, @defaults, []})
+    ref = Process.monitor(pid)
 
     # Nothing changed
     config_change(meta.test, [], [])
@@ -39,6 +46,8 @@ defmodule Phoenix.ConfigTest do
 
     # Module removed
     config_change(meta.test, [], [meta.test])
+
+    assert_receive {:DOWN, ^ref, :process, ^pid, :normal}
     assert :ets.info(meta.test, :name) == :undefined
   end
 
diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs
index 7c1c179ab..b84477dd8 100644
--- a/test/phoenix/endpoint/endpoint_test.exs
+++ b/test/phoenix/endpoint/endpoint_test.exs
@@ -124,42 +124,6 @@ defmodule Phoenix.Endpoint.EndpointTest do
     assert_receive {^pid, :sample}
   end
 
-  test "invokes log level callback from Plug.Telemetry" do
-    defmodule TelemetryEndpoint do
-      use Phoenix.Endpoint, otp_app: :phoenix
-      plug Plug.Telemetry,
-        event_prefix: [:phoenix, :endpoint],
-        log: {__MODULE__, :log_level, []}
-
-      def log_level(conn) do
-        case conn.path_info do
-          [] -> :debug
-          ["warn" | _] -> :warn
-          ["error" | _] -> :error
-          _ -> :info
-        end
-      end
-    end
-
-    ExUnit.CaptureLog.capture_log(fn -> start_supervised! TelemetryEndpoint end)
-
-    assert ExUnit.CaptureLog.capture_log(fn ->
-      TelemetryEndpoint.call(conn(:get, "/"), [])
-    end) =~ "[debug] GET /"
-
-    assert ExUnit.CaptureLog.capture_log(fn ->
-      TelemetryEndpoint.call(conn(:get, "/warn"), [])
-    end) =~ "[warn]  GET /warn"
-
-    assert ExUnit.CaptureLog.capture_log(fn ->
-      TelemetryEndpoint.call(conn(:get, "/error/404"), [])
-    end) =~ "[error] GET /error/404"
-
-    assert ExUnit.CaptureLog.capture_log(fn ->
-      TelemetryEndpoint.call(conn(:get, "/any"), [])
-    end) =~ "[info]  GET /any"
-  end
-
   @tag :capture_log
   test "uses url configuration for static path" do
     Application.put_env(:phoenix, __MODULE__.UrlEndpoint, url: [path: "/api"])
diff --git a/test/phoenix/logger_test.exs b/test/phoenix/logger_test.exs
index 4c43f7079..95230a3ad 100644
--- a/test/phoenix/logger_test.exs
+++ b/test/phoenix/logger_test.exs
@@ -1,5 +1,6 @@
 defmodule Phoenix.LoggerTest do
   use ExUnit.Case, async: true
+  use RouterHelper
 
   describe "filter_values/2 with discard strategy" do
     test "in top level map" do
@@ -63,4 +64,46 @@ defmodule Phoenix.LoggerTest do
               "ids" => ["[FILTERED]", "[FILTERED]"]}
     end
   end
+
+  describe "telemetry" do
+    def log_level(conn) do
+      case conn.path_info do
+        [] -> :debug
+        ["warn" | _] -> :warn
+        ["error" | _] -> :error
+        _ -> :info
+      end
+    end
+
+    test "invokes log level callback from Plug.Telemetry" do
+      opts =
+        Plug.Telemetry.init(
+          event_prefix: [:phoenix, :endpoint],
+          log: {__MODULE__, :log_level, []}
+        )
+
+      assert ExUnit.CaptureLog.capture_log(fn ->
+        Plug.Telemetry.call(conn(:get, "/"), opts)
+      end) =~ "[debug] GET /"
+
+      assert ExUnit.CaptureLog.capture_log(fn ->
+        Plug.Telemetry.call(conn(:get, "/warn"), opts)
+      end) =~ "[warn]  GET /warn"
+
+      assert ExUnit.CaptureLog.capture_log(fn ->
+        Plug.Telemetry.call(conn(:get, "/error/404"), opts)
+      end) =~ "[error] GET /error/404"
+
+      assert ExUnit.CaptureLog.capture_log(fn ->
+        Plug.Telemetry.call(conn(:get, "/any"), opts)
+      end) =~ "[info]  GET /any"
+    end
+
+    test "invokes default log level callback from Plug.Telemetry" do
+      assert ExUnit.CaptureLog.capture_log(fn ->
+        opts = Plug.Telemetry.init(event_prefix: [:phoenix, :endpoint])
+        Plug.Telemetry.call(conn(:get, "/"), opts)
+      end) =~ "[info]  GET /"
+    end
+  end
 end
diff --git a/test/phoenix/router/pipeline_test.exs b/test/phoenix/router/pipeline_test.exs
index 053838686..2838c2b2c 100644
--- a/test/phoenix/router/pipeline_test.exs
+++ b/test/phoenix/router/pipeline_test.exs
@@ -158,4 +158,19 @@ defmodule Phoenix.Router.PipelineTest do
       end
     end
   end
+
+  test "pipeline raises on conflict" do
+    assert_raise ArgumentError, ~r{there is an import from Kernel with the same nam}, fn ->
+      defmodule ConflictingPipeline do
+        use Phoenix.Router, otp_app: :phoenix
+        pipeline :raise do
+          plug Plug.Head
+        end
+        scope "/" do
+          pipe_through [:raise]
+          get "/", UnknownController, :index
+        end
+      end
+    end
+  end
 end
diff --git a/test/phoenix/router/routing_test.exs b/test/phoenix/router/routing_test.exs
index c5d1b5933..8dc01c095 100644
--- a/test/phoenix/router/routing_test.exs
+++ b/test/phoenix/router/routing_test.exs
@@ -23,6 +23,11 @@ defmodule Phoenix.Router.RoutingTest do
     def any(conn, _params), do: text(conn, "users any")
     def raise(_conn, _params), do: raise("boom")
     def exit(_conn, _params), do: exit(:boom)
+    def halt(conn, _params) do
+      conn
+      |> send_resp(401, "Unauthorized")
+      |> halt()
+    end
   end
 
   defmodule LogLevel do
@@ -44,6 +49,7 @@ defmodule Phoenix.Router.RoutingTest do
     get "/backups/*path", UserController, :image
     get "/static/images/icons/*image", UserController, :image
     get "/exit", UserController, :exit
+    get "/halt-controller", UserController, :halt
 
     trace("/trace", UserController, :trace)
     options "/options", UserController, :options
@@ -60,9 +66,18 @@ defmodule Phoenix.Router.RoutingTest do
     get "/no_log", SomePlug, [], log: false
     get "/fun_log", SomePlug, [], log: {LogLevel, :log_level, []}
     get "/users/:user_id/files/:id", UserController, :image
+
+    scope "/halt-plug" do
+      pipe_through :halt
+      get "/", UserController, :raise
+    end
+
     get "/*path", UserController, :not_found
 
     defp noop(conn, _), do: conn
+    defp halt(conn, _) do
+      conn |> Plug.Conn.send_resp(401, "Unauthorized") |> halt()
+    end
   end
 
   setup do
@@ -274,9 +289,47 @@ defmodule Phoenix.Router.RoutingTest do
 
     test "phoenix.router_dispatch.start and .stop are emitted on success" do
       call(Router, :get, "users/123")
-      assert_received {:telemetry_event, @router_start_event, _event}
-      assert_received {:telemetry_event, @router_stop_event, _event}
-      refute_received {:telemetry_event, @router_exception_event, _event}
+
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/users/:id"}, _}}
+
+      assert_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/users/:id"}, _}}
+
+      refute_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/users/:id"}, _}}
+    end
+
+    test "phoenix.router_dispatch.start and .stop are emitted when conn halted in router" do
+      conn = call(Router, :get, "/halt-plug")
+
+      assert conn.halted
+      assert conn.status == 401
+
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/halt-plug"}, _}}
+
+      assert_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/halt-plug"}, _}}
+
+      refute_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/halt-plug"}, _}}
+    end
+
+    test "phoenix.router_dispatch.start and .stop are emitted when conn is halted in controller" do
+      conn = call(Router, :get, "/halt-controller")
+
+      assert conn.halted
+      assert conn.status == 401
+
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/halt-controller"}, _}}
+
+      assert_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/halt-controller"}, _}}
+
+      refute_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/halt-controller"}, _}}
     end
 
     test "phoenix.router_dispatch.start and .exception are emitted on crash" do
@@ -284,23 +337,35 @@ defmodule Phoenix.Router.RoutingTest do
         call(Router, :get, "route_that_crashes")
       end
 
-      assert_received {:telemetry_event, @router_start_event, _event}
-      assert_received {:telemetry_event, @router_exception_event, _event}
-      refute_received {:telemetry_event, @router_stop_event, _event}
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/route_that_crashes"}, _}}
+
+      assert_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/route_that_crashes"}, _}}
+
+      refute_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/route_that_crashes"}, _}}
     end
 
     test "phoenix.router_dispatch.start and .exception are emitted on exit" do
       catch_exit(call(Router, :get, "exit"))
 
-      assert_received {:telemetry_event, @router_start_event, _event}
-      assert_received {:telemetry_event, @router_exception_event, _event}
-      refute_received {:telemetry_event, @router_stop_event, _event}
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/exit"}, _}}
+
+      assert_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/exit"}, _}}
+
+      refute_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/exit"}, _}}
     end
 
     test "phoenix.router_dispatch.start has supported measurements and metadata" do
       call(Router, :get, "users/123")
 
-      assert_received {:telemetry_event, @router_start_event, {measures, meta, _config}}
+      assert_received {:telemetry_event, @router_start_event,
+                       {measures, %{route: "/users/:id"} = meta, _config}}
+
       assert is_integer(measures.system_time)
 
       assert %{
@@ -318,7 +383,9 @@ defmodule Phoenix.Router.RoutingTest do
     test "phoenix.router_dispatch.stop has supported measurements and metadata" do
       call(Router, :get, "users/123")
 
-      assert_received {:telemetry_event, @router_stop_event, {measures, meta, _config}}
+      assert_received {:telemetry_event, @router_stop_event,
+                       {measures, %{route: "/users/:id"} = meta, _config}}
+
       assert is_integer(measures.duration)
 
       assert %{
@@ -338,7 +405,9 @@ defmodule Phoenix.Router.RoutingTest do
         call(Router, :get, "users/123/raise")
       end
 
-      assert_received {:telemetry_event, @router_exception_event, {measures, meta, _config}}
+      assert_received {:telemetry_event, @router_exception_event,
+                       {measures, %{route: "/users/:id/raise"} = meta, _config}}
+
       assert is_integer(measures.duration)
 
       assert %{
@@ -366,7 +435,9 @@ defmodule Phoenix.Router.RoutingTest do
     test "phoenix.router_dispatch.exception has supported measurements and metadata on exit" do
       catch_exit(call(Router, :get, "exit"))
 
-      assert_received {:telemetry_event, @router_exception_event, {measures, meta, _config}}
+      assert_received {:telemetry_event, @router_exception_event,
+                       {measures, %{route: "/exit"} = meta, _config}}
+
       assert is_integer(measures.duration)
 
       assert %{