Repository: phoenixframework/phoenix (elixir)

Issue:
phx.new: run ecto migrator in app start if --database sqlite3

Pull request:
Automatically migrate when release starts using sqlite 3
When using the sqlite3 ecto adapter, the new and umbrella generators will add the `Ecto.Migrator` to run the migrations.

By default these run only when inside of a release, but the intent is to allow the end user to customize this in a way that suits their production environment.

This closes #5262 

Patch:
diff --git a/installer/templates/phx_single/lib/app_name/application.ex b/installer/templates/phx_single/lib/app_name/application.ex
index 8373d2a7a..86a8f9a1b 100644
--- a/installer/templates/phx_single/lib/app_name/application.ex
+++ b/installer/templates/phx_single/lib/app_name/application.ex
@@ -9,7 +9,10 @@ defmodule <%= @app_module %>.Application do
   def start(_type, _args) do
     children = [
       <%= @web_namespace %>.Telemetry,<%= if @ecto do %>
-      <%= @app_module %>.Repo,<% end %>
+      <%= @app_module %>.Repo,<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
+      {Ecto.Migrator,
+        repos: Application.fetch_env!(<%= inspect String.to_atom(@app_name) %>, :ecto_repos),
+        skip: skip_migrations?()},<% end %>
       {Phoenix.PubSub, name: <%= @app_module %>.PubSub},<%= if @mailer do %>
       # Start the Finch HTTP client for sending emails
       {Finch, name: <%= @app_module %>.Finch},<% end %>
@@ -31,5 +34,10 @@ defmodule <%= @app_module %>.Application do
   def config_change(changed, _new, removed) do
     <%= @endpoint_module %>.config_change(changed, removed)
     :ok
-  end
+  end<%= if @adapter_app == :ecto_sqlite3 do %>
+
+  defp skip_migrations?() do
+    # By default, sqlite migrations are run when using a release
+    System.get_env("RELEASE_NAME") != nil
+  end<% end %>
 end
diff --git a/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex b/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
index 4f526f22a..93cfe6798 100644
--- a/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
+++ b/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
@@ -8,7 +8,10 @@ defmodule <%= @app_module %>.Application do
   @impl true
   def start(_type, _args) do
     children = [<%= if @ecto do %>
-      <%= @app_module %>.Repo,<% end %>
+      <%= @app_module %>.Repo,<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
+      {Ecto.Migrator,
+        repos: Application.fetch_env!(<%= inspect String.to_atom(@app_name) %>, :ecto_repos),
+        skip: skip_migrations?()},<% end %>
       {Phoenix.PubSub, name: <%= @app_module %>.PubSub}<%= if @mailer do %>,
       # Start the Finch HTTP client for sending emails
       {Finch, name: <%= @app_module %>.Finch}<% end %>
@@ -17,5 +20,10 @@ defmodule <%= @app_module %>.Application do
     ]
 
     Supervisor.start_link(children, strategy: :one_for_one, name: <%= @app_module %>.Supervisor)
-  end
+  end<%= if @adapter_app == :ecto_sqlite3 do %>
+
+  defp skip_migrations?() do
+    # By default, sqlite migrations are run when using a release
+    System.get_env("RELEASE_NAME") != nil
+  end<% end %>
 end
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index 670322864..8e9c11c9a 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -28,6 +28,7 @@ defmodule Mix.Phoenix.Schema do
             migration_defaults: nil,
             migration?: false,
             params: %{},
+            optionals: [],
             sample_id: nil,
             web_path: nil,
             web_namespace: nil,
@@ -86,7 +87,6 @@ defmodule Mix.Phoenix.Schema do
     api_prefix = Application.get_env(otp_app, :generators)[:api_prefix] || "/api"
     embedded? = Keyword.get(opts, :embedded, false)
     generate? = Keyword.get(opts, :schema, true)
-
     singular =
       module
       |> Module.split()
@@ -97,6 +97,8 @@ defmodule Mix.Phoenix.Schema do
     string_attr = string_attr(types)
     create_params = params(attrs, :create)
 
+    optionals = for {key, :map} <- types, do: key, into: []
+
     default_params_key =
       case Enum.at(create_params, 0) do
         {key, _} -> key
@@ -118,6 +120,7 @@ defmodule Mix.Phoenix.Schema do
       plural: schema_plural,
       singular: singular,
       collection: collection,
+      optionals: optionals,
       assocs: assocs,
       types: types,
       defaults: schema_defaults(attrs),
@@ -250,6 +253,14 @@ defmodule Mix.Phoenix.Schema do
     end)
   end
 
