Repository: phoenixframework/phoenix (elixir)

Issue:
Phoenix generators use naive Ecto timestamps - UTC would be much better
### Actual behavior

Currently when using the Phoenix generators the timestamps are NaiveDateTime. That means you can't compare them with other `DateTime`s and see which happened first. You can't use `DateTime.to_unix/1` to convert them to unix timestamps. When serializing them, for instance in a JSON API, the consumers of the API won't know if it's in UTC because there won't automatically be a UTC offset indicator (the Z at the end of the ISO8601 timestamp).

Another thing is that a lot of Elixir projects start with the Phoenix generators and are perhaps seen as a good example or a good starting place. I think this could be improved if UTC DateTime were used for timestamps instead of NaiveDateTime.

NaiveDateTime is meant for when you don't know the timezone. We know the Ecto `timestamps` are UTC, so it makes sense that they are UTC DateTime. It's a very good example of when to use DateTime instead of NaiveDateTime. The Ecto default to NaiveDateTime is there for historical reasons when there was less support for DateTime in the Elixir standard library. As far as I understand it, the reason UTC datetimes have not be made the default in Ecto is because it would require releasing a new major version (Ecto 4) to avoid breaking existing behaviour. The Phoenix generators don't have that same restriction since the generated schemas and tables are new.

### Expected behavior

