Repository: elixir-ecto/ecto
Pull request #2054 (2017-05-08 10:45:55)

Support utc offset when casting string to utc_datetime
Currently `utc_datetime` fields silently drops any offset present in a string when `cast`. This PR allows `utc_datetime` to support the same inputs as `DateTime.from_iso8601`.

Since `utc_datetime` also allows for strings without an offset, it will fall back to `NaiveDateTime.from_iso8601` then then convert to `DateTime` in `Etc/UTC` timezone.

The motivation for this PR is a bug that we experienced in our web application, where we are using `utc_datetime` with `embedded_schema` to validate and cast incoming query params:

```elixir
defmodule MyApp.ReportRequest do
  use Ecto.Schema
  alias Ecto.Changeset

  @primary_key false
  embedded_schema do
    field :start_date, :utc_datetime
    field :end_date, :utc_datetime
    field :customer_id, :string
  end

  def validate(params) do
    changeset = 
      %MyApp.ReportRequest{}
      |> Changeset.cast(params, [:start_date, :end_date, :customer_id])
      |> Changeset.validate_required([:start_date, :end_date, :customer_id])

    if changeset.valid? do 
      {:ok, Changeset.apply_changes(changeset)} 
    else 
      {:error, changeset}
    end
  end
end
```

Usage: 

```elixir
iex(2)> MyApp.ReportRequest.validate(%{
  "start_date" => "2001-01-01T12:00:00+10:00", 
  "end_date" => "2010-01-01T12:00:00+10:00", 
  "customer_id" => "abc"})
{:ok,
 %MyApp.ReportRequest{customer_id: "abc",
  end_date: %DateTime{calendar: Calendar.ISO, day: 1, hour: 12,
   microsecond: {0, 0}, minute: 0, month: 1, second: 0, std_offset: 0,
   time_zone: "Etc/UTC", utc_offset: 0, year: 2010, zone_abbr: "UTC"},
  start_date: %DateTime{calendar: Calendar.ISO, day: 1, hour: 12,
   microsecond: {0, 0}, minute: 0, month: 1, second: 0, std_offset: 0,
   time_zone: "Etc/UTC", utc_offset: 0, year: 2001, zone_abbr: "UTC"}}}
```

Notice that it silently dropped the `+10:00` offsets while converting to UTC.

With the changes in this PR, the new output is:

```elixir
iex(2)> MyApp.ReportRequest.validate(%{
  "start_date" => "2001-01-01T12:00:00+10:00", 
  "end_date" => "2010-01-01T12:00:00+10:00", 
  "customer_id" => "abc"})
{:ok,
 %MyApp.ReportRequest{customer_id: "abc",
  end_date: %DateTime{calendar: Calendar.ISO, day: 1, hour: 2,
   microsecond: {0, 0}, minute: 0, month: 1, second: 0, std_offset: 0,
   time_zone: "Etc/UTC", utc_offset: 0, year: 2010, zone_abbr: "UTC"},
  start_date: %DateTime{calendar: Calendar.ISO, day: 1, hour: 2,
   microsecond: {0, 0}, minute: 0, month: 1, second: 0, std_offset: 0,
   time_zone: "Etc/UTC", utc_offset: 0, year: 2001, zone_abbr: "UTC"}}}
```