+  @doc """
+  Return the required fields in the schema. Anything not in the `optionals` list
+  is considered required.
+  """
+  def required_fields(schema) do
+    Enum.reject(schema.attrs, fn {key, _} -> key in schema.optionals end)
+  end
+
   def type_and_opts_for_schema({:enum, opts}),
     do: ~s|Ecto.Enum, values: #{inspect(Keyword.get(opts, :values))}|
 
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index 01262692b..3243dc463 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -172,7 +172,9 @@ defmodule Mix.Tasks.Phx.Gen.Html do
 
   @doc false
   def inputs(%Schema{} = schema) do
-    Enum.map(schema.attrs, fn
+    schema.attrs
+    |> Enum.reject(fn {_key, type} -> type == :map end)
+    |> Enum.map(fn
       {key, :integer} ->
         ~s(<.input field={f[#{inspect(key)}]} type="number" label="#{label(key)}" />)
 
@@ -246,6 +248,9 @@ defmodule Mix.Tasks.Phx.Gen.Html do
       lines = input |> String.split("\n") |> Enum.reject(&(&1 == ""))
 
       case lines do
+        [] ->
+          []
+
         [line] ->
           [columns, line]
 
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
index ea9a29135..825b27190 100644
--- a/lib/mix/tasks/phx.gen.live.ex
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -271,7 +271,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do
 
   @doc false
   def inputs(%Schema{} = schema) do
-    Enum.map(schema.attrs, fn
+    schema.attrs
+    |> Enum.reject(fn {_key, type} -> type == :map end)
+    |> Enum.map(fn
       {_, {:references, _}} ->
         nil
 
diff --git a/priv/templates/phx.gen.schema/schema.ex b/priv/templates/phx.gen.schema/schema.ex
index fc1d4788e..2a8018d7e 100644
--- a/priv/templates/phx.gen.schema/schema.ex
+++ b/priv/templates/phx.gen.schema/schema.ex
@@ -16,7 +16,7 @@ defmodule <%= inspect schema.module %> do
   def changeset(<%= schema.singular %>, attrs) do
     <%= schema.singular %>
     |> cast(attrs, [<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
-    |> validate_required([<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
+    |> validate_required([<%= Enum.map_join(Mix.Phoenix.Schema.required_fields(schema), ", ", &inspect(elem(&1, 0))) %>])
 <%= for k <- schema.uniques do %>    |> unique_constraint(<%= inspect k %>)
 <% end %>  end
 end


Test patch:
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index 617b6a336..e5b0a887f 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -695,6 +695,15 @@ defmodule Mix.Tasks.Phx.NewTest do
       assert_file("custom_path/config/runtime.exs", [~r/database: database_path/])
       assert_file("custom_path/lib/custom_path/repo.ex", "Ecto.Adapters.SQLite3")
 
+      assert_file("custom_path/lib/custom_path/application.ex", fn file ->
+        assert file =~ "{Ecto.Migrator"
+        assert file =~ "repos: Application.fetch_env!(:custom_path, :ecto_repos)"
+        assert file =~ "skip: skip_migrations?()"
+
+        assert file =~ "defp skip_migrations?() do"
+        assert file =~ ~s/!!System.get_env("RELEASE_NAME")/
+      end)
+
       assert_file("custom_path/test/support/conn_case.ex", "DataCase.setup_sandbox(tags)")
 
       assert_file(
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 9fc4258df..871289169 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -654,6 +654,15 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
       assert_file(app_path(app, "mix.exs"), ":ecto_sqlite3")
       assert_file(app_path(app, "lib/custom_path/repo.ex"), "Ecto.Adapters.SQLite3")
 
+      assert_file(app_path(app, "lib/custom_path/application.ex"), fn file ->
+        assert file =~ "{Ecto.Migrator"
+        assert file =~ "repos: Application.fetch_env!(:custom_path, :ecto_repos)"
+        assert file =~ "skip: skip_migrations?()"
+
+        assert file =~ "defp skip_migrations?() do"
+        assert file =~ ~s/!!System.get_env("RELEASE_NAME")/
+      end)
+
       assert_file(root_path(app, "config/dev.exs"), [~r/database: .*_dev.db/])
       assert_file(root_path(app, "config/test.exs"), [~r/database: .*_test.db/])
       assert_file(root_path(app, "config/runtime.exs"), [~r/database: database_path/])
diff --git a/test/mix/tasks/phx.gen.html_test.exs b/test/mix/tasks/phx.gen.html_test.exs
index 13723b146..b0eb2d309 100644
--- a/test/mix/tasks/phx.gen.html_test.exs
+++ b/test/mix/tasks/phx.gen.html_test.exs
@@ -54,6 +54,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
                       alarm:time
                       alarm_usec:time_usec
                       secret:uuid:redact announcement_date:date alarm:time
+                      metadata:map
                       weight:float user_id:references:users))
 
       assert_file("lib/phoenix/blog/post.ex")
@@ -167,6 +168,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
         assert file =~ ~s(<.input field={f[:announcement_date]} type="date")
         assert file =~ ~s(<.input field={f[:alarm]} type="time")
         assert file =~ ~s(<.input field={f[:secret]} type="text" label="Secret" />)
+        refute file =~ ~s(field={f[:metadata]})
 
         assert file =~ """
                  <.input
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index bd8e73132..20108f396 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -65,6 +65,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
                       alarm:time
                       alarm_usec:time_usec
                       secret:uuid:redact announcement_date:date alarm:time
+                      metadata:map
                       weight:float user_id:references:users))
 
       assert_file "lib/phoenix/blog/post.ex"
@@ -116,6 +117,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ ~s(<.input field={@form[:announcement_date]} type="date")
         assert file =~ ~s(<.input field={@form[:alarm]} type="time")
         assert file =~ ~s(<.input field={@form[:secret]} type="text" label="Secret" />)
+        refute file =~ ~s(<field={@form[:metadata]})
         assert file =~ """
                 <.input
                   field={@form[:status]}
diff --git a/test/mix/tasks/phx.gen.schema_test.exs b/test/mix/tasks/phx.gen.schema_test.exs
index caab17f49..efb914f5f 100644
--- a/test/mix/tasks/phx.gen.schema_test.exs
+++ b/test/mix/tasks/phx.gen.schema_test.exs
@@ -16,7 +16,7 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
 
   test "build" do
     in_tmp_project "build", fn ->
-      schema = Gen.Schema.build(~w(Blog.Post posts title:string), [])
+      schema = Gen.Schema.build(~w(Blog.Post posts title:string tags:map), [])
 
       assert %Schema{
         alias: Post,
@@ -28,10 +28,11 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
         singular: "post",
         human_plural: "Posts",
         human_singular: "Post",
-        attrs: [title: :string],
-        types: %{title: :string},
+        attrs: [title: :string, tags: :map],
+        types: %{title: :string, tags: :map},
+        optionals: [:tags],
         route_helper: "post",
-        defaults: %{title: ""},
+        defaults: %{title: "", tags: ""},
       } = schema
       assert String.ends_with?(schema.file, "lib/phoenix/blog/post.ex")
     end
@@ -112,6 +113,16 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
     end
   end
 
+  test "does not add maps to the required list", config do
+    in_tmp_project config.test, fn ->
+      Gen.Schema.run(~w(Blog.Post blog_posts title:string tags:map published_at:naive_datetime))
+      assert_file "lib/phoenix/blog/post.ex", fn file ->
+        assert file =~ "cast(attrs, [:title, :tags, :published_at]"
+        assert file =~ "validate_required([:title, :published_at]"
+      end
+    end
+  end
+
   test "generates nested schema", config do
     in_tmp_project config.test, fn ->
       Gen.Schema.run(~w(Blog.Admin.User users name:string))