When using the phx generators timestamps should be [UTC DateTime](http://www.creativedeletion.com/2019/06/17/utc-timestamps-in-ecto.html), not NaiveDateTime. One way to do that is to have the schema line specifying the timestamp say `timestamps([type: :utc_datetime])` instead of `timestamps()`.

Pull request description:
Allow UTC datetimes in the generators
The generators now consider the :timestamp_type value in the generator config for an application.

For backwards compatability, if this value is not set, it will default to `:naive_datetime` which will generate the timestamps without an explicit type, otherwise the type will be set explicitly, such as:

`timestamps(:type, :utc_datetime)`

This closes #5564 

Patch:
diff --git a/installer/lib/phx_new/project.ex b/installer/lib/phx_new/project.ex
index c8a20df76..da093e465 100644
--- a/installer/lib/phx_new/project.ex
+++ b/installer/lib/phx_new/project.ex
@@ -16,7 +16,7 @@ defmodule Phx.New.Project do
             opts: :unset,
             in_umbrella?: false,
             binding: [],
-            generators: []
+            generators: [timestamp_type: :utc_datetime]
 
   def new(project_path, opts) do
     project_path = Path.expand(project_path)
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..f77af333c 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,
@@ -38,7 +39,8 @@ defmodule Mix.Phoenix.Schema do
             migration_module: nil,
             fixture_unique_functions: [],
             fixture_params: [],
-            prefix: nil
+            prefix: nil,
+            timestamp_type: :naive_datetime
 
   @valid_types [
     :integer,
@@ -86,7 +88,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 +98,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 +121,7 @@ defmodule Mix.Phoenix.Schema do
       plural: schema_plural,
       singular: singular,
       collection: collection,
+      optionals: optionals,
       assocs: assocs,
       types: types,
       defaults: schema_defaults(attrs),
@@ -127,6 +131,7 @@ defmodule Mix.Phoenix.Schema do
       human_singular: Phoenix.Naming.humanize(singular),
       human_plural: Phoenix.Naming.humanize(schema_plural),
       binary_id: opts[:binary_id],
+      timestamp_type: opts[:timestamp_type] || :naive_datetime,
       migration_defaults: migration_defaults(attrs),
       string_attr: string_attr,
       params: %{
@@ -250,6 +255,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.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 7bbf7aed4..edb9a3388 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -55,6 +55,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
       config :your_app, :generators,
         migration: true,
         binary_id: false,
+        timestamp_type: :naive_datetime,
         sample_binary_id: "11111111-1111-1111-1111-111111111111"
 
   You can override those options per invocation by providing corresponding
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/lib/mix/tasks/phx.gen.schema.ex b/lib/mix/tasks/phx.gen.schema.ex
index 6d28156a4..c6f49051e 100644
--- a/lib/mix/tasks/phx.gen.schema.ex
+++ b/lib/mix/tasks/phx.gen.schema.ex
@@ -111,11 +111,20 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
       config :your_app, :generators,
         migration: true,
         binary_id: false,
+        timestamp_type: :naive_datetime,
         sample_binary_id: "11111111-1111-1111-1111-111111111111"
 
   You can override those options per invocation by providing corresponding
   switches, e.g. `--no-binary-id` to use normal ids despite the default
   configuration or `--migration` to force generation of the migration.
+
+  ## UTC timestamps
+
+  By setting the `:timestamp_type` to `:utc_datetime`, the timestamps
+  will be created using the UTC timezone. This results in a `DateTime` struct
+  instead of a `NaiveDateTime`. This can also be set to `:utc_datetime_usec` for
+  microsecond precision.
+
   """
   use Mix.Task
 
diff --git a/priv/templates/phx.gen.auth/migration.ex b/priv/templates/phx.gen.auth/migration.ex
index e8cdd476a..6ebe6ee86 100644
--- a/priv/templates/phx.gen.auth/migration.ex
+++ b/priv/templates/phx.gen.auth/migration.ex
@@ -9,7 +9,7 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
 <% end %>      <%= migration.column_definitions[:email] %>
       add :hashed_password, :string, null: false
       add :confirmed_at, :naive_datetime
-      timestamps()
+      timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
     end
 
     create unique_index(:<%= schema.table %>, [:email])
diff --git a/priv/templates/phx.gen.auth/schema.ex b/priv/templates/phx.gen.auth/schema.ex
index 2624e96f3..9623020f3 100644
--- a/priv/templates/phx.gen.auth/schema.ex
+++ b/priv/templates/phx.gen.auth/schema.ex
@@ -9,7 +9,7 @@ defmodule <%= inspect schema.module %> do
     field :hashed_password, :string, redact: true
     field :confirmed_at, :naive_datetime
 
-    timestamps()
+    timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
   end
 
   @doc """
diff --git a/priv/templates/phx.gen.schema/migration.exs b/priv/templates/phx.gen.schema/migration.exs
index 34a91963f..ed6c032fe 100644
--- a/priv/templates/phx.gen.schema/migration.exs
+++ b/priv/templates/phx.gen.schema/migration.exs
@@ -7,7 +7,7 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
 <% end %><%= for {k, v} <- schema.attrs do %>      add <%= inspect k %>, <%= inspect Mix.Phoenix.Schema.type_for_migration(v) %><%= schema.migration_defaults[k] %>
 <% end %><%= for {_, i, _, s} <- schema.assocs do %>      add <%= inspect(i) %>, references(<%= inspect(s) %>, on_delete: :nothing<%= if schema.binary_id do %>, type: :binary_id<% end %>)
 <% end %>
-      timestamps()
+      timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
     end
 <%= if Enum.any?(schema.indexes) do %><%= for index <- schema.indexes do %>
     <%= index %><% end %>
diff --git a/priv/templates/phx.gen.schema/schema.ex b/priv/templates/phx.gen.schema/schema.ex
index fc1d4788e..94c40751d 100644
--- a/priv/templates/phx.gen.schema/schema.ex
+++ b/priv/templates/phx.gen.schema/schema.ex
@@ -9,14 +9,14 @@ defmodule <%= inspect schema.module %> do
 <%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %>
 <%= for {_, k, _, _} <- schema.assocs do %>    field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>
 <% end %>
-    timestamps()
+    timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
   end
 
   @doc false
   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..e11511ccb 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -54,6 +54,7 @@ defmodule Mix.Tasks.Phx.NewTest do
 
       assert_file("phx_blog/config/config.exs", fn file ->
         assert file =~ "ecto_repos: [PhxBlog.Repo]"
+        assert file =~ "generators: [timestamp_type: :utc_datetime]"
         assert file =~ "config :phoenix, :json_library, Jason"
         assert file =~ ~s[cd: Path.expand("../assets", __DIR__),]
         refute file =~ "namespace: PhxBlog"
@@ -557,7 +558,7 @@ defmodule Mix.Tasks.Phx.NewTest do
   test "new with binary_id" do
     in_tmp("new with binary_id", fn ->
       Mix.Tasks.Phx.New.run([@app_name, "--binary-id"])
-      assert_file("phx_blog/config/config.exs", ~r/generators: \[binary_id: true\]/)
+      assert_file("phx_blog/config/config.exs", ~r/generators: \[.*binary_id: true\.*]/)
     end)
   end
 
@@ -695,6 +696,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.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index 7e463a2b8..009792d9a 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -935,6 +935,31 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
     end
   end
 
+  test "allows utc_datetime", config do
+    in_tmp_phx_project(config.test, fn ->
+      send self(), {:mix_shell_input, :yes?, false}
+      with_generator_env(:my_app, [timestamp_type: :utc_datetime], fn ->
+
+        Gen.Auth.run(
+        ~w(Accounts User users),
+        ecto_adapter: Ecto.Adapters.Postgres,
+        validate_dependencies?: false
+        )
+
+        assert [migration] = Path.wildcard("priv/repo/migrations/*_create_users_auth_tables.exs")
+
+        assert_file migration, fn file ->
+          assert file =~ "timestamps(type: :utc_datetime)"
+        end
+
+
+        assert_file("lib/my_app/accounts/user.ex", fn file ->
+          assert file =~ "timestamps(type: :utc_datetime)"
+        end)
+      end)
+    end)
+  end
+
   test "supports --binary-id option", config do
     in_tmp_phx_project(config.test, fn ->
       send self(), {:mix_shell_input, :yes?, false}
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..85d39bef2 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))
@@ -301,6 +312,22 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
         end
       end
     end
+
+    in_tmp_project "uses defaults from generators configuration (:utc_datetime)", fn ->
+      with_generator_env [timestamp_type: :utc_datetime], fn ->
+        Gen.Schema.run(~w(Blog.Post posts))
+
+        assert [migration] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
+
+        assert_file migration, fn file ->
+          assert file =~ "timestamps(type: :utc_datetime)"
+        end
+
+        assert_file "lib/phoenix/blog/post.ex", fn file ->
+          assert file =~ "timestamps(type: :utc_datetime)"
+        end
+      end
+    end
   end
 
   test "generates migrations with a custom migration module", config do