ebert[bot]: Hello, @mbuhot! This is your first Pull Request that will be reviewed by [Ebert](https://ebertapp.io/), an automatic Code Review service. It will leave comments on this diff with potential issues and style violations found in the code as you push new commits. You can also see all the issues found on this Pull Request on its [review page](https://ebertapp.io/github/elixir-ecto/ecto/pulls/2054). Please check our [documentation](https://ebertapp.io/docs) for more information. 
josevalim: This looks good to me. @lau, what are your thoughts? Should this be fixed here or in Elixir?
mbuhot: @josevalim looks like `DateTime.from_iso8601` isn't available in elixir 1.3.2 :(

Is it too soon to bump the minimum elixir version for ecto?
Alternatively could this be conditionally defined?

```elixir
if function_exported?(DateTime, :from_iso8601, 1) do 
... 
end
```


josevalim: Yes, we can bump the Elixir requirement as this will be in 2.2 only and there is still some time until we get there.
lau: If there is an offset it isn't UTC. The `utc_datetime` type is for storing UTC datetimes. Maybe it should refuse to save strings with offsets if that offset it not 0. If you want to convert it to UTC you can parse it and convert it to UTC before saving it.

Aren't non-UTC `DateTime` structs already refused if they are not "Etc/UTC"? Why should it be different for strings?

Imagine that you have a bunch of strings on different dates and times and hours in different timezones. If you save them to the database and it automatically converts it to UTC all of that information is lost.

2009-12-31T21:00:00-03:00 and 2010-01-01T00:00:00Z are  different years, months, hours, but would be saved to be the same.

Maybe a an error message mentioning a function to transform the string to UTC if that is what is wanted.
mbuhot: @lau 

> If there is an offset it isn't UTC. 

Yes the incomming string isn't UTC - it would be helpful if during the Ecto `cast` process it would be normalized to UTC before storage.

> The utc_datetime type is for storing UTC datetimes. Maybe it should refuse to save strings with offsets if that offset it not 0. 

Forbidding non-utc string inputs would be better than the current behaviour of silently dropping offsets.

> If you want to convert it to UTC you can parse it and convert it to UTC before saving it.

Can't ecto do this for me? Particularly for the use-case of embedded schemas used to validate and cast API requests (not for DB storage) this would be very useful.

> Aren't non-UTC DateTime structs already refused if they are not "Etc/UTC"? Why should it be different for strings?

No, we've found that even parsing the string into a `DateTime` before passing to ecto, it will silently drop the offset from the struct.

> Imagine that you have a bunch of strings on different dates and times and hours in different timezones. If you save them to the database and it automatically converts it to UTC all of that information is lost.

Yes, that is expected. My columns in the db are declared as `datetime without timezone` and the schema is declared as `:utc_datetime`, so I'm expecting to have UTC whenever values are cast or read from the database.

> 2009-12-31T21:00:00-03:00 and 2010-01-01T00:00:00Z are different years, months, hours, but would be saved to be the same.

That's expected.

> Maybe a an error message mentioning a function to transform the string to UTC if that is what is wanted.

Yes, this would be better than the current situation.
I'd prefer if ecto could just normalize the offsets during cast, saving everyone the headache of having to perform this normalization themselves.

josevalim: @lau the goal for utc_datetime was always to convert from any timezone to UTC. The reason we don't do that yet is because there is no conversion function in Elixir standard library yet. It is something we have already talked about by e-mail and that we can still hopefully include in time for Elixir v1.5.

As you mentioned, there is a huge concern about converting to UTC, especially for future dates, but if we want to keep all of the information you mentioned, then the best option for now is to use Calecto. The goal is eventually to have Calecto types in Ecto as well but we can't do that before we finish the work on Elixir side of things.

To sum up, we should either error or convert to UTC. I tried to argue above that converting to UTC is the best way to go, because that's what the utc_datetime type advertises. We still have to introduce the raw `:datetime` that will store everything correctly in the database without loss of information (or use Calecto).

Thanks everyone for the discussion! :heart:
mbuhot: Thanks for the guidance @josevalim.

As @lau mentions, we should consider what the behaviour is when casting a `%DateTime{}` struct that is already in a non-UTC timezone.

I've added a commit that will normalise these `DateTime` to UTC by round-tripping via a unix timestamp. Maybe there is a more direct way?

The tests also show usage of casting maps with string and atom keys to `:utc_datetime`:

```elixir
    assert Ecto.Type.cast(:utc_datetime, %{"year" => 2015, "month" => 1, "day" => 23,
                                           "hour" => 23, "minute" => 50, "second" => 07,
                                           "microsecond" => 8_000}) ==
           {:ok, @datetime_usec}
```

Is this for backwards compatibility with code that was previously using [Ecto.DateTime.cast/1](https://hexdocs.pm/ecto/Ecto.DateTime.html#cast/1) ?

If so, then since `Ecto.DateTime` didn't perform any timezone conversion on these maps, then I don't think `:utc_datetime` would need to either.
mbuhot: Thanks for the guidance @josevalim.

As @lau mentions, we should consider what the behaviour is when casting a `%DateTime{}` struct that is already in a non-UTC timezone.

I've added a commit that will normalise these `DateTime` to UTC by round-tripping via a unix timestamp. Maybe there is a more direct way?

The tests also show usage of casting maps with string and atom keys to `:utc_datetime`:

```elixir
    assert Ecto.Type.cast(:utc_datetime, %{"year" => 2015, "month" => 1, "day" => 23,
                                           "hour" => 23, "minute" => 50, "second" => 07,
                                           "microsecond" => 8_000}) ==
           {:ok, @datetime_usec}
```

Is this for backwards compatibility with code that was previously using [Ecto.DateTime.cast/1](https://hexdocs.pm/ecto/Ecto.DateTime.html#cast/1) ?

If so, then since `Ecto.DateTime` didn't perform any timezone conversion on these maps, then I don't think `:utc_datetime` would need to either.
josevalim: > As @lau mentions, we should consider what the behaviour should be when casting a %DateTime{} struct that is already in a non-UTC timezone.

In order to do this, we need to consult the TZ database. The goal is to have this functionality provided as part of Elixir. Round-tripping around Unix is going to fail because Elixir currently only converts to Unix for Etc/UTC. There is nothing we can do here besides waiting for this feature to be added to Elixir or use Calecto.

`Ecto.DateTime` did not know about timezones and therefore cannot be used as a comparison.
josevalim: Actually, my last comment was incorrect. It seems the to_unix conversion can be done by any timezone, which means we can always go from a timezone to UTC with the information in the struct without the TZ database. @lau can you please confirm? If that's the case, then I believe we should always convert to UTC, as that is the goal of `:utc_datetime`.
mbuhot: > `Ecto.DateTime` did not know about timezones and therefore cannot be used as a comparison.

OK, I'm just trying to determine if there needs to be special handling of any other data types beyond String and `%DateTime{}`.

The tests show that maps containing `year,month,day,hour,minute,second` keys (as strings or atoms) are supported. Do these maps ever contain a timezone offset?

Is there a scenario where a `DateTime` struct is somehow converted to a plain map that would then need to be cast back to a `DateTime` without data loss?
ericmj: @mbuhot Refrain from updating the changelog since it can easily cause conflicts with other PRs.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 11f59ecb..4e20692b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,17 +1,9 @@
 language: elixir
 elixir:
-  - 1.3.2
   - 1.4.0
 otp_release:
   - 18.2
   - 19.1
-matrix:
-  # We are only interested on the newest/oldest pair
-  exclude:
-    - elixir: 1.4.0
-      otp_release: 18.2
-    - elixir: 1.3.2
-      otp_release: 19.1
 addons:
   apt:
     packages:
diff --git a/lib/ecto/changeset.ex b/lib/ecto/changeset.ex
index 202a102c..493f3798 100644
--- a/lib/ecto/changeset.ex
+++ b/lib/ecto/changeset.ex
@@ -708,18 +708,27 @@ defmodule Ecto.Changeset do
   end
 
   defp on_cast_default(type, module) do
-    if Code.ensure_loaded?(module) and function_exported?(module, :changeset, 2) do
-      &module.changeset/2
-    else
-      raise ArgumentError, """
-      the module #{inspect module} does not define a changeset/2 function, which is used by cast_#{type}/3.
-      You need to either:
-
-        1. implement the #{type}.changeset/2 function
-        2. pass the :with option to cast_#{type}/3 with an anonymous function that expects 2 args
-
-      When using an inline embed, the :with option must be given
-      """
+    fn struct, params ->
+      try do
+        module.changeset(struct, params)
+      rescue
+        e in UndefinedFunctionError ->
+          case System.stacktrace do
+            [{^module, :changeset, args_or_arity, _}] when args_or_arity == 2
+                                                      when length(args_or_arity) == 2 ->
+              raise ArgumentError, """
+              the module #{inspect module} does not define a changeset/2 function,
+              which is used by cast_#{type}/3. You need to either:
+
+                1. implement the #{type}.changeset/2 function
+                2. pass the :with option to cast_#{type}/3 with an anonymous function that expects 2 args
+
+              When using an inline embed, the :with option must be given
+              """
+            stacktrace ->
+              reraise e, stacktrace
+          end
+      end
     end
   end
 
diff --git a/lib/ecto/migration/schema_migration.ex b/lib/ecto/migration/schema_migration.ex
index 07f6925c..acf77d79 100644
--- a/lib/ecto/migration/schema_migration.ex
+++ b/lib/ecto/migration/schema_migration.ex
@@ -19,15 +19,21 @@ defmodule Ecto.Migration.SchemaMigration do
   end
 
   def migrated_versions(repo, prefix) do
-    repo.all from(p in {get_source(repo), __MODULE__}, select: p.version) |> Map.put(:prefix, prefix), @opts
+    from(p in {get_source(repo), __MODULE__}, select: p.version)
+    |> Map.put(:prefix, prefix)
+    |> repo.all(@opts)
   end
 
   def up(repo, version, prefix) do
-    repo.insert! %__MODULE__{version: version} |> Ecto.put_meta(prefix: prefix, source: get_source(repo)), @opts
+    %__MODULE__{version: version}
+    |> Ecto.put_meta(prefix: prefix, source: get_source(repo))
+    |> repo.insert!(@opts)
   end
 
   def down(repo, version, prefix) do
-    repo.delete_all from(p in {get_source(repo), __MODULE__}, where: p.version == ^version) |> Map.put(:prefix, prefix), @opts
+    from(p in {get_source(repo), __MODULE__}, where: p.version == ^version)
+    |> Map.put(:prefix, prefix)
+    |> repo.delete_all(@opts)
   end
 
   def get_source(repo) do
diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex
index 871295ef..5a58c642 100644
--- a/lib/ecto/repo.ex
+++ b/lib/ecto/repo.ex
@@ -798,9 +798,9 @@ defmodule Ecto.Repo do
   Please note that for this to work, you will have to load existing structs from
   the database. So even if the struct exists, this won't work:
 
-      struct = %Post{id: 'existing_id', ...}
+      struct = %Post{id: "existing_id", ...}
       MyRepo.insert_or_update changeset
-      # => {:error, "id already exists"}
+      # => {:error, changeset} # id already exists
 
   ## Options
 
@@ -825,7 +825,7 @@ defmodule Ecto.Repo do
         {:error, changeset} -> # Something went wrong
       end
   """
-  @callback insert_or_update(struct_or_changeset :: Ecto.Schema.t | Ecto.Changeset.t, opts :: Keyword.t) ::
+  @callback insert_or_update(changeset :: Ecto.Changeset.t, opts :: Keyword.t) ::
             {:ok, Ecto.Schema.t} | {:error, Ecto.Changeset.t}
 
   @doc """
@@ -874,7 +874,7 @@ defmodule Ecto.Repo do
   Same as `insert_or_update/2` but returns the struct or raises if the changeset
   is invalid.
   """
-  @callback insert_or_update!(struct_or_changeset :: Ecto.Schema.t | Ecto.Changeset.t, opts :: Keyword.t) ::
+  @callback insert_or_update!(changeset :: Ecto.Changeset.t, opts :: Keyword.t) ::
             Ecto.Schema.t | no_return
 
   @doc """
@@ -922,9 +922,11 @@ defmodule Ecto.Repo do
 
   ## Examples
 
+      import Ecto.Changeset, only: [change: 2]
+
       MyRepo.transaction(fn ->
-        MyRepo.update!(%{alice | balance: alice.balance - 10})
-        MyRepo.update!(%{bob | balance: bob.balance + 10})
+        MyRepo.update!(change(alice, balance: alice.balance - 10))
+        MyRepo.update!(change(bob, balance: bob.balance + 10))
       end)
 
       # Roll back a transaction explicitly
diff --git a/lib/ecto/type.ex b/lib/ecto/type.ex
index 7ff8e143..08c31d92 100644
--- a/lib/ecto/type.ex
+++ b/lib/ecto/type.ex
@@ -799,13 +799,28 @@ defmodule Ecto.Type do
 
   ## UTC datetime
 
+  defp cast_utc_datetime(binary) when is_binary(binary) do
+    case DateTime.from_iso8601(binary) do
+      {:ok, datetime, _offset} -> {:ok, datetime}
+      {:error, :missing_offset} ->
+        case cast_naive_datetime(binary) do
+          {:ok, naive_datetime} -> {:ok, DateTime.from_naive!(naive_datetime, "Etc/UTC")}
+          :error -> :error
+        end
+      {:error, _} -> :error
+    end
+  end
+  defp cast_utc_datetime(%DateTime{time_zone: "Etc/UTC"} = datetime), do: {:ok, datetime}
+  defp cast_utc_datetime(%DateTime{} = datetime) do
+    case (datetime |> DateTime.to_unix() |> DateTime.from_unix()) do
+      {:ok, _} = ok -> ok
+      {:error, _} -> :error
+    end
+  end
   defp cast_utc_datetime(value) do
     case cast_naive_datetime(value) do
-      {:ok, %NaiveDateTime{year: year, month: month, day: day,
-                           hour: hour, minute: minute, second: second, microsecond: microsecond}} ->
-        {:ok, %DateTime{year: year, month: month, day: day,
-                        hour: hour, minute: minute, second: second, microsecond: microsecond,
-                        std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}}
+      {:ok, %NaiveDateTime{} = naive_datetime} ->
+        {:ok, DateTime.from_naive!(naive_datetime, "Etc/UTC")}
       {:ok, _} = ok ->
         ok
       :error ->
diff --git a/lib/ecto/uuid.ex b/lib/ecto/uuid.ex
index 49fb795b..196578d5 100644
--- a/lib/ecto/uuid.ex
+++ b/lib/ecto/uuid.ex
@@ -29,9 +29,9 @@ defmodule Ecto.UUID do
   catch
     :error -> :error
   else
-    casted ->
-      {:ok, casted}
+    casted -> {:ok, casted}
   end
+  def cast(<< _::128 >> = binary), do: encode(binary)
   def cast(_), do: :error
 
   @doc """
@@ -145,14 +145,14 @@ defmodule Ecto.UUID do
   Converts a binary UUID into a string.
   """
   def load(<<_::128>> = uuid) do
-   {:ok, encode(uuid)}
+    encode(uuid)
   end
   def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do
     raise "trying to load string UUID as Ecto.UUID: #{inspect string}. " <>
           "Maybe you wanted to declare :uuid as your database field?"
   end
   def load(%Ecto.Query.Tagged{type: :uuid, value: uuid}) do
-    {:ok, encode(uuid)}
+    encode(uuid)
   end
   def load(_), do: :error
 
@@ -160,7 +160,8 @@ defmodule Ecto.UUID do
   Generates a version 4 (random) UUID.
   """
   def generate do
-    bingenerate() |> encode
+    {:ok, uuid} = encode(bingenerate())
+    uuid
   end
 
   @doc """
@@ -185,11 +186,32 @@ defmodule Ecto.UUID do
                  e9::4, e10::4, e11::4, e12::4 >>) do
     << e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-,
        e(b1), e(b2), e(b3), e(b4), ?-,
-       e(c1), e(c2), e(c3), e(c4), ?-,
-       e(d1), e(d2), e(d3), e(d4), ?-,
+       encode_version(c1), e(c2), e(c3), e(c4), ?-,
+       encode_variant(d1), e(d2), e(d3), e(d4), ?-,
        e(e1), e(e2), e(e3), e(e4), e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12) >>
+  catch
+    :error -> :error
+  else
+    encoded -> {:ok, encoded}
   end
 
+  @compile {:inline, encode_version: 1}
+
+  defp encode_version(1), do: ?1
+  defp encode_version(2), do: ?2
+  defp encode_version(3), do: ?3
+  defp encode_version(4), do: ?4
+  defp encode_version(5), do: ?5
+  defp encode_version(_), do: throw(:error)
+
+  @compile {:inline, encode_variant: 1}
+
+  defp encode_variant(8), do: ?8
+  defp encode_variant(9), do: ?9
+  defp encode_variant(10), do: ?a
+  defp encode_variant(11), do: ?b
+  defp encode_variant(_), do: throw(:error)
+
   @compile {:inline, e: 1}
 
   defp e(0),  do: ?0
diff --git a/mix.exs b/mix.exs
index 201e3462..32e3ac72 100644
--- a/mix.exs
+++ b/mix.exs
@@ -7,7 +7,7 @@ defmodule Ecto.Mixfile do
   def project do
     [app: :ecto,
      version: @version,
-     elixir: "~> 1.3",
+     elixir: "~> 1.4",
      deps: deps(),
      build_per_environment: false,
      consolidate_protocols: false,


Test patch:
diff --git a/test/ecto/type_test.exs b/test/ecto/type_test.exs
index 337b27e6..14e50670 100644
--- a/test/ecto/type_test.exs
+++ b/test/ecto/type_test.exs
@@ -313,11 +313,13 @@ defmodule Ecto.TypeTest do
     assert Ecto.Type.cast(:utc_datetime, "2015-01-23 23:50:07") == {:ok, @datetime}
     assert Ecto.Type.cast(:utc_datetime, "2015-01-23T23:50:07") == {:ok, @datetime}
     assert Ecto.Type.cast(:utc_datetime, "2015-01-23T23:50:07Z") == {:ok, @datetime}
+    assert Ecto.Type.cast(:utc_datetime, "2015-01-24T09:50:07+10:00") == {:ok, @datetime}
     assert Ecto.Type.cast(:utc_datetime, "2000-02-29T23:50:07") == {:ok, @datetime_leapyear}
     assert Ecto.Type.cast(:utc_datetime, "2015-01-23P23:50:07") == :error
 
     assert Ecto.Type.cast(:utc_datetime, "2015-01-23T23:50:07.008000") == {:ok, @datetime_usec}
     assert Ecto.Type.cast(:utc_datetime, "2015-01-23T23:50:07.008000Z") == {:ok, @datetime_usec}
+    assert Ecto.Type.cast(:utc_datetime, "2015-01-23T17:50:07.008000-06:00") == {:ok, @datetime_usec}
 
     assert Ecto.Type.cast(:utc_datetime, %{"year" => "2015", "month" => "1", "day" => "23",
                                            "hour" => "23", "minute" => "50", "second" => "07"}) ==
@@ -326,6 +328,12 @@ defmodule Ecto.TypeTest do
     assert Ecto.Type.cast(:utc_datetime, %{year: 2015, month: 1, day: 23, hour: 23, minute: 50, second: 07}) ==
            {:ok, @datetime}
 
+    assert Ecto.Type.cast(:utc_datetime, %DateTime{calendar: Calendar.ISO, year: 2015, month: 1, day: 24,
+                                                   hour: 9, minute: 50, second: 7, microsecond: {0, 0},
+                                                   std_offset: 0, utc_offset: 36000,
+                                                   time_zone: "Etc/GMT-10", zone_abbr: "+10"}) ==
+           {:ok, @datetime}
+
     assert Ecto.Type.cast(:utc_datetime, %{"year" => "", "month" => "", "day" => "",
                                            "hour" => "", "minute" => ""}) ==
            {:ok, nil}
diff --git a/test/ecto/uuid_test.exs b/test/ecto/uuid_test.exs
index 594de932..b0b50acc 100644
--- a/test/ecto/uuid_test.exs
+++ b/test/ecto/uuid_test.exs
@@ -10,18 +10,15 @@ defmodule Ecto.UUIDTest do
 
   test "cast" do
     assert Ecto.UUID.cast(@test_uuid) == {:ok, @test_uuid}
+    assert Ecto.UUID.cast(@test_uuid_binary) == {:ok, @test_uuid}
     assert Ecto.UUID.cast(@test_uuid_upper_case) == {:ok, String.downcase(@test_uuid_upper_case)}
     assert Ecto.UUID.cast(@test_uuid_invalid_characters) == :error
     assert Ecto.UUID.cast(@test_uuid_invalid_shape) == :error
-    assert Ecto.UUID.cast(@test_uuid_binary) == :error
     assert Ecto.UUID.cast(nil) == :error
   end
 
   test "cast!" do
     assert Ecto.UUID.cast!(@test_uuid) == @test_uuid
-    assert_raise Ecto.CastError, "cannot cast <<96, 29, 116, 228, 168, 211, 75, 110, 131, 101, 237, 219, 76, 137, 51, 39>> to Ecto.UUID", fn ->
-      assert Ecto.UUID.cast!(@test_uuid_binary)
-    end
     assert_raise Ecto.CastError, "cannot cast nil to Ecto.UUID", fn ->
       assert Ecto.UUID.cast!(nil)
